# Authentication

GearBox uses a public-read, authenticated-write model. All GET endpoints are publicly accessible with no credentials required. Any request that modifies data (POST, PUT, PATCH, DELETE) requires authentication.

This is a single-user app. There is exactly one admin account.

## Table of Contents

- [First-Time Setup](#first-time-setup)
- [Web UI Authentication](#web-ui-authentication)
- [API Keys](#api-keys)
- [Auth Middleware Behavior](#auth-middleware-behavior)
- [Auth API Reference](#auth-api-reference)
- [Frontend Behavior](#frontend-behavior)

---

## First-Time Setup

When no users exist, all write endpoints return `403` with `{ "error": "setup_required" }`. To create the admin account, visit `/login` in the browser and complete the setup form, or call the setup endpoint directly:

```http
POST /api/auth/setup
Content-Type: application/json

{
  "username": "admin",
  "password": "yourpassword"
}
```

Requirements:
- `username`: any non-empty string
- `password`: minimum 6 characters

This endpoint only works when no users exist. Subsequent calls return `403 { "error": "Setup already completed" }`.

On success, a session cookie is set and `201` is returned:

```json
{ "username": "admin" }
```

---

## Web UI Authentication

Sessions use an `httpOnly` cookie named `gearbox_session`.

| Property   | Value              |
|------------|--------------------|
| Cookie name | `gearbox_session` |
| httpOnly   | true               |
| sameSite   | Lax                |
| path       | /                  |
| Max age    | 30 days            |

The session expiry is **automatically refreshed** on each authenticated request. As long as the app is used at least once every 30 days, the session stays active.

Passwords are hashed with **argon2** via `Bun.password`.

### Changing Your Password

Requires an active session cookie.

```http
PUT /api/auth/password
Content-Type: application/json

{
  "currentPassword": "oldpassword",
  "newPassword": "newpassword"
}
```

---

## API Keys

API keys are intended for programmatic access (scripts, MCP clients, integrations). They are managed under **Settings > API Keys** in the web UI, or via the API endpoints listed below.

### Key behavior

- Keys are shown **once** at creation time. Store them securely.
- Keys are stored as an argon2 hash. Only the 8-character prefix is stored in plaintext for display and lookup purposes.
- Pass the key via the `X-API-Key` request header on any write request.

```http
POST /api/items
X-API-Key: gbk_a1b2c3d4...
Content-Type: application/json

{ "name": "Revelate Tangle", "categoryId": 2 }
```

If both a session cookie and an `X-API-Key` header are present, the API key is checked first.

---

## Auth Middleware Behavior

The middleware applied to `/api/*` (excluding `/api/auth/*`) follows these rules:

1. `GET` requests — always allowed, no auth check.
2. No users exist — returns `403 { "error": "setup_required" }`.
3. `X-API-Key` header present — verified against stored hashes; `401` on failure.
4. `gearbox_session` cookie present — verified against sessions table; refreshed on success; `401` on failure.
5. Neither credential present — returns `401 { "error": "Authentication required" }`.

The `/api/auth/*` routes handle their own auth logic and are excluded from the global middleware.

---

## Auth API Reference

### `GET /api/auth/me`

Returns the current session state. Always public.

**Response when logged in:**
```json
{
  "user": { "id": 1 },
  "setupRequired": false
}
```

**Response when logged out, setup complete:**
```json
{
  "user": null,
  "setupRequired": false
}
```

**Response when no users exist:**
```json
{
  "user": null,
  "setupRequired": true
}
```

---

### `POST /api/auth/setup`

Create the first admin account. Only works when no users exist.

**Request:**
```json
{
  "username": "admin",
  "password": "yourpassword"
}
```

**Response:** `201`
```json
{ "username": "admin" }
```

Sets `gearbox_session` cookie.

---

### `POST /api/auth/login`

Log in with username and password.

**Request:**
```json
{
  "username": "admin",
  "password": "yourpassword"
}
```

**Response:** `200`
```json
{ "username": "admin" }
```

Sets `gearbox_session` cookie. Returns `401` on invalid credentials.

---

### `POST /api/auth/logout`

Clear the current session. No request body needed.

**Response:**
```json
{ "ok": true }
```

Clears the `gearbox_session` cookie and deletes the session from the database.

---

### `PUT /api/auth/password`

Change the admin password. Requires an active session cookie (not API key).

**Request:**
```json
{
  "currentPassword": "oldpassword",
  "newPassword": "newpassword"
}
```

**Response:**
```json
{ "ok": true }
```

Returns `401` if `currentPassword` is incorrect.

---

### `GET /api/auth/keys`

List all API keys. Returns name, prefix, and creation timestamp — never the full key.

Requires auth.

**Response:**
```json
[
  {
    "id": 1,
    "name": "Claude Code",
    "prefix": "gbk_a1b2",
    "createdAt": "2025-03-01T10:00:00.000Z"
  }
]
```

---

### `POST /api/auth/keys`

Create a new API key. The full key is returned **once** and cannot be retrieved again.

Requires auth.

**Request:**
```json
{ "name": "Claude Code" }
```

**Response:** `201`
```json
{
  "id": 1,
  "name": "Claude Code",
  "key": "gbk_a1b2c3d4e5f6g7h8i9j0...",
  "prefix": "gbk_a1b2"
}
```

---

### `DELETE /api/auth/keys/:id`

Revoke an API key by ID. Requires auth.

**Response:**
```json
{ "ok": true }
```

---

## Frontend Behavior

- A login button is shown in the top-right corner of the UI (Gitea-style).
- The floating action button (FAB) for adding items is hidden when not logged in.
- Edit and delete actions on items, threads, and setups require auth. Unauthenticated users see read-only views.
- When `setupRequired` is true, the UI redirects to the setup flow.
