Initial commit
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from backend.code.modules.database.database_sqlite import DatabaseSqliteModule
|
||||
from backend.code.modules.sessions.repository import SessionRepository
|
||||
from backend.code.services.token import TokenService
|
||||
from backend.code.services.mailing import MailingService
|
||||
from backend.code.enums.errors_enum import ErrorTypeEnum
|
||||
from backend.code.models.responses.session_token import SessionTokenResponse
|
||||
|
||||
|
||||
from backend.code.modules.users.repository import UserRepository
|
||||
|
||||
class SessionsController():
|
||||
def __init__(self):
|
||||
database = DatabaseSqliteModule()
|
||||
self.repo = SessionRepository(database)
|
||||
self.token_service = TokenService()
|
||||
self.mailing_service = MailingService()
|
||||
|
||||
def Login(self, user: str, password: str, domain: str = None):
|
||||
account = self.repo.SelectByUsername(user)
|
||||
if account is None:
|
||||
return
|
||||
|
||||
# Array to properties
|
||||
account_id = account[0]
|
||||
user_id = account[1]
|
||||
_password = account[3]
|
||||
attempts = account[4]
|
||||
locked = account[5]
|
||||
name = account[7]
|
||||
surnames = account[8]
|
||||
email = account[9]
|
||||
|
||||
# Validations
|
||||
if locked:
|
||||
return ErrorTypeEnum.USER_ACCOUNT_LOCKED
|
||||
|
||||
# Default user has no password
|
||||
if _password is None:
|
||||
return ErrorTypeEnum.USER_ACCOUNT_NO_PASSWORD
|
||||
|
||||
if password != _password:
|
||||
attempts = attempts + 1
|
||||
self._OnUpdateAttempts(account_id, attempts)
|
||||
return ErrorTypeEnum.USER_ACCOUNT_WRONG_PASSWORD
|
||||
|
||||
# Reset when password is correct and registered some attempts
|
||||
if attempts > 0:
|
||||
self._OnUpdateAttempts(account_id, 0)
|
||||
|
||||
# Generate token
|
||||
payload = {
|
||||
'UserId': user_id,
|
||||
'Name': name,
|
||||
'Surnames': surnames,
|
||||
'Email': email
|
||||
}
|
||||
token = self.token_service.GenerateToken(payload)
|
||||
|
||||
session = self.repo.RegisterSession(account_id, token)
|
||||
return SessionTokenResponse(Token=token)
|
||||
|
||||
def ValidateToken(self, token: str):
|
||||
exists = self.repo.SelectByToken(token)
|
||||
if exists is None:
|
||||
return ErrorTypeEnum.USER_SESSION_NOT_EXISTS
|
||||
|
||||
# Array to properties
|
||||
account_id = exists[0]
|
||||
user_id = exists[1]
|
||||
|
||||
# Get metadata token
|
||||
payload = self.token_service.ValidateToken(token)
|
||||
if payload == False:
|
||||
return
|
||||
|
||||
# Check token integrity
|
||||
if payload['UserId'] != user_id:
|
||||
return
|
||||
|
||||
autorefresh = self._OnCheckTokenAutoRefresh(payload['exp'])
|
||||
if autorefresh:
|
||||
token = self._OnRefreshToken(account_id, token, payload)
|
||||
|
||||
return token
|
||||
|
||||
def Logout(self, token: str):
|
||||
exists = self.repo.SelectByToken(token)
|
||||
if exists is None:
|
||||
return
|
||||
|
||||
disabled = self.repo.DisableToken(token)
|
||||
if disabled is None:
|
||||
return
|
||||
|
||||
return disabled == 1
|
||||
|
||||
def RecoverAccount(self, username: str, domain: str = None):
|
||||
account = self.repo.SelectByUsername(username)
|
||||
if account is None:
|
||||
return
|
||||
|
||||
# Array to properties
|
||||
AccountId = account[0]
|
||||
Fullname = f'{account[7]} {account[8]}'
|
||||
Email = account[9]
|
||||
now = datetime.now()
|
||||
|
||||
# Generate token
|
||||
payload = {'Account': AccountId}
|
||||
token = self.token_service.GenerateToken(payload)
|
||||
url = f'{os.getenv('DOMAIN_URL')}/restore/{token}'
|
||||
|
||||
registered = self.repo.InsertRecoverRequest(AccountId, token)
|
||||
if registered is None:
|
||||
return
|
||||
|
||||
# Prepare notification
|
||||
template = '''
|
||||
Hola {},
|
||||
|
||||
Has pedido restaurar tu contraseña de {}. Accede al siguiente enlace para establecer una nueva contraseña:
|
||||
{}
|
||||
|
||||
Si no has realizado esta petición de restauración de la cuenta, envia un correo a: {}
|
||||
'''
|
||||
message = template.format(Fullname, '[APP_NAME]', url, os.getenv('SUPPORT_EMAIL'))
|
||||
|
||||
# Send notification
|
||||
self.mailing_service.SendMessage(
|
||||
Email,
|
||||
'Petición restaurar cuenta [APP_NAME]',
|
||||
message
|
||||
)
|
||||
|
||||
def ValidateTokenRecoverAccount(self, token: str):
|
||||
validated = self.token_service.ValidateToken(token)
|
||||
if validated is False:
|
||||
return
|
||||
|
||||
exists = self.repo.SelectRecoverRequest(validated['Account'], token)
|
||||
if exists is None:
|
||||
return
|
||||
|
||||
disabled = self.repo.DisableRecoverRequest(token)
|
||||
if disabled == 0:
|
||||
return
|
||||
|
||||
return True
|
||||
|
||||
# region Utilities
|
||||
def _OnUpdateAttempts(self, account_id: int, attempts: int):
|
||||
locked = False
|
||||
limit = int(os.getenv('USER_LIMIT_ATTEMPTS'))
|
||||
|
||||
if attempts > limit:
|
||||
attempts = limit
|
||||
locked = True
|
||||
|
||||
return self.repo.RefreshAttempts(account_id, attempts, locked)
|
||||
|
||||
def _OnCheckTokenAutoRefresh(self, expiration: int):
|
||||
limit_minutes_left = 10
|
||||
now = datetime.now()
|
||||
expires_at = datetime.fromtimestamp(expiration)
|
||||
difference = ((expires_at - now).total_seconds()) / 60 # seconds to minutes
|
||||
return difference <= limit_minutes_left
|
||||
|
||||
def _OnRefreshToken(self, account_id: int, token: str, payload: dict):
|
||||
old_disabled = self.repo.DisableToken(token)
|
||||
if old_disabled is None:
|
||||
return
|
||||
|
||||
new_token = self.token_service.GenerateToken(payload)
|
||||
|
||||
new_registered = self.repo.RegisterSession(account_id, new_token)
|
||||
if new_registered is None:
|
||||
return
|
||||
|
||||
return new_token
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,89 @@
|
||||
from backend.code.modules.database.database import DatabaseModule
|
||||
|
||||
class SessionRepository():
|
||||
def __init__(self, repo: DatabaseModule):
|
||||
self.conn = repo.conn
|
||||
self.cursor = repo.cursor
|
||||
|
||||
def SelectByUsername(self, username: str):
|
||||
sql = '''
|
||||
SELECT a.*, u.name, u.surnames, u.email
|
||||
FROM user_accounts a
|
||||
JOIN users u ON u.id = a.user_id
|
||||
WHERE a.username = ?
|
||||
AND a.active
|
||||
'''
|
||||
self.cursor.execute(sql, [username])
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def SelectByToken(self, token: str):
|
||||
sql = '''
|
||||
SELECT uas.account_id, u.id
|
||||
FROM user_account_sessions uas
|
||||
JOIN user_accounts ua ON uas.account_id = ua.id
|
||||
JOIN users u ON ua.user_id = u.id
|
||||
WHERE token = ?
|
||||
AND uas.active
|
||||
'''
|
||||
self.cursor.execute(sql, [token])
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def RefreshAttempts(self, account_id: int, attempts: int, locked: bool):
|
||||
sql = '''
|
||||
UPDATE user_accounts
|
||||
SET attempts = ?, locked = ?
|
||||
WHERE id = ?
|
||||
'''
|
||||
self.cursor.execute(sql, [attempts, locked, account_id])
|
||||
self.conn.commit()
|
||||
return self.cursor.rowcount
|
||||
|
||||
def RegisterSession(self, accountId: int, token: str):
|
||||
sql = '''
|
||||
INSERT INTO user_account_sessions(account_id, token)
|
||||
VALUES (?, ?)
|
||||
'''
|
||||
self.cursor.execute(sql, [accountId, token])
|
||||
self.conn.commit()
|
||||
return self.cursor.rowcount
|
||||
|
||||
def DisableToken(self, token: str):
|
||||
sql = '''
|
||||
UPDATE user_account_sessions
|
||||
SET active = false
|
||||
WHERE token = ?
|
||||
'''
|
||||
self.cursor.execute(sql, [token])
|
||||
self.conn.commit()
|
||||
return self.cursor.rowcount
|
||||
|
||||
def InsertRecoverRequest(self, accountId: int, token: str):
|
||||
sql = '''
|
||||
INSERT INTO user_account_recover_requests(account_id, token)
|
||||
VALUES(?, ?)
|
||||
'''
|
||||
self.cursor.execute(sql, [accountId, token])
|
||||
self.conn.commit()
|
||||
return self.cursor.rowcount
|
||||
|
||||
def SelectRecoverRequest(self, accountId: int, token: str):
|
||||
sql = '''
|
||||
SELECT *
|
||||
FROM user_account_recover_requests
|
||||
WHERE account_id = ?
|
||||
AND token = ?
|
||||
AND active
|
||||
'''
|
||||
self.cursor.execute(sql, [accountId, token])
|
||||
return self.cursor.fetchone()
|
||||
|
||||
def DisableRecoverRequest(self, token: str):
|
||||
sql = '''
|
||||
UPDATE user_account_recover_requests
|
||||
SET active = false
|
||||
WHERE token = ?
|
||||
AND active
|
||||
'''
|
||||
self.cursor.execute(sql, [token])
|
||||
self.conn.commit()
|
||||
self.cursor.rowcount
|
||||
@@ -0,0 +1,45 @@
|
||||
from fastapi import APIRouter, status, Depends, HTTPException, Header
|
||||
from backend.code.middlewares.private_middleware import PrivateMiddleware
|
||||
from backend.code.models.requests.account_login import AccountLoginRequest
|
||||
from backend.code.models.requests.account_recover import AccountRecoverRequest
|
||||
from backend.code.models.responses.session_token import SessionTokenResponse
|
||||
from backend.code.modules.sessions.controller import SessionsController
|
||||
|
||||
routes = APIRouter(prefix="/account", tags=['Session'])
|
||||
middleware = PrivateMiddleware()
|
||||
controller = SessionsController()
|
||||
|
||||
@routes.post('/login', status_code = status.HTTP_200_OK, response_model = SessionTokenResponse)
|
||||
def Login(data: AccountLoginRequest):
|
||||
session = controller.Login(data.Username, data.Password, data.Domain)
|
||||
if type(session) is not SessionTokenResponse:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
return session
|
||||
|
||||
@routes.post('/token/validate', status_code = status.HTTP_200_OK, response_model = SessionTokenResponse)
|
||||
def ValidateToken(token: str = Depends(middleware.HasToken)):
|
||||
validation = controller.ValidateToken(token)
|
||||
if type(validation) is not SessionTokenResponse:
|
||||
raise HTTPException(status_code = status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
return validation
|
||||
|
||||
@routes.post('/logout', status_code = status.HTTP_204_NO_CONTENT)
|
||||
def Logout(token: str = Depends(middleware.HasToken)):
|
||||
closed = controller.Logout(token)
|
||||
if closed is None:
|
||||
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@routes.post('/recover', status_code = status.HTTP_200_OK)
|
||||
def RecoverAccount(data: AccountRecoverRequest):
|
||||
recovered = controller.RecoverAccount(data.Username, data.Domain)
|
||||
if recovered is None:
|
||||
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
@routes.post("/recover/validate/{token}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def ValidateTokenRecoverAccount(token: str):
|
||||
account = controller.ValidateTokenRecoverAccount(token)
|
||||
if account is None:
|
||||
raise HTTPException(status_code = status.HTTP_404_NOT_FOUND)
|
||||
Reference in New Issue
Block a user