mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-09 14:11:51 +02:00
🐛 Fix merge guest user into logged in user (#907)
This commit is contained in:
parent
5be17fd249
commit
7c54268b58
3 changed files with 202 additions and 201 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
apps/web/src/utils/auth/merge-user.ts
Normal file
39
apps/web/src/utils/auth/merge-user.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue