diff --git a/frontend/index.html b/frontend/index.html
index 15c0448..b84bdd7 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -331,10 +331,27 @@
Pause
Resume
Upgrade Chart
+ Resend Welcome
Decommission
Remove Record Only
+
+
+
+
Resend Welcome Email
+
Send a welcome email with a password setup link to a user on .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -630,6 +664,7 @@
else if (action === 'deactivate') deactivate(slug);
else if (action === 'reactivate') reactivate(slug);
else if (action === 'upgrade') upgradeCustomer(slug);
+ else if (action === 'resend-welcome') openResendWelcome(slug);
else if (action === 'delete') openDeleteDialog(slug);
else if (action === 'record') removeRecord(slug);
}
@@ -836,6 +871,44 @@
// ── Delete ────────────────────────────────────────────────────────────────
+ let resendSlug = null;
+ function openResendWelcome(slug) {
+ resendSlug = slug;
+ document.getElementById('resend-slug-label').textContent = slug;
+ document.getElementById('resend-email').value = '';
+ document.getElementById('resend-overlay').classList.add('open');
+ }
+ function closeResendDialog() {
+ resendSlug = null;
+ document.getElementById('resend-overlay').classList.remove('open');
+ }
+ async function confirmResendWelcome() {
+ const slug = resendSlug;
+ const email = document.getElementById('resend-email').value.trim();
+ if (!slug || !email) return;
+ const btn = document.getElementById('resend-confirm-btn');
+ btn.disabled = true;
+ btn.textContent = 'Sending…';
+ try {
+ const res = await apiFetch(`/api/customers/${slug}/resend-welcome`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email }),
+ });
+ if (!res.ok) {
+ const d = await res.json().catch(() => ({}));
+ throw new Error(d.message ?? 'Failed to send');
+ }
+ closeResendDialog();
+ alert('Welcome email sent to ' + email);
+ } catch (err) {
+ alert(err.message);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Send';
+ }
+ }
+
function openDeleteDialog(slug) {
pendingDeleteSlug = slug;
document.getElementById('delete-slug-label').textContent = slug;
@@ -945,7 +1018,16 @@
const startDate = document.getElementById('start-date').value;
const expirationDate = document.getElementById('expiration-date').value || null;
const modules = [...document.querySelectorAll('.module-check input:checked')].map(el => el.value);
+ const initialFirst = document.getElementById('initial-first').value.trim();
+ const initialLast = document.getElementById('initial-last').value.trim();
+ const initialEmail = document.getElementById('initial-email').value.trim();
if (!name || !slug) return;
+
+ const body = { slug, name, modules, startDate, expirationDate };
+ if (initialEmail && initialFirst && initialLast) {
+ body.initialUser = { email: initialEmail, firstName: initialFirst, lastName: initialLast };
+ }
+
const btn = document.getElementById('provision-btn');
const status = document.getElementById('provision-status');
btn.disabled = true;
@@ -955,7 +1037,7 @@
const res = await apiFetch('/api/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ slug, name, modules, startDate, expirationDate }),
+ body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Unknown error');
@@ -963,6 +1045,9 @@
document.getElementById('customer-name').value = '';
document.getElementById('slug').value = '';
document.getElementById('expiration-date').value = '';
+ document.getElementById('initial-first').value = '';
+ document.getElementById('initial-last').value = '';
+ document.getElementById('initial-email').value = '';
document.querySelectorAll('.module-check').forEach(l => { l.classList.remove('checked'); l.querySelector('input').checked = false; });
loadCustomers();
} catch (err) {
diff --git a/src/routes/customers.ts b/src/routes/customers.ts
index 96bf3f6..e3a00a1 100644
--- a/src/routes/customers.ts
+++ b/src/routes/customers.ts
@@ -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 };