All files / apps/web/src/handlers/auth register.ts

100% Statements 18/18
100% Branches 8/8
100% Functions 1/1
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94                              6x                           4x 4x           4x 1x     1x         3x 3x 3x 3x 3x 2x           2x                         2x                       2x       2x     2x 2x        
import { TRPCError } from "@trpc/server";
import { z } from "zod";
 
import { AUTH_COOKIE } from "~app/utils/auth";
import { passwordSchema, peerNameSchema } from "~app/utils/validation";
import type { PeerId, UserId } from "~db/ids";
import { generatePasswordData } from "~utils/server/crypto";
import {
	createAuthorizationSession,
	sendVerificationEmail,
} from "~web/handlers/auth/utils";
import { unauthProcedure } from "~web/handlers/trpc";
import { emailSchema } from "~web/handlers/validation";
import { setCookie } from "~web/utils/cookies";
 
export const procedure = unauthProcedure
	.meta({
		title: "Register account",
		description:
			"Creates a new account and its self-peer, sends a verification email if enabled, and starts a new session.",
	})
	.input(
		z.strictObject({
			email: emailSchema,
			password: passwordSchema,
			name: peerNameSchema,
		}),
	)
	.mutation(async ({ input, ctx }) => {
		const { database } = ctx;
		const account = await database
			.selectFrom("users")
			.select([])
			.where("email", "=", input.email.lowercase)
			.limit(1)
			.executeTakeFirst();
		if (account) {
			ctx.logger.debug(
				`Registration of account "${input.email.original}" failed: email already exists.`,
			);
			throw new TRPCError({
				code: "CONFLICT",
				message: `Email "${input.email.original}" already exists.`,
			});
		}
		const id: UserId = ctx.getUuid();
		const confirmationToken = ctx.getUuid();
		const emailServiceActive = ctx.emailOptions.getActive();
		const passwordData = await generatePasswordData(ctx, input.password);
		if (emailServiceActive) {
			await sendVerificationEmail(
				ctx,
				input.email.lowercase,
				confirmationToken,
			);
		}
		await database
			.insertInto("users")
			.values({
				id,
				email: input.email.lowercase,
				passwordHash: passwordData.hash,
				passwordSalt: passwordData.salt,
				confirmationToken: emailServiceActive ? confirmationToken : null,
				confirmationTokenTimestamp: emailServiceActive
					? Temporal.Now.zonedDateTimeISO()
					: null,
			})
			.execute();
		await database
			.insertInto("peers")
			.values({
				// Typesystem doesn't know that we use account id as self peer id
				id: id as PeerId,
				name: input.name,
				ownerUserId: id,
				connectedUserId: id,
				exposeReceipts: true,
				acceptReceipts: true,
			})
			.execute();
		const { authToken, expirationDate } = await createAuthorizationSession(
			ctx,
			id,
		);
		ctx.logger.debug(
			`Registration of account "${input.email.original}" succeed.`,
		);
		setCookie(ctx, AUTH_COOKIE, authToken, { expires: expirationDate });
		return {
			user: { id, verified: !emailServiceActive },
		};
	});