"""Authentication service: challenge generation, token management, current user resolution.""" from __future__ import annotations import hashlib import secrets import uuid from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database import get_db from app.models.user import DuniterIdentity, Session security = HTTPBearer(auto_error=False) def _hash_token(token: str) -> str: """SHA-256 hash of a bearer token for storage.""" return hashlib.sha256(token.encode()).hexdigest() async def create_session(db: AsyncSession, identity: DuniterIdentity) -> str: """Create a new session for the given identity, return the raw bearer token.""" raw_token = secrets.token_urlsafe(48) token_hash = _hash_token(raw_token) session = Session( token_hash=token_hash, identity_id=identity.id, expires_at=datetime.now(timezone.utc) + timedelta(hours=settings.TOKEN_EXPIRE_HOURS), ) db.add(session) await db.commit() return raw_token async def invalidate_session(db: AsyncSession, token: str) -> None: """Delete the session matching the given raw token.""" token_hash = _hash_token(token) result = await db.execute(select(Session).where(Session.token_hash == token_hash)) session = result.scalar_one_or_none() if session: await db.delete(session) await db.commit() async def get_current_identity( db: AsyncSession = Depends(get_db), credentials: HTTPAuthorizationCredentials | None = Depends(security), ) -> DuniterIdentity: """Dependency: resolve the current authenticated identity from the bearer token. Raises 401 if the token is missing, invalid, or expired. """ if credentials is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentification requise") token_hash = _hash_token(credentials.credentials) result = await db.execute( select(Session).where( Session.token_hash == token_hash, Session.expires_at > datetime.now(timezone.utc), ) ) session = result.scalar_one_or_none() if session is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token invalide ou expire") result = await db.execute(select(DuniterIdentity).where(DuniterIdentity.id == session.identity_id)) identity = result.scalar_one_or_none() if identity is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Identite introuvable") return identity async def get_or_create_identity(db: AsyncSession, address: str) -> DuniterIdentity: """Get an existing identity by address or create a new one.""" result = await db.execute(select(DuniterIdentity).where(DuniterIdentity.address == address)) identity = result.scalar_one_or_none() if identity is None: identity = DuniterIdentity(address=address) db.add(identity) await db.commit() await db.refresh(identity) return identity