feat: wire GHL webhook to Resend email templates
- Replace raw Resend text emails with sendMail using Resend templates - automated-user-registration template: firstName, tempPassword, verifyUrl, loginUrl - license-key-delivery template: firstName, license_key - Remove requestPasswordReset call (temp password is the one-time credential; verification link handles email confirmation) - Add .catch() to Better Auth email callbacks to prevent unhandled rejections from missing templates crashing the server
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
const existingChildren = (record.children as Array<{ id: number; name: string }>) ?? [];
|
||||
children = existingChildren.map(c => ({ id: c.id, name: c.name }));
|
||||
children = existingChildren.map((c) => ({ id: c.id, name: c.name }));
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
|
||||
@@ -108,17 +108,16 @@
|
||||
<select
|
||||
id="field-solfeggioFrequencyId"
|
||||
name="solfeggioFrequencyId"
|
||||
onchange={(e) => solfeggioFrequencyId = (e.target as HTMLSelectElement).value}
|
||||
onchange={(e) => (solfeggioFrequencyId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.solfeggioFrequencyId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Frequency --</option>
|
||||
{#each relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
|
||||
<option
|
||||
value={opt.value}
|
||||
selected={String(opt.value) === solfeggioFrequencyId}
|
||||
>{opt.label}</option>
|
||||
<option value={opt.value} selected={String(opt.value) === solfeggioFrequencyId}
|
||||
>{opt.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.solfeggioFrequencyId}
|
||||
@@ -143,10 +142,7 @@
|
||||
>
|
||||
<option value="">-- Select Category --</option>
|
||||
{#each parentOptions as cat (String(cat.id))}
|
||||
<option
|
||||
value={cat.id}
|
||||
selected={String(cat.id) === parentCategoryId}
|
||||
>{cat.name}</option>
|
||||
<option value={cat.id} selected={String(cat.id) === parentCategoryId}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.parentCategoryId}
|
||||
@@ -164,17 +160,14 @@
|
||||
<select
|
||||
id="field-categoryId"
|
||||
name="categoryId"
|
||||
onchange={(e) => categoryId = (e.target as HTMLSelectElement).value}
|
||||
onchange={(e) => (categoryId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.categoryId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Sub-Category --</option>
|
||||
{#each childOptions as cat (String(cat.id))}
|
||||
<option
|
||||
value={cat.id}
|
||||
selected={String(cat.id) === categoryId}
|
||||
>{cat.name}</option>
|
||||
<option value={cat.id} selected={String(cat.id) === categoryId}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.categoryId}
|
||||
@@ -192,17 +185,16 @@
|
||||
<select
|
||||
id="field-subscriptionLevelId"
|
||||
name="subscriptionLevelId"
|
||||
onchange={(e) => subscriptionLevelId = (e.target as HTMLSelectElement).value}
|
||||
onchange={(e) => (subscriptionLevelId = (e.target as HTMLSelectElement).value)}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.subscriptionLevelId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Level --</option>
|
||||
{#each relationOptionsData.subscriptionLevelId ?? [] as opt (String(opt.value))}
|
||||
<option
|
||||
value={opt.value}
|
||||
selected={String(opt.value) === subscriptionLevelId}
|
||||
>{opt.label}</option>
|
||||
<option value={opt.value} selected={String(opt.value) === subscriptionLevelId}
|
||||
>{opt.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.subscriptionLevelId}
|
||||
@@ -218,14 +210,14 @@
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div id="field-content">
|
||||
{#key record.id}
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div id="field-content">
|
||||
{#key record.id}
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
|
||||
@@ -42,24 +42,16 @@ describe('ProclamationForm', () => {
|
||||
isSuper: true
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('textbox', { name: 'Name' }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Frequency$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Category$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect.element(page.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Frequency$/ })).toBeInTheDocument();
|
||||
await expect.element(page.getByRole('combobox', { name: /^Category$/ })).toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Sub-Category$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('combobox', { name: /^Subscription Level$/ }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Save' }))
|
||||
.toBeInTheDocument();
|
||||
await expect.element(page.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills name input when editing an existing record', async () => {
|
||||
@@ -141,9 +133,7 @@ describe('ProclamationForm', () => {
|
||||
submitLabel: 'Update'
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Update' }))
|
||||
.toBeDisabled();
|
||||
await expect.element(page.getByRole('button', { name: 'Update' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('enables submit button when editing and user modifies a field', async () => {
|
||||
@@ -157,8 +147,6 @@ describe('ProclamationForm', () => {
|
||||
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill('New Name');
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Update' }))
|
||||
.toBeEnabled();
|
||||
await expect.element(page.getByRole('button', { name: 'Update' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { json, error } from '@sveltejs/kit';
|
||||
import { auth } from '$lib/auth';
|
||||
import { prisma } from '$lib/db';
|
||||
import { sendMail } from '$lib/server/resend';
|
||||
import { BETTER_AUTH_URL } from '$env/static/private';
|
||||
|
||||
function generateLicenseKey(): string {
|
||||
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
|
||||
@@ -43,10 +44,27 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
|
||||
user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
await auth.api.requestPasswordReset({
|
||||
body: {
|
||||
email,
|
||||
redirectTo: '/reset-password'
|
||||
const verification = await prisma.verification.findFirst({
|
||||
where: {
|
||||
identifier: email,
|
||||
expiresAt: { gt: new Date() }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
const verifyUrl = verification
|
||||
? `${BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true`
|
||||
: `${BETTER_AUTH_URL}/login`;
|
||||
|
||||
await sendMail({
|
||||
to: email,
|
||||
templateId: 'automated-user-registration',
|
||||
subject: 'Your account is ready',
|
||||
data: {
|
||||
firstName: name ?? email.split('@')[0],
|
||||
tempPassword,
|
||||
verifyUrl,
|
||||
loginUrl: `${BETTER_AUTH_URL}/login`
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -73,10 +91,11 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
await sendMail({
|
||||
to: email,
|
||||
templateId: 'license-key-delivery',
|
||||
subject: 'Your License Key',
|
||||
data: {
|
||||
firstName: name ?? 'Valued Customer',
|
||||
licenseKey: key
|
||||
license_key: key
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user