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
20 changes: 13 additions & 7 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,23 @@ On first boot you will see something like:

```
================================================================
A new Management API secret was generated for this instance.
Copy it now - it will not be shown again unless you reset it.
Secret: mgmt_8c2c1f0a-...-c9af0e
First run detected.
Open the web UI to create your administrator password.
Until you do, the management API will reject every request
except /v0/management/auth/{status,setup,login}.
================================================================
```

Open `http://your-server:8080/`, paste the secret in the login screen, and
start adding providers.
Open `http://your-server:8080/`. You will be greeted with a "Create
administrator password" screen — pick a password (8+ chars), submit, and
you are in. The password can be changed later from **Settings → Security →
Administrator Password**.

> **Tip:** in production, set `CHAT2API_MANAGEMENT_SECRET` in `.env` so the
> secret stays stable across container rebuilds.
> **Tip:** in headless deployments you can skip the web first-run flow
> entirely by setting `CHAT2API_MANAGEMENT_SECRET` in `.env`. Chat2API
> then uses that value as the long-lived secret and treats setup as
> already done. You will still need to set a password from the UI later
> if you want a friendly login screen.

## Quick start without Docker

Expand Down
74 changes: 38 additions & 36 deletions backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as fs from 'fs'
import * as path from 'path'
import { proxyServer } from './proxy/server'
import { storeManager } from './store/store'
import { generateManagementSecret } from './proxy/middleware/managementAuth'
import * as dotenv from 'dotenv'

// Load environment variables
Expand All @@ -18,48 +17,48 @@ process.on('unhandledRejection', (reason) => {
})

/**
* On first run (or whenever the management secret is missing) auto-enable
* the management API and generate a strong secret. The secret is printed
* to stdout exactly once so the operator can capture it from the logs.
* Make sure the management API is enabled and surface the first-run state
* to the operator. The actual secret is created when the user sets a
* password from the web UI (see backend/proxy/routes/management/auth.ts).
*
* Set CHAT2API_MANAGEMENT_SECRET to use a fixed value (recommended for
* production deployments).
* Operators can still inject a fixed secret via CHAT2API_MANAGEMENT_SECRET
* (useful for headless / scripted deployments).
*/
function ensureManagementApiBootstrap(): { secret: string; generated: boolean } {
function ensureManagementApiBootstrap(): { firstRun: boolean; envSecretApplied: boolean } {
const config = storeManager.getConfig()
const current = config.managementApi || {
enableManagementApi: false,
enableManagementApi: true,
managementApiSecret: '',
}

let secret = current.managementApiSecret
let generated = false

// Allow operators to inject a fixed secret via env (e.g. Docker secret).
const envSecret = process.env.CHAT2API_MANAGEMENT_SECRET
if (envSecret && envSecret.trim()) {
secret = envSecret.trim()
} else if (!secret) {
secret = generateManagementSecret()
generated = true
firstRunCompleted: false,
}

const enable = process.env.CHAT2API_DISABLE_MANAGEMENT_API !== '1'
const envSecret = process.env.CHAT2API_MANAGEMENT_SECRET?.trim()

let next = { ...current, enableManagementApi: enable }
let envSecretApplied = false

if (envSecret) {
// Treat env-provided secret as an explicit operator decision: enable
// the API and bypass the password-based first-run flow.
next = {
...next,
managementApiSecret: envSecret,
firstRunCompleted: true,
}
envSecretApplied = true
}

if (
current.managementApiSecret !== secret ||
current.enableManagementApi !== enable
next.managementApiSecret !== current.managementApiSecret ||
next.enableManagementApi !== current.enableManagementApi ||
next.firstRunCompleted !== current.firstRunCompleted
) {
storeManager.updateConfig({
managementApi: {
...current,
enableManagementApi: enable,
managementApiSecret: secret,
},
})
storeManager.updateConfig({ managementApi: next })
}

return { secret, generated }
const firstRun = !next.firstRunCompleted
return { firstRun, envSecretApplied }
}

/**
Expand Down Expand Up @@ -91,17 +90,20 @@ async function initializeApp(): Promise<void> {
console.log('Storage initialized successfully')

// 2. Make sure the management API is reachable from the web UI.
const { secret, generated } = ensureManagementApiBootstrap()
if (generated) {
const { firstRun, envSecretApplied } = ensureManagementApiBootstrap()
if (envSecretApplied) {
console.log('Management API: secret loaded from CHAT2API_MANAGEMENT_SECRET')
} else if (firstRun) {
console.log('')
console.log('================================================================')
console.log(' A new Management API secret was generated for this instance.')
console.log(' Copy it now - it will not be shown again unless you reset it.')
console.log(` Secret: ${secret}`)
console.log(' First run detected.')
console.log(' Open the web UI to create your administrator password.')
console.log(' Until you do, the management API will reject every request')
console.log(' except /v0/management/auth/{status,setup,login}.')
console.log('================================================================')
console.log('')
} else {
console.log('Management API: secret loaded from configuration / environment')
console.log('Management API: ready (password set; awaiting login)')
}

// 3. Optionally serve the built frontend from the same Koa server so
Expand Down
21 changes: 21 additions & 0 deletions backend/proxy/middleware/managementAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,34 @@ function extractAuthToken(ctx: Context): string | null {
return null
}

/**
* Public auth endpoints that must remain reachable before any secret has
* been issued. The status / setup / login routes need to work on first run
* so the operator can create their password from the web UI.
*/
const PUBLIC_AUTH_PATHS = new Set<string>([
'/v0/management/auth/status',
'/v0/management/auth/setup',
'/v0/management/auth/login',
])

export function isPublicManagementPath(path: string): boolean {
return PUBLIC_AUTH_PATHS.has(path)
}

/**
* Management API Authentication Middleware
* Validates Bearer token from Authorization header or X-Management-Secret header
* Compares against managementApiSecret from config
* Returns 401 Unauthorized for invalid/missing authentication
*/
export async function managementAuthMiddleware(ctx: Context, next: Next): Promise<void> {
// First-run / login endpoints are intentionally public.
if (isPublicManagementPath(ctx.path)) {
await next()
return
}

const config = storeManager.getConfig()
const managementConfig = config.managementApi

Expand Down
Loading