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>
This commit is contained in:
133
docs/superpowers/specs/2026-04-03-authentication-design.md
Normal file
133
docs/superpowers/specs/2026-04-03-authentication-design.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user