Files
Hermes-ui/packages/server/src/services/credentials.ts
T
ekko 70ddbd0bcd feat: add username/password login, account settings, and changelog (#133) (#134)
- Add username/password login as additional auth mechanism alongside existing token
- First login must use token; password can be configured in Settings > Account
- Password login returns the existing static token (no auth middleware changes)
- Add account settings: setup, change password, change username, remove password
- Add logout button to sidebar footer
- Add version changelog popup (click version number in sidebar)
- Support all 8 locales (en, zh, de, es, fr, ja, ko, pt)
- Bump version to 0.4.3

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 20:27:33 +08:00

60 lines
1.8 KiB
TypeScript

import { readFile, writeFile, mkdir, unlink } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import { scryptSync, randomBytes } from 'node:crypto'
const APP_HOME = join(homedir(), '.hermes-web-ui')
const CREDENTIALS_FILE = join(APP_HOME, '.credentials')
export interface Credentials {
username: string
password_hash: string
salt: string
created_at: number
}
const SCRYPT_OPTIONS = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }
function hashPassword(password: string, salt: string): string {
return scryptSync(password, salt, 64, SCRYPT_OPTIONS).toString('hex')
}
export async function getCredentials(): Promise<Credentials | null> {
try {
const data = await readFile(CREDENTIALS_FILE, 'utf-8')
return JSON.parse(data)
} catch {
return null
}
}
export async function setCredentials(username: string, password: string): Promise<Credentials> {
const salt = randomBytes(16).toString('hex')
const password_hash = hashPassword(password, salt)
const cred: Credentials = { username, password_hash, salt, created_at: Date.now() }
await mkdir(APP_HOME, { recursive: true })
await writeFile(CREDENTIALS_FILE, JSON.stringify(cred, null, 2), { mode: 0o600 })
return cred
}
export async function deleteCredentials(): Promise<void> {
try {
await unlink(CREDENTIALS_FILE)
} catch {
// File may not exist
}
}
export async function verifyCredentials(username: string, password: string): Promise<boolean> {
const cred = await getCredentials()
if (!cred) return false
if (cred.username !== username) return false
const computed = hashPassword(password, cred.salt)
return computed === cred.password_hash
}
export function credentialsFileExists(): boolean {
return existsSync(CREDENTIALS_FILE)
}