- 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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user