Files
GearBox/docs/superpowers/specs/2026-04-03-authentication-design.md
Jean-Luc Makiola dde2fc241d docs: add design specs for image URL fetching, auth, and MCP server
Three independent feature specs covering:
- API endpoint for fetching images from URLs with local storage
- Public-read/authenticated-write auth with sessions and API keys
- Built-in MCP server for Claude Code/Desktop integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:53:51 +02:00

134 lines
4.6 KiB
Markdown

# 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)