diff --git a/bin/hermes-web-ui.mjs b/bin/hermes-web-ui.mjs index 9c87e27..654b45f 100755 --- a/bin/hermes-web-ui.mjs +++ b/bin/hermes-web-ui.mjs @@ -3,8 +3,9 @@ import { spawn, execSync, execFileSync } from 'child_process' import { resolve, dirname, join, delimiter } from 'path' import { fileURLToPath } from 'url' import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, chmodSync, statSync, existsSync, realpathSync } from 'fs' -import { randomBytes } from 'crypto' +import { randomBytes, scryptSync } from 'crypto' import { homedir } from 'os' +import { DatabaseSync } from 'node:sqlite' const __dirname = dirname(fileURLToPath(import.meta.url)) const __filename = fileURLToPath(import.meta.url) @@ -19,7 +20,11 @@ const PID_DIR = WEB_UI_HOME const PID_FILE = join(PID_DIR, 'server.pid') const LOG_FILE = join(PID_DIR, 'server.log') const TOKEN_FILE = join(PID_DIR, '.token') +const LOGIN_LOCK_FILE = join(WEB_UI_HOME, '.login-lock.json') +const WEB_UI_DB_FILE = join(WEB_UI_HOME, 'hermes-web-ui.db') const DEFAULT_PORT = 8648 +const DEFAULT_USERNAME = 'admin' +const DEFAULT_PASSWORD = '123456' // ─── Auto-fix node-pty native module ────────────────────────── function ensureNativeModules() { @@ -466,6 +471,86 @@ function showStatus() { } } +function clearLoginLocks(options = {}) { + const { silent = false, checkRunning = true } = options + const serverRunning = checkRunning ? !!getPid() : false + let removed = false + + try { + unlinkSync(LOGIN_LOCK_FILE) + removed = true + if (!silent) console.log(` ✓ Removed login lock file: ${LOGIN_LOCK_FILE}`) + } catch (err) { + if (err?.code === 'ENOENT') { + if (!silent) console.log(` ✓ No login lock file found: ${LOGIN_LOCK_FILE}`) + } else { + if (!silent) console.log(` ✗ Failed to remove login lock file: ${err.message}`) + throw err + } + } + + if (!silent && serverRunning) { + console.log(' ⚠ hermes-web-ui is running; restart it to clear in-memory login locks.') + console.log(' Run: hermes-web-ui restart') + } + + return { path: LOGIN_LOCK_FILE, removed, serverRunning } +} + +function hashPassword(password) { + const salt = randomBytes(16).toString('hex') + const hash = scryptSync(password, salt, 64).toString('hex') + return `scrypt:${salt}:${hash}` +} + +function resetDefaultLogin(options = {}) { + const { silent = false } = options + mkdirSync(WEB_UI_HOME, { recursive: true }) + const db = new DatabaseSync(WEB_UI_DB_FILE) + try { + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'admin', + status TEXT NOT NULL DEFAULT 'active', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_login_at INTEGER + ) + `) + + const now = Date.now() + const passwordHash = hashPassword(DEFAULT_PASSWORD) + const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(DEFAULT_USERNAME) + if (existing?.id) { + db.prepare( + `UPDATE users + SET password_hash = ?, role = 'super_admin', status = 'active', updated_at = ? + WHERE id = ?` + ).run(passwordHash, now, existing.id) + if (!silent) { + console.log(` ✓ Reset default login: ${DEFAULT_USERNAME} / ${DEFAULT_PASSWORD}`) + console.log(` Database: ${WEB_UI_DB_FILE}`) + } + return { path: WEB_UI_DB_FILE, username: DEFAULT_USERNAME, password: DEFAULT_PASSWORD, action: 'updated' } + } + + db.prepare( + `INSERT INTO users (username, password_hash, role, status, created_at, updated_at) + VALUES (?, ?, 'super_admin', 'active', ?, ?)` + ).run(DEFAULT_USERNAME, passwordHash, now, now) + if (!silent) { + console.log(` ✓ Created default login: ${DEFAULT_USERNAME} / ${DEFAULT_PASSWORD}`) + console.log(` Database: ${WEB_UI_DB_FILE}`) + } + return { path: WEB_UI_DB_FILE, username: DEFAULT_USERNAME, password: DEFAULT_PASSWORD, action: 'created' } + } finally { + db.close() + } +} + function main() { const command = process.argv[2] || 'start' @@ -485,6 +570,8 @@ Commands: stop Stop the server restart [port] Restart the server status Show server status + clear-login-locks Delete the login IP lock file + reset-default-login Create or reset the default login (${DEFAULT_USERNAME} / ${DEFAULT_PASSWORD}) update Update to latest version and restart upgrade Alias for update version Show version number @@ -493,6 +580,7 @@ Options: -v, --version Show version number -h, --help Show this help message --port Specify port (used with start/restart) + --restart Restart after clear-login-locks `) process.exit(0) } @@ -511,6 +599,19 @@ Options: case 'status': showStatus() break + case 'clear-login-locks': { + const restartAfterClear = process.argv.includes('--restart') + const result = clearLoginLocks() + if (restartAfterClear && result.serverRunning) { + const port = getRunningPort() ?? getPort() + stopDaemon() + setTimeout(() => startDaemon(port), 500) + } + break + } + case 'reset-default-login': + resetDefaultLogin() + break case 'update': case 'upgrade': doUpdate() @@ -599,7 +700,9 @@ if (process.argv[1] && realpathSync(resolve(process.argv[1])) === __filename) { } export { + clearLoginLocks, commandExists, getListeningPids, parseUnixNetstatListeningPids, + resetDefaultLogin, } diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue index 7324e72..896a44a 100644 --- a/packages/client/src/App.vue +++ b/packages/client/src/App.vue @@ -10,6 +10,7 @@ import { useKeyboard } from '@/composables/useKeyboard' import { useAppStore } from '@/stores/hermes/app' import SessionSearchModal from '@/components/hermes/chat/SessionSearchModal.vue' import AuthEventListener from '@/components/auth/AuthEventListener.vue' +import DefaultCredentialPrompt from '@/components/auth/DefaultCredentialPrompt.vue' const { isDark, isComic } = useTheme() const { t } = useI18n() @@ -73,6 +74,7 @@ useKeyboard() + diff --git a/packages/client/src/api/auth.ts b/packages/client/src/api/auth.ts index 3bb457b..c2ebfd8 100644 --- a/packages/client/src/api/auth.ts +++ b/packages/client/src/api/auth.ts @@ -36,6 +36,7 @@ export interface CurrentUser { created_at: number updated_at: number last_login_at: number | null + requiresCredentialChange?: boolean } export async function fetchCurrentUser(): Promise { diff --git a/packages/client/src/components/auth/DefaultCredentialPrompt.vue b/packages/client/src/components/auth/DefaultCredentialPrompt.vue new file mode 100644 index 0000000..e85b0b4 --- /dev/null +++ b/packages/client/src/components/auth/DefaultCredentialPrompt.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/packages/client/src/i18n/locales/de.ts b/packages/client/src/i18n/locales/de.ts index 1c0cb39..1b5f671 100644 --- a/packages/client/src/i18n/locales/de.ts +++ b/packages/client/src/i18n/locales/de.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'Token', usernamePlaceholder: 'Benutzername', passwordPlaceholder: 'Passwort', + defaultCredentialsHint: 'Standard-Benutzername: admin. Standard-Passwort: 123456.', credentialsRequired: 'Bitte Benutzername und Passwort eingeben', invalidCredentials: 'Ungultiger Benutzername oder Passwort', tooManyAttempts: 'Zu viele fehlgeschlagene Versuche, bitte versuchen Sie es spater erneut', @@ -37,6 +38,10 @@ export default { removeConfirm: 'Passwort-Login ist fur Benutzerkonten erforderlich.', passwordLoginNotConfigured: 'Passwort-Login ist nicht konfiguriert', passwordLoginConfigured: 'Aktuelles Konto: {username}', + defaultCredentialTitle: 'Standardkonto und Passwort andern', + defaultCredentialMessage: 'Das aktuelle Konto verwendet noch den Standard-Benutzernamen oder das Standard-Passwort. Um unbefugten Zugriff zu vermeiden, andern Sie Benutzername und Passwort des aktuellen Kontos so bald wie moglich.', + defaultCredentialAction: 'Jetzt andern', + defaultCredentialLater: 'Spater erinnern', }, users: { diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index d5b4608..4a86f04 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'Token', usernamePlaceholder: 'Username', passwordPlaceholder: 'Password', + defaultCredentialsHint: 'Default username: admin. Default password: 123456.', credentialsRequired: 'Please enter username and password', invalidCredentials: 'Invalid username or password', tooManyAttempts: 'Too many failed attempts, please try again later', @@ -37,6 +38,10 @@ export default { removeConfirm: 'Password login is required for user accounts.', passwordLoginNotConfigured: 'Password login is not configured', passwordLoginConfigured: 'Current account: {username}', + defaultCredentialTitle: 'Change the default account credentials', + defaultCredentialMessage: 'This account is still using the default username or password. To prevent unauthorized access, update the username and password as soon as possible.', + defaultCredentialAction: 'Update now', + defaultCredentialLater: 'Remind me later', }, users: { diff --git a/packages/client/src/i18n/locales/es.ts b/packages/client/src/i18n/locales/es.ts index ae2d327..dfcdc33 100644 --- a/packages/client/src/i18n/locales/es.ts +++ b/packages/client/src/i18n/locales/es.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'Token', usernamePlaceholder: 'Nombre de usuario', passwordPlaceholder: 'Contrasena', + defaultCredentialsHint: 'Nombre de usuario predeterminado: admin. Contrasena predeterminada: 123456.', credentialsRequired: 'Por favor, introduzca nombre de usuario y contrasena', invalidCredentials: 'Nombre de usuario o contrasena incorrectos', tooManyAttempts: 'Demasiados intentos fallidos, por favor intente mas tarde', @@ -37,6 +38,10 @@ export default { removeConfirm: 'El login con contrasena es obligatorio para las cuentas de usuario.', passwordLoginNotConfigured: 'Login con contrasena no configurado', passwordLoginConfigured: 'Cuenta actual: {username}', + defaultCredentialTitle: 'Cambia la cuenta y contrasena predeterminadas', + defaultCredentialMessage: 'La cuenta actual aun usa el nombre de usuario o la contrasena predeterminados. Para evitar accesos no autorizados, cambia cuanto antes el nombre de usuario y la contrasena de la cuenta actual.', + defaultCredentialAction: 'Cambiar ahora', + defaultCredentialLater: 'Recordar mas tarde', }, users: { diff --git a/packages/client/src/i18n/locales/fr.ts b/packages/client/src/i18n/locales/fr.ts index 5b9f946..4259e57 100644 --- a/packages/client/src/i18n/locales/fr.ts +++ b/packages/client/src/i18n/locales/fr.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'Jeton', usernamePlaceholder: 'Nom d\'utilisateur', passwordPlaceholder: 'Mot de passe', + defaultCredentialsHint: 'Nom d utilisateur par defaut : admin. Mot de passe par defaut : 123456.', credentialsRequired: 'Veuillez entrer le nom d\'utilisateur et le mot de passe', invalidCredentials: 'Nom d\'utilisateur ou mot de passe incorrect', tooManyAttempts: 'Trop de tentatives echouees, veuillez reessayer plus tard', @@ -37,6 +38,10 @@ export default { removeConfirm: 'Le login par mot de passe est requis pour les comptes utilisateur.', passwordLoginNotConfigured: 'Login par mot de passe non configure', passwordLoginConfigured: 'Compte actuel : {username}', + defaultCredentialTitle: 'Modifiez le compte et le mot de passe par defaut', + defaultCredentialMessage: 'Le compte connecte utilise encore le nom d utilisateur ou le mot de passe par defaut. Pour eviter tout acces non autorise, modifiez rapidement le nom d utilisateur et le mot de passe du compte actuel.', + defaultCredentialAction: 'Modifier maintenant', + defaultCredentialLater: 'Me le rappeler plus tard', }, users: { diff --git a/packages/client/src/i18n/locales/ja.ts b/packages/client/src/i18n/locales/ja.ts index 13d7d37..1007660 100644 --- a/packages/client/src/i18n/locales/ja.ts +++ b/packages/client/src/i18n/locales/ja.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'トークン', usernamePlaceholder: 'ユーザー名', passwordPlaceholder: 'パスワード', + defaultCredentialsHint: '既定のユーザー名:admin、既定のパスワード:123456', credentialsRequired: 'ユーザー名とパスワードを入力してください', invalidCredentials: 'ユーザー名またはパスワードが正しくありません', tooManyAttempts: 'ログイン試行回数が多すぎます。しばらくしてからお試しください', @@ -37,6 +38,10 @@ export default { removeConfirm: 'ユーザーアカウントにはパスワードログインが必要です。', passwordLoginNotConfigured: 'パスワードログイン未設定', passwordLoginConfigured: '現在のアカウント:{username}', + defaultCredentialTitle: '既定のアカウント情報を変更してください', + defaultCredentialMessage: '現在のログインアカウントは、既定のユーザー名または既定のパスワードをまだ使用しています。不正アクセスを防ぐため、できるだけ早く現在のアカウントでユーザー名とパスワードを変更してください。', + defaultCredentialAction: '変更する', + defaultCredentialLater: '後で通知', }, users: { diff --git a/packages/client/src/i18n/locales/ko.ts b/packages/client/src/i18n/locales/ko.ts index d6bdcc8..2475990 100644 --- a/packages/client/src/i18n/locales/ko.ts +++ b/packages/client/src/i18n/locales/ko.ts @@ -12,6 +12,7 @@ export default { tokenLogin: '토큰', usernamePlaceholder: '사용자 이름', passwordPlaceholder: '비밀번호', + defaultCredentialsHint: '기본 로그인 이름: admin, 기본 비밀번호: 123456', credentialsRequired: '사용자 이름과 비밀번호를 입력해 주세요', invalidCredentials: '사용자 이름 또는 비밀번호가 올바르지 않습니다', tooManyAttempts: '로그인 시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요', @@ -37,6 +38,10 @@ export default { removeConfirm: '사용자 계정에는 비밀번호 로그인이 필요합니다.', passwordLoginNotConfigured: '비밀번호 로그인 미설정', passwordLoginConfigured: '현재 계정: {username}', + defaultCredentialTitle: '기본 계정과 비밀번호를 변경하세요', + defaultCredentialMessage: '현재 로그인 계정이 아직 기본 사용자 이름 또는 기본 비밀번호를 사용하고 있습니다. 무단 접근을 방지하려면 현재 계정에서 사용자 이름과 비밀번호를 가능한 한 빨리 변경하세요.', + defaultCredentialAction: '변경하기', + defaultCredentialLater: '나중에 알림', }, users: { diff --git a/packages/client/src/i18n/locales/pt.ts b/packages/client/src/i18n/locales/pt.ts index 81ad7c5..742a331 100644 --- a/packages/client/src/i18n/locales/pt.ts +++ b/packages/client/src/i18n/locales/pt.ts @@ -12,6 +12,7 @@ export default { tokenLogin: 'Token', usernamePlaceholder: 'Nome de usuario', passwordPlaceholder: 'Senha', + defaultCredentialsHint: 'Nome de usuario padrao: admin. Senha padrao: 123456.', credentialsRequired: 'Por favor, insira nome de usuario e senha', invalidCredentials: 'Nome de usuario ou senha incorretos', tooManyAttempts: 'Muitas tentativas falhadas, por favor tente novamente mais tarde', @@ -37,6 +38,10 @@ export default { removeConfirm: 'Login por senha e obrigatorio para contas de usuario.', passwordLoginNotConfigured: 'Login por senha nao configurado', passwordLoginConfigured: 'Conta atual: {username}', + defaultCredentialTitle: 'Altere a conta e senha padrao', + defaultCredentialMessage: 'A conta atual ainda usa o nome de usuario ou a senha padrao. Para evitar acesso nao autorizado, altere o nome de usuario e a senha da conta atual o quanto antes.', + defaultCredentialAction: 'Alterar agora', + defaultCredentialLater: 'Lembrar depois', }, users: { diff --git a/packages/client/src/i18n/locales/zh-TW.ts b/packages/client/src/i18n/locales/zh-TW.ts index 731d7f9..20c6122 100644 --- a/packages/client/src/i18n/locales/zh-TW.ts +++ b/packages/client/src/i18n/locales/zh-TW.ts @@ -12,6 +12,7 @@ export default { tokenLogin: '權杖登入', usernamePlaceholder: '使用者名稱', passwordPlaceholder: '密碼', + defaultCredentialsHint: '預設登入名:admin,預設密碼:123456', credentialsRequired: '請輸入使用者名稱和密碼', invalidCredentials: '使用者名稱或密碼錯誤', tooManyAttempts: '登入失敗次數過多,請稍後再試', @@ -37,6 +38,10 @@ export default { removeConfirm: '使用者帳號必須保留密碼登入。', passwordLoginNotConfigured: '密碼登入未設定', passwordLoginConfigured: '目前帳號:{username}', + defaultCredentialTitle: '請修改預設帳號和密碼', + defaultCredentialMessage: '目前登入帳號仍在使用預設使用者名稱或預設密碼。為避免未授權存取,請盡快進入目前帳號修改使用者名稱和密碼。', + defaultCredentialAction: '去修改', + defaultCredentialLater: '稍後提醒', }, users: { diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 339edbc..223d194 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -12,6 +12,7 @@ export default { tokenLogin: '令牌登录', usernamePlaceholder: '用户名', passwordPlaceholder: '密码', + defaultCredentialsHint: '默认登录名:admin,默认密码:123456', credentialsRequired: '请输入用户名和密码', invalidCredentials: '用户名或密码错误', tooManyAttempts: '登录失败次数过多,请稍后重试', @@ -37,6 +38,10 @@ export default { removeConfirm: '用户账号必须保留密码登录。', passwordLoginNotConfigured: '密码登录未配置', passwordLoginConfigured: '当前账户:{username}', + defaultCredentialTitle: '请修改默认账户和密码', + defaultCredentialMessage: '当前登录账户仍在使用默认用户名或默认密码。为了避免未授权访问,请尽快进入当前账户修改用户名和密码。', + defaultCredentialAction: '去修改', + defaultCredentialLater: '稍后提醒', }, users: { diff --git a/packages/client/src/views/LoginView.vue b/packages/client/src/views/LoginView.vue index df32381..47c2447 100644 --- a/packages/client/src/views/LoginView.vue +++ b/packages/client/src/views/LoginView.vue @@ -63,6 +63,7 @@ async function handlePasswordLogin() {

{{ t("login.title") }}

+