import * as UsersService from './users/users.admin.service.js'; import * as InvitationsService from './invitations/invitations.service.js'; import {sendInvitationMail} from './admin.mail.js'; import {ACCESS_ROLE} from './admin.schema.js'; import {ADMIN_APP_URL, ADMIN_BOOTSTRAP_EMAIL, LOG_INVITE_LINKS} from './admin.config.js'; import logger from '../../middleware/logger.js'; /** * Solves the empty-database problem: with invite-only accounts and no public * sign-up, a fresh nachklang_admin has nobody who can invite anybody. Rather * than a CLI script somebody has to remember to run against production, the API * makes sure on every start that ADMIN_BOOTSTRAP_EMAIL can get in. * * Idempotent by design - it is safe on every restart: * - an active admin already exists -> do nothing * - the address exists as a user -> grant it `admin` * - an open invitation exists -> do nothing (do not re-mail on restart) * - otherwise -> invite, and mail the link * * Never throws: a database blip at boot must not stop the API from serving the * calendar, feedback and tickets domains. */ export const bootstrapAdmin = async (): Promise => { try { if (!ADMIN_BOOTSTRAP_EMAIL) { return; } const email = ADMIN_BOOTSTRAP_EMAIL.trim().toLowerCase(); if ((await UsersService.countActiveAdmins()) > 0) { return; } const existing = await UsersService.findUserByEmail(email); if (existing) { await UsersService.grantPermission(existing.id, 'admin', null); logger.info('Admin bootstrap: granted the admin permission to the existing bootstrap user', {email}); return; } // An expired invitation is not "open", so the next restart re-issues // one - which is the recovery path if the first mail never arrived. if (await InvitationsService.hasOpenInvitationFor(email)) { logger.info('Admin bootstrap: an open invitation already exists', {email}); return; } const invitation = await InvitationsService.createInvitation( email, 'Nachklang Admin', [{app: 'admin', role: ACCESS_ROLE}], null ); const mailed = await sendInvitationMail(email, 'Nachklang Admin', invitation.token, invitation.expiresAt); logger.info('Admin bootstrap: invitation created', {email, mailed}); // With the mail relay off, the logged link is how a local setup gets its // first admin. Explicit opt-in (see LOG_INVITE_LINKS): the link is a // live credential, so this must never depend on NODE_ENV alone. if (LOG_INVITE_LINKS) { logger.info(`Admin bootstrap: ${ADMIN_APP_URL}/accept-invite?token=${invitation.token}`); } } catch (e: any) { logger.error('Admin bootstrap failed', {detail: e?.message}); } };