Spaces:
Running
Running
| from __future__ import annotations | |
| import logging | |
| from datetime import datetime, timedelta, timezone | |
| from typing import Annotated, Any, AsyncGenerator, Optional | |
| import jwt | |
| from argon2 import PasswordHasher | |
| from argon2.exceptions import VerifyMismatchError | |
| from fastapi import Depends, HTTPException, Request, status | |
| from sqlalchemy import select | |
| from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine | |
| from app.config import get_settings | |
| from app.core.auth.models import Base, Permission, RefreshSession, Role, User | |
| logger = logging.getLogger("auth") | |
| _settings = get_settings() | |
| _is_temp_db = ( | |
| "sqlite" in _settings.database_url | |
| and "localhost" not in _settings.database_url | |
| and "postgres" not in _settings.database_url.lower() | |
| and "mysql" not in _settings.database_url.lower() | |
| ) | |
| _raw_url: str = getattr(_settings, "database_url", None) or "sqlite+aiosqlite:///data/auth.db" | |
| if "sqlite" in _raw_url and "sqlite+aiosqlite" not in _raw_url: | |
| _raw_url = _raw_url.replace("sqlite:///", "sqlite+aiosqlite:///") | |
| DATABASE_URL: str = _raw_url | |
| engine = create_async_engine(DATABASE_URL, echo=False) | |
| AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) | |
| ph = PasswordHasher() | |
| def _now() -> datetime: | |
| return datetime.now(timezone.utc) | |
| class TempDatabaseWarning: | |
| code: str = "TEMP_DATABASE" | |
| message: str = ( | |
| "No external database configuration was provided. " | |
| "The service is currently using its built-in SQLite database intended for development and temporary use. " | |
| "Data stored in this database should not be considered permanent." | |
| ) | |
| def get_temp_db_warning() -> Optional[dict]: | |
| if _is_temp_db: | |
| return {"code": TempDatabaseWarning.code, "message": TempDatabaseWarning.message} | |
| return None | |
| async def init_auth_db(): | |
| async with engine.begin() as conn: | |
| await conn.run_sync(Base.metadata.create_all) | |
| async with AsyncSessionLocal() as session: | |
| result = await session.execute(select(Role).where(Role.name == "SuperAdmin")) | |
| if not result.scalars().first(): | |
| sa_role = Role(name="SuperAdmin", description="Full system access") | |
| admin_role = Role(name="Admin", description="Administrative access") | |
| user_role = Role(name="User", description="Standard user access") | |
| session.add_all([sa_role, admin_role, user_role]) | |
| perms = [] | |
| for code, desc in [ | |
| ("users:read", "Read users"), | |
| ("users:write", "Modify users"), | |
| ("users:delete", "Delete users"), | |
| ("admin:access", "Access admin panel"), | |
| ]: | |
| p = Permission(code=code, description=desc) | |
| perms.append(p) | |
| session.add(p) | |
| sa_role.permissions.extend(perms) | |
| admin_role.permissions.extend(perms[:-1]) | |
| user_role.permissions.append(perms[0]) | |
| await session.commit() | |
| admin_email = "admin@example.com" | |
| result = await session.execute(select(User).where(User.email == admin_email)) | |
| if not result.scalars().first(): | |
| admin_password = _settings.admin_password or "Admin123!" | |
| admin_user = User( | |
| email=admin_email, | |
| full_name="System Administrator", | |
| password_hash=ph.hash(admin_password), | |
| is_verified=True, | |
| ) | |
| role_result = await session.execute(select(Role).where(Role.name == "SuperAdmin")) | |
| admin_role = role_result.scalars().first() | |
| if admin_role: | |
| admin_user.roles.append(admin_role) | |
| session.add(admin_user) | |
| await session.commit() | |
| from app.services.auth_service import AuthService | |
| await AuthService.cleanup_expired_sessions(session) | |
| async def get_db() -> AsyncGenerator[AsyncSession, None]: | |
| async with AsyncSessionLocal() as session: | |
| try: | |
| yield session | |
| finally: | |
| await session.close() | |
| async def get_current_user( | |
| request: Request, | |
| db: Annotated[AsyncSession, Depends(get_db)], | |
| ) -> User: | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Could not validate credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| auth_header = request.headers.get("Authorization") | |
| if not auth_header or not auth_header.startswith("Bearer "): | |
| raise credentials_exception | |
| token = auth_header.split(" ", 1)[1] | |
| try: | |
| payload = jwt.decode(token, _settings.jwt_secret_key, algorithms=[_settings.jwt_algorithm]) | |
| user_id: str = payload.get("sub") | |
| token_type: str = payload.get("type") | |
| if user_id is None or token_type != "access": | |
| raise credentials_exception | |
| except jwt.PyJWTError: | |
| raise credentials_exception | |
| result = await db.execute(select(User).where(User.id == user_id, User.deleted_at.is_(None))) | |
| user = result.scalars().first() | |
| if user is None or not user.is_active: | |
| raise credentials_exception | |
| return user | |
| def require_application_id(request: Request): | |
| app_id = request.headers.get("X-Application-Id") | |
| expected = _settings.application_id | |
| if not expected: | |
| return True | |
| if not app_id: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Missing X-Application-Id header", | |
| ) | |
| if app_id != expected: | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Invalid application_id", | |
| ) | |
| return True | |
| def require_permissions(*required_perms: str): | |
| async def permission_checker( | |
| user: Annotated[User, Depends(get_current_user)], | |
| ) -> User: | |
| user_perms = set() | |
| for role in user.roles: | |
| for perm in role.permissions: | |
| user_perms.add(perm.code) | |
| missing = [p for p in required_perms if p not in user_perms] | |
| if missing: | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail=f"Missing required permissions: {', '.join(missing)}", | |
| ) | |
| return user | |
| return permission_checker | |