mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-24 03:37:23 +02:00
♻️ Create backend package (#643)
This commit is contained in:
parent
7fc08c6736
commit
05fe2edaea
68 changed files with 476 additions and 391 deletions
239
packages/backend/trpc/routers/auth.ts
Normal file
239
packages/backend/trpc/routers/auth.ts
Normal file
|
@ -0,0 +1,239 @@
|
|||
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 = await generateOtp();
|
||||
|
||||
const token = await createToken<RegistrationTokenPayload>({
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
code,
|
||||
});
|
||||
|
||||
await sendEmail("RegisterEmail", {
|
||||
to: input.email,
|
||||
subject: "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 = await generateOtp();
|
||||
|
||||
const token = await createToken<LoginTokenPayload>({
|
||||
userId: user.id,
|
||||
code,
|
||||
});
|
||||
|
||||
await sendEmail("LoginEmail", {
|
||||
to: input.email,
|
||||
subject: "Login",
|
||||
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 };
|
||||
}),
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue