fix(auth): Casdoor JWT verification and 401 semantics; add /auth/login password-grant endpoint

- verify kid from the JWT header (pyjwt decode() returns claims, not the
  header — the old lookup was always None and rejected every token)
- convert JWK entries to crypto keys explicitly (pyjwt 2.13 dropped dict
  keys) and fetch the JWKS from /.well-known/jwks
- 401 for missing/invalid/expired/wrong-issuer/wrong-audience tokens
  (was 403/400); 403 stays for valid-but-roleless users; 503 when Casdoor
  is unreachable; new CASDOOR_ISSUER setting separates the in-network
  endpoint from the browser-facing issuer
- dev-header fallback in inventory/pm scopes now requires an explicit
  X-User-Role header (no auth info at all -> 401)
- new POST /api/v1/auth/login: password grant against Casdoor, local JWT
  verification, auto-provisioning, returns token + profile with the role
  resolved from the app users table
This commit is contained in:
ali_mohaghegh
2026-08-29 14:43:49 +03:30
parent 06a9a654a5
commit 3cd76d335f
7 changed files with 202 additions and 38 deletions
+13 -3
View File
@@ -36,7 +36,12 @@ from app.services.inventory_service import (
apply_transaction,
get_warehouse_or_404,
)
from app.utils.errors import ForbiddenError, NotFoundError, ValidationError
from app.utils.errors import (
ForbiddenError,
NotFoundError,
UnauthorizedError,
ValidationError,
)
from app.utils.pagination import paginate, sort_query
from app.utils.serialization import to_iso
@@ -59,7 +64,9 @@ async def inventory_scope(request: Request, db: AsyncSession = Depends(get_db))
# projects.pm_user_id (single PM per project).
if request.headers.get("authorization"):
user: UserContext = await get_optional_user(request, db)
if user is not None and user.role is not None:
if user is not None:
if user.role is None:
raise ForbiddenError("نقش شما هنوز در سیستم تعیین نشده است")
# v2.6: مدیر تدارکات = ادمین دفتر — دسترسی فقط‌خواندنی به پنل انبار.
if user.role == "procurement_manager":
return Scope(role="admin", project_id=None, actor=user.display_name)
@@ -73,7 +80,10 @@ async def inventory_scope(request: Request, db: AsyncSession = Depends(get_db))
return Scope(role="pm", project_id=user.project_id, actor=user.display_name)
return Scope(role="admin", project_id=None, actor=user.display_name)
# Dev fallback: header-based identity (fork's placeholder mode).
# Dev fallback: header-based identity (fork's placeholder mode) — only
# when an explicit X-User-Role header is sent. No auth info at all → 401.
if not request.headers.get("x-user-role"):
raise UnauthorizedError("احراز هویت لازم است — توکن Casdoor یا حالت توسعه را ارسال کنید")
role = (request.headers.get("x-user-role") or settings.INVENTORY_DEFAULT_ROLE or ROLE_PM).strip().lower()
if role == "procurement_manager":
role = ROLE_ADMIN # مدیر تدارکات = ادمین (فقط‌خواندنی)