"""Local auth + role/scope introspection. `POST /auth/login` is the real entry point; `GET /auth/me` re-reads the caller's role/permissions/scope (used after a role or assignment change, and by the frontend to self-heal a session stored before RBAC existed). The `/auth/dev-*` endpoints implement the dashboard role switcher and only exist while `PM_DEV_ROLE_SWITCH` is enabled. """ from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.authz import build_context, dev_login_usernames, dev_switch_enabled from app.db import get_db from app.dev_accounts import demo_account, ensure_demo_assignments from app.errors import AuthError, NotFoundError from app.models import PMUser, ProjectAssignment from app.security import authenticate, create_token, get_current_user from app.serializers import user_payload router = APIRouter(prefix="/auth", tags=["auth"]) class LoginIn(BaseModel): username: str password: str @router.post("/login") async def login(payload: LoginIn, db: AsyncSession = Depends(get_db)): user = await authenticate(db, payload.username, payload.password) if user is None: raise AuthError("نام کاربری یا رمز عبور اشتباه است") ctx = await build_context(db, user) return {"data": {"token": create_token(user.id), "user": user_payload(ctx)}} @router.get("/me") async def me(user: PMUser = Depends(get_current_user), db: AsyncSession = Depends(get_db)): return {"data": user_payload(await build_context(db, user))} # --- development role switcher ------------------------------------------------- def _require_dev_switch() -> None: if not dev_switch_enabled(): raise NotFoundError("تغییر نقش فقط در فاز توسعه فعال است") @router.get("/dev-roles") async def dev_roles(db: AsyncSession = Depends(get_db)): """Demo identities offered by the dashboard role picker (dev only).""" _require_dev_switch() # projects only exist after the first pull — self-heal the demo scope here await ensure_demo_assignments(db) data = [] for username in dev_login_usernames(): account = demo_account(username) if account is None: continue user = await db.scalar(select(PMUser).where(PMUser.username == account.username)) if user is None: continue projects = await db.scalar( select(func.count()).where(ProjectAssignment.user_id == user.id) ) data.append( { "username": user.username, "display_name": user.display_name, "role": account.role.value, "project_count": projects or 0, "all_projects": account.share is None, } ) return {"data": data} class DevLoginIn(BaseModel): username: str @router.post("/dev-login") async def dev_login(payload: DevLoginIn, db: AsyncSession = Depends(get_db)): """Issue a token for a demo account — dev role switching without a password. Restricted to the configured demo usernames, never arbitrary users. """ _require_dev_switch() if payload.username not in dev_login_usernames(): raise AuthError("این حساب در فهرست حساب‌های توسعه نیست") user = await db.scalar(select(PMUser).where(PMUser.username == payload.username)) if user is None: raise AuthError("حساب نمایشی ساخته نشده است؛ سرور را دوباره راه‌اندازی کنید") ctx = await build_context(db, user) return {"data": {"token": create_token(user.id), "user": user_payload(ctx)}}