Add default credential reset safeguards
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { scryptSync, timingSafeEqual } from 'crypto'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
|
||||
type ChildProcessMocks = {
|
||||
execFileSync: ReturnType<typeof vi.fn>
|
||||
@@ -21,10 +26,20 @@ async function loadCli(overrides: Partial<ChildProcessMocks> = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function verifyPassword(password: string, passwordHash: string): boolean {
|
||||
const [scheme, salt, expectedHex] = passwordHash.split(':')
|
||||
if (scheme !== 'scrypt' || !salt || !expectedHex) return false
|
||||
const expected = Buffer.from(expectedHex, 'hex')
|
||||
const actual = scryptSync(password, salt, expected.length)
|
||||
return actual.length === expected.length && timingSafeEqual(actual, expected)
|
||||
}
|
||||
|
||||
describe('CLI port detection', () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
vi.doUnmock('child_process')
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
@@ -89,4 +104,61 @@ describe('CLI port detection', () => {
|
||||
8648,
|
||||
)).toEqual([2468])
|
||||
})
|
||||
|
||||
it('clears the login lock file from the configured Web UI home', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'hermes-web-ui-cli-locks-'))
|
||||
process.env.HERMES_WEB_UI_HOME = home
|
||||
const lockFile = join(home, '.login-lock.json')
|
||||
writeFileSync(lockFile, '{"passwordIpMap":{}}\n')
|
||||
|
||||
try {
|
||||
const { clearLoginLocks } = await loadCli()
|
||||
const result = clearLoginLocks({ silent: true, checkRunning: false })
|
||||
|
||||
expect(result).toEqual({ path: lockFile, removed: true, serverRunning: false })
|
||||
expect(existsSync(lockFile)).toBe(false)
|
||||
|
||||
const second = clearLoginLocks({ silent: true, checkRunning: false })
|
||||
expect(second).toEqual({ path: lockFile, removed: false, serverRunning: false })
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('resets an existing admin user to the default password', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'hermes-web-ui-cli-default-login-'))
|
||||
process.env.HERMES_WEB_UI_HOME = home
|
||||
const dbPath = join(home, 'hermes-web-ui.db')
|
||||
|
||||
try {
|
||||
const { resetDefaultLogin } = await loadCli()
|
||||
const created = resetDefaultLogin({ silent: true })
|
||||
expect(created.action).toBe('created')
|
||||
|
||||
const db = new DatabaseSync(dbPath)
|
||||
try {
|
||||
const initial = db.prepare('SELECT id, username, password_hash FROM users WHERE username = ?').get('admin') as any
|
||||
expect(verifyPassword('123456', initial.password_hash)).toBe(true)
|
||||
db.prepare('UPDATE users SET password_hash = ? WHERE username = ?').run('scrypt:bad:bad', 'admin')
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
|
||||
const updated = resetDefaultLogin({ silent: true })
|
||||
expect(updated.action).toBe('updated')
|
||||
|
||||
const verifyDb = new DatabaseSync(dbPath)
|
||||
try {
|
||||
const rows = verifyDb.prepare('SELECT id, username, password_hash, role, status FROM users WHERE username = ?').all('admin') as any[]
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(verifyPassword('123456', rows[0].password_hash)).toBe(true)
|
||||
expect(rows[0].role).toBe('super_admin')
|
||||
expect(rows[0].status).toBe('active')
|
||||
} finally {
|
||||
verifyDb.close()
|
||||
}
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user