mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-07 05:01:49 +02:00
♻️ Move auth (#1422)
This commit is contained in:
parent
c0c363ca5f
commit
6009a8edae
9 changed files with 9 additions and 9 deletions
345
apps/web/src/auth.ts
Normal file
345
apps/web/src/auth.ts
Normal file
|
@ -0,0 +1,345 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next";
|
||||
import { NextAuthOptions, User } from "next-auth";
|
||||
import NextAuth, {
|
||||
getServerSession as getServerSessionWithOptions,
|
||||
} from "next-auth/next";
|
||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { Provider } from "next-auth/providers/index";
|
||||
|
||||
import { posthog } from "@/app/posthog";
|
||||
import { CustomPrismaAdapter } from "@/auth/custom-prisma-adapter";
|
||||
import { mergeGuestsIntoUser } from "@/auth/merge-user";
|
||||
import { env } from "@/env";
|
||||
import type { RegistrationTokenPayload } from "@/trpc/types";
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
import { getEmailClient } from "@/utils/emails";
|
||||
import { getValueByPath } from "@/utils/get-value-by-path";
|
||||
import { generateOtp, randomid } from "@/utils/nanoid";
|
||||
import { decryptToken } from "@/utils/session";
|
||||
|
||||
const providers: Provider[] = [
|
||||
// When a user registers, we don't want to go through the email verification process
|
||||
// so this provider 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,
|
||||
image: 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,
|
||||
locale: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await getEmailClient(user.locale ?? undefined).sendTemplate(
|
||||
"LoginEmail",
|
||||
{
|
||||
to: email,
|
||||
props: {
|
||||
magicLink: absoluteUrl("/auth/login", {
|
||||
magicLink: url,
|
||||
}),
|
||||
code: token,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
// If we have an OAuth provider configured, we add it to the list of providers
|
||||
if (
|
||||
process.env.OIDC_DISCOVERY_URL &&
|
||||
process.env.OIDC_CLIENT_ID &&
|
||||
process.env.OIDC_CLIENT_SECRET
|
||||
) {
|
||||
providers.push({
|
||||
id: "oidc",
|
||||
name: process.env.OIDC_NAME ?? "OpenID Connect",
|
||||
type: "oauth",
|
||||
wellKnown: process.env.OIDC_DISCOVERY_URL,
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
clientId: process.env.OIDC_CLIENT_ID,
|
||||
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||
idToken: true,
|
||||
checks: ["state"],
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: getValueByPath(profile, env.OIDC_NAME_CLAIM_PATH),
|
||||
email: getValueByPath(profile, env.OIDC_EMAIL_CLAIM_PATH),
|
||||
image: getValueByPath(profile, env.OIDC_PICTURE_CLAIM_PATH),
|
||||
} as User;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
providers.push(
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.MICROSOFT_TENANT_ID &&
|
||||
process.env.MICROSOFT_CLIENT_ID &&
|
||||
process.env.MICROSOFT_CLIENT_SECRET
|
||||
) {
|
||||
providers.push(
|
||||
AzureADProvider({
|
||||
tenantId: process.env.MICROSOFT_TENANT_ID,
|
||||
clientId: process.env.MICROSOFT_CLIENT_ID,
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
wellKnown:
|
||||
"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||
({
|
||||
adapter: CustomPrismaAdapter(prisma),
|
||||
secret: process.env.SECRET_PASSWORD,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
providers: providers,
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/auth/error",
|
||||
},
|
||||
events: {
|
||||
signIn({ user, account }) {
|
||||
posthog?.capture({
|
||||
distinctId: user.id,
|
||||
event: "login",
|
||||
properties: {
|
||||
method: account?.provider,
|
||||
$set: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
timeZone: user.timeZone,
|
||||
locale: user.locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
async signIn({ user, email, profile }) {
|
||||
const distinctId = user.id;
|
||||
// prevent sign in if email is not verified
|
||||
if (
|
||||
profile &&
|
||||
"email_verified" in profile &&
|
||||
profile.email_verified === false
|
||||
) {
|
||||
posthog?.capture({
|
||||
distinctId,
|
||||
event: "login failed",
|
||||
properties: {
|
||||
reason: "email not verified",
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// Make sure email is allowed
|
||||
if (user.email) {
|
||||
const isBlocked = isEmailBlocked(user.email);
|
||||
if (isBlocked) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For now, we don't allow users to login unless they have
|
||||
// registered an account. This is just because we need a name
|
||||
// to display on the dashboard. The flow can be modified so that
|
||||
// the name is requested after the user has logged in.
|
||||
if (email?.verificationRequest) {
|
||||
const isUnregisteredUser =
|
||||
(await prisma.user.count({
|
||||
where: {
|
||||
email: user.email as string,
|
||||
},
|
||||
})) === 0;
|
||||
|
||||
if (isUnregisteredUser) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// merge guest user into newly logged in user
|
||||
const session = await getServerSession(...args);
|
||||
if (session && session.user.email === null) {
|
||||
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async jwt({ token, user, trigger, account, session }) {
|
||||
if (trigger === "signUp" && account?.providerAccountId) {
|
||||
// merge accounts assigned to provider account id to the current user id
|
||||
await mergeGuestsIntoUser(user.id, [account.providerAccountId]);
|
||||
}
|
||||
|
||||
if (session) {
|
||||
token.locale = session.locale;
|
||||
token.timeFormat = session.timeFormat;
|
||||
token.timeZone = session.timeZone;
|
||||
token.weekStart = session.weekStart;
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token.sub?.startsWith("user-")) {
|
||||
session.user.id = token.sub as string;
|
||||
session.user.locale = token.locale;
|
||||
session.user.timeFormat = token.timeFormat;
|
||||
session.user.timeZone = token.timeZone;
|
||||
session.user.locale = token.locale;
|
||||
session.user.weekStart = token.weekStart;
|
||||
return session;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: token.sub as string,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
timeFormat: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
weekStart: true,
|
||||
email: true,
|
||||
image: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
session.user.id = user.id;
|
||||
session.user.name = user.name;
|
||||
session.user.email = user.email;
|
||||
session.user.image = user.image;
|
||||
} else {
|
||||
session.user.id = token.sub || `user-${randomid()}`;
|
||||
}
|
||||
|
||||
const source = user ?? token;
|
||||
|
||||
session.user.locale = source.locale;
|
||||
session.user.timeFormat = source.timeFormat;
|
||||
session.user.timeZone = source.timeZone;
|
||||
session.user.weekStart = source.weekStart;
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
}) satisfies NextAuthOptions;
|
||||
|
||||
type GetServerSessionParams =
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
|
||||
| [NextApiRequest, NextApiResponse]
|
||||
| [];
|
||||
|
||||
export async function getServerSession(...args: GetServerSessionParams) {
|
||||
return await getServerSessionWithOptions(...args, getAuthOptions(...args));
|
||||
}
|
||||
|
||||
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
|
||||
const authOptions = getAuthOptions(req, res);
|
||||
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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue