feat(admin): single Sales role, welcome-email password setup, Director=sales
- Collapse the two sales roles in the create-user dropdown to one "Sales" (sales_manager relabelled). Hide super_admin + sales_agent from selection via NON_ASSIGNABLE_ROLE_NAMES; the form keeps a user's *current* role even if hidden so existing assignments stay editable. - Director becomes a senior-title twin of Sales: DIRECTOR_PERMISSIONS now equals SALES_MANAGER_PERMISSIONS (no admin/settings — Super-Admin only). Migration 0097 updates the existing global director row (idempotent, data-only; 0 users assigned on prod, so no blast radius). - Admin create-user defaults to emailing a set-password link instead of an inline password (manual entry still available via a toggle). createUserSchema: password optional + sendSetupEmail; createUser provisions with a throwaway password then triggers the set-password email. - New users get a dedicated, unique WELCOME email (crmWelcomeEmail), not the self-service "reset your password" email. A pending-welcome flag routes the shared better-auth sendResetPassword callback via account-setup-email.ts. - Phone confirmed already optional for staff accounts (no change needed). Tests: +welcome-routing, +create-user-setup; permission-matrix director block realigned to no-admin. 1662 vitest pass; tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
src/lib/auth/account-setup-email.ts
Normal file
62
src/lib/auth/account-setup-email.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { BrandingShell } from '@/lib/email/shell';
|
||||
|
||||
import { consumePendingWelcome } from './pending-welcome';
|
||||
|
||||
interface AuthBranding {
|
||||
appName?: string | null;
|
||||
logoUrl?: string | null;
|
||||
backgroundUrl?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the email body for better-auth's `sendResetPassword` callback,
|
||||
* choosing between two framings of the same set-password link:
|
||||
*
|
||||
* - a unique **welcome** email when the recipient was flagged by the admin
|
||||
* "create user" flow (a brand-new user has nothing to reset), or
|
||||
* - the standard **password-reset** email for genuine self-service resets.
|
||||
*
|
||||
* Pure aside from rendering — no SMTP, no DB — so the welcome-vs-reset routing
|
||||
* is directly unit-testable.
|
||||
*/
|
||||
export async function buildAccountPasswordEmail(opts: {
|
||||
email: string;
|
||||
name?: string | null;
|
||||
url: string;
|
||||
appName: string;
|
||||
authBranding: AuthBranding | null;
|
||||
}): Promise<{ subject: string; html: string; text: string }> {
|
||||
const emailBranding: BrandingShell | null = opts.authBranding
|
||||
? {
|
||||
logoUrl: opts.authBranding.logoUrl ?? null,
|
||||
backgroundUrl: opts.authBranding.backgroundUrl ?? null,
|
||||
primaryColor: null,
|
||||
emailHeaderHtml: null,
|
||||
emailFooterHtml: null,
|
||||
}
|
||||
: null;
|
||||
|
||||
if (consumePendingWelcome(opts.email)) {
|
||||
const { crmWelcomeEmail } = await import('@/lib/email/templates/crm-welcome');
|
||||
return crmWelcomeEmail(
|
||||
{ link: opts.url, recipientName: opts.name ?? undefined, appName: opts.appName },
|
||||
{ branding: emailBranding },
|
||||
);
|
||||
}
|
||||
|
||||
const { renderShell, safeUrl } = await import('@/lib/email/shell');
|
||||
const subject = `Reset your ${opts.appName} password`;
|
||||
const safeName = (opts.name || 'there').replace(/[<>&]/g, '');
|
||||
const body = `
|
||||
<p style="margin-bottom:16px;">Hi ${safeName},</p>
|
||||
<p style="margin-bottom:16px;">You requested a password reset for your ${opts.appName} account.</p>
|
||||
<p style="margin-bottom:16px;">
|
||||
<a href="${safeUrl(opts.url)}" style="color:#2563eb;font-weight:600;">Click here to set a new password</a>
|
||||
- the link expires in 1 hour.
|
||||
</p>
|
||||
<p style="color:#64748b;">If you didn't request this, you can safely ignore this email.</p>
|
||||
`;
|
||||
const html = renderShell({ title: subject, body, branding: emailBranding });
|
||||
const text = `Reset your password: ${opts.url}`;
|
||||
return { subject, html, text };
|
||||
}
|
||||
Reference in New Issue
Block a user