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

4.6 KiB

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

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

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

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)