perfect-postcode/video/src/pb-admin.ts
2026-05-05 22:15:29 +01:00

121 lines
3.3 KiB
TypeScript

interface SuperuserAuthResponse {
token: string;
}
interface UserRecord {
id: string;
email: string;
is_admin?: boolean;
subscription?: string;
}
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
async function pbJson<T>(
url: string,
init: RequestInit,
label: string
): Promise<T> {
const res = await fetch(url, init);
if (!res.ok) {
throw new Error(`${label} ${res.status}: ${await res.text()}`);
}
return (await res.json()) as T;
}
async function superuserToken(pbUrl: string, email: string, password: string): Promise<string> {
const data = await pbJson<SuperuserAuthResponse>(
`${pbUrl}/api/collections/_superusers/auth-with-password`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: email, password }),
},
'PocketBase superuser auth'
);
return data.token;
}
async function findUser(pbUrl: string, token: string, email: string): Promise<UserRecord | null> {
const filter = `email="${email.replaceAll('"', '\\"')}"`;
const data = await pbJson<{ items: UserRecord[] }>(
`${pbUrl}/api/collections/users/records?filter=${encodeURIComponent(filter)}&perPage=1`,
{ headers: { Authorization: `Bearer ${token}` } },
'PocketBase user lookup'
);
return data.items[0] ?? null;
}
function recorderUserBody(email: string, password: string): Record<string, unknown> {
return {
email,
emailVisibility: true,
verified: true,
password,
passwordConfirm: password,
is_admin: true,
subscription: 'licensed',
};
}
export async function ensureRecorderAdminUser(): Promise<void> {
const pbUrl = requireEnv('PB_URL').replace(/\/$/, '');
const email = requireEnv('PB_EMAIL');
const password = requireEnv('PB_PASSWORD');
const adminEmail = process.env.PB_ADMIN_EMAIL ?? process.env.POCKETBASE_ADMIN_EMAIL;
const adminPassword = process.env.PB_ADMIN_PASSWORD ?? process.env.POCKETBASE_ADMIN_PASSWORD;
if (!adminEmail || !adminPassword) {
throw new Error('PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD are required to bootstrap the recorder user');
}
const token = await superuserToken(pbUrl, adminEmail, adminPassword);
const existing = await findUser(pbUrl, token, email);
const body = recorderUserBody(email, password);
if (existing) {
await pbJson<UserRecord>(
`${pbUrl}/api/collections/users/records/${existing.id}`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
'PocketBase recorder user update'
);
console.log(`Updated recorder admin user ${email}.`);
return;
}
await pbJson<UserRecord>(
`${pbUrl}/api/collections/users/records`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
'PocketBase recorder user create'
);
console.log(`Created recorder admin user ${email}.`);
}
async function main() {
await ensureRecorderAdminUser();
}
if (process.argv[1]?.endsWith('pb-admin.js')) {
main().catch((err) => {
console.error(err);
process.exit(1);
});
}