feat(admin): single Sales role, welcome-email password setup, Director=sales
All checks were successful
Build & Push Docker Images / lint (push) Successful in 3m5s
Build & Push Docker Images / build-and-push (push) Successful in 9m24s

- 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:
2026-06-22 12:40:55 +02:00
parent 5b9560531e
commit 93989b1e1d
16 changed files with 593 additions and 156 deletions

View File

@@ -0,0 +1,64 @@
/**
* The shared better-auth sendResetPassword callback must send a unique WELCOME
* email to admin-created users (flagged via pending-welcome) and the standard
* RESET email to everyone else — same link, different framing.
*/
import { describe, it, expect } from 'vitest';
import { buildAccountPasswordEmail } from '@/lib/auth/account-setup-email';
import { markPendingWelcome } from '@/lib/auth/pending-welcome';
const url = 'https://crm.example.com/set-password#token=tok';
describe('buildAccountPasswordEmail routing', () => {
it('sends a welcome email when the recipient was flagged by create-user', async () => {
const email = 'new-hire@example.test';
markPendingWelcome(email);
const mail = await buildAccountPasswordEmail({
email,
name: 'New Hire',
url,
appName: 'Port Nimara CRM',
authBranding: null,
});
expect(mail.subject.toLowerCase()).toContain('welcome');
expect(mail.subject.toLowerCase()).not.toContain('reset');
expect(mail.html).toContain('tok');
});
it('sends the standard reset email for an unflagged self-service reset', async () => {
const mail = await buildAccountPasswordEmail({
email: 'existing@example.test',
name: 'Existing User',
url,
appName: 'Port Nimara CRM',
authBranding: null,
});
expect(mail.subject.toLowerCase()).toContain('reset');
expect(mail.subject.toLowerCase()).not.toContain('welcome');
});
it('consumes the welcome flag (a second build for the same email is a reset)', async () => {
const email = 'once@example.test';
markPendingWelcome(email);
const first = await buildAccountPasswordEmail({
email,
url,
appName: 'Port Nimara CRM',
authBranding: null,
});
const second = await buildAccountPasswordEmail({
email,
url,
appName: 'Port Nimara CRM',
authBranding: null,
});
expect(first.subject.toLowerCase()).toContain('welcome');
expect(second.subject.toLowerCase()).toContain('reset');
});
});