Initial commit

This commit is contained in:
2026-11-05 08:28:15 +00:00
commit 08582a4f70
65 changed files with 1886 additions and 0 deletions
+183
View File
@@ -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
+45
View File
@@ -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)