Automated signups use the registration-email set-password link
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user