70ddbd0bcd
- 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>
60 lines
1.8 KiB
TypeScript
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)
|
|
}
|