mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(cli): add --require-auth flag for /_web/* API protection
When enabled, all /_web/* routes require Bearer token authentication, same as /api/v1/*. This hardens security for server/headless/public deployments where the same-origin assumption doesn't hold. - Add `require_auth` field to config schema (default: false) - Add `setRequireAuth()` to auth hook, conditionally enforce auth on /_web/* - Support `--require-auth` in both `chatlab start` and `--daemon` mode - Read from config.api.require_auth or CLI flag override Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
a6039d95ab
commit
a75fd0186d
5 files changed
+25
-6
No files matched your search
@@ -286,6 +286,7 @@ program
|
||||
.option('--host <host>', 'Listen address', '127.0.0.1')
|
||||
.option('--token <token>', 'Custom Bearer Token (reads from config or auto-generates if omitted)')
|
||||
.option('--headless', 'API-only mode, do not serve the Web UI')
|
||||
.option('--require-auth', 'Require Bearer token for all routes including /_web/*')
|
||||
.option('--no-open', 'Do not auto-open the browser')
|
||||
.option('--daemon', 'Run as a resident system service (auto-start on login, macOS/Linux)')
|
||||
.action(async (options) => {
|
||||
@@ -297,6 +298,7 @@ program
|
||||
host: options.host,
|
||||
token: options.token || undefined,
|
||||
headless: options.headless,
|
||||
requireAuth: options.requireAuth || undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -328,6 +330,7 @@ program
|
||||
host: options.host,
|
||||
token: options.token || undefined,
|
||||
webRoot,
|
||||
requireAuth: options.requireAuth || undefined,
|
||||
})
|
||||
|
||||
const { startPeriodicUpdateCheck } = await import('./update-checker')
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface ServiceInstallOptions {
|
||||
host?: string
|
||||
token?: string
|
||||
headless?: boolean
|
||||
requireAuth?: boolean
|
||||
}
|
||||
|
||||
export interface ServiceStatus {
|
||||
@@ -74,10 +75,9 @@ function buildPlist(options: ServiceInstallOptions): string {
|
||||
const args = ['start', '--no-open', '--port', String(port), '--host', host]
|
||||
if (options.token) args.push('--token', options.token)
|
||||
if (options.headless) args.push('--headless')
|
||||
if (options.requireAuth) args.push('--require-auth')
|
||||
|
||||
const programArgs = [process.execPath, cliEntry, ...args]
|
||||
.map((a) => ` <string>${a}</string>`)
|
||||
.join('\n')
|
||||
const programArgs = [process.execPath, cliEntry, ...args].map((a) => ` <string>${a}</string>`).join('\n')
|
||||
|
||||
fs.mkdirSync(path.join(SYSTEM_DIR, 'logs'), { recursive: true })
|
||||
|
||||
@@ -195,6 +195,7 @@ function buildUnit(options: ServiceInstallOptions): string {
|
||||
const args = ['start', '--no-open', '--port', String(port), '--host', host]
|
||||
if (options.token) args.push('--token', options.token)
|
||||
if (options.headless) args.push('--headless')
|
||||
if (options.requireAuth) args.push('--require-auth')
|
||||
const execStart = [process.execPath, cliEntry, ...args].map(escapeSystemdArg).join(' ')
|
||||
|
||||
return `[Unit]
|
||||
|
||||
@@ -10,6 +10,7 @@ import { unauthorized, errorResponse } from './errors'
|
||||
|
||||
let cachedToken: string | null = null
|
||||
let webModeEnabled = false
|
||||
let requireAuthEnabled = false
|
||||
|
||||
/**
|
||||
* 设置 auth hook 使用的 token(由 server 启动时注入)
|
||||
@@ -26,6 +27,14 @@ export function setWebMode(enabled: boolean): void {
|
||||
webModeEnabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* When enabled, /_web/* routes also require Bearer token (same as /api/*).
|
||||
* Used for server/headless deployments where same-origin assumption doesn't hold.
|
||||
*/
|
||||
export function setRequireAuth(enabled: boolean): void {
|
||||
requireAuthEnabled = enabled
|
||||
}
|
||||
|
||||
function safeTokenCompare(a: string, b: string): boolean {
|
||||
const bufA = Buffer.from(a)
|
||||
const bufB = Buffer.from(b)
|
||||
@@ -36,8 +45,8 @@ function safeTokenCompare(a: string, b: string): boolean {
|
||||
export async function authHook(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
if (!cachedToken) return
|
||||
|
||||
// /_web/ internal API: no auth required (same-origin Web UI only)
|
||||
if (request.url.startsWith('/_web/')) return
|
||||
// /_web/ internal API: skip auth unless require_auth is enabled
|
||||
if (request.url.startsWith('/_web/') && !requireAuthEnabled) return
|
||||
|
||||
// Web mode: only /api/ routes require auth; static files and SPA are public
|
||||
if (webModeEnabled && !request.url.startsWith('/api/')) return
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
verifyCliDataPath,
|
||||
} from '@openchatlab/node-runtime'
|
||||
import { createServer } from './server'
|
||||
import { setAuthToken, setWebMode } from './auth'
|
||||
import { setAuthToken, setRequireAuth, setWebMode } from './auth'
|
||||
import { registerSystemRoutes } from './routes/system'
|
||||
import { registerSessionRoutes } from './routes/sessions'
|
||||
import { registerWebRoutes } from './routes/web'
|
||||
@@ -40,6 +40,8 @@ export interface HttpServerOptions {
|
||||
token?: string
|
||||
/** dist-web/ 目录路径,启用后托管 Web SPA 静态资源 */
|
||||
webRoot?: string
|
||||
/** When true, /_web/* also requires Bearer token (for server/headless deployments) */
|
||||
requireAuth?: boolean
|
||||
}
|
||||
|
||||
function resolveNativeBinding(): string | undefined {
|
||||
@@ -122,6 +124,9 @@ export async function startHttpServer(options?: HttpServerOptions): Promise<{
|
||||
initServerAiLogger(pathProvider.getLogsDir())
|
||||
|
||||
setAuthToken(token)
|
||||
if (options?.requireAuth ?? config.api.require_auth) {
|
||||
setRequireAuth(true)
|
||||
}
|
||||
|
||||
server = createServer()
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const apiConfigSchema = z.object({
|
||||
port: z.number().int().min(1).max(65535).default(DEFAULT_API_PORT),
|
||||
host: z.string().default('127.0.0.1'),
|
||||
token: z.string().default(''),
|
||||
require_auth: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export const localeConfigSchema = z.object({
|
||||
|
||||
Reference in new issue
Block a user