Both download endpoints (invoice/proforma) put the stored file_name raw into the Content-Disposition header. HTTP headers are latin-1, so any non-ASCII name (i.e. every Persian filename) raised UnicodeEncodeError before the body was sent -> HTTP 500. Uploads were unaffected.
Content-Disposition is now built by a helper following RFC 6266 / RFC 5987: the UTF-8 name is percent-encoded in filename*=UTF-8''..., with a header-safe ASCII fallback in filename= (quotes and control characters stripped; a name that folds away entirely becomes attachment<ext>).
Verified locally after reproducing the exact production error (same latin-1 positions 22-27): Persian proforma + invoice downloads 500 -> 200 with filename*= carrying the full Persian name and bytes sha256-matching the MinIO object; ASCII filename regression 200; helper edge cases (CRLF/quote injection stripped, empty name -> attachment, pure-Persian png -> attachment.png); no UnicodeEncodeError in the api logs after the fix.
Part 1 — show/hide password: an eye/eye-off button inside the login password input switches its type between password and text. It sits on the right (the input is LTR) with pr-10 clearance so dots never overlap the icon; type=button so a click cannot submit the form, aria-label Show/Hide password, aria-pressed + aria-controls, plus a Persian tooltip.
Part 2 — Organization field removed end-to-end after the audit proved it was a no-op: LoginPage input/state/payload spread (main + dev-mode admin login), api.ts payload type, backend LoginRequest.organization (declared but never forwarded to Casdoor — a bogus org value still logged in successfully), and the casdoor-auth.md flow diagram. The organization actually used remains CASDOOR_ORGANIZATION=inventory for /users/sync.
Verified: api + frontend images rebuilt and redeployed; login works with {username, password}; the served LoginPage chunk contains the toggle and zero org-field leftovers.
- pm.py/inventory.py: remove db.begin() on already-begun sessions — the
scope dependencies read from the session first, so opening a new
transaction raised InvalidRequestError -> 500 on every receipt/transaction
(JWT path); get_db already commits/rolls back atomically
- pm.py: receipt response returns the real derived state (was hardcoded
'arrived'); dev-user creation is race-free (insert on conflict)
- expand_service: aggregate 1:N receipts per line item (received_qty sum +
count) so the ledger can show partial-receipt totals
- users.py: role changes are admin-only (RBAC); new POST /users/sync mirrors
Casdoor org users into the app users table (identity only, roles untouched)
- config/compose: CASDOOR_ADMIN_USER/PASSWORD for the sync endpoint
- seed_casdoor: redirect URI updated for the Traefik edge
- 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