Bootstrap: print first-run login banner (URL + seeded creds + change-before-prod warning)

This commit is contained in:
2026-06-17 16:22:48 +02:00
parent a6900217cb
commit 4d65665063
5 changed files with 35 additions and 5 deletions

View File

@@ -5,7 +5,7 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { ensureJwks, identityPayload, roleTuple, seedAdmin } from "./bootstrap.ts";
import { ensureJwks, firstRunBanner, identityPayload, roleTuple, seedAdmin } from "./bootstrap.ts";
const json = (status: number, body?: unknown) =>
new Response(body === undefined ? null : JSON.stringify(body), {
@@ -98,6 +98,14 @@ test("seedAdmin fails loud on an unexpected Kratos error", async () => {
);
});
test("firstRunBanner prints the login URL, seeded creds, and a change-before-production warning", () => {
const banner = firstRunBanner({ appUrl: "http://localhost:3000", email: "admin@plainpages.local", password: "admin" });
assert.match(banner, /http:\/\/localhost:3000/);
assert.match(banner, /admin@plainpages\.local/);
assert.match(banner, /admin/); // the password
assert.match(banner, /before production/i);
});
test("ensureJwks generates a key only when the file is absent", () => {
const writes: { content: string; path: string }[] = [];
const write = (path: string, content: string) => writes.push({ content, path });

View File

@@ -4,6 +4,7 @@
// 1. generate the JWKS signing key if absent (committed dev key makes this a safety net);
// 2. seed a demo admin identity (admin@plainpages.local / admin) in Kratos;
// 3. grant it the `admin` role in Keto so menu/permission checks resolve out of the box.
// On finish it prints a first-run banner (login URL + creds + change-before-prod warning).
// Fails loud on any unexpected upstream error.
import { existsSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
@@ -100,6 +101,22 @@ async function findIdentityId(http: typeof fetch, adminUrl: string, email: strin
return found.id;
}
// --- First-run banner ----------------------------------------------------------------
// Loud, scannable block in the compose logs: where to log in + the seeded demo creds +
// the "change before production" warning. Pure so it's testable; main() console.logs it.
export function firstRunBanner(opts: { appUrl: string; email: string; password: string }): string {
const rule = "─".repeat(58);
return [
`${rule}`,
`│ Plainpages is ready — log in at ${opts.appUrl}`,
`│ email: ${opts.email}`,
`│ password: ${opts.password}`,
`│ ⚠ Demo admin credentials — change them before production.`,
`${rule}`,
].join("\n");
}
// --- CLI (the bootstrap container entrypoint) ----------------------------------------
async function main() {
@@ -107,14 +124,17 @@ async function main() {
if (ensureJwks(env["JWKS_FILE"] ?? "/etc/config/kratos/tokenizer/jwks.json")) console.log("bootstrap: generated a JWKS signing key");
const role = env["ADMIN_ROLE"] ?? "admin";
const email = env["ADMIN_EMAIL"] ?? "admin@plainpages.local";
const password = env["ADMIN_PASSWORD"] ?? "admin";
const result = await seedAdmin({
email: env["ADMIN_EMAIL"] ?? "admin@plainpages.local",
email,
ketoWriteUrl: env["KETO_WRITE_URL"] ?? "http://keto:4467",
kratosAdminUrl: env["KRATOS_ADMIN_URL"] ?? "http://kratos:4434",
password: env["ADMIN_PASSWORD"] ?? "admin",
password,
role,
});
console.log(`bootstrap: admin ${result.created ? "created" : "already present"} (${result.id}); role "${role}" granted`);
console.log(firstRunBanner({ appUrl: env["APP_URL"] ?? "http://localhost:3000", email, password }));
}
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();