feat: initial user provisioning with welcome email, resend welcome
Some checks failed
Build & Release / build (push) Has been cancelled

- Provision form accepts optional initial admin user (no password needed)
- POST /customers/:slug/resend-welcome sends welcome email via customer backend
- Kebab menu "Resend Welcome" option with email input dialog
- Query latest version from backend image tags instead of chart tags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-05 17:09:32 +00:00
parent 561f1bbff3
commit 7f2cf14d38
2 changed files with 117 additions and 8 deletions

View File

@@ -24,9 +24,8 @@ const ProvisionSchema = z.object({
modules: z.array(z.enum(MODULES)).default([]),
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).default(() => new Date().toISOString().slice(0, 10)),
expirationDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().default(null),
initialEmail: z.object({
initialUser: z.object({
email: z.string().email(),
password: z.string().min(12).max(128),
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
}).optional(),
@@ -145,11 +144,10 @@ export async function customerRoutes(app: FastifyInstance) {
"resend-api-key": config.resendApiKey,
"mail-from": `noreply@${slug}.lunarfront.tech`,
"business-name": body.name,
...(body.initialEmail ? {
"initial-user-email": body.initialEmail.email,
"initial-user-password": body.initialEmail.password,
"initial-user-first-name": body.initialEmail.firstName,
"initial-user-last-name": body.initialEmail.lastName,
...(body.initialUser ? {
"initial-user-email": body.initialUser.email,
"initial-user-first-name": body.initialUser.firstName,
"initial-user-last-name": body.initialUser.lastName,
} : {}),
});
await setStep("storage", "done");
@@ -361,6 +359,32 @@ export async function customerRoutes(app: FastifyInstance) {
return reply.send({ upgraded: slugs, version });
});
// Resend welcome email — generates a new reset link via the customer's backend
app.post("/customers/:slug/resend-welcome", async (req, reply) => {
const { slug } = req.params as { slug: string };
const { email } = req.body as { email: string };
if (!email) return reply.code(400).send({ message: "email is required" });
const [customer] = await db`SELECT * FROM customers WHERE slug = ${slug}`;
if (!customer) return reply.code(404).send({ message: "Not found" });
// Call the customer's forgot-password endpoint with welcome type
const res = await fetch(`http://customer-${slug}-backend.customer-${slug}.svc:8000/v1/auth/forgot-password?type=welcome`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
if (!res.ok) {
const body = await res.text();
app.log.error({ slug, email, status: res.status, body }, "Failed to send welcome email");
return reply.code(502).send({ message: "Failed to send welcome email" });
}
app.log.info({ slug, email }, "Welcome email resent");
return reply.send({ message: "Welcome email sent" });
});
// Remove only the manager DB record without touching infrastructure
app.delete("/customers/:slug/record", async (req, reply) => {
const { slug } = req.params as { slug: string };