Skip to content

feat(access): admin/operator roles + server-side login sessions - #81

Open
pyramation wants to merge 1 commit into
mainfrom
feat/admin-sessions-roles
Open

feat(access): admin/operator roles + server-side login sessions#81
pyramation wants to merge 1 commit into
mainfrom
feat/admin-sessions-roles

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Adds an admin vs. operator distinction plus cheap server-visible login sessions, so you can see who's using the UI and manage access — without touching the receiver, WebSocket protocol, or sockets. No PGlite; everything lives in the appstash/project JSON that already syncs.

Roles (packages/settings/src/users.ts). Each user carries role: 'admin' | 'operator'. Role resolution is defensive so nothing breaks:

roleOf(user) = user.role === 'operator' ? 'operator' : 'admin'  // legacy/no-role → admin
addUser(...role?) // first user in an empty project → admin; later users → operator
setUserRole / removeUser // guarded: can't demote/remove the last admin while others remain

listUsers(project): string[] is preserved for existing callers; new listUserInfos / authenticate return {username, role}.

Sessions (packages/settings/src/sessions.ts, new). A login writes a session row to data/projects/<p>/sessions.json (mode 0600, pruned on read, not part of portable export):

Session { id, username, role, ip, userAgent, issuedAt, lastSeen, expiresAt }

The JWT is bound to it — signJwt(sub, { sid, role, ttlSec }) now carries sid, role, and a 1h exp; verifyJwt rejects expired tokens (legacy no-exp tokens still accepted).

HTTP admin surface (packages/server/src/http-app.ts). /api/login creates a session and returns { username, role, token, expiresAt }; /api/me echoes role. New role-gated endpoints, each requiring a valid Bearer JWT + still-active session via requireAdmin (the shared receiver key can never administer):

GET/DELETE /api/admin/sessions[/:id]
GET/POST   /api/admin/users
POST       /api/admin/users/:username/role
DELETE     /api/admin/users/:username   // also revokes that user's sessions

Revocation is intentionally lazy — it drops the session row; the client loses access on its next token refresh. Open sockets are never force-closed, keeping the socket layer untouched.

Desktop Access route (packages/desktop/...). The Users tab gains a role column with inline promote/demote (last-admin guarded), Add-user gets a role picker, and a new Sessions tab lists who's logged in (user, role, IP, last-seen) with revoke. This is the local privileged path — Electron main reads/writes the store directly over typed IPC; no secrets or password hashes cross the bridge. A remote Electron would instead use the role-gated /api/admin endpoints above.

Testing

  • packages/settings/__tests__/roles-sessions.test.ts (new): role defaults, explicit roles, legacy records, last-admin protection, session create/list/get/touch/revoke/prune, revoke-all-for-user.
  • packages/server/__tests__/http-app.test.ts: login returns role+expiry, admin session list/revoke, operator + anonymous denial, admin user list/create/promote/delete, /api/me.
  • Full pnpm build, pnpm -r lint, pnpm -r test green (in a clean env — the one local layout-config failure is this box's ~/.wavegrid being set to the nova project, not a code issue).

Link to Devin session: https://app.devin.ai/sessions/972698f89f494b86828010666a002b8f
Requested by: @pyramation

- Roles on users (admin/operator); first user in an empty project is admin,
  later users default to operator; last-admin demote/remove is guarded.
- Cheap server-side login sessions in appstash JSON (sessions.json, 0600),
  bound to short-lived JWTs (sid + role + 1h exp); pruned on read.
- Role-gated HTTP admin surface: /api/admin/sessions, /api/admin/users
  (list/create/role/delete). Bearer JWT + active session required; the
  shared receiver key can never administer.
- Desktop Access route: role column + inline promote/demote, a Sessions tab
  (who's logged in, revoke), role on Add-user. Local Electron uses the
  main-process store directly; no secrets cross IPC.
- Receiver + WebSocket protocol unchanged.
@pyramation pyramation self-assigned this Aug 6, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant