mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-28 21:57:23 +02:00
♻️ Switch to next-auth for handling authentication (#899)
This commit is contained in:
parent
5f9e428432
commit
6fa66da681
65 changed files with 1514 additions and 1586 deletions
251
apps/web/src/utils/auth.ts
Normal file
251
apps/web/src/utils/auth.ts
Normal file
|
@ -0,0 +1,251 @@
|
|||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { RegistrationTokenPayload } from "@rallly/backend";
|
||||
import { decryptToken } from "@rallly/backend/session";
|
||||
import { generateOtp, randomid } from "@rallly/backend/utils/nanoid";
|
||||
import { prisma } from "@rallly/database";
|
||||
import cookie from "cookie";
|
||||
import { IronSession, unsealData } from "iron-session";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next";
|
||||
import { NextAuthOptions, RequestInternal } from "next-auth";
|
||||
import NextAuth, {
|
||||
getServerSession as getServerSessionWithOptions,
|
||||
} from "next-auth/next";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
|
||||
import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider";
|
||||
import { emailClient } from "@/utils/emails";
|
||||
|
||||
const authOptions = {
|
||||
adapter: PrismaAdapter(prisma),
|
||||
secret: process.env.SECRET_PASSWORD,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
providers: [
|
||||
LegacyTokenProvider,
|
||||
// When a user registers, we don't want to go through the email verification process
|
||||
// so this providers allows us exchange the registration token for a session token
|
||||
CredentialsProvider({
|
||||
id: "registration-token",
|
||||
name: "Registration Token",
|
||||
credentials: {
|
||||
token: {
|
||||
label: "Token",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (credentials?.token) {
|
||||
const payload = await decryptToken<RegistrationTokenPayload>(
|
||||
credentials.token,
|
||||
);
|
||||
if (payload) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: payload.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
timeZone: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: "guest",
|
||||
name: "Guest",
|
||||
credentials: {},
|
||||
async authorize() {
|
||||
return {
|
||||
id: `user-${randomid()}`,
|
||||
email: null,
|
||||
};
|
||||
},
|
||||
}),
|
||||
EmailProvider({
|
||||
server: "",
|
||||
from: process.env.NOREPLY_EMAIL,
|
||||
generateVerificationToken() {
|
||||
return generateOtp();
|
||||
},
|
||||
async sendVerificationRequest({ identifier: email, token, url }) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await emailClient.sendTemplate("LoginEmail", {
|
||||
to: email,
|
||||
subject: `${token} is your 6-digit code`,
|
||||
props: {
|
||||
name: user.name,
|
||||
magicLink: url,
|
||||
code: token,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
signOut: "/logout",
|
||||
},
|
||||
callbacks: {
|
||||
async redirect({ url, baseUrl }) {
|
||||
// Allows relative callback URLs
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
||||
// Allows callback URLs on the same origin
|
||||
else if (new URL(url).origin === baseUrl) return url;
|
||||
return baseUrl;
|
||||
},
|
||||
async signIn({ user }) {
|
||||
if (user.email) {
|
||||
const userExists =
|
||||
(await prisma.user.count({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
})) > 0;
|
||||
if (userExists) {
|
||||
if (isEmailBlocked(user.email)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user, trigger, session }) {
|
||||
if (trigger === "update" && session) {
|
||||
if (token.email) {
|
||||
// For registered users we want to save the preferences to the database
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: token.sub,
|
||||
},
|
||||
data: {
|
||||
locale: session.locale,
|
||||
timeFormat: session.timeFormat,
|
||||
timeZone: session.timeZone,
|
||||
weekStart: session.weekStart,
|
||||
name: session.name,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to update user preferences", session);
|
||||
}
|
||||
}
|
||||
token = { ...token, ...session };
|
||||
}
|
||||
|
||||
if (trigger === "signIn" && user) {
|
||||
token.locale = user.locale;
|
||||
token.timeFormat = user.timeFormat;
|
||||
token.timeZone = user.timeZone;
|
||||
token.weekStart = user.weekStart;
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
session.user.id = token.sub as string;
|
||||
session.user.name = token.name;
|
||||
session.user.timeFormat = token.timeFormat;
|
||||
session.user.timeZone = token.timeZone;
|
||||
session.user.locale = token.locale;
|
||||
session.user.weekStart = token.weekStart;
|
||||
return session;
|
||||
},
|
||||
},
|
||||
} satisfies NextAuthOptions;
|
||||
|
||||
export function getServerSession(
|
||||
...args:
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
||||
| [NextApiRequest, NextApiResponse]
|
||||
| []
|
||||
) {
|
||||
return getServerSessionWithOptions(...args, authOptions);
|
||||
}
|
||||
|
||||
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
|
||||
return NextAuth(req, res, authOptions);
|
||||
}
|
||||
|
||||
export 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
|
||||
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
||||
.replaceAll(/[*]/g, ".*")}$`,
|
||||
);
|
||||
return regex.test(email);
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const legacySessionConfig = {
|
||||
password: process.env.SECRET_PASSWORD ?? "",
|
||||
cookieName: "rallly-session",
|
||||
cookieOptions: {
|
||||
secure: process.env.NEXT_PUBLIC_BASE_URL?.startsWith("https://") ?? false,
|
||||
},
|
||||
ttl: 60 * 60 * 24 * 30, // 30 days
|
||||
};
|
||||
|
||||
export const getUserFromLegacySession = async (
|
||||
req: Pick<RequestInternal, "headers">,
|
||||
) => {
|
||||
const parsedCookie = cookie.parse(req.headers?.cookie);
|
||||
if (parsedCookie[legacySessionConfig.cookieName]) {
|
||||
try {
|
||||
const session = await unsealData<IronSession>(
|
||||
parsedCookie[legacySessionConfig.cookieName],
|
||||
{
|
||||
password: process.env.SECRET_PASSWORD,
|
||||
},
|
||||
);
|
||||
if (session.user) {
|
||||
return session.user;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue