Add shared registration invite helper and use it for admin-created users
This commit is contained in:
@@ -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<void> {
|
||||||
|
await auth.api.requestPasswordReset({
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
redirectTo: '/reset-password?invite=1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
import { auth } from '$lib/auth';
|
import { sendRegistrationInvite } from '$lib/server/registration-invite';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -96,15 +96,7 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
if (!rawId) {
|
if (!rawId) {
|
||||||
try {
|
try {
|
||||||
// Send a registration email with a set-password link. This reuses
|
await sendRegistrationInvite(data.email);
|
||||||
// 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'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to send registration email:', err);
|
console.error('Failed to send registration email:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ vi.mock('$lib/db', () => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { requestPasswordReset } = vi.hoisted(() => ({
|
const { sendRegistrationInvite } = vi.hoisted(() => ({
|
||||||
requestPasswordReset: vi.fn()
|
sendRegistrationInvite: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('$lib/auth', () => ({
|
vi.mock('$lib/server/registration-invite', () => ({
|
||||||
auth: { api: { requestPasswordReset } }
|
sendRegistrationInvite
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
@@ -47,7 +47,7 @@ describe('users admin page server upsert', () => {
|
|||||||
userFindUnique.mockResolvedValue({ roleId: 1 });
|
userFindUnique.mockResolvedValue({ roleId: 1 });
|
||||||
userCreate.mockResolvedValue({ id: 'new-user' });
|
userCreate.mockResolvedValue({ id: 'new-user' });
|
||||||
userUpdate.mockResolvedValue({ id: 'user-1' });
|
userUpdate.mockResolvedValue({ id: 'user-1' });
|
||||||
requestPasswordReset.mockResolvedValue(undefined);
|
sendRegistrationInvite.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks a created user verified and emails a set-password link', async () => {
|
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
|
emailVerified: true
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
expect(requestPasswordReset).toHaveBeenCalledWith({
|
expect(sendRegistrationInvite).toHaveBeenCalledWith('jane@example.com');
|
||||||
body: {
|
|
||||||
email: 'jane@example.com',
|
|
||||||
redirectTo: '/reset-password?invite=1'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not email a registration link when updating an existing user', async () => {
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(userUpdate).toHaveBeenCalled();
|
expect(userUpdate).toHaveBeenCalled();
|
||||||
expect(userCreate).not.toHaveBeenCalled();
|
expect(userCreate).not.toHaveBeenCalled();
|
||||||
expect(requestPasswordReset).not.toHaveBeenCalled();
|
expect(sendRegistrationInvite).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('still succeeds when the registration email fails to send', async () => {
|
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 locals = createLocals(mockUser() as any);
|
||||||
const result = await invokeUpsert(upsertRequest(), locals);
|
const result = await invokeUpsert(upsertRequest(), locals);
|
||||||
|
|||||||
Reference in New Issue
Block a user