63 lines
4.2 KiB
JavaScript
63 lines
4.2 KiB
JavaScript
const assert = require('node:assert/strict')
|
|||
|
|
const fs = require('node:fs')
|
||
|
|
const path = require('node:path')
|
||
|
|
const { execFileSync } = require('node:child_process')
|
||
|
|
|
||
|
|
const root = path.resolve(__dirname, '../..')
|
||
|
|
const credentialsPath = path.join(root, 'acceptance/super-admin-login.json')
|
||
|
|
const base = 'http://127.0.0.1:18080/api/v1'
|
||
|
|
const literal = value => `'${String(value).replaceAll("'", "''")}'`
|
||
|
|
const sql = statement => execFileSync('docker', ['--context', 'colima-yawan', 'exec', '-i', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915', '-v', 'ON_ERROR_STOP=1', '-At'], { input: statement, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
||
|
|
|
||
|
|
async function request(route, data, token) {
|
||
|
|
const response = await fetch(base + route, {
|
||
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||
|
|
body: JSON.stringify(data), signal: AbortSignal.timeout(30000),
|
||
|
|
})
|
||
|
|
const body = await response.json()
|
||
|
|
assert.ok(response.ok && body.success !== false, `${route}: HTTP ${response.status}`)
|
||
|
|
return body
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const desired = JSON.parse(fs.readFileSync(0, 'utf8'))
|
||
|
|
assert.match(desired.username, /^[a-zA-Z0-9_-]{2,50}$/)
|
||
|
|
assert.ok(typeof desired.password === 'string' && [...desired.password].length >= 3)
|
||
|
|
const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'))
|
||
|
|
const oldLogin = await request('/auth/login', { email: credentials.email, password: credentials.password })
|
||
|
|
assert.equal(oldLogin.user.is_system_admin, true, 'Saved identity must already be a system administrator')
|
||
|
|
assert.equal(oldLogin.user.is_active, true)
|
||
|
|
const userID = oldLogin.user.id
|
||
|
|
assert.match(userID, /^[a-f0-9-]{36}$/)
|
||
|
|
assert.equal(sql(`SELECT count(*) FROM users WHERE id<>${literal(userID)} AND LOWER(username)=LOWER(${literal(desired.username)});`), '0', 'Requested account name already exists')
|
||
|
|
|
||
|
|
const backup = path.join(root, 'acceptance', `super-admin-login-before-${Date.now()}.json`)
|
||
|
|
fs.writeFileSync(backup, JSON.stringify(credentials, null, 2) + '\n', { flag: 'wx', mode: 0o600 })
|
||
|
|
// Keep the user ID and all ownership links; only change the login name.
|
||
|
|
sql(`BEGIN;
|
||
|
|
SELECT id FROM users WHERE id=${literal(userID)} AND is_system_admin=true AND is_active=true AND deleted_at IS NULL FOR UPDATE;
|
||
|
|
UPDATE users SET username=${literal(desired.username)},updated_at=now() WHERE id=${literal(userID)} AND is_system_admin=true AND is_active=true AND deleted_at IS NULL;
|
||
|
|
INSERT INTO audit_logs (tenant_id,actor_user_id,actor_role,action,target_type,target_id,target_user_id,outcome,details,created_at)
|
||
|
|
SELECT tenant_id,id,'admin','system.user_updated','user',id,id,'success',${literal(JSON.stringify({ username_before: oldLogin.user.username, username_after: desired.username, source: 'authorized_account_update' }))}::jsonb,now() FROM users WHERE id=${literal(userID)};
|
||
|
|
COMMIT;`)
|
||
|
|
const renamed = await request('/auth/login', { username: desired.username, password: credentials.password })
|
||
|
|
assert.equal(renamed.user.id, userID)
|
||
|
|
if (credentials.password !== desired.password) {
|
||
|
|
await request('/auth/change-password', { old_password: credentials.password, new_password: desired.password }, renamed.token)
|
||
|
|
}
|
||
|
|
const updated = { ...credentials, username: desired.username, password: desired.password }
|
||
|
|
const temporary = credentialsPath + '.next'
|
||
|
|
fs.writeFileSync(temporary, JSON.stringify(updated, null, 2) + '\n', { mode: 0o600 })
|
||
|
|
fs.renameSync(temporary, credentialsPath)
|
||
|
|
const current = await request('/auth/login', { username: desired.username, password: desired.password })
|
||
|
|
assert.equal(current.user.id, userID)
|
||
|
|
assert.equal(current.user.is_system_admin, true)
|
||
|
|
if (credentials.password !== desired.password) {
|
||
|
|
const revoked = await fetch(base + '/auth/me', { headers: { Authorization: `Bearer ${oldLogin.token}` }, signal: AbortSignal.timeout(15000) })
|
||
|
|
assert.equal(revoked.status, 401, 'Previous login must be revoked after password rotation')
|
||
|
|
}
|
||
|
|
console.log(JSON.stringify({ passed: true, username: current.user.username, identity_preserved: true, system_admin: true, password_rotated: credentials.password !== desired.password }))
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch(error => { console.error(error.message); process.exitCode = 1 })
|