Initial commit: Rentman Clone - Phase 0-6 (T001-T023)
Completed: - Phase 0: Project Setup (T001-T003) - Docker Compose, FastAPI skeleton, React SPA - Phase 1: Auth System (T004-T008) - DB models, JWT auth, RBAC middleware, user management - Phase 2: Contacts & Tags (T009-T011) - CRUD API + UI - Phase 3: Equipment Catalog (T012-T014) - Models, API, UI with barcode/QR - Phase 4: Crew Management (T015-T017) - Models, availability, UI - Phase 5: Vehicle Fleet (T018-T020) - Models, assignments, UI - Phase 6: Projects (T021-T023) - Project hierarchy models, CRUD API, list/detail UI
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""FastAPI dependencies for authentication, authorization, and tenant filtering."""
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from jose import JWTError, ExpiredSignatureError
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.core.security import verify_token
|
||||
from app.db.session import get_async_session
|
||||
from app.models import User
|
||||
|
||||
security_scheme = HTTPBearer()
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
) -> User:
|
||||
"""Extract and validate JWT from Authorization header, return the current user."""
|
||||
token = credentials.credentials
|
||||
try:
|
||||
payload = verify_token(token)
|
||||
except ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has expired",
|
||||
)
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token payload",
|
||||
)
|
||||
|
||||
# Load user with role (and its permissions) eagerly
|
||||
result = await session.execute(
|
||||
select(User)
|
||||
.options(joinedload(User.role))
|
||||
.where(User.id == user_id)
|
||||
)
|
||||
user = result.unique().scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is deactivated",
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
"""Return the current user if they are active (redundant check, kept for clarity)."""
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is deactivated",
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
def require_permission(permission: str):
|
||||
"""Factory that returns a dependency checking for a specific permission."""
|
||||
|
||||
async def permission_checker(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not current_user.role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No role assigned",
|
||||
)
|
||||
|
||||
user_permissions: list[str] = current_user.role.permissions or []
|
||||
if permission not in user_permissions:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Missing required permission: {permission}",
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
return permission_checker
|
||||
Reference in New Issue
Block a user