108 lines
3.1 KiB
TypeScript
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: null,
|
|
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
|
|
},
|
|
)
|