feat(admin+search): user-mgmt polish, role labels, search keyword index
Admin search now matches against per-card keyword lists so typing "client portal", "smtp", "tier ladder" lands on the System Settings card (which hosts those flags). The same keyword list extends the topbar global search (NAV_CATALOG) so any setting key resolves from the cmd-K input — settings results sort to the bottom of the dropdown beneath entity hits. User management: - Third action button (Power/PowerOff) enables/disables sign-in from the desktop list; mobile card dropdown gains the same item. Backed by the existing userProfiles.isActive flag — withAuth already refuses disabled sessions with 403. - UserForm collects first + last name (canonical) alongside displayName, with admin email-change behind a confirmation modal. On confirm we send the OLD address an automated "your admin changed your sign-in email" notice (new template at admin-email-change.ts) and rewrite the Better Auth user row. - Phone field swaps the bare tel input for the shared PhoneInput (country combobox + AsYouType formatting + E.164 storage). - "Manage permissions" link points to /admin/roles?focusUser=… as a stepping stone for the future fine-tuned-permissions UI. Role names normalize through a new ROLE_LABELS + formatRole() helper in constants.ts. Replaces the ad-hoc humanizeRole in sidebar and the prettifyRoleName in role-list; user-list and user-card now render "Sales Agent" instead of "sales_agent". Custom roles pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
src/lib/email/templates/admin-email-change.ts
Normal file
93
src/lib/email/templates/admin-email-change.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { brandingPrimaryColor, renderShell, type BrandingShell } from '@/lib/email/shell';
|
||||
|
||||
interface AdminEmailChangeData {
|
||||
recipientName?: string;
|
||||
/** New address the user should sign in with from now on. */
|
||||
newEmail: string;
|
||||
/** Display name of the admin who initiated the change — surfaced so the
|
||||
* recipient knows who to follow up with. */
|
||||
changedByDisplayName?: string;
|
||||
/** Optional URL for the sign-in page. */
|
||||
loginUrl?: string;
|
||||
portName?: string;
|
||||
}
|
||||
|
||||
interface RenderOpts {
|
||||
branding?: BrandingShell | null;
|
||||
}
|
||||
|
||||
export function adminEmailChangeEmail(
|
||||
data: AdminEmailChangeData,
|
||||
overrides?: RenderOpts,
|
||||
): { subject: string; html: string; text: string } {
|
||||
const portName = data.portName ?? 'Port Nimara';
|
||||
const subject = `An administrator updated your ${portName} sign-in email`;
|
||||
const greeting = data.recipientName ? `Hello ${escapeHtml(data.recipientName)},` : 'Hello,';
|
||||
const accent = brandingPrimaryColor(overrides?.branding);
|
||||
|
||||
const adminLine = data.changedByDisplayName
|
||||
? `${escapeHtml(data.changedByDisplayName)} (an administrator)`
|
||||
: 'an administrator';
|
||||
|
||||
const body = `
|
||||
<p style="margin-bottom:10px; font-size:18px; font-weight:bold; color:${accent};">
|
||||
Your sign-in email was changed
|
||||
</p>
|
||||
<p style="margin-bottom:10px; font-size:16px; line-height:1.5;">${greeting}</p>
|
||||
<p style="margin-bottom:20px; font-size:16px; line-height:1.5;">
|
||||
${adminLine} just updated the email address linked to your ${escapeHtml(
|
||||
portName,
|
||||
)} account. From now on, please sign in with the new address below:
|
||||
</p>
|
||||
<p style="margin:20px 0; text-align:center; font-size:16px;">
|
||||
<strong>${escapeHtml(data.newEmail)}</strong>
|
||||
</p>
|
||||
${
|
||||
data.loginUrl
|
||||
? `<p style="text-align:center; margin:30px 0;">
|
||||
<a href="${data.loginUrl}" style="display:inline-block; background-color:${accent}; color:#ffffff; text-decoration:none; padding:14px 35px; border-radius:5px; font-weight:bold; font-size:16px;">
|
||||
Sign in
|
||||
</a>
|
||||
</p>`
|
||||
: ''
|
||||
}
|
||||
<p style="font-size:14px; color:#666; line-height:1.5; padding:15px 0; border-top:1px solid #eee; margin-top:20px;">
|
||||
If you weren't expecting this change, contact your administrator immediately.
|
||||
Your old address (the one this message was sent to) can no longer be used to
|
||||
sign in.
|
||||
</p>
|
||||
<p style="font-size:16px; margin-top:30px;">
|
||||
Thanks,<br />
|
||||
<strong>${escapeHtml(portName)}</strong>
|
||||
</p>`;
|
||||
|
||||
const text = [
|
||||
`Your sign-in email was changed`,
|
||||
'',
|
||||
`${data.changedByDisplayName ?? 'An administrator'} updated the email linked to your ${portName} account.`,
|
||||
`From now on, sign in with: ${data.newEmail}`,
|
||||
'',
|
||||
data.loginUrl ? `Sign in: ${data.loginUrl}` : '',
|
||||
'',
|
||||
`If you weren't expecting this change, contact your administrator immediately.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
const html = renderShell({
|
||||
branding: overrides?.branding ?? null,
|
||||
title: subject,
|
||||
body,
|
||||
});
|
||||
|
||||
return { subject, html, text };
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
Reference in New Issue
Block a user