rallly/packages/backend/trpc/routers/auth.ts
2023-04-04 08:45:52 +00:00

239 lines
5.5 KiB
TypeScript

import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails";
import { absoluteUrl } from "@rallly/utils";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createToken, decryptToken } from "../../session";
import { generateOtp } from "../../utils/nanoid";
import { publicProcedure, router } from "../trpc";
import { LoginTokenPayload, RegistrationTokenPayload } from "../types";
// assigns participants and comments created by guests to a user
// we could have multiple guests because a login might be triggered from one device
// and opened in another one.
const mergeGuestsIntoUser = async (userId: string, guestIds: string[]) => {
await prisma.participant.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
await prisma.comment.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
};
const isEmailBlocked = (email: string) => {
if (process.env.ALLOWED_EMAILS) {
const allowedEmails = process.env.ALLOWED_EMAILS.split(",");
// Check whether the email matches enough of the patterns specified in ALLOWED_EMAILS
const isAllowed = allowedEmails.some((allowedEmail) => {
const regex = new RegExp(allowedEmail.trim().replace("*", ".*"), "i");
return regex.test(email);
});
if (!isAllowed) {
return true;
}
}
return false;
};
export const auth = router({
requestRegistration: publicProcedure
.input(
z.object({
name: z.string(),
email: z.string(),
}),
)
.mutation(
async ({
input,
}): Promise<
| { ok: true; token: string }
| { ok: false; reason: "userAlreadyExists" | "emailNotAllowed" }
> => {
if (isEmailBlocked(input.email)) {
return { ok: false, reason: "emailNotAllowed" };
}
const user = await prisma.user.findUnique({
select: {
id: true,
},
where: {
email: input.email,
},
});
if (user) {
return { ok: false, reason: "userAlreadyExists" };
}
const code = generateOtp();
const token = await createToken<RegistrationTokenPayload>({
name: input.name,
email: input.email,
code,
});
await sendEmail("RegisterEmail", {
to: input.email,
subject: `${input.name}, please verify your email address`,
props: {
code,
name: input.name,
},
});
return { ok: true, token };
},
),
authenticateRegistration: publicProcedure
.input(
z.object({
token: z.string(),
code: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const payload = await decryptToken<RegistrationTokenPayload>(input.token);
if (!payload) {
return { user: null };
}
const { name, email, code } = payload;
if (input.code !== code) {
return { ok: false };
}
const user = await prisma.user.create({
select: { id: true, name: true, email: true },
data: {
name,
email,
},
});
if (ctx.session.user?.isGuest) {
await mergeGuestsIntoUser(user.id, [ctx.session.user.id]);
}
ctx.session.user = {
isGuest: false,
id: user.id,
};
await ctx.session.save();
return { ok: true, user };
}),
requestLogin: publicProcedure
.input(
z.object({
email: z.string(),
}),
)
.mutation(
async ({
input,
}): Promise<
| { ok: true; token: string }
| { ok: false; reason: "emailNotAllowed" | "userNotFound" }
> => {
if (isEmailBlocked(input.email)) {
return { ok: false, reason: "emailNotAllowed" };
}
const user = await prisma.user.findUnique({
where: {
email: input.email,
},
});
if (!user) {
return { ok: false, reason: "userNotFound" };
}
const code = generateOtp();
const token = await createToken<LoginTokenPayload>({
userId: user.id,
code,
});
await sendEmail("LoginEmail", {
to: input.email,
subject: `${code} is your 6-digit code`,
props: {
name: user.name,
code,
magicLink: absoluteUrl(`/auth/login?token=${token}`),
},
});
return { ok: true, token };
},
),
authenticateLogin: publicProcedure
.input(
z.object({
token: z.string(),
code: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const payload = await decryptToken<LoginTokenPayload>(input.token);
if (!payload) {
return { user: null };
}
const { userId, code } = payload;
if (input.code !== code) {
return { user: null };
}
const user = await prisma.user.findUnique({
select: { id: true, name: true, email: true },
where: { id: userId },
});
if (!user) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "The user doesn't exist anymore",
});
}
if (ctx.session.user?.isGuest) {
await mergeGuestsIntoUser(user.id, [ctx.session.user.id]);
}
ctx.session.user = {
isGuest: false,
id: user.id,
};
await ctx.session.save();
return { user };
}),
});