feat(profile): self-service name/email/password + desktop installers in CI
Build desktop / desktop (push) Failing after 5m2s
Build & push images / images (push) Successful in 3m1s
CI / check (push) Successful in 40s

Self-service profile: any signed-in user edits their OWN fullName/email and
changes their OWN password (proving the current one), without any user:*
permission. New routes PUT /api/auth/profile + /api/auth/password act only on
req.user.sub (cannot touch username/role), CSRF-guarded; SPA screen at /profile
reachable from the header username chip. email added to the session view +
SessionUser. 7 tests (routes/profile.test.ts); 148 server tests green.

Desktop in CI: new .gitea/workflows/build-desktop.yml builds .deb + .AppImage
on every push to dev/main and uploads them as unsigned workflow artifacts
(per-commit test build). Signed/versioned release stays on release.yml (tag v*).

Wiki: local-jwt-auth (self-service routes), desktop-shell-tauri (two-workflow CI
split), log entry.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-24 10:15:34 +02:00
parent f9bd586265
commit 8129b63a8c
11 changed files with 602 additions and 3 deletions
+15
View File
@@ -69,6 +69,21 @@ The SPA never sees the JWT. Login (`POST /api/auth/login`) verifies bcrypt and s
requires header == cookie == the signed claim (**double-submit CSRF**). Safe reads are exempt.
Routes: `login`, `logout` (clears cookies), `me` (bootstraps SPA session on load). The dev
**Self-service profile (added 2026-06-24).** Alongside the admin user-manager (`routes/users.ts`,
gated on `user:*`), any signed-in user has two **self-only** routes (no permission needed — they
act solely on `req.user.sub`):
- `PUT /api/auth/profile` — edit own `fullName` / `email` (`""` clears → null). Returns the
refreshed session (so the SPA header updates). **Cannot** touch `username` or `role` — those stay
admin-only, so this is not a privilege-escalation surface.
- `PUT /api/auth/password` — change own password, but **must prove the current one** first
(`bcrypt.compare`) → defends a walked-up, already-logged-in booth from a silent re-key. New
password ≥ 8 chars. Distinct from the admin reset (`PUT /api/users/:id/password`), which needs no
current password but DOES need `user:update` + the no-escalation guard.
Both are still CSRF-guarded (mutations). The SPA surfaces them at `/profile` (`apps/web/src/Profile.tsx`),
reachable from the header username chip. Covered by `apps/server/src/routes/profile.test.ts`.
The dev
[[react-vite-spa|Vite]] proxy and the prod **nginx** reverse proxy keep the SPA and API
**same-origin**, so the cookies work without CORS. (This replaced an earlier dev-only
`SETUP_AUTH_BYPASS` shim, now removed.)