- api_tokens.ejs: created_on/last_used_on come back from Redis as strings
(model-redis only coerces fields with an explicit `type`), so `new Date(ms)`
yielded "Invalid date". Use `moment(ms, "x")` (the hosts.ejs/dns.ejs
precedent) which parses a numeric string-or-number as a Unix-ms timestamp.
- api_tokens.ejs: `isExpired` is a class getter not serialized to the client
JSON, so the "expired" badge never showed — compute expiry in the view via
`Date.now() > Number(expires_at)`. Also guard the `last_used_on: 0` / falsy
case (string "0" is truthy) so unset timestamps render "—" not "1970".
- middleware/auth.js: authIO did `checkToken(socket.handshake.auth.token || 0)`,
so any socket connect without a token (login page, pre-login) did an
`AuthToken.get(0)` lookup and logged a noisy `EntryNotFound` trace. Guard:
reject the socket with a generic 401 when there's no token (behavior-
preserving — unauth sockets were already rejected; just no Redis lookup / 404).
- dns_provider.js: drop a stray `console.log('currentDomains:', ...)` debug
line in updateDomains() (unrelated, noticed while investigating).
Co-authored-by: Claude <noreply@anthropic.com>
Personal access tokens so scripts/CI can call the management API without an
OIDC browser session. Each logged-in user mints their own token; it
authenticates as the creator (groups snapshotted at mint, mirroring the proxy's
browser AuthToken), and the existing authz layer (Permission.effectiveFor /
roles.resolveEffective) applies unchanged. Local groups and owned-domain rights
are recomputed live; only SSO/LDAP group membership is the mint-time snapshot.
- models/api_token.js: new ApiToken model (prx_<id>_<secret> format; id is the
lookup key, secret bcrypt-hashed + isPrivate, shown once). add()/rotate()/
authenticate(); optional expires_at; best-effort last_used_on; groups
snapshot. No _ttl (persists). Deliberately NOT wrapped in ModelPs (so the
last_used_on write on the auth path doesn't spam the socket).
- routes/api_token.js: self-service CRUD (list/get/update/delete/rotate),
owner-scoped (created_by === reqUsername(req), 403 otherwise).
- middleware/auth.js + models/auth.js: accept `Authorization: Bearer prx_...`
(precedence over the auth-token session header). Builds a synthetic req.token
that satisfies the only three req.token reads (auth.js .user/.groupsArray,
authz.js reqUsername .created_by) so the authz layer works unchanged.
checkApiToken collapses every failure to one generic 401 (no leak).
- views/api_tokens.ejs + routes/render.js (GET /api-tokens): self-service page
(forceLogin, no admin gate) — create (token shown once), rotate, revoke.
- views/top.ejs: "API Tokens" nav entry visible to all logged-in users.
- public/js/app.js: app.apiToken client module.
- DEPLOYMENT.md + docs/docker.md: API tokens section.
Co-authored-by: Claude <noreply@anthropic.com>