# Authentication — Design Spec ## Overview Add authentication to GearBox with a public-read, authenticated-write model. Web UI uses cookie-based sessions. Programmatic access (MCP server, scripts) uses API keys. Single-user app — one admin account, created on first setup. ## Database Schema ### `users` table ```typescript export const users = sqliteTable("users", { id: integer("id").primaryKey({ autoIncrement: true }), username: text("username").notNull().unique(), passwordHash: text("password_hash").notNull(), createdAt: integer("created_at", { mode: "timestamp" }).notNull(), }); ``` ### `sessions` table ```typescript export const sessions = sqliteTable("sessions", { id: text("id").primaryKey(), // random token userId: integer("user_id").notNull().references(() => users.id), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), }); ``` ### `apiKeys` table ```typescript export const apiKeys = sqliteTable("api_keys", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), keyHash: text("key_hash").notNull(), keyPrefix: text("key_prefix").notNull(), // first 8 chars for identification createdAt: integer("created_at", { mode: "timestamp" }).notNull(), }); ``` ## Password Hashing Use `Bun.password.hash()` and `Bun.password.verify()` with argon2 (Bun's default). No external dependencies needed. ## Auth Middleware Hono middleware applied to all write endpoints (POST, PUT, DELETE): 1. Check for `X-API-Key` header — if present, hash and compare against `api_keys` table 2. Check for session cookie (`gearbox_session`) — if present, look up in `sessions` table, verify not expired 3. If neither is valid, return `401 Unauthorized` GET endpoints remain public — no middleware applied. **Exempt from auth:** `/api/auth/login`, `/api/auth/setup`, and `/api/auth/me` (GET) are not protected by the write middleware. **Before setup:** If no user account exists yet, write endpoints return `403` with `{ error: "setup_required" }` so the frontend can prompt account creation. ## API Endpoints ### Auth routes (`/api/auth`) - `POST /api/auth/login` — accepts `{ username, password }`, creates session, sets cookie - `POST /api/auth/logout` — clears session cookie, deletes session record - `GET /api/auth/me` — returns current user info if authenticated, or `null` - `POST /api/auth/setup` — initial account creation (only works if no users exist) - `PUT /api/auth/password` — change password (requires current password) ### API key routes (`/api/auth/keys`) — all authenticated - `GET /api/auth/keys` — list API keys (name, prefix, createdAt — never the full key) - `POST /api/auth/keys` — create new key, returns the full key once - `DELETE /api/auth/keys/:id` — revoke a key ## Session Management - Session token: 32-byte random hex string - Cookie: `gearbox_session`, httpOnly, sameSite=lax, path=/ - Session expiry: 30 days, refreshed on each authenticated request - Sessions stored in SQLite — simple cleanup of expired rows ## Frontend Changes ### Login button (top-right, Gitea-style) - When not logged in: "Sign in" button in the header/navbar - When logged in: username display with dropdown (logout, settings link) ### Login page - Route: `/login` - Simple form: username + password + submit - Redirects back to previous page on success - Error message on invalid credentials ### Initial setup - If `GET /api/auth/me` returns `null` and no users exist, show a setup prompt - Setup form: create username + password - Only shown once — after account creation, normal login flow applies ### Conditional UI - Add/edit/delete buttons: hidden when not authenticated - Forms (ItemForm, CandidateForm, etc.): only accessible when authenticated - The app is fully browseable without login — you just can't modify anything ### Auth state - `useAuth` hook using React Query: calls `GET /api/auth/me` - Returns `{ user, isAuthenticated, login, logout }` - Cached and invalidated on login/logout ### Settings page additions - API key management section: list, create, revoke - Change password form ## Testing - Auth service tests: login, logout, session creation/validation, password change - API key tests: create, verify, revoke - Middleware tests: write endpoints reject without auth, read endpoints work without auth - Setup flow test: first-user creation ## Security Considerations - Passwords hashed with argon2 via Bun built-in - Session tokens are cryptographically random - API keys hashed before storage (only shown once on creation) - httpOnly cookies prevent XSS access to session - No CORS changes needed (single-origin app)