TorbatYar/backend/services/identity-access/app/api/v1/auth.py
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

195 lines
7.3 KiB
Python

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,
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.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)