from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Request, status from app.config import get_settings from app.core.auth.deps import get_current_user, get_db from app.core.auth.models import User from app.core.auth.schemas import ( ChangePasswordSchema, ForgotPasswordSchema, LoginSchema, MessageDataResponse, ProfileResponse, RegisterSchema, ResetPasswordSchema, SchemaResponse, SessionListResponse, SessionOut, TokenRefreshSchema, TokenResponse, UpdateProfileSchema, UserSchemaField, UserSchemaResponse, ) from app.services.supabase import SupabaseClient from app.services.auth_service import AuthService router = APIRouter(prefix="/auth", tags=["Authentication"]) _settings = get_settings() @router.post("/register", response_model=ProfileResponse, status_code=status.HTTP_201_CREATED) async def register( schema: RegisterSchema, db: Annotated[SupabaseClient, Depends(get_db)], ): user = await AuthService.register(db, schema) profile = await AuthService.user_to_profile(db, user) return {"success": True, "data": profile.model_dump()} @router.post("/login", response_model=TokenResponse) async def login( schema: LoginSchema, request: Request, db: Annotated[SupabaseClient, Depends(get_db)], ): tokens = await AuthService.login(db, request, schema) return {"success": True, "data": tokens.model_dump()} @router.post("/refresh", response_model=TokenResponse) async def refresh_token( schema: TokenRefreshSchema, db: Annotated[SupabaseClient, Depends(get_db)], ): tokens = await AuthService.refresh(db, schema.refresh_token) return {"success": True, "data": tokens.model_dump()} @router.post("/logout", response_model=MessageDataResponse) async def logout( schema: TokenRefreshSchema, current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.logout(db, current_user, schema.refresh_token) return {"success": True, "data": {"message": "Logged out successfully"}} @router.post("/logout-all", response_model=MessageDataResponse) async def logout_all( current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.logout_all(db, current_user) return {"success": True, "data": {"message": "All sessions revoked"}} @router.post("/forgot-password", response_model=MessageDataResponse) async def forgot_password( schema: ForgotPasswordSchema, db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.forgot_password(db, schema) return {"success": True, "data": {"message": "If that email exists, a password reset link has been sent."}} @router.post("/reset-password", response_model=MessageDataResponse) async def reset_password( schema: ResetPasswordSchema, db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.reset_password(db, schema) return {"success": True, "data": {"message": "Password reset successfully"}} @router.post("/change-password", response_model=MessageDataResponse) async def change_password( schema: ChangePasswordSchema, current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.change_password(db, current_user, schema) return {"success": True, "data": {"message": "Password changed successfully"}} @router.get("/me", response_model=ProfileResponse) async def get_me( current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): profile = await AuthService.user_to_profile(db, current_user) return {"success": True, "data": profile.model_dump()} @router.patch("/me", response_model=ProfileResponse) async def update_me( schema: UpdateProfileSchema, current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): user = await AuthService.update_profile(db, current_user, schema) profile = await AuthService.user_to_profile(db, user) return {"success": True, "data": profile.model_dump()} @router.delete("/me", response_model=MessageDataResponse) async def delete_me( current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.soft_delete(db, current_user) return {"success": True, "data": {"message": "Account deleted successfully"}} @router.get("/schema", response_model=SchemaResponse) async def get_user_schema(): columns = [ UserSchemaField(field="id", type="string (UUID)", required=True, description="Unique user identifier", constraints="Auto-generated"), UserSchemaField(field="email", type="string", required=True, description="User email address", constraints="Unique, max 255 chars"), UserSchemaField(field="username", type="string", required=False, description="Unique username", constraints="Unique, 2-50 chars, alphanumeric + underscore"), UserSchemaField(field="full_name", type="string", required=False, description="Display name", constraints="Max 100 chars"), UserSchemaField(field="password", type="string", required=True, description="User password (write-only)", constraints="Min 8 chars, uppercase, lowercase, digit"), UserSchemaField(field="password_hash", type="string", required=True, description="Argon2id password hash (internal)", constraints="Auto-generated"), UserSchemaField(field="is_active", type="boolean", required=False, description="Whether the user account is active", constraints="Default: true"), UserSchemaField(field="is_verified", type="boolean", required=False, description="Whether the email is verified", constraints="Default: false"), UserSchemaField(field="failed_login_attempts", type="integer", required=False, description="Consecutive failed login count", constraints="Default: 0"), UserSchemaField(field="locked_until", type="datetime (ISO 8601)", required=False, description="Account lock expiry", constraints="Nullable"), UserSchemaField(field="last_login", type="datetime (ISO 8601)", required=False, description="Last successful login timestamp", constraints="Nullable"), UserSchemaField(field="password_changed_at", type="datetime (ISO 8601)", required=False, description="Last password change", constraints="Nullable"), UserSchemaField(field="created_at", type="datetime (ISO 8601)", required=False, description="Account creation timestamp", constraints="Auto-set"), UserSchemaField(field="updated_at", type="datetime (ISO 8601)", required=False, description="Last update timestamp", constraints="Auto-updated"), UserSchemaField(field="deleted_at", type="datetime (ISO 8601)", required=False, description="Soft delete timestamp", constraints="Nullable"), UserSchemaField(field="roles", type="array[string]", required=False, description="Assigned role names", constraints="Via user_roles association table"), ] return {"success": True, "data": UserSchemaResponse(table_name="users", columns=columns).model_dump()} @router.get("/sessions", response_model=SessionListResponse) async def list_sessions( current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): sessions = await AuthService.list_sessions(db, current_user) return {"success": True, "data": [SessionOut.model_validate(s).model_dump() for s in sessions]} @router.delete("/sessions/{session_id}", response_model=MessageDataResponse) async def revoke_session( session_id: str, current_user: Annotated[User, Depends(get_current_user)], db: Annotated[SupabaseClient, Depends(get_db)], ): await AuthService.revoke_session(db, current_user, session_id) return {"success": True, "data": {"message": "Session revoked successfully"}}