fix: auth bypass, SPA serving, and provider improvements (#97)
* feat(chat): polish syntax highlighting and tool payload rendering (#94) * [verified] feat(chat): polish syntax highlighting and tool payload rendering * [verified] fix(chat): tighten large tool payload rendering * docs: update data volume path in Docker docs Align documentation with docker-compose.yml change: hermes-web-ui-data -> hermes-web-ui, /app/dist/data -> /root/.hermes-web-ui Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: bundle server build and restructure service modules - Add build-server.mjs script for standalone server compilation - Add logger service with structured output - Restructure auth, gateway-manager, hermes-cli, hermes services - Update docker-compose volume mount path - Update tsconfig and entry point for bundled server Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: separate controllers from routes and centralize route registration - Extract business logic from route handlers into controllers/ - Add centralized route registry in routes/index.ts with public/auth/protected layers - Replace global auth whitelist with sequential middleware registration - Extract shared helpers to services/config-helpers.ts - Allow custom provider name to be user-editable in ProviderFormModal - Deduplicate custom providers by poolKey instead of base_url in getAvailable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: auth bypass via path case, SPA serving, and provider improvements - Fix auth bypass: path case-insensitive check for /api, /v1, /upload - Fix SPA returning 401: skip auth for non-API paths (static files) - Fix profile switch: use local loading state instead of shared store ref - Auto-append /v1 to base_url when fetching models (frontend + backend) - Guard .env writing to built-in providers only - Add builtin field to provider presets, enable base_url input in form - Print auth token to console on startup (pino only writes to file) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Zhicheng Han <43314240+hanzckernel@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,45 +6,57 @@ import send from 'koa-send'
|
||||
import os from 'os'
|
||||
import { resolve } from 'path'
|
||||
import { mkdir } from 'fs/promises'
|
||||
import { readFileSync } from 'fs'
|
||||
import { config } from './config'
|
||||
import { hermesRoutes, setupTerminalWebSocket, proxyMiddleware } from './routes/hermes'
|
||||
import { uploadRoutes } from './routes/upload'
|
||||
import { webhookRoutes } from './routes/webhook'
|
||||
import { updateRoutes } from './routes/update'
|
||||
import { healthRoutes, startVersionCheck } from './routes/health'
|
||||
import { getToken, authMiddleware } from './services/auth'
|
||||
import { getToken, requireAuth } from './services/auth'
|
||||
import { initGatewayManager } from './services/gateway-bootstrap'
|
||||
import { bindShutdown } from './services/shutdown'
|
||||
import { setupTerminalWebSocket } from './routes/hermes/terminal'
|
||||
import { startVersionCheck } from './routes/health'
|
||||
import { registerRoutes } from './routes'
|
||||
import { logger } from './services/logger'
|
||||
|
||||
// Injected by esbuild at build time; fallback to reading package.json in dev mode
|
||||
declare const __APP_VERSION__: string
|
||||
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined'
|
||||
? __APP_VERSION__
|
||||
: (() => { try { return JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8')).version } catch { return 'dev' } } )()
|
||||
|
||||
// Global error handlers
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.fatal(err, 'Uncaught exception')
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logger.error(reason, 'Unhandled rejection')
|
||||
})
|
||||
|
||||
let server: any = null
|
||||
|
||||
export async function bootstrap() {
|
||||
console.log(`hermes-web-ui v${APP_VERSION} starting...`)
|
||||
await mkdir(config.uploadDir, { recursive: true })
|
||||
await mkdir(config.dataDir, { recursive: true })
|
||||
|
||||
const authToken = await getToken()
|
||||
const app = new Koa()
|
||||
|
||||
if (authToken) {
|
||||
app.use(await authMiddleware(authToken))
|
||||
console.log(`🔐 Auth enabled — token: ${authToken}`)
|
||||
}
|
||||
|
||||
await initGatewayManager()
|
||||
console.log('[bootstrap] gateway manager initialized')
|
||||
app.use(cors({ origin: config.corsOrigins }))
|
||||
app.use(bodyParser())
|
||||
console.log('[bootstrap] cors + bodyParser registered')
|
||||
|
||||
// Shared routes (no agent prefix)
|
||||
app.use(webhookRoutes.routes())
|
||||
app.use(uploadRoutes.routes())
|
||||
app.use(updateRoutes.routes())
|
||||
|
||||
// Hermes routes (must be after update — proxy catch-all matches everything)
|
||||
app.use(hermesRoutes.routes())
|
||||
// Register all routes (handles auth internally)
|
||||
const proxyMiddleware = registerRoutes(app, requireAuth(authToken))
|
||||
app.use(proxyMiddleware)
|
||||
console.log('[bootstrap] routes registered')
|
||||
|
||||
// Health check
|
||||
app.use(healthRoutes.routes())
|
||||
if (authToken) {
|
||||
console.log(`Auth enabled — token: ${authToken}`)
|
||||
logger.info('Auth enabled — token: %s', authToken)
|
||||
}
|
||||
|
||||
// SPA fallback
|
||||
const distDir = resolve(__dirname, '..', 'client')
|
||||
@@ -57,21 +69,29 @@ export async function bootstrap() {
|
||||
await send(ctx, 'index.html', { root: distDir })
|
||||
}
|
||||
})
|
||||
console.log('[bootstrap] SPA fallback registered')
|
||||
|
||||
// Start server
|
||||
console.log(`[bootstrap] listening on port ${config.port}`)
|
||||
server = app.listen(config.port, '0.0.0.0')
|
||||
console.log('[bootstrap] app.listen called')
|
||||
|
||||
setupTerminalWebSocket(server)
|
||||
console.log('[bootstrap] terminal websocket setup')
|
||||
|
||||
server.on('listening', () => {
|
||||
const interfaces = os.networkInterfaces()
|
||||
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
||||
console.log(`➜ Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
||||
console.log(`➜ Upstream: ${config.upstream}`)
|
||||
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
||||
console.log(`Upstream: ${config.upstream}`)
|
||||
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
||||
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
||||
logger.info('Upstream: %s', config.upstream)
|
||||
})
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
console.error('Server error:', err.message)
|
||||
console.error('[bootstrap] server error:', err.code || err.message)
|
||||
logger.error({ err }, 'Server error')
|
||||
})
|
||||
|
||||
bindShutdown(server)
|
||||
|
||||
Reference in New Issue
Block a user