| 1 | from __future__ import annotations |
| 2 | |
| 3 | from collections.abc import Generator |
| 4 | |
| 5 | from fastapi import Depends, Header, HTTPException, status |
| 6 | from fastapi.security import OAuth2PasswordBearer |
| 7 | from jose import JWTError |
| 8 | from sqlalchemy.orm import Session |
| 9 | |
| 10 | from datetime import datetime, timezone |
| 11 | |
| 12 | from sqlalchemy import select |
| 13 | |
| 14 | from app.core.permissions import Perm, role_has |
| 15 | from app.core.security import decode_token, hash_api_key |
| 16 | from app.db.session import get_db |
| 17 | from app.models import ApiKey, User |
| 18 | |
| 19 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False) |
| 20 | |
| 21 | |
| 22 | def get_current_user( |
| 23 | token: str | None = Depends(oauth2_scheme), |
| 24 | x_api_key: str | None = Header(default=None), |
| 25 | db: Session = Depends(get_db), |
| 26 | ) -> User: |
| 27 | if x_api_key: |
| 28 | key = db.scalar(select(ApiKey).where(ApiKey.key_hash == hash_api_key(x_api_key))) |
| 29 | if key is None or key.revoked_at is not None: |
| 30 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid or revoked api key") |
| 31 | key.last_used_at = datetime.now(timezone.utc) |
| 32 | db.commit() |
| 33 | user = db.get(User, key.user_id) |
| 34 | if user is None or not user.is_active: |
| 35 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, "api key owner inactive") |
| 36 | return user |
| 37 | |
| 38 | if not token: |
| 39 | from app.core.config import settings |
| 40 | if settings.open_mode: |
| 41 | user = db.scalar(select(User).where(User.role == "admin").order_by(User.created_at)) \ |
| 42 | or db.scalar(select(User).order_by(User.created_at)) |
| 43 | if user: |
| 44 | return user |
| 45 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, "missing credentials") |
| 46 | try: |
| 47 | payload = decode_token(token) |
| 48 | except JWTError: |
| 49 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token") |
| 50 | user = db.get(User, payload.get("sub")) |
| 51 | if user is None or not user.is_active: |
| 52 | raise HTTPException(status.HTTP_401_UNAUTHORIZED, "user not found or inactive") |
| 53 | return user |
| 54 | |
| 55 | |
| 56 | def require(perm: Perm): |
| 57 | def _dep(user: User = Depends(get_current_user)) -> User: |
| 58 | if not role_has(user.role, perm): |
| 59 | raise HTTPException(status.HTTP_403_FORBIDDEN, f"requires permission: {perm.value}") |
| 60 | return user |
| 61 | return _dep |
| 62 | |
| 63 | |
| 64 | __all__ = ["get_db", "get_current_user", "require", "Perm", "Session", "Generator"] |