llm-ready-data / app /core /auth /deps.py
light-infer-chat's picture
ok
08919be
Raw
History Blame Contribute Delete
6.33 kB
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