mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
243 lines
6.4 KiB
TypeScript
243 lines
6.4 KiB
TypeScript
import { prisma } from "@rallly/database";
|
|
import { posthog } from "@rallly/posthog/server";
|
|
import { redirect } from "next/navigation";
|
|
import NextAuth from "next-auth";
|
|
import type { Provider } from "next-auth/providers";
|
|
import { cache } from "react";
|
|
import z from "zod";
|
|
|
|
import { CustomPrismaAdapter } from "./auth/adapters/prisma";
|
|
import { isEmailBanned, isEmailBlocked } from "./auth/helpers/is-email-blocked";
|
|
import { mergeGuestsIntoUser } from "./auth/helpers/merge-user";
|
|
import { EmailProvider } from "./auth/providers/email";
|
|
import { GoogleProvider } from "./auth/providers/google";
|
|
import { GuestProvider } from "./auth/providers/guest";
|
|
import { MicrosoftProvider } from "./auth/providers/microsoft";
|
|
import { OIDCProvider } from "./auth/providers/oidc";
|
|
import { RegistrationTokenProvider } from "./auth/providers/registration-token";
|
|
import { nextAuthConfig } from "./next-auth.config";
|
|
|
|
const sessionUpdateSchema = z.object({
|
|
locale: z.string().nullish(),
|
|
timeFormat: z.enum(["hours12", "hours24"]).nullish(),
|
|
timeZone: z.string().nullish(),
|
|
weekStart: z.number().nullish(),
|
|
});
|
|
|
|
const {
|
|
auth: originalAuth,
|
|
handlers,
|
|
signIn,
|
|
signOut,
|
|
} = NextAuth({
|
|
...nextAuthConfig,
|
|
adapter: CustomPrismaAdapter({
|
|
migrateData: async (userId) => {
|
|
const session = await auth();
|
|
if (session?.user && session.user.email === null) {
|
|
await mergeGuestsIntoUser(userId, [session.user.id]);
|
|
}
|
|
},
|
|
}),
|
|
providers: [
|
|
RegistrationTokenProvider,
|
|
EmailProvider,
|
|
GuestProvider,
|
|
...([GoogleProvider(), OIDCProvider(), MicrosoftProvider()].filter(
|
|
Boolean,
|
|
) as Provider[]),
|
|
],
|
|
pages: {
|
|
signIn: "/login",
|
|
verifyRequest: "/login/verify",
|
|
error: "/auth/error",
|
|
},
|
|
session: {
|
|
strategy: "jwt",
|
|
},
|
|
jwt: {
|
|
maxAge: 60 * 60 * 24 * 60,
|
|
},
|
|
cookies: {
|
|
sessionToken: {
|
|
options: {
|
|
maxAge: 60 * 60 * 24 * 60,
|
|
},
|
|
},
|
|
},
|
|
events: {
|
|
createUser({ user }) {
|
|
if (user.id) {
|
|
posthog?.capture({
|
|
distinctId: user.id,
|
|
event: "register",
|
|
properties: {
|
|
$set: {
|
|
name: user.name,
|
|
email: user.email,
|
|
timeZone: user.timeZone ?? undefined,
|
|
locale: user.locale ?? undefined,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
},
|
|
signIn({ user, account }) {
|
|
if (user.id) {
|
|
posthog?.capture({
|
|
distinctId: user.id,
|
|
event: "login",
|
|
properties: {
|
|
method: account?.provider,
|
|
$set: {
|
|
name: user.name,
|
|
email: user.email,
|
|
timeZone: user.timeZone ?? undefined,
|
|
locale: user.locale ?? undefined,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
},
|
|
},
|
|
callbacks: {
|
|
...nextAuthConfig.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 &&
|
|
distinctId
|
|
) {
|
|
posthog?.capture({
|
|
distinctId,
|
|
event: "login failed",
|
|
properties: {
|
|
reason: "email not verified",
|
|
},
|
|
});
|
|
return false;
|
|
}
|
|
|
|
if (user.banned) {
|
|
return false;
|
|
}
|
|
|
|
// Make sure email is allowed
|
|
if (user.email) {
|
|
if (isEmailBlocked(user.email) || (await isEmailBanned(user.email))) {
|
|
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 isRegisteredUser =
|
|
(await prisma.user.count({
|
|
where: {
|
|
email: user.email as string,
|
|
},
|
|
})) > 0;
|
|
|
|
return isRegisteredUser;
|
|
}
|
|
|
|
// when we login with a social account for the first time, the user is not created yet
|
|
// and the user id will be the same as the provider account id
|
|
// we handle this case the the prisma adapter when we link accounts
|
|
const isInitialSocialLogin = user.id === profile?.sub;
|
|
|
|
if (!isInitialSocialLogin) {
|
|
// merge guest user into newly logged in user
|
|
const session = await auth();
|
|
if (user.id && session?.user && !session.user.email) {
|
|
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
async jwt({ token, session }) {
|
|
if (session) {
|
|
const parsed = sessionUpdateSchema.safeParse(session);
|
|
if (parsed.success) {
|
|
Object.entries(parsed.data).forEach(([key, value]) => {
|
|
token[key] = value;
|
|
});
|
|
} else {
|
|
console.error(parsed.error);
|
|
}
|
|
} else {
|
|
const userId = token.sub;
|
|
const isGuest = userId?.startsWith("guest-");
|
|
|
|
if (userId && !isGuest) {
|
|
const user = await prisma.user.findUnique({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
select: {
|
|
name: true,
|
|
email: true,
|
|
timeFormat: true,
|
|
timeZone: true,
|
|
weekStart: true,
|
|
image: true,
|
|
},
|
|
});
|
|
|
|
if (user) {
|
|
token.name = user.name;
|
|
token.email = user.email;
|
|
token.picture = user.image;
|
|
token.timeFormat = user.timeFormat;
|
|
token.timeZone = user.timeZone;
|
|
token.weekStart = user.weekStart;
|
|
}
|
|
}
|
|
}
|
|
|
|
return token;
|
|
},
|
|
},
|
|
});
|
|
|
|
const auth = cache(async () => {
|
|
try {
|
|
const session = await originalAuth();
|
|
if (session) {
|
|
return session;
|
|
}
|
|
} catch (e) {
|
|
console.error("FAILED TO GET SESSION", e);
|
|
}
|
|
});
|
|
|
|
const requireUser = async () => {
|
|
const session = await auth();
|
|
if (!session?.user) {
|
|
redirect("/login");
|
|
}
|
|
return { userId: session.user.id };
|
|
};
|
|
|
|
/**
|
|
* If email is not set it means the user is a guest
|
|
* @returns
|
|
*/
|
|
export const getUserId = async () => {
|
|
const session = await auth();
|
|
return session?.user?.email ? session.user.id : undefined;
|
|
};
|
|
|
|
export const getLoggedIn = async () => {
|
|
const session = await auth();
|
|
return !!session?.user?.email;
|
|
};
|
|
|
|
export { auth, handlers, requireUser, signIn, signOut };
|