You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace the in-memory MultiAuthState in src/channels/web/auth.rs with a DB-backed authenticator:
auth_middleware must call store.authenticate_token(hash) instead of iterating hashed_tokens: Vec<([u8; 32], UserIdentity)>
Keep constant-time hashing (hash_token() with SHA-256) — the DB lookup is by hash, not by plaintext
Cache recent authentications in an LRU to avoid hitting DB on every request (TTL ~60s)
UserIdentity gains a db_user_id: Uuid field alongside the existing string user_id
Backward compatibility: if GATEWAY_USER_TOKENS is set, continue using the env-var path. If a users table exists with rows, prefer DB. Log a deprecation warning when env-var tokens are used.
New API Endpoints
All under /api/admin/users and /api/tokens (protected by auth):
POST /api/admin/users — create user (admin only, for now any authenticated user)
GET /api/admin/users — list users
GET /api/admin/users/{id} — get user details
PATCH /api/admin/users/{id} — update profile / status
POST /api/admin/users/{id}/suspend — suspend user
POST /api/admin/users/{id}/activate — reactivate user
POST /api/tokens — create API token (returns plaintext ONCE)
GET /api/tokens — list my tokens (no plaintext, just metadata)
DELETE /api/tokens/{id} — revoke token
POST /api/invitations — create invitation
GET /api/invitations — list my sent invitations
POST /api/invitations/accept — accept invitation (creates user + first token)
Migration Path
Add a startup migration that:
Creates the new tables
If GATEWAY_USER_TOKENS is set, inserts corresponding users and api_tokens rows
Logs which users were migrated
Architecture Notes
Follow the handler pattern in src/channels/web/handlers/ — one file per domain (users.rs, tokens.rs, invitations.rs)
Follow the AuthenticatedUser extractor pattern already used by jobs/routines/settings handlers
Token generation: use rand::thread_rng().gen::<[u8; 32]>() → hex-encode for the user-facing token, SHA-256 hash for storage. Same pattern as hash_token() in auth.rs
Integration tests for invitation create → accept → user created cycle
cargo clippy zero warnings, cargo fmt clean
Pitfalls & Landmines
Dual-backend requirement is non-negotiable. Every SQL statement must work in both PostgreSQL and libSQL. libSQL doesn't support gen_random_uuid() — use application-generated UUIDs. libSQL doesn't support BYTEA — use BLOB. libSQL doesn't support TEXT[] — use JSON text. See src/db/libsql_migrations.rs for the translation patterns.
Token plaintext must never be logged. The hash_token() function in auth.rs is the only place tokens should be hashed. Don't add Debug derives on structs containing plaintext tokens.
Auth cache invalidation is subtle. If you add an LRU cache for DB-backed auth, revoking a token or suspending a user must either evict from cache or the cache TTL must be short enough to be acceptable. Document the chosen tradeoff.
The user_id string in existing tables (jobs, memory, settings, routines) currently holds arbitrary strings like "alice". The migration must handle mapping these to the new UUID-based users.id. Consider keeping the string user_id column as-is and adding a nullable user_uuid FK that gets populated during migration. Don't break existing queries.
IncomingMessage in src/channels/channel.rs carries a user_id: String. This flows through the entire agent pipeline. Changing it to UUID is a massive refactor — instead, keep user_id as the string identifier and use it as the users.id display/lookup key, or add a parallel field.
Don't forget to update GatewayState builder methods in src/channels/web/mod.rs — the with_* pattern must include any new state the auth layer needs.
Problem / Motivation
User management is currently static: users are defined via the
GATEWAY_USER_TOKENSenv var (a JSON map of secret tokens →UserIdentity). This means:UserIdentityis just{ user_id: String, workspace_read_scopes: Vec<String> }MultiAuthStatelives in memory, not in DBThis is the foundation that Issues #2 (Workspaces), #3 (RBAC), and #4 (Admin Panel) depend on.
Proposed Solution
New Database Tables
Both PostgreSQL and libSQL backends must be updated (see dual-backend requirement in
src/db/CLAUDE.md).New Database Traits
Add a
UserStoresub-trait to theDatabasesupertrait insrc/db/mod.rs:Auth Refactor
Replace the in-memory
MultiAuthStateinsrc/channels/web/auth.rswith a DB-backed authenticator:auth_middlewaremust callstore.authenticate_token(hash)instead of iteratinghashed_tokens: Vec<([u8; 32], UserIdentity)>hash_token()with SHA-256) — the DB lookup is by hash, not by plaintextUserIdentitygains adb_user_id: Uuidfield alongside the existing stringuser_idGATEWAY_USER_TOKENSis set, continue using the env-var path. If auserstable exists with rows, prefer DB. Log a deprecation warning when env-var tokens are used.New API Endpoints
All under
/api/admin/usersand/api/tokens(protected by auth):Migration Path
Add a startup migration that:
GATEWAY_USER_TOKENSis set, inserts correspondingusersandapi_tokensrowsArchitecture Notes
src/channels/web/handlers/— one file per domain (users.rs,tokens.rs,invitations.rs)AuthenticatedUserextractor pattern already used by jobs/routines/settings handlersrand::thread_rng().gen::<[u8; 32]>()→ hex-encode for the user-facing token, SHA-256 hash for storage. Same pattern ashash_token()inauth.rsauth_middleware— user creation is initially open to any authenticated user (RBAC in Issue Onboarding: show Telegram in channel selection and auto-install bundled channel #3 will restrict it)UserStoretrait must be implemented for bothPostgresBackendandLibSqlBackendCode Pointers
src/channels/web/auth.rs—MultiAuthState,UserIdentity,hash_token(),auth_middleware— this is the code being replaced/augmentedsrc/channels/web/server.rs—GatewayState(addstorereference, already present),WorkspacePoolsrc/channels/web/handlers/jobs.rs— pattern to follow for new handler filessrc/db/mod.rs—Databasesupertrait whereUserStoremust be addedsrc/db/postgres.rs— PostgreSQL implementation to extendsrc/db/libsql/mod.rs— libSQL implementation to extendsrc/db/libsql_migrations.rs— consolidated libSQL schema (add tables here)migrations/— PostgreSQL migration files (addV14__users_tokens.sql)src/config/channels.rs—GatewayConfig,UserTokenConfig— env-var parsing to deprecatesrc/main.rslines ~595-632 — multi-tenant startup wiringAcceptance Criteria
users,api_tokens, andinvitationstables created in both PostgreSQL and libSQL backendsUserStoretrait added toDatabasesupertrait with implementations for both backendsGATEWAY_USER_TOKENSis unsetGATEWAY_USER_TOKENSpath continues to work with deprecation warning loggedGATEWAY_USER_TOKENS+ DB migrates env-var users into DB tablesAuthenticatedUserextractor patternUserStoremethods (both backends)cargo clippyzero warnings,cargo fmtcleanPitfalls & Landmines
gen_random_uuid()— use application-generated UUIDs. libSQL doesn't supportBYTEA— useBLOB. libSQL doesn't supportTEXT[]— use JSON text. Seesrc/db/libsql_migrations.rsfor the translation patterns.hash_token()function inauth.rsis the only place tokens should be hashed. Don't addDebugderives on structs containing plaintext tokens.user_idstring in existing tables (jobs, memory, settings, routines) currently holds arbitrary strings like "alice". The migration must handle mapping these to the new UUID-basedusers.id. Consider keeping the stringuser_idcolumn as-is and adding a nullableuser_uuidFK that gets populated during migration. Don't break existing queries.IncomingMessageinsrc/channels/channel.rscarries auser_id: String. This flows through the entire agent pipeline. Changing it to UUID is a massive refactor — instead, keepuser_idas the string identifier and use it as theusers.iddisplay/lookup key, or add a parallel field.GatewayStatebuilder methods insrc/channels/web/mod.rs— thewith_*pattern must include any new state the auth layer needs.Non-Goals
user_idscoping.Part 1 of 4 — see also: Workspaces (#2), RBAC (#3), Admin Panel (#4)