Add user-scoped Hermes profile access
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user