Spaces:
Configuration error
Configuration error
| from datetime import datetime, timezone | |
| from fastapi import APIRouter, Depends, HTTPException, Request, status | |
| from prisma.models import User | |
| from app.api.dependencies import get_current_user, reusable_oauth2 | |
| from app.core.config import settings | |
| from app.core.logging_config import get_logger | |
| from app.core.security import decode_access_token | |
| from app.db.client import db | |
| from app.db.redis import get_redis | |
| from app.repositories.student_repo import StudentRepository | |
| from app.schemas.auth import Token, UserLogin, UserProfileResponse, DeviceChangeRequestCreate | |
| from app.schemas.student import StudentCreate, StudentResponse | |
| from app.schemas.teacher import TeacherCreate, TeacherResponse | |
| from app.services.auth_service import AuthService | |
| from app.services.device_change_service import DeviceChangeService | |
| logger = get_logger("app.api.auth") | |
| router = APIRouter(prefix="/auth", tags=["Authentication"]) | |
| _RATE_LIMIT_WINDOW = 60 | |
| _RATE_LIMIT_MAX = 10 | |
| async def _rate_limit(request: Request) -> None: | |
| if settings.ENVIRONMENT == "development": | |
| return | |
| forwarded = request.headers.get("X-Forwarded-For") | |
| ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else "unknown") | |
| key = f"ratelimit:auth:{ip}" | |
| r = await get_redis() | |
| count = await r.incr(key) | |
| if count == 1: | |
| await r.expire(key, _RATE_LIMIT_WINDOW) | |
| if count > _RATE_LIMIT_MAX: | |
| raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many requests. Please try again later.") | |
| async def login(login_data: UserLogin, request: Request, auth_service: AuthService = Depends()) -> Token: | |
| await _rate_limit(request) | |
| token = await auth_service.authenticate(login_data) | |
| if not token: | |
| logger.warning("Failed login: %s", login_data.email) | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password") | |
| return token | |
| async def register_student(data: StudentCreate, request: Request, auth_service: AuthService = Depends()) -> StudentResponse: | |
| await _rate_limit(request) | |
| student = await auth_service.register_student(data) | |
| if not student: | |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User with this email is already registered") | |
| return student | |
| async def register_teacher(data: TeacherCreate, request: Request, auth_service: AuthService = Depends()) -> TeacherResponse: | |
| await _rate_limit(request) | |
| teacher = await auth_service.register_teacher(data) | |
| if not teacher: | |
| raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User with this email is already registered") | |
| return teacher | |
| async def get_me(current_user: User = Depends(get_current_user)) -> UserProfileResponse: | |
| student_profile = None | |
| teacher_profile = None | |
| if current_user.role == "STUDENT": | |
| student = await db.student.find_unique(where={"userId": current_user.id}) | |
| if student: | |
| embedding = await StudentRepository().get_face_embedding(student.id) | |
| student_profile = { | |
| "id": student.id, | |
| "enrollment_number": student.enrollmentNumber, | |
| "first_name": student.firstName, | |
| "last_name": student.lastName, | |
| "face_registered": embedding is not None and len(embedding) > 0, | |
| } | |
| elif current_user.role == "TEACHER": | |
| teacher = await db.teacher.find_unique(where={"userId": current_user.id}) | |
| if teacher: | |
| teacher_profile = { | |
| "id": teacher.id, | |
| "department": teacher.department.name if teacher.department else "", | |
| "designation": teacher.designation.name if teacher.designation else "", | |
| "employee_id": teacher.employeeId, | |
| "first_name": teacher.firstName, | |
| "last_name": teacher.lastName, | |
| } | |
| return UserProfileResponse( | |
| id=current_user.id, | |
| email=current_user.email, | |
| role=current_user.role, | |
| is_active=current_user.isActive, | |
| student_profile=student_profile, | |
| teacher_profile=teacher_profile, | |
| ) | |
| async def logout(token: str = Depends(reusable_oauth2)) -> dict: | |
| payload = decode_access_token(token) | |
| if payload: | |
| exp = payload.get("exp") | |
| if exp: | |
| ttl = exp - int(datetime.now(timezone.utc).timestamp()) | |
| if ttl > 0: | |
| try: | |
| await get_redis().setex(f"denylist:{token}", ttl, "revoked") | |
| logger.info("Token revoked: user=%s", payload.get("sub")) | |
| except Exception as cache_err: | |
| logger.warning("Failed to add token to Redis denylist: %s", cache_err) | |
| return {"status": "success", "message": "Successfully logged out."} | |
| async def request_device_change( | |
| data: DeviceChangeRequestCreate, | |
| request: Request, | |
| device_change_service: DeviceChangeService = Depends(), | |
| ) -> dict: | |
| await _rate_limit(request) | |
| await device_change_service.request_device_change(data) | |
| return {"status": "success", "message": "Device change request submitted successfully."} | |
| # --- System Config (Public) --- | |
| from app.schemas.system_config import SystemConfigResponse | |
| from app.services.system_config_service import SystemConfigService | |
| async def get_public_system_config(config_service: SystemConfigService = Depends()): | |
| return await config_service.get_config() | |