diff --git a/src/lib/server/registration-invite.ts b/src/lib/server/registration-invite.ts new file mode 100644 index 0000000..f15a14b --- /dev/null +++ b/src/lib/server/registration-invite.ts @@ -0,0 +1,18 @@ +import { auth } from '$lib/auth'; + +/** + * Email an invited user a link to set their password and finish their profile. + * + * Reuses Better Auth's password-reset token flow; the `invite=1` marker makes + * the reset-password hook send the `registration-email` template instead of the + * usual "you requested a reset" email. The link lands on /reset-password, and + * after setting a password the user logs in and is sent to /profile/setup. + */ +export async function sendRegistrationInvite(email: string): Promise { + await auth.api.requestPasswordReset({ + body: { + email, + redirectTo: '/reset-password?invite=1' + } + }); +} diff --git a/src/routes/(protected)/admin/users/+page.server.ts b/src/routes/(protected)/admin/users/+page.server.ts index c08fcae..490aa03 100644 --- a/src/routes/(protected)/admin/users/+page.server.ts +++ b/src/routes/(protected)/admin/users/+page.server.ts @@ -1,6 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; import { prisma } from '$lib/db'; -import { auth } from '$lib/auth'; +import { sendRegistrationInvite } from '$lib/server/registration-invite'; import { superValidate } from 'sveltekit-superforms/server'; import { zod4 } from 'sveltekit-superforms/adapters'; import { z } from 'zod'; @@ -96,15 +96,7 @@ export const actions: Actions = { if (!rawId) { try { - // Send a registration email with a set-password link. This reuses - // Better Auth's reset-password token flow; `invite=1` tells the - // email hook to send the welcome/registration template instead. - await auth.api.requestPasswordReset({ - body: { - email: data.email, - redirectTo: '/reset-password?invite=1' - } - }); + await sendRegistrationInvite(data.email); } catch (err) { console.error('Failed to send registration email:', err); } diff --git a/src/routes/(protected)/admin/users/page.server.spec.ts b/src/routes/(protected)/admin/users/page.server.spec.ts index 8ccfa8a..72c40e7 100644 --- a/src/routes/(protected)/admin/users/page.server.spec.ts +++ b/src/routes/(protected)/admin/users/page.server.spec.ts @@ -14,12 +14,12 @@ vi.mock('$lib/db', () => ({ } })); -const { requestPasswordReset } = vi.hoisted(() => ({ - requestPasswordReset: vi.fn() +const { sendRegistrationInvite } = vi.hoisted(() => ({ + sendRegistrationInvite: vi.fn() })); -vi.mock('$lib/auth', () => ({ - auth: { api: { requestPasswordReset } } +vi.mock('$lib/server/registration-invite', () => ({ + sendRegistrationInvite })); import { prisma } from '$lib/db'; @@ -47,7 +47,7 @@ describe('users admin page server upsert', () => { userFindUnique.mockResolvedValue({ roleId: 1 }); userCreate.mockResolvedValue({ id: 'new-user' }); userUpdate.mockResolvedValue({ id: 'user-1' }); - requestPasswordReset.mockResolvedValue(undefined); + sendRegistrationInvite.mockResolvedValue(undefined); }); it('marks a created user verified and emails a set-password link', async () => { @@ -62,12 +62,7 @@ describe('users admin page server upsert', () => { emailVerified: true }) }); - expect(requestPasswordReset).toHaveBeenCalledWith({ - body: { - email: 'jane@example.com', - redirectTo: '/reset-password?invite=1' - } - }); + expect(sendRegistrationInvite).toHaveBeenCalledWith('jane@example.com'); }); it('does not email a registration link when updating an existing user', async () => { @@ -77,11 +72,11 @@ describe('users admin page server upsert', () => { expect(result).toEqual({ success: true, action: 'upsert' }); expect(userUpdate).toHaveBeenCalled(); expect(userCreate).not.toHaveBeenCalled(); - expect(requestPasswordReset).not.toHaveBeenCalled(); + expect(sendRegistrationInvite).not.toHaveBeenCalled(); }); it('still succeeds when the registration email fails to send', async () => { - requestPasswordReset.mockRejectedValue(new Error('resend down')); + sendRegistrationInvite.mockRejectedValue(new Error('resend down')); const locals = createLocals(mockUser() as any); const result = await invokeUpsert(upsertRequest(), locals);