mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 13:30:14 +00:00
remove auth disabled support (#1013)
This commit is contained in:
@@ -134,7 +134,6 @@ Unified configuration for **8 platforms** in one page:
|
||||
- Username/password login with account management in Settings
|
||||
- Default bootstrap credentials are `admin` / `123456`; users are prompted after login to change the default username and password
|
||||
- Super administrators can manage users and profile bindings; regular administrators can manage their own account details
|
||||
- Auth can be disabled with `AUTH_DISABLED=1`
|
||||
|
||||
CLI maintenance commands:
|
||||
|
||||
@@ -240,7 +239,6 @@ These variables configure Hermes Web UI itself. Provider API keys and Hermes Age
|
||||
| `HERMES_WEB_UI_HOME` | `~/.hermes-web-ui` | Web UI data home for auth token, credentials, logs, DB, and default uploads. `HERMES_WEBUI_STATE_DIR` is also supported as a compatibility alias. |
|
||||
| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | Upload root override. Files are stored below profile-scoped subdirectories. |
|
||||
| `CORS_ORIGINS` | `*` | Koa CORS origin setting. |
|
||||
| `AUTH_DISABLED` | unset | Set to `1` or `true` to disable Web UI auth. |
|
||||
| `AUTH_TOKEN` | auto-generated | Explicit bearer token. If unset, Web UI creates one under `HERMES_WEB_UI_HOME`. |
|
||||
| `PROFILE` | `default` | Startup/default Hermes profile. Runtime requests use the profile selected by the frontend and authorized for the current account. |
|
||||
| `LOG_LEVEL` | `info` | Server log level. |
|
||||
|
||||
@@ -142,7 +142,6 @@
|
||||
- 用户名/密码登录,并在设置页提供账户管理
|
||||
- 默认登录名/密码为 `admin` / `123456`;登录后会提示尽快修改默认账户和密码
|
||||
- 超级管理员可以管理用户和 Profile 绑定;普通管理员只能管理自己的账户信息
|
||||
- 可通过 `AUTH_DISABLED=1` 禁用认证
|
||||
|
||||
CLI 维护命令:
|
||||
|
||||
@@ -247,7 +246,6 @@ Web UI 启动后端聊天能力时,会优先使用包含 `run_agent.py` 的源
|
||||
| `HERMES_WEB_UI_HOME` | `~/.hermes-web-ui` | Web UI 数据目录,用于认证 token、登录凭据、日志、数据库和默认上传目录。兼容支持 `HERMES_WEBUI_STATE_DIR` 作为别名。 |
|
||||
| `UPLOAD_DIR` | `$HERMES_WEB_UI_HOME/upload` | 覆盖上传根目录。文件会保存在按 Profile 隔离的子目录下。 |
|
||||
| `CORS_ORIGINS` | `*` | Koa CORS origin 配置。 |
|
||||
| `AUTH_DISABLED` | 未设置 | 设置为 `1` 或 `true` 可关闭 Web UI 认证。 |
|
||||
| `AUTH_TOKEN` | 自动生成 | 显式指定 bearer token。未设置时,Web UI 会在 `HERMES_WEB_UI_HOME` 下自动生成。 |
|
||||
| `PROFILE` | `default` | 启动/默认 Hermes profile。运行时请求使用前端当前选择且当前账号有权限访问的 Profile。 |
|
||||
| `LOG_LEVEL` | `info` | Server 日志级别。 |
|
||||
|
||||
@@ -43,8 +43,7 @@ function getToken() {
|
||||
}
|
||||
|
||||
function ensureToken() {
|
||||
// If AUTH_DISABLED or AUTH_TOKEN is set, let server handle it
|
||||
if (process.env.AUTH_DISABLED === '1' || process.env.AUTH_DISABLED === 'true') return null
|
||||
// If AUTH_TOKEN is set, let server handle it.
|
||||
if (process.env.AUTH_TOKEN) return process.env.AUTH_TOKEN
|
||||
|
||||
let token = getToken()
|
||||
|
||||
@@ -18,7 +18,6 @@ services:
|
||||
- HERMES_WEB_UI_MANAGED_GATEWAY=1
|
||||
- HERMES_WEB_UI_XAI_CALLBACK_BIND_HOST=0.0.0.0
|
||||
- PATH=/opt/hermes/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
- AUTH_DISABLED=${AUTH_DISABLED:-false}
|
||||
- HERMES_ALLOW_ROOT_GATEWAY=1
|
||||
restart: unless-stopped
|
||||
stdin_open: true
|
||||
|
||||
@@ -220,7 +220,7 @@ io(`${baseUrl}/chat-run`, {
|
||||
})
|
||||
```
|
||||
|
||||
如果未设置 `AUTH_DISABLED=1`,服务端会与 Web UI token 比对。
|
||||
服务端会与 Web UI token 比对。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-6
@@ -40,14 +40,11 @@ All key runtime settings are configured from compose variables.
|
||||
| `HERMES_AGENT_IMAGE` | `nousresearch/hermes-agent:latest` | Hermes Agent base image (used only during build) |
|
||||
| `WEBUI_IMAGE` | `hermes-web-ui-local:latest` | Web UI image (set to `ekkoye8888/hermes-web-ui` to use pre-built) |
|
||||
| `HERMES_DATA_DIR` | `./hermes_data` | Hermes runtime data directory |
|
||||
| `AUTH_DISABLED` | `false` | Set to `true` to disable login authentication |
|
||||
|
||||
Override variables directly from shell:
|
||||
|
||||
```bash
|
||||
PORT=16060 \
|
||||
AUTH_DISABLED=true \
|
||||
docker compose up -d
|
||||
PORT=16060 docker compose up -d
|
||||
```
|
||||
|
||||
Or create a `.env` file in the project root:
|
||||
@@ -55,7 +52,6 @@ Or create a `.env` file in the project root:
|
||||
```
|
||||
WEBUI_IMAGE=ekkoye8888/hermes-web-ui
|
||||
PORT=6060
|
||||
AUTH_DISABLED=false
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
@@ -67,7 +63,7 @@ AUTH_DISABLED=false
|
||||
|
||||
- Hermes data persists in `./hermes_data`, mapped to `/home/agent/.hermes` in the container.
|
||||
- Web UI data persists in `./hermes_data/hermes-web-ui/`, mapped to `/home/agent/.hermes-web-ui` in the container.
|
||||
- When `AUTH_DISABLED=false`, the auth token is auto-generated on first run and printed to container logs.
|
||||
- The auth token is auto-generated on first run and printed to container logs.
|
||||
- Deleting the token file and restarting will generate a new one.
|
||||
|
||||
## Port Mapping
|
||||
|
||||
@@ -16,7 +16,6 @@ import { homedir } from 'os'
|
||||
* - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload').
|
||||
*
|
||||
* Auth:
|
||||
* - AUTH_DISABLED: Set to 1 or true to disable Web UI auth.
|
||||
* - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME.
|
||||
*
|
||||
* Runtime behavior:
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function login(ctx: Context) {
|
||||
token = await issueUserJwt(user)
|
||||
} catch (err: any) {
|
||||
ctx.status = 500
|
||||
ctx.body = { error: err?.message || 'Auth is disabled on this server' }
|
||||
ctx.body = { error: err?.message || 'Failed to issue login token' }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ function safeEqual(a: string, b: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
async function getJwtSecret(): Promise<string | null> {
|
||||
async function getJwtSecret(): Promise<string> {
|
||||
return process.env.AUTH_JWT_SECRET || await getToken()
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ const SERVER_TOKEN_MEDIA_PATHS = new Set([
|
||||
async function allowServerTokenForMedia(ctx: Context, token: string): Promise<boolean> {
|
||||
if (!token || !SERVER_TOKEN_MEDIA_PATHS.has(ctx.path)) return false
|
||||
const serverToken = await getToken()
|
||||
if (!serverToken || token !== serverToken) return false
|
||||
if (token !== serverToken) return false
|
||||
ctx.state.serverTokenAuth = true
|
||||
return true
|
||||
}
|
||||
@@ -128,7 +128,6 @@ export function verifyUserJwt(token: string, secret: string, now = Date.now()):
|
||||
|
||||
export async function issueUserJwt(user: Pick<UserRecord, 'id' | 'username' | 'role'>): Promise<string> {
|
||||
const secret = await getJwtSecret()
|
||||
if (!secret) throw new Error('Auth is disabled on this server')
|
||||
return signUserJwt(user, secret)
|
||||
}
|
||||
|
||||
@@ -146,7 +145,6 @@ export function toAuthenticatedUser(user: Pick<UserRecord, 'id' | 'username' | '
|
||||
|
||||
export async function authenticateUserToken(token: string): Promise<AuthenticatedUser | null> {
|
||||
const secret = await getJwtSecret()
|
||||
if (!secret) return null
|
||||
|
||||
const payload = token ? verifyUserJwt(token, secret) : null
|
||||
if (!payload) return null
|
||||
@@ -157,7 +155,8 @@ export async function authenticateUserToken(token: string): Promise<Authenticate
|
||||
}
|
||||
|
||||
export async function isAuthEnabled(): Promise<boolean> {
|
||||
return !!await getJwtSecret()
|
||||
await getJwtSecret()
|
||||
return true
|
||||
}
|
||||
|
||||
export async function requireUserJwt(ctx: Context, next: Next): Promise<void> {
|
||||
@@ -167,11 +166,6 @@ export async function requireUserJwt(ctx: Context, next: Next): Promise<void> {
|
||||
}
|
||||
|
||||
const secret = await getJwtSecret()
|
||||
if (!secret) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
const token = requestToken(ctx)
|
||||
const payload = token ? verifyUserJwt(token, secret) : null
|
||||
if (!payload) {
|
||||
|
||||
@@ -12,13 +12,9 @@ function generateToken(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the auth token. Returns null if auth is disabled.
|
||||
* Get or create the auth token.
|
||||
*/
|
||||
export async function getToken(): Promise<string | null> {
|
||||
if (process.env.AUTH_DISABLED === '1' || process.env.AUTH_DISABLED === 'true') {
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getToken(): Promise<string> {
|
||||
if (process.env.AUTH_TOKEN) {
|
||||
return process.env.AUTH_TOKEN
|
||||
}
|
||||
@@ -45,11 +41,6 @@ export async function getToken(): Promise<string | null> {
|
||||
*/
|
||||
export function requireAuth(token: string | null) {
|
||||
return async (ctx: any, next: () => Promise<void>) => {
|
||||
if (!token) {
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
const auth = ctx.headers.authorization || ''
|
||||
const provided = auth.startsWith('Bearer ')
|
||||
? auth.slice(7)
|
||||
|
||||
@@ -133,7 +133,6 @@ export default {
|
||||
envVars: {
|
||||
title: 'Environment Variables',
|
||||
rows: [
|
||||
['AUTH_DISABLED', 'Set to "1" to disable authentication'],
|
||||
['AUTH_TOKEN', 'Custom auth token (overrides auto-generated)'],
|
||||
['PORT', 'Server listen port (default: 8648)'],
|
||||
['BIND_HOST', 'Server bind host (default: 0.0.0.0). Set :: explicitly to enable IPv6 listening.'],
|
||||
|
||||
@@ -133,7 +133,6 @@ export default {
|
||||
envVars: {
|
||||
title: '环境变量',
|
||||
rows: [
|
||||
['AUTH_DISABLED', '设为 "1" 禁用认证'],
|
||||
['AUTH_TOKEN', '自定义认证令牌(覆盖自动生成的令牌)'],
|
||||
['PORT', '服务器监听端口(默认:8648)'],
|
||||
['BIND_HOST', '服务器绑定地址(默认:0.0.0.0)。如需 IPv6,请显式设置为 ::。'],
|
||||
|
||||
@@ -50,21 +50,17 @@ describe('Auth Service', () => {
|
||||
})
|
||||
|
||||
describe('getToken', () => {
|
||||
it('returns null when AUTH_DISABLED=1', async () => {
|
||||
it('ignores legacy AUTH_DISABLED=1 and still creates an auth token', async () => {
|
||||
process.env.AUTH_DISABLED = '1'
|
||||
const { getToken, mocks } = await loadAuth()
|
||||
const readFile = vi.fn().mockRejectedValue(new Error('ENOENT'))
|
||||
const writeFile = vi.fn()
|
||||
const mkdir = vi.fn()
|
||||
const { getToken } = await loadAuth({ readFile, writeFile, mkdir })
|
||||
|
||||
const token = await getToken()
|
||||
|
||||
expect(token).toBeNull()
|
||||
expect(mocks.readFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns null when AUTH_DISABLED=true', async () => {
|
||||
process.env.AUTH_DISABLED = 'true'
|
||||
const { getToken } = await loadAuth()
|
||||
|
||||
await expect(getToken()).resolves.toBeNull()
|
||||
expect(token).toMatch(/^[a-f0-9]{64}$/)
|
||||
expect(writeFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns AUTH_TOKEN env var if set', async () => {
|
||||
@@ -108,17 +104,6 @@ describe('Auth Service', () => {
|
||||
})
|
||||
|
||||
describe('requireAuth', () => {
|
||||
it('allows all requests when auth is disabled (null token)', async () => {
|
||||
const { requireAuth } = await loadAuth()
|
||||
const middleware = requireAuth(null)
|
||||
const ctx = createMockCtx('/api/hermes/sessions')
|
||||
const next = vi.fn(async () => {})
|
||||
|
||||
await middleware(ctx, next)
|
||||
|
||||
expect(next).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('skips /health', async () => {
|
||||
const { requireAuth } = await loadAuth()
|
||||
const middleware = requireAuth('secret')
|
||||
|
||||
Reference in New Issue
Block a user