bf7be65b03
Jenkins Production Deployment
Reviewed-on: #12 Co-authored-by: Patrick Müller <mail@pmueller.me> Co-committed-by: Patrick Müller <mail@pmueller.me>
69 lines
2.6 KiB
TypeScript
69 lines
2.6 KiB
TypeScript
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<void> => {
|
|
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});
|
|
}
|
|
};
|