Add user-scoped Hermes profile access

This commit is contained in:
ekko
2026-05-24 10:11:03 +08:00
committed by ekko
parent 56e7716302
commit 3f6a25d8f1
54 changed files with 2656 additions and 592 deletions
+6
View File
@@ -1,5 +1,6 @@
import Router from '@koa/router'
import * as ctrl from '../controllers/auth'
import { requireSuperAdmin } from '../middleware/user-auth'
// Public routes (no auth required)
export const authPublicRoutes = new Router()
@@ -9,8 +10,13 @@ authPublicRoutes.post('/api/auth/login', ctrl.login)
// Protected routes (auth required)
export const authProtectedRoutes = new Router()
authProtectedRoutes.post('/api/auth/setup', ctrl.setupPassword)
authProtectedRoutes.get('/api/auth/me', ctrl.currentUser)
authProtectedRoutes.post('/api/auth/change-password', ctrl.changePassword)
authProtectedRoutes.post('/api/auth/change-username', ctrl.changeUsername)
authProtectedRoutes.delete('/api/auth/password', ctrl.removePassword)
authProtectedRoutes.get('/api/auth/users', requireSuperAdmin, ctrl.listManagedUsers)
authProtectedRoutes.post('/api/auth/users', requireSuperAdmin, ctrl.createManagedUser)
authProtectedRoutes.put('/api/auth/users/:id', requireSuperAdmin, ctrl.updateManagedUser)
authProtectedRoutes.delete('/api/auth/users/:id', requireSuperAdmin, ctrl.deleteManagedUser)
authProtectedRoutes.get('/api/auth/locked-ips', ctrl.listLockedIps)
authProtectedRoutes.delete('/api/auth/locked-ips', ctrl.unlockIpHandler)
@@ -1,12 +1,14 @@
import { WebSocketServer } from 'ws'
import type { WebSocket } from 'ws'
import type { Server as HttpServer, IncomingMessage } from 'http'
import { getToken } from '../../services/auth'
import { authenticateUserToken, isAuthEnabled } from '../../middleware/user-auth'
import { userCanAccessProfile } from '../../db/hermes/users-store'
import { logger } from '../../services/logger'
import * as kanbanCli from '../../services/hermes/hermes-kanban'
interface KanbanEventsRequest extends IncomingMessage {
kanbanBoard?: string
kanbanProfile?: string
}
function sendJson(ws: WebSocket, payload: Record<string, unknown>) {
@@ -35,14 +37,21 @@ export function setupKanbanEventsWebSocket(httpServers: HttpServer | HttpServer[
const url = new URL(req.url || '', `http://${req.headers.host}`)
if (url.pathname !== '/api/hermes/kanban/events') return
const authToken = await getToken()
if (authToken) {
if (await isAuthEnabled()) {
const token = url.searchParams.get('token') || ''
if (token !== authToken) {
const user = await authenticateUserToken(token)
if (!user) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy()
return
}
const profile = (url.searchParams.get('profile') || '').trim()
if (profile && user.role !== 'super_admin' && !userCanAccessProfile(user.id, profile)) {
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n')
socket.destroy()
return
}
req.kanbanProfile = profile || undefined
}
try {
@@ -74,7 +83,7 @@ export function setupKanbanEventsWebSocket(httpServers: HttpServer | HttpServer[
child.stdout?.on('data', streamLines((line) => {
if (line.toLowerCase().startsWith('watching kanban events')) return
sendJson(ws, { type: 'event', board, line })
sendJson(ws, { type: 'event', board })
}))
child.stderr?.on('data', streamLines((line) => {
@@ -1,6 +1,7 @@
import Router from '@koa/router'
import * as ctrl from '../../controllers/hermes/performance-monitor'
import { requireSuperAdmin } from '../../middleware/user-auth'
export const performanceMonitorRoutes = new Router()
performanceMonitorRoutes.get('/api/hermes/performance/runtime', ctrl.runtime)
performanceMonitorRoutes.get('/api/hermes/performance/runtime', requireSuperAdmin, ctrl.runtime)
@@ -1,5 +1,6 @@
import Router from '@koa/router'
import * as ctrl from '../../controllers/hermes/profiles'
import { requireSuperAdmin } from '../../middleware/user-auth'
export const profileRoutes = new Router()
@@ -14,6 +15,6 @@ profileRoutes.delete('/api/hermes/profiles/:name/avatar', ctrl.deleteAvatar)
profileRoutes.get('/api/hermes/profiles/:name', ctrl.get)
profileRoutes.delete('/api/hermes/profiles/:name', ctrl.remove)
profileRoutes.post('/api/hermes/profiles/:name/rename', ctrl.rename)
profileRoutes.put('/api/hermes/profiles/active', ctrl.switchProfile)
profileRoutes.put('/api/hermes/profiles/active', requireSuperAdmin, ctrl.switchProfile)
profileRoutes.post('/api/hermes/profiles/:name/export', ctrl.exportProfile)
profileRoutes.post('/api/hermes/profiles/import', ctrl.importProfile)
@@ -5,7 +5,7 @@ import { dirname, join, isAbsolute, resolve as resolvePath } from 'path'
import { homedir } from 'os'
import { getActiveProfileDir } from '../../services/hermes/hermes-profile'
import { getTerminalConfig, type TerminalConfig } from '../../services/hermes/file-provider'
import { getToken } from '../../services/auth'
import { authenticateUserToken, isAuthEnabled } from '../../middleware/user-auth'
import { logger } from '../../services/logger'
let pty: any = null
@@ -151,10 +151,9 @@ export function setupTerminalWebSocket(httpServers: HttpServer | HttpServer[]) {
}
// Auth check
const authToken = await getToken()
if (authToken) {
if (await isAuthEnabled()) {
const token = url.searchParams.get('token') || ''
if (token !== authToken) {
if (!await authenticateUserToken(token)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
socket.destroy()
return
+2 -2
View File
@@ -38,7 +38,7 @@ import { performanceMonitorRoutes } from './hermes/performance-monitor'
* Public routes are registered first, then auth middleware,
* then all protected routes. Returns the proxy middleware (must be mounted last).
*/
export function registerRoutes(app: any, requireAuth: (ctx: Context, next: Next) => Promise<void>) {
export function registerRoutes(app: any, authMiddleware: Array<(ctx: Context, next: Next) => Promise<void>>) {
// --- Public routes (no auth required) ---
app.use(healthRoutes.routes())
app.use(webhookRoutes.routes())
@@ -46,7 +46,7 @@ export function registerRoutes(app: any, requireAuth: (ctx: Context, next: Next)
app.use(ttsRoutes.routes()) // TTS proxy/generation — must be before auth
// --- Auth middleware: all routes below require authentication ---
app.use(requireAuth)
authMiddleware.forEach((middleware) => app.use(middleware))
// --- Protected routes (auth required) ---
app.use(authProtectedRoutes.routes())