diff --git a/src/routes/api/v1/automations/create/account/+server.ts b/src/routes/api/v1/automations/create/account/+server.ts index c3a452c..8d69376 100644 --- a/src/routes/api/v1/automations/create/account/+server.ts +++ b/src/routes/api/v1/automations/create/account/+server.ts @@ -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) { diff --git a/src/routes/api/v2/automations/create/account/+server.ts b/src/routes/api/v2/automations/create/account/+server.ts index 451f3f2..fb5d8b1 100644 --- a/src/routes/api/v2/automations/create/account/+server.ts +++ b/src/routes/api/v2/automations/create/account/+server.ts @@ -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) { diff --git a/src/routes/api/v2/automations/create/account/server.spec.ts b/src/routes/api/v2/automations/create/account/server.spec.ts index 82513d0..725b002 100644 --- a/src/routes/api/v2/automations/create/account/server.spec.ts +++ b/src/routes/api/v2/automations/create/account/server.spec.ts @@ -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; -const mockVerification = prisma.verification.findFirst as ReturnType; +const mockUserCreate = prisma.user.create as ReturnType; const mockLicenseCreate = prisma.licenseKey.create as ReturnType; 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 () => {