Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ NUXT_ADMIN_USERNAME=admin
# Default from nuxt.config.ts: 123
NUXT_ADMIN_PASSWORD=123

# Optional static bearer token for software admin authentication.
# Leave empty to disable this auth path. Only one exact token is accepted.
NUXT_ADMIN_TOKEN=

# Private internal port for the embedded Drizzle Studio worker.
# Default from nuxt.config.ts: 64983
NUXT_DRIZZLE_STUDIO_INTERNAL_PORT=64983
Expand Down
19 changes: 17 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ REST API endpoints documentation.

Login as administrator.

This endpoint is for browser-style admin login with username and password. It sets the session cookie used by the admin UI.

**Request:**

```json
Expand All @@ -18,7 +20,7 @@ Login as administrator.
```

**Response:**
Sets `admin_token` HTTP-only cookie and returns JWT token.
Sets `admin_token` HTTP-only cookie and returns success.

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

Expand All @@ -41,17 +43,30 @@ Verify authentication token (admin only).
- Cookie: `admin_token` (set automatically by login)
- Or `Authorization: Bearer <token>`

`Authorization: Bearer <token>` accepts two admin auth modes:

- A valid admin JWT only when a client already has that token. `POST /api/auth/login` does not return the JWT in the response body; it sets the `admin_token` HTTP-only cookie for browser admin sessions.
- The exact static token configured in `NUXT_ADMIN_TOKEN` for software-to-software admin access. External API clients should use this static token path.

**Response:**

```json
{
"valid": true,
"user": {
/* decoded JWT payload */
/* decoded JWT payload or static-token admin payload */
}
}
```

**Static token example:**

```bash
curl \
-H "Authorization: Bearer $NUXT_ADMIN_TOKEN" \
http://localhost:3000/api/auth/verify
```

## Database Admin

### GET `/api/admin/drizzle-studio/app`
Expand Down
3 changes: 3 additions & 0 deletions docs/deployment-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ Set a strong admin password as well:
# In .env:
NUXT_ADMIN_USERNAME=admin
NUXT_ADMIN_PASSWORD=<your-strong-password>
NUXT_ADMIN_TOKEN=<optional-static-admin-token>
NUXT_JWT_SECRET=<paste-output-from-openssl>
```

Set `NUXT_ADMIN_TOKEN` only if external software needs direct admin API access with `Authorization: Bearer <token>`. Leave it empty to disable that path. Only one exact token is accepted.

Optional override for the embedded Drizzle Studio worker port:

```bash
Expand Down
3 changes: 3 additions & 0 deletions docs/setup-production.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ Set production values for:

- `NUXT_ADMIN_USERNAME`
- `NUXT_ADMIN_PASSWORD`
- `NUXT_ADMIN_TOKEN` when external software needs direct admin bearer-token access
- `NUXT_JWT_SECRET`
- `NUXT_DRIZZLE_STUDIO_INTERNAL_PORT` when you need a non-default private Studio worker port

Leave `NUXT_ADMIN_TOKEN` empty if you do not want static admin-token authentication. If you set it, keep it secret and rotate it by updating the environment and redeploying.

## Data Persistence

- Quiz data is stored in SQLite at `.data/db/stage-flow-tools.sqlite3`.
Expand Down
1 change: 1 addition & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const configBase: InputConfig<NuxtConfig, ConfigLayerMeta> = {
runtimeConfig: {
// Private keys (only available server-side)
adminPassword: '123',
adminToken: '',
adminUsername: 'admin',
drizzleStudioInternalPort: '64983',
jwtSecret: 'tryUJ0zQbstPbTOrezme+Fv+KndzDNRx5lmSeelr2ial2/2yV8HqLeQ2felJafqf',
Expand Down
28 changes: 26 additions & 2 deletions server/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { jwtVerify } from 'jose'
import type { H3Event } from 'h3'

type VerifiedAdminPayload = {
authMethod?: 'static-token'
isAdmin: true
username: string
}

/** Sets the admin_token cookie with protocol-aware security attributes. */
export function setAdminCookie(event: H3Event, value: string, maxAge: number) {
const isSecure = getRequestProtocol(event) === 'https'
Expand All @@ -14,12 +20,24 @@ export function setAdminCookie(event: H3Event, value: string, maxAge: number) {
}

/**
* Extracts the JWT from cookies or headers.
* Extracts the admin auth token from cookies or headers.
* @param event The H3 event object.
* @returns The token string or undefined.
*/
export function getToken(event: H3Event): string | undefined {
return getCookie(event, 'admin_token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
return getHeader(event, 'authorization')?.replace('Bearer ', '') || getCookie(event, 'admin_token')
}

function getStaticAdminPayload(token: string, configuredToken: string): VerifiedAdminPayload | null {
if (!configuredToken || token !== configuredToken) {
return null
}

return {
authMethod: 'static-token',
isAdmin: true,
username: 'admin-token',
}
}

/**
Expand All @@ -38,6 +56,12 @@ export async function verifyAdmin(event: H3Event) {
})
}

const staticAdminPayload = getStaticAdminPayload(token, config.adminToken)

if (staticAdminPayload) {
return staticAdminPayload
}

try {
const secret = new TextEncoder().encode(config.jwtSecret)
const { payload } = await jwtVerify(token, secret, { algorithms: [
Expand Down