🐛 Fix merge guest user into logged in user (#907)

This commit is contained in:
Luke Vella 2023-10-20 16:45:43 +01:00 committed by GitHub
parent 5be17fd249
commit 7c54268b58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 201 deletions

View file

@ -18,185 +18,189 @@ import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email"; import EmailProvider from "next-auth/providers/email";
import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider"; import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
import { emailClient } from "@/utils/emails"; import { emailClient } from "@/utils/emails";
const authOptions = { const getAuthOptions = (...args: GetServerSessionParams) =>
adapter: PrismaAdapter(prisma), ({
secret: process.env.SECRET_PASSWORD, adapter: PrismaAdapter(prisma),
session: { secret: process.env.SECRET_PASSWORD,
strategy: "jwt", session: {
}, strategy: "jwt",
providers: [ },
LegacyTokenProvider, providers: [
// When a user registers, we don't want to go through the email verification process LegacyTokenProvider,
// so this providers allows us exchange the registration token for a session token // When a user registers, we don't want to go through the email verification process
CredentialsProvider({ // so this providers allows us exchange the registration token for a session token
id: "registration-token", CredentialsProvider({
name: "Registration Token", id: "registration-token",
credentials: { name: "Registration Token",
token: { credentials: {
label: "Token", token: {
type: "text", label: "Token",
type: "text",
},
}, },
}, async authorize(credentials) {
async authorize(credentials) { if (credentials?.token) {
if (credentials?.token) { const payload = await decryptToken<RegistrationTokenPayload>(
const payload = await decryptToken<RegistrationTokenPayload>( credentials.token,
credentials.token, );
); if (payload) {
if (payload) { const user = await prisma.user.findUnique({
const user = await prisma.user.findUnique({ where: {
where: { email: payload.email,
email: payload.email, },
}, select: {
select: { id: true,
id: true, email: true,
email: true, name: true,
name: true, locale: true,
locale: true, timeFormat: true,
timeFormat: true, timeZone: true,
timeZone: true, },
}, });
});
if (user) { if (user) {
return user; return user;
}
} }
} }
}
return null; return null;
}, },
}), }),
CredentialsProvider({ CredentialsProvider({
id: "guest", id: "guest",
name: "Guest", name: "Guest",
credentials: {}, credentials: {},
async authorize() { async authorize() {
return { return {
id: `user-${randomid()}`, id: `user-${randomid()}`,
email: null, email: null,
}; };
}, },
}), }),
EmailProvider({ EmailProvider({
server: "", server: "",
from: process.env.NOREPLY_EMAIL, from: process.env.NOREPLY_EMAIL,
generateVerificationToken() { generateVerificationToken() {
return generateOtp(); return generateOtp();
}, },
async sendVerificationRequest({ identifier: email, token, url }) { async sendVerificationRequest({ identifier: email, token, url }) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
email, email,
}, },
select: { select: {
name: true, 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",
error: "/auth/error",
},
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; if (user) {
}, await emailClient.sendTemplate("LoginEmail", {
async jwt({ token, user, trigger, session }) { to: email,
if (trigger === "update" && session) { subject: `${token} is your 6-digit code`,
if (token.email) { props: {
// For registered users we want to save the preferences to the database name: user.name,
try { magicLink: url,
await prisma.user.update({ code: token,
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); },
}),
],
pages: {
signIn: "/login",
signOut: "/logout",
error: "/auth/error",
},
callbacks: {
async signIn({ user, email }) {
if (email?.verificationRequest) {
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;
}
}
} else {
const session = await getServerSession(...args);
if (session && session.user.email === null) {
await mergeGuestsIntoUser(user.id, [session.user.id]);
} }
} }
token = { ...token, ...session };
}
if (trigger === "signIn" && user) { return true;
token.locale = user.locale; },
token.timeFormat = user.timeFormat; async jwt({ token, user, trigger, session }) {
token.timeZone = user.timeZone; if (trigger === "update" && session) {
token.weekStart = user.weekStart; 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 };
}
return token; 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;
},
}, },
async session({ session, token }) { } satisfies NextAuthOptions);
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 async function getServerSession( type GetServerSessionParams =
...args: | [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] | [NextApiRequest, NextApiResponse]
| [NextApiRequest, NextApiResponse] | [];
| []
) { export function getServerSession(...args: GetServerSessionParams) {
return getServerSessionWithOptions(...args, authOptions); return getServerSessionWithOptions(...args, getAuthOptions(...args));
} }
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) { export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
const authOptions = getAuthOptions(req, res);
return NextAuth(req, res, authOptions); return NextAuth(req, res, authOptions);
} }

View file

@ -0,0 +1,39 @@
import { prisma } from "@rallly/database";
export const mergeGuestsIntoUser = async (
userId: string,
guestIds: string[],
) => {
await prisma.poll.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
await prisma.participant.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
await prisma.comment.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
};

View file

@ -6,44 +6,6 @@ import { generateOtp } from "../../utils/nanoid";
import { publicProcedure, router } from "../trpc"; import { publicProcedure, router } from "../trpc";
import { RegistrationTokenPayload } from "../types"; import { 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.poll.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
await prisma.participant.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
await prisma.comment.updateMany({
where: {
userId: {
in: guestIds,
},
},
data: {
userId: userId,
},
});
};
export const auth = router({ export const auth = router({
// @deprecated // @deprecated
requestRegistration: publicProcedure requestRegistration: publicProcedure
@ -107,7 +69,7 @@ export const auth = router({
locale: z.string().optional(), locale: z.string().optional(),
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
const payload = await decryptToken<RegistrationTokenPayload>(input.token); const payload = await decryptToken<RegistrationTokenPayload>(input.token);
if (!payload) { if (!payload) {
@ -129,10 +91,6 @@ export const auth = router({
}, },
}); });
if (ctx.user.isGuest) {
await mergeGuestsIntoUser(user.id, [ctx.user.id]);
}
return { ok: true, user }; return { ok: true, user };
}), }),
getUserPermission: publicProcedure getUserPermission: publicProcedure