fastcheck/fastcheck_api/app/api/dependencies.py
2026-04-29 12:03:48 -04:00

196 lines
5.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import Annotated, Any
from bson import ObjectId
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from motor.motor_asyncio import AsyncIOMotorDatabase
from fastcheck_api.app.core.mongodb import get_db
from fastcheck_api.app.core.security import AuthError, decode_token
_bearer_scheme = HTTPBearer(auto_error=False)
@dataclass(frozen=True)
class CurrentUser:
id: str
email: str
role: str
tenant: str
async def get_current_user(
request: Request,
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(_bearer_scheme)] = None,
db: AsyncIOMotorDatabase = Depends(get_db),
) -> CurrentUser:
if credentials is None or not credentials.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="No token provided")
token = credentials.credentials
try:
payload = decode_token(token)
except AuthError as e:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e)) from e
user_id = str(payload.get("id") or "")
email = str(payload.get("email") or "")
role = str(payload.get("role") or "")
tenant = str(payload.get("tenant") or "")
if not user_id or not email or not role or not tenant:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
try:
user_oid = ObjectId(user_id)
except Exception:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid user id")
user_doc = await db["users"].find_one({"_id": user_oid}, projection={"isActive": 1, "tenant": 1})
if not user_doc or not user_doc.get("isActive", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"message": "Account not activated. Please wait for superadmin approval.", "code": "ACCOUNT_NOT_ACTIVATED"},
)
stored_tenant = user_doc.get("tenant")
stored_tenant_id = str(stored_tenant) if stored_tenant is not None else ""
if stored_tenant_id and stored_tenant_id != tenant:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
if role != "superuser":
try:
tenant_oid = ObjectId(tenant)
except Exception:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid tenant id")
tenant_doc = await db["tenants"].find_one({"_id": tenant_oid}, projection={"isActive": 1})
if not tenant_doc or not tenant_doc.get("isActive", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"message": "Tenant not activated. Please wait for superadmin approval.", "code": "TENANT_NOT_ACTIVATED"},
)
current = CurrentUser(id=user_id, email=email, role=role, tenant=tenant)
request.state.current_user = current
request.state.tenant_id = tenant
return current
Permission = str
ROLE_PERMISSIONS: dict[str, set[Permission]] = {
"superuser": {
"user:create",
"user:read",
"user:update",
"user:delete",
"evaluation:create",
"evaluation:read",
"evaluation:update",
"evaluation:delete",
"evaluation:bulk",
"company:read",
"company:update",
"rut:lookup",
"rut:read",
"sheriff:logs:read",
"monitoring:create",
"monitoring:read",
"monitoring:update",
"monitoring:delete",
"monitoring:execute",
"tenant:read",
"tenant:update",
"tenant:users:manage",
"admin:dashboard",
"admin:settings",
"admin:logs",
"notification:read",
"notification:create",
},
"tenant_admin": {
"user:create",
"user:read",
"user:update",
"user:delete",
"evaluation:create",
"evaluation:read",
"evaluation:update",
"evaluation:delete",
"evaluation:bulk",
"company:read",
"company:update",
"rut:lookup",
"rut:read",
"sheriff:logs:read",
"monitoring:create",
"monitoring:read",
"monitoring:update",
"monitoring:delete",
"monitoring:execute",
"tenant:read",
"tenant:update",
"tenant:users:manage",
"admin:dashboard",
"notification:read",
"notification:create",
},
"evaluator": {
"user:read",
"evaluation:create",
"evaluation:read",
"evaluation:update",
"evaluation:delete",
"evaluation:bulk",
"company:read",
"rut:lookup",
"rut:read",
"sheriff:logs:read",
"monitoring:create",
"monitoring:read",
"monitoring:update",
"monitoring:execute",
"tenant:read",
"notification:read",
},
"read_only": {
"user:read",
"evaluation:read",
"company:read",
"rut:read",
"sheriff:logs:read",
"monitoring:read",
"tenant:read",
"notification:read",
},
"write_only": {
"evaluation:create",
"evaluation:read",
"company:read",
"rut:lookup",
"monitoring:create",
"monitoring:execute",
"notification:create",
"tenant:read",
"tenant:update",
},
}
def require_permission(permission: Permission):
async def _dep(user: Annotated[CurrentUser, Depends(get_current_user)]) -> CurrentUser:
perms = ROLE_PERMISSIONS.get(user.role, set())
if permission not in perms:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"message": "Forbidden: Insufficient permissions", "required": permission, "userRole": user.role},
)
return user
return _dep