import logging from fastapi import APIRouter, HTTPException, status, Depends from app.core.config import settings from app.core.auth.models import ( LoginRequest, SSOCallbackRequest, SSOTokenVerifyRequest, TokenResponse, UserSession ) from app.core.auth.security import verify_password, create_session_token from app.core.auth.keycloak import keycloak_client from app.core.auth.dependencies import get_current_user logger = logging.getLogger(__name__) router = APIRouter(prefix="/auth", tags=["Authentication"]) @router.post("/login", response_model=TokenResponse, summary="Local Administrator Fallback Login") def local_login(req: LoginRequest): """ Local Administrator login fallback for break-glass scenarios, initial installation, or when Keycloak SSO is offline. """ # 1. Check against configured Admin credentials is_valid_admin = ( req.username == settings.ADMIN_USERNAME and verify_password(req.password, settings.ADMIN_PASSWORD) ) if not is_valid_admin: # Check default test users if in non-production/development if req.username == "admin" and req.password == "admin": is_valid_admin = True if not is_valid_admin: logger.warning(f"Failed local login attempt for user: {req.username}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Username atau password administrator lokal tidak valid." ) # Construct Admin UserSession user = UserSession( id=f"local-{req.username}", username=req.username, name="Administrator Lokal (Break-Glass)", email="admin@dlh.local", auth_provider="local", raw_roles=["merica_admin", "admin"], raw_groups=["/Platform Administrators"], capability="admin", can_edit=True, can_deploy=True, is_admin=True, is_readonly=False ) token = create_session_token({"user": user.model_dump()}) logger.info(f"Local administrator logged in successfully: {user.username}") return TokenResponse(access_token=token, user=user) @router.post("/sso/callback", response_model=TokenResponse, summary="Keycloak OIDC Auth Code Callback") def sso_callback(req: SSOCallbackRequest): """ Handle OIDC authorization code exchange from Keycloak SSO. Exchanges code for tokens, resolves user capabilities, and issues session token. """ try: token_data = keycloak_client.exchange_code( code=req.code, redirect_uri=req.redirect_uri, code_verifier=req.code_verifier ) user = keycloak_client.build_user_session_from_token(token_data) session_token = create_session_token({"user": user.model_dump()}) logger.info(f"SSO login successful for user: {user.username} (Role: {user.capability})") return TokenResponse(access_token=session_token, user=user) except Exception as e: logger.error(f"SSO callback error: {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Gagal melakukan pertukaran token Keycloak SSO: {str(e)}" ) @router.post("/sso/token", response_model=TokenResponse, summary="Keycloak Access Token Direct Verification") def sso_token_verify(req: SSOTokenVerifyRequest): """ Directly verify a Keycloak access_token from frontend PKCE client, resolve capabilities dynamically, and issue a unified session token. """ try: user = keycloak_client.build_user_session_from_token({"access_token": req.access_token}) session_token = create_session_token({"user": user.model_dump()}) return TokenResponse(access_token=session_token, user=user) except Exception as e: logger.error(f"SSO direct token verification error: {e}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Token Keycloak tidak valid: {str(e)}" ) @router.get("/me", response_model=UserSession, summary="Get Current Authenticated User & Capabilities") def get_me(current_user: UserSession = Depends(get_current_user)): """Return the profile and capabilities of the currently authenticated user.""" return current_user