Automated signups use the registration-email set-password link

This commit is contained in:
NeilClack
2026-09-10 14:08:56 -05:00
parent 84c905c9ff
commit 809ced6d8a
3 changed files with 48 additions and 87 deletions
@@ -1,9 +1,8 @@
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { auth } from '$lib/auth';
import { prisma } from '$lib/db';
import { sendMail } from '$lib/server/resend';
import { env } from '$env/dynamic/private';
import { sendRegistrationInvite } from '$lib/server/registration-invite';
function generateLicenseKey(): string {
const hex = crypto.randomUUID().replace(/-/g, '').slice(0, 16).toUpperCase();
@@ -21,46 +20,27 @@ export const POST: RequestHandler = async ({ request }) => {
let user = await prisma.user.findUnique({ where: { email } });
if (!user) {
const tempPassword = crypto.randomUUID().replace(/-/g, '').slice(0, 20);
try {
await auth.api.signUpEmail({
body: {
email,
password: tempPassword,
name: name ?? email.split('@')[0]
}
});
user = await prisma.user.findUnique({ where: { email } });
const verification = await prisma.verification.findFirst({
where: {
identifier: email,
expiresAt: { gt: new Date() }
},
orderBy: { createdAt: 'desc' }
});
const verifyUrl = verification
? `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true`
: `${env.BETTER_AUTH_URL}/login`;
await sendMail({
to: email,
subject: 'Your account is ready',
templateId: 'automated-user-registration',
user = await prisma.user.create({
data: {
firstName: name ?? email.split('@')[0],
tempPassword,
verifyUrl,
loginUrl: `${env.BETTER_AUTH_URL}/login`
id: crypto.randomUUID(),
name: name ?? email.split('@')[0],
email,
// The onboarding flow vouches for the email address; the user
// still has to use the emailed link to set a password.
emailVerified: true
}
});
} catch (err) {
console.error('Failed to create user:', err);
error(500, 'Failed to create account');
}
try {
await sendRegistrationInvite(email);
} catch (err) {
console.error('Failed to send registration email:', err);
}
}
if (!user) {
@@ -1,8 +1,8 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { auth } from '$lib/auth';
import { prisma } from '$lib/db';
import { sendMail } from '$lib/server/resend';
import { sendRegistrationInvite } from '$lib/server/registration-invite';
import { env } from '$env/dynamic/private';
import { clientIp, rateLimited } from '$lib/server/v2-helpers';
import { rateLimit } from '$lib/server/v2-rate-limit';
@@ -86,46 +86,27 @@ export const POST: RequestHandler = async ({ request, url }) => {
let user = await prisma.user.findUnique({ where: { email: cleanEmail } });
if (!user) {
const tempPassword = crypto.randomUUID().replace(/-/g, '').slice(0, 20);
try {
await auth.api.signUpEmail({
body: {
email: cleanEmail,
password: tempPassword,
name: displayName
}
});
user = await prisma.user.findUnique({ where: { email: cleanEmail } });
const verification = await prisma.verification.findFirst({
where: {
identifier: cleanEmail,
expiresAt: { gt: new Date() }
},
orderBy: { createdAt: 'desc' }
});
const verifyUrl = verification
? `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${verification.value}&callbackURL=/login?verified=true`
: `${env.BETTER_AUTH_URL}/login`;
await sendMail({
to: cleanEmail,
subject: 'Your account is ready',
templateId: 'automated-user-registration',
user = await prisma.user.create({
data: {
firstName: displayName,
tempPassword,
verifyUrl,
loginUrl: `${env.BETTER_AUTH_URL}/login`
id: crypto.randomUUID(),
name: displayName,
email: cleanEmail,
// The onboarding flow vouches for the email address; the user
// still has to use the emailed link to set a password.
emailVerified: true
}
});
} catch (err) {
console.error('Failed to create user:', err);
error(500, 'Failed to create account');
}
try {
await sendRegistrationInvite(cleanEmail);
} catch (err) {
console.error('Failed to send registration email:', err);
}
}
if (!user) {
@@ -3,9 +3,9 @@ import { isHttpError } from '@sveltejs/kit';
import { computeSignature, buildCanonicalRequest, _clearNonces } from '$lib/server/v2-signature';
import { _clearRateLimits } from '$lib/server/v2-rate-limit';
const { authApi, sendMailMock } = vi.hoisted(() => ({
authApi: { signUpEmail: vi.fn() },
sendMailMock: vi.fn()
const { sendMailMock, sendRegistrationInviteMock } = vi.hoisted(() => ({
sendMailMock: vi.fn(),
sendRegistrationInviteMock: vi.fn()
}));
vi.mock('$env/dynamic/private', () => ({
@@ -15,8 +15,8 @@ vi.mock('$env/dynamic/private', () => ({
}
}));
vi.mock('$lib/auth', () => ({
auth: { api: authApi }
vi.mock('$lib/server/registration-invite', () => ({
sendRegistrationInvite: sendRegistrationInviteMock
}));
vi.mock('$lib/server/resend', () => ({
@@ -25,8 +25,7 @@ vi.mock('$lib/server/resend', () => ({
vi.mock('$lib/db', () => ({
prisma: {
user: { findUnique: vi.fn() },
verification: { findFirst: vi.fn() },
user: { findUnique: vi.fn(), create: vi.fn() },
licenseKey: { create: vi.fn() }
}
}));
@@ -40,7 +39,7 @@ const SECRET = 'test-onboarding-secret';
const PATH = '/api/v2/automations/create/account';
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockVerification = prisma.verification.findFirst as ReturnType<typeof vi.fn>;
const mockUserCreate = prisma.user.create as ReturnType<typeof vi.fn>;
const mockLicenseCreate = prisma.licenseKey.create as ReturnType<typeof vi.fn>;
function signedOnboardingRequest(body: object, secret = SECRET): Request {
@@ -101,23 +100,21 @@ describe('POST /api/v2/automations/create/account', () => {
message: 'Account and license key created',
user_id: 'user-1'
});
expect(authApi.signUpEmail).not.toHaveBeenCalled();
expect(mockLicenseCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ active: true })
})
);
expect(sendMailMock).toHaveBeenCalledTimes(1);
expect(sendRegistrationInviteMock).not.toHaveBeenCalled();
});
it('signs up a new user and sends registration + license emails', async () => {
mockUserFindUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: 'user-2', email: 'new@example.com' });
mockVerification.mockResolvedValue({ value: 'verify-token' });
it('creates a new user and sends the registration + license emails', async () => {
mockUserFindUnique.mockResolvedValue(null);
mockUserCreate.mockResolvedValue({ id: 'user-2', email: 'new@example.com' });
mockLicenseCreate.mockResolvedValue({ id: 'lic-2' });
sendMailMock.mockResolvedValue({});
authApi.signUpEmail.mockResolvedValue({});
sendRegistrationInviteMock.mockResolvedValue(undefined);
const res = await POST({
request: signedOnboardingRequest({ email: 'new@example.com' }),
@@ -130,12 +127,15 @@ describe('POST /api/v2/automations/create/account', () => {
message: 'Account and license key created',
user_id: 'user-2'
});
expect(authApi.signUpEmail).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({ email: 'new@example.com', name: 'new' })
expect(mockUserCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
email: 'new@example.com',
name: 'new',
emailVerified: true
})
);
expect(sendMailMock).toHaveBeenCalledTimes(2);
});
expect(sendRegistrationInviteMock).toHaveBeenCalledWith('new@example.com');
expect(sendMailMock).toHaveBeenCalledTimes(1);
});
it('rejects a request without a valid signature', async () => {