from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import require_authenticated, require_platform_admin from app.schemas.auth import ( AuthConfigResponse, AuthSessionResponse, HandoffRedeemRequest, LoginUrlResponse, MeResponse, MobileAuthStartResponse, MobileOtpRequest, MobileOtpVerify, MobileRegisterVerify, OtpRequestResponse, PasswordChangeRequest, PasswordChangeResponse, ProfileUpdateRequest, RefreshTokenRequest, RegisterCompleteResponse, RegisterRequest, TokenExchangeRequest, TokenResponse, VerifyMobileResponse, ) from app.services.auth_service import AuthService from app.services.handoff_store import handoff_store from app.services.mobile_auth_service import MobileAuthService from app.services.registration_service import RegistrationService from shared.exceptions import NotFoundError, UnauthorizedError from shared.security import CurrentUser router = APIRouter(prefix="/auth", tags=["auth"]) @router.get("/config", response_model=AuthConfigResponse) async def auth_config(db: AsyncSession = Depends(get_db)) -> AuthConfigResponse: """پیکربندی OIDC برای frontend — endpoint عمومی.""" return AuthService(db).get_oidc_config() @router.get("/login-url", response_model=LoginUrlResponse) async def login_url(db: AsyncSession = Depends(get_db)) -> LoginUrlResponse: """آدرس ورود Keycloak با state و PKCE سمت سرور — endpoint عمومی.""" return AuthService(db).build_login_url() @router.post("/token", response_model=TokenResponse) async def exchange_token( payload: TokenExchangeRequest, db: AsyncSession = Depends(get_db) ) -> TokenResponse: """تبدیل authorization code به access token (BFF).""" return await AuthService(db).exchange_token(payload) @router.post("/refresh", response_model=TokenResponse) async def refresh_token( payload: RefreshTokenRequest, db: AsyncSession = Depends(get_db) ) -> TokenResponse: """تمدید access token با refresh_token تا نشست کاربر قطع نشود.""" return await AuthService(db).refresh_token(payload.refresh_token) @router.get("/me", response_model=MeResponse) async def get_me( db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_authenticated), ) -> MeResponse: return await AuthService(db).get_me(user) @router.patch("/me", response_model=MeResponse) async def update_me( payload: ProfileUpdateRequest, db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_authenticated), ) -> MeResponse: """به‌روزرسانی پروفایل کاربر جاری (نام نمایشی و ایمیل).""" return await AuthService(db).update_profile(user, payload) @router.post("/password", response_model=PasswordChangeResponse) async def change_password( payload: PasswordChangeRequest, db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_authenticated), ) -> PasswordChangeResponse: """تغییر یا تنظیم رمز عبور کاربر جاری.""" return await AuthService(db).change_password(user, payload) @router.post("/register", response_model=OtpRequestResponse, status_code=201) async def register_user( payload: RegisterRequest, db: AsyncSession = Depends(get_db), ) -> OtpRequestResponse: """ثبت‌نام با ایمیل/نام کاربری/رمز + ارسال OTP موبایل.""" return await RegistrationService(db).register_with_credentials(payload) @router.post("/register/verify-mobile", response_model=AuthSessionResponse) async def verify_registration_mobile( payload: MobileOtpVerify, db: AsyncSession = Depends(get_db), ) -> AuthSessionResponse: """تکمیل ثبت‌نام پس از تأیید OTP (پس از register).""" return await RegistrationService(db).complete_registration_verify(payload) @router.post("/session/redeem", response_model=TokenResponse) async def redeem_session_handoff(payload: HandoffRedeemRequest) -> TokenResponse: """تبدیل کد handoff (از Keycloak theme) به توکن SSO — یکبارمصرف.""" raw = handoff_store.pop(payload.handoff_code) if raw is None: raise UnauthorizedError("کد ورود منقضی یا نامعتبر است") return TokenResponse( access_token=raw["access_token"], refresh_token=raw.get("refresh_token"), expires_in=raw.get("expires_in", 3600), token_type=raw.get("token_type", "Bearer"), ) @router.post("/mobile/start", response_model=MobileAuthStartResponse) async def mobile_auth_start( payload: MobileOtpRequest, db: AsyncSession = Depends(get_db), ) -> MobileAuthStartResponse: """ورود/ثبت‌نام یکپارچه — بک‌اند تشخیص می‌دهد کاربر عضو است یا نه.""" return await MobileAuthService(db).start(payload) @router.post("/mobile/complete", response_model=AuthSessionResponse) async def mobile_auth_complete( payload: MobileRegisterVerify, db: AsyncSession = Depends(get_db), ) -> AuthSessionResponse: """تأیید OTP و ورود یا ثبت‌نام + صدور توکن SSO.""" return await MobileAuthService(db).complete(payload) @router.post("/login/mobile", response_model=OtpRequestResponse) async def login_mobile_request_otp( payload: MobileOtpRequest, db: AsyncSession = Depends(get_db), ) -> OtpRequestResponse: """درخواست OTP برای ورود کاربر موجود.""" start = await MobileAuthService(db).start(payload) if start.intent != "login": raise NotFoundError("حسابی با این شماره یافت نشد. ابتدا ثبت‌نام کنید") return OtpRequestResponse( message=start.message, expires_in=start.expires_in, skipped=start.skipped, ) @router.post("/login/mobile/verify", response_model=AuthSessionResponse) async def login_mobile_verify( payload: MobileOtpVerify, db: AsyncSession = Depends(get_db), ) -> AuthSessionResponse: """ورود با OTP برای کاربر موجود.""" return await MobileAuthService(db).complete_login(payload) @router.post("/register/mobile", response_model=OtpRequestResponse) async def register_mobile_request_otp( payload: MobileOtpRequest, db: AsyncSession = Depends(get_db), ) -> OtpRequestResponse: """ثبت‌نام فقط با موبایل — مرحله ارسال OTP.""" return await RegistrationService(db).request_mobile_register_otp(payload) @router.post("/register/mobile/verify", response_model=AuthSessionResponse, status_code=201) async def register_mobile_verify( payload: MobileRegisterVerify, db: AsyncSession = Depends(get_db), ) -> AuthSessionResponse: """ثبت‌نام فقط با موبایل — تأیید OTP و ساخت حساب.""" return await RegistrationService(db).complete_mobile_register(payload) @router.post("/mobile/request", response_model=OtpRequestResponse) async def request_mobile_verification( payload: MobileOtpRequest, db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_authenticated), ) -> OtpRequestResponse: """ارسال OTP برای تأیید موبایل کاربر SSO (پس از لاگین).""" return await RegistrationService(db).request_verify_mobile(user, payload.mobile) @router.post("/mobile/verify", response_model=VerifyMobileResponse) async def verify_mobile( payload: MobileOtpVerify, db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(require_authenticated), ) -> VerifyMobileResponse: """تأیید موبایل کاربر SSO.""" return await RegistrationService(db).verify_mobile_for_user(user, payload)