Reverting the production revert so the pm branch can merge cleanly on top of the restored RBAC base. Conflicts against newer integration work resolved by combining: - App.tsx: v7's bot-draft wizard bypass + login returnUrl kept alongside the supervisor gating (no-project block screen, panel scoping) - LoginPage: returnUrl navigation kept, supervisor routing added - migration.sql: bot_drafts table kept, unified-projects ALTERs restored
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""Auth router — in-page login against Casdoor (password grant).
|
|
|
|
POST /auth/login: the frontend sends username/password; this router exchanges
|
|
them at Casdoor's OAuth token endpoint (grant_type=password), verifies the
|
|
issued JWT locally (JWKS), auto-provisions the `users` row and returns the
|
|
token + profile (role resolved from users.role). No browser redirect.
|
|
|
|
Error mapping (documented in docs/casdoor-auth.md):
|
|
- wrong credentials -> 401 UNAUTHORIZED
|
|
- Casdoor unreachable -> 503 UPSTREAM_UNAVAILABLE
|
|
- Casdoor misconfigured -> 503 UPSTREAM_UNAVAILABLE (details in api logs)
|
|
"""
|
|
import logging
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, Request
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth import _verify_token, context_from_claims
|
|
from app.config import settings
|
|
from app.db.engine import get_db
|
|
from app.routers.users import _user_projects, user_to_dict
|
|
from app.services import analytics
|
|
from app.utils.errors import ForbiddenError, ServiceUnavailableError, UnauthorizedError
|
|
from app.utils.ratelimit import limiter
|
|
|
|
logger = logging.getLogger("procurement.auth")
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
@router.post("/login")
|
|
@limiter.limit("10/minute", error_message="تلاشهای ورود بیش از حد مجاز — کمی بعد دوباره امتحان کنید")
|
|
async def login(request: Request, payload: LoginRequest, db: AsyncSession = Depends(get_db)):
|
|
"""Exchange username/password for a Casdoor JWT (OAuth password grant)."""
|
|
url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token"
|
|
form = {
|
|
"grant_type": "password",
|
|
"client_id": settings.CASDOOR_CLIENT_ID,
|
|
"client_secret": settings.CASDOOR_CLIENT_SECRET,
|
|
"username": payload.username.strip(),
|
|
"password": payload.password,
|
|
"scope": "openid profile email",
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
resp = await client.post(url, data=form)
|
|
except httpx.HTTPError as exc:
|
|
logger.error("Casdoor token endpoint unreachable (%s): %s", url, exc)
|
|
raise ServiceUnavailableError("سرویس احراز هویت (Casdoor) در دسترس نیست") from exc
|
|
|
|
try:
|
|
body = resp.json()
|
|
except ValueError as exc:
|
|
logger.error("Casdoor token endpoint returned non-JSON (status %s)", resp.status_code)
|
|
raise ServiceUnavailableError("پاسخ نامعتبر از سرویس احراز هویت") from exc
|
|
|
|
access_token = body.get("access_token")
|
|
if not access_token:
|
|
err = body.get("error") or "unknown_error"
|
|
if err in ("invalid_username_or_password", "invalid_grant"):
|
|
raise UnauthorizedError("نام کاربری یا رمز عبور نادرست است", field="password")
|
|
logger.error(
|
|
"Casdoor rejected password grant: error=%s description=%s",
|
|
err,
|
|
body.get("error_description", ""),
|
|
)
|
|
raise ServiceUnavailableError("سرویس احراز هویت به درستی پیکربندی نشده است")
|
|
|
|
# Verify the freshly issued token with our own JWKS checks, then resolve
|
|
# the app-side user (auto-provision on first login).
|
|
claims = await _verify_token(access_token)
|
|
ctx = await context_from_claims(claims, db)
|
|
projects = await _user_projects(db, ctx.user)
|
|
if ctx.role == "supervisor" and not projects:
|
|
raise ForbiddenError("پروژه تخصیص داده نشده است — تا زمان تخصیص پروژه، اجازه ورود به سامانه را ندارید")
|
|
await analytics.track(
|
|
"login_success",
|
|
{"username": payload.username, "role": ctx.role},
|
|
profile_id=ctx.user.casdoor_id,
|
|
)
|
|
await analytics.identify(
|
|
ctx.user.casdoor_id,
|
|
{"name": ctx.display_name, "role": ctx.role, "username": payload.username},
|
|
)
|
|
return {
|
|
"data": {
|
|
"access_token": access_token,
|
|
"token_type": "bearer",
|
|
"expires_in": body.get("expires_in"),
|
|
"user": user_to_dict(ctx.user, projects),
|
|
}
|
|
}
|