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