Files
gongxue-base/scripts/bootstrap-platform-admin.js
2026-07-12 19:26:57 +08:00

352 lines
12 KiB
JavaScript

import { fileURLToPath, pathToFileURL } from 'node:url';
import pg from 'pg';
const APPLY_CONFIRMATION = 'BOOTSTRAP_FIRST_PLATFORM_ADMIN';
const BOOTSTRAP_LOCK_KEY = 'tiku-saas:first-platform-admin:v1';
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function envString(env, key, fallback = '') {
return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback;
}
function argumentValue(argv, name) {
const directIndex = argv.indexOf(name);
if (directIndex >= 0) return String(argv[directIndex + 1] || '').trim();
const prefix = `${name}=`;
return String(argv.find(value => value.startsWith(prefix)) || '').slice(prefix.length).trim();
}
function normalizeOptionalEmail(value) {
if (!value) return null;
const email = value.toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || email.length > 254) {
throw new Error('BOOTSTRAP_PLATFORM_ADMIN_EMAIL must be a valid email address');
}
return email;
}
function normalizeOptionalPhone(value) {
if (!value) return null;
const phone = value.replace(/\s+/g, '');
if (!/^\+?[0-9-]{6,32}$/.test(phone)) {
throw new Error('BOOTSTRAP_PLATFORM_ADMIN_PHONE must be a valid phone number');
}
return phone;
}
function normalizeUsername(value, fallback) {
const username = (value || fallback).trim();
if (!/^[a-zA-Z0-9_.@-]{3,80}$/.test(username)) {
throw new Error('BOOTSTRAP_PLATFORM_ADMIN_USERNAME must contain 3-80 safe characters');
}
return username;
}
function buildConfig(env = process.env, argv = process.argv.slice(2)) {
const databaseUrl = envString(env, 'DATABASE_URL');
const authUserId = argumentValue(argv, '--auth-user-id') || envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_AUTH_USER_ID');
const apply = argv.includes('--apply');
const confirmation = argumentValue(argv, '--confirm') || envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_CONFIRM');
const email = normalizeOptionalEmail(envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_EMAIL'));
const phone = normalizeOptionalPhone(envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_PHONE'));
const username = normalizeUsername(
envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_USERNAME'),
`platform_${authUserId.slice(0, 8)}`,
);
const name = envString(env, 'BOOTSTRAP_PLATFORM_ADMIN_NAME', 'Platform Administrator');
if (!databaseUrl) throw new Error('Missing required env: DATABASE_URL');
if (!UUID_RE.test(authUserId)) {
throw new Error('BOOTSTRAP_PLATFORM_ADMIN_AUTH_USER_ID or --auth-user-id must be a valid UUID');
}
if (!name || name.length > 120) throw new Error('BOOTSTRAP_PLATFORM_ADMIN_NAME must contain 1-120 characters');
if (apply && confirmation !== APPLY_CONFIRMATION) {
throw new Error(`--apply requires --confirm ${APPLY_CONFIRMATION}`);
}
return { databaseUrl, authUserId, apply, confirmation, username, email, phone, name };
}
function maskEmail(value) {
if (!value) return null;
const [local = '', domain = ''] = String(value).split('@');
return `${local.slice(0, 2)}***@${domain}`;
}
function maskPhone(value) {
if (!value) return null;
const phone = String(value);
return phone.length > 7 ? `${phone.slice(0, 3)}****${phone.slice(-4)}` : '***';
}
function maskUuid(value) {
if (!value) return null;
const id = String(value);
return `${id.slice(0, 8)}...${id.slice(-4)}`;
}
function maskUsername(value) {
if (!value) return null;
const username = String(value);
if (username.includes('@')) return maskEmail(username);
if (/^\+?[0-9-]{6,32}$/.test(username)) return maskPhone(username);
if (username.length <= 3) return '***';
return `${username.slice(0, 2)}***${username.slice(-1)}`;
}
function publicResult(result) {
return {
dryRun: result.dryRun,
action: result.action,
platformUserId: maskUuid(result.platformUserId),
authUserId: maskUuid(result.authUserId),
username: maskUsername(result.username),
email: maskEmail(result.email),
phone: maskPhone(result.phone),
primaryRole: 'platform_admin',
status: 'active',
permissions: ['*'],
auditAction: result.auditAction || null,
};
}
function sanitizeErrorMessage(error, databaseUrl = '') {
let message = error instanceof Error ? error.message : String(error);
if (databaseUrl) message = message.split(databaseUrl).join('[DATABASE_URL_REDACTED]');
return message
.replace(/postgres(?:ql)?:\/\/[^\s'"<>]+/gi, '[DATABASE_URL_REDACTED]')
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@:]+:[^\s/@]+@/gi, '$1[REDACTED]@');
}
async function withTransaction(client, callback) {
await client.query('begin');
try {
await client.query('select pg_advisory_xact_lock(hashtextextended($1, 0))', [BOOTSTRAP_LOCK_KEY]);
const result = await callback();
await client.query('commit');
return result;
} catch (error) {
await client.query('rollback').catch(() => undefined);
throw error;
}
}
async function bootstrapPlatformAdmin(inputConfig, options = {}) {
const config = inputConfig?.databaseUrl
? inputConfig
: buildConfig(options.env || process.env, options.argv || process.argv.slice(2));
if (config.apply && config.confirmation !== APPLY_CONFIRMATION) {
throw new Error(`Apply requires confirmation ${APPLY_CONFIRMATION}`);
}
const pool = options.pool || new pg.Pool({ connectionString: config.databaseUrl, max: 1 });
const closePool = !options.pool;
try {
const client = await pool.connect();
try {
return await withTransaction(client, async () => {
const authUserResult = await client.query(
`
select app.auth_user_exists($1::uuid) as exists
`,
[config.authUserId],
);
if (!authUserResult.rows[0]?.exists) throw new Error('Supabase Auth user not found');
const activeBoundResult = await client.query(
`
select id
from public.platform_users
where primary_role = 'platform_admin'
and status = 'active'
and auth_user_id is not null
order by created_at asc
limit 2
for update
`,
);
if (activeBoundResult.rowCount > 0) {
throw new Error('An active, Auth-bound platform admin already exists; bootstrap is permanently refused');
}
const boundUserResult = await client.query(
`
select id, primary_role as "primaryRole"
from public.platform_users
where auth_user_id = $1::uuid
limit 1
for update
`,
[config.authUserId],
);
if (boundUserResult.rows[0]?.primaryRole !== undefined) {
throw new Error('Supabase Auth user is already bound to a platform user');
}
const legacyResult = await client.query(
`
select id, username, email::text, phone, name
from public.platform_users
where primary_role = 'platform_admin'
and auth_user_id is null
order by created_at asc
limit 2
for update
`,
);
if (legacyResult.rowCount > 1) {
throw new Error('Multiple unbound legacy platform admins exist; resolve the ambiguity manually');
}
const legacy = legacyResult.rows[0] || null;
const action = legacy ? 'bind_legacy' : 'create';
const platformUserId = legacy?.id || null;
const resolvedEmail = legacy?.email || config.email || null;
const resolvedPhone = legacy?.phone || config.phone || null;
const resolvedUsername = legacy?.username || config.username;
const resolvedName = legacy?.name || config.name;
if (!config.apply) {
return {
dryRun: true,
action,
platformUserId,
authUserId: config.authUserId,
username: resolvedUsername,
email: resolvedEmail,
phone: resolvedPhone,
auditAction: null,
};
}
const savedResult = await client.query(
`
insert into public.platform_users (
id, auth_user_id, username, email, phone, name,
primary_role, status, platform_permissions, raw_profile
)
values (
coalesce($1::uuid, pg_catalog.gen_random_uuid()), $2::uuid, $3, $4::extensions.citext, $5, $6,
'platform_admin', 'active', '{"*":true}'::jsonb,
jsonb_build_object('source', 'server-bootstrap', 'bootstrapVersion', 1)
)
on conflict (id)
do update set auth_user_id = excluded.auth_user_id,
username = coalesce(public.platform_users.username, excluded.username),
email = coalesce(public.platform_users.email, excluded.email),
phone = coalesce(public.platform_users.phone, excluded.phone),
name = coalesce(public.platform_users.name, excluded.name),
primary_role = 'platform_admin',
status = 'active',
platform_permissions = '{"*":true}'::jsonb,
raw_profile = jsonb_strip_nulls(public.platform_users.raw_profile || excluded.raw_profile),
updated_at = now()
returning id, auth_user_id as "authUserId", username, email::text, phone
`,
[platformUserId, config.authUserId, resolvedUsername, resolvedEmail, resolvedPhone, resolvedName],
);
const saved = savedResult.rows[0];
const auditAction = 'platform.admin.bootstrapped';
await client.query(
`
insert into public.audit_logs (
tenant_id, actor_user_id, action, target_type, target_id, details
)
values (
null, null, $2, 'platform_user', $1::text,
jsonb_build_object(
'source', 'server-bootstrap',
'invokedBy', 'system_cli',
'bootstrapVersion', 1,
'mode', $3::text,
'authUserBound', true,
'permissions', jsonb_build_array('*')
)
)
`,
[saved.id, auditAction, action],
);
return {
dryRun: false,
action,
platformUserId: saved.id,
authUserId: saved.authUserId,
username: saved.username,
email: saved.email,
phone: saved.phone,
auditAction,
};
});
} finally {
client.release();
}
} finally {
if (closePool) await pool.end();
}
}
function helpText() {
return `
Bootstrap the first production platform administrator from an existing Supabase Auth user.
Safety contract:
- Runs as a local server CLI and talks directly to DATABASE_URL.
- Defaults to dry-run. Writing requires --apply and the exact confirmation phrase.
- Refuses once any active, Auth-bound platform admin exists.
- May bind exactly one unbound legacy platform_admin row; multiple candidates are refused.
- Uses a boolean Auth existence boundary and never reads auth.users profile fields.
- Provide optional email/phone explicitly when a new business profile needs them.
- Grants {"*":true} and writes a redacted system-CLI audit event in the same locked transaction.
Usage:
DATABASE_URL=<server-database-url> \\
BOOTSTRAP_PLATFORM_ADMIN_AUTH_USER_ID=<existing-auth.users-id> \\
BOOTSTRAP_PLATFORM_ADMIN_USERNAME=<username> \\
BOOTSTRAP_PLATFORM_ADMIN_NAME=<display-name> \\
npm run bootstrap:platform-admin
npm run bootstrap:platform-admin -- --apply --confirm ${APPLY_CONFIRMATION}
Optional identity fields:
BOOTSTRAP_PLATFORM_ADMIN_EMAIL
BOOTSTRAP_PLATFORM_ADMIN_PHONE
`;
}
async function main() {
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log(helpText().trim());
return;
}
let config;
try {
config = buildConfig();
const result = await bootstrapPlatformAdmin(config);
console.log(JSON.stringify({ ok: true, ...publicResult(result) }, null, 2));
if (result.dryRun) {
console.error(`Dry-run only. Re-run with --apply --confirm ${APPLY_CONFIRMATION} after reviewing the target.`);
}
} catch (error) {
console.error(sanitizeErrorMessage(error, config?.databaseUrl || envString(process.env, 'DATABASE_URL')));
console.error(helpText());
process.exitCode = 1;
}
}
const currentFile = fileURLToPath(import.meta.url);
if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) {
await main();
}
export {
APPLY_CONFIRMATION,
BOOTSTRAP_LOCK_KEY,
bootstrapPlatformAdmin,
buildConfig,
helpText,
publicResult,
sanitizeErrorMessage,
};