Files
recap-relay/startos/actions/setAdminPassword.ts
T
2026-05-11 22:12:02 -05:00

108 lines
3.1 KiB
TypeScript

import { sdk } from '../sdk'
import { configFile } from '../file-models/config.json'
import { randomBytes, scryptSync } from 'crypto'
const { InputSpec, Value } = sdk
const SCRYPT_KEYLEN = 64
// Mirror of Recap's setAdminPassword — same shape so server-side
// admin-auth code can be lifted with minimal change.
const inputSpec = InputSpec.of({
relay_admin_username: Value.text({
name: 'Admin Username',
description: 'Username for the relay admin dashboard. Defaults to "admin".',
required: true,
default: 'admin',
minLength: 1,
maxLength: 64,
}),
relay_admin_password: Value.text({
name: 'Admin Password',
description:
'Password for the relay admin dashboard. Must be at least 8 characters. Leave blank to disable /admin entirely (useful while testing /relay/* endpoints).',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
relay_admin_password_confirm: Value.text({
name: 'Confirm Password',
description: 'Re-enter the password to confirm.',
required: false,
default: null,
masked: true,
minLength: 0,
maxLength: 256,
}),
})
export const setAdminPassword = sdk.Action.withInput(
'set-admin-password',
async ({ effects }) => ({
name: 'Set Admin Password',
description:
"Gate the relay's /admin dashboard. The public /relay/* endpoints are unaffected — they're per-call authenticated via X-Recap-Install-Id + Authorization headers.",
warning: null,
allowedStatuses: 'any',
group: 'Setup',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const config = await configFile.read().once()
return {
relay_admin_username: config?.relay_admin_username || 'admin',
relay_admin_password: undefined,
relay_admin_password_confirm: undefined,
}
},
async ({ effects, input }) => {
const username = (input.relay_admin_username || '').trim()
const password = input.relay_admin_password || ''
const confirm = input.relay_admin_password_confirm || ''
if (!username) throw new Error('Username is required.')
if (password === '' && confirm === '') {
await configFile.merge(effects, {
relay_admin_username: username,
relay_admin_password_hash: '',
relay_admin_password_salt: '',
})
return null
}
if (password !== confirm) {
throw new Error('Password and confirmation do not match.')
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters.')
}
const salt = randomBytes(16).toString('hex')
const hash = scryptSync(password, salt, SCRYPT_KEYLEN).toString('hex')
const existing = await configFile.read().once()
const sessionSecret =
existing?.relay_admin_session_secret &&
existing.relay_admin_session_secret.length > 0
? existing.relay_admin_session_secret
: randomBytes(32).toString('hex')
await configFile.merge(effects, {
relay_admin_username: username,
relay_admin_password_hash: hash,
relay_admin_password_salt: salt,
relay_admin_session_secret: sessionSecret,
})
return null
},
)