mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 02:36:30 +02:00
🐛 Fix login error (#932)
This commit is contained in:
parent
3f6a5603b3
commit
e3aacbe668
8 changed files with 141 additions and 176 deletions
|
@ -1,10 +0,0 @@
|
|||
"use client";
|
||||
import { signOut } from "next-auth/react";
|
||||
import React from "react";
|
||||
|
||||
export default function Page() {
|
||||
React.useEffect(() => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
});
|
||||
return null;
|
||||
}
|
10
apps/web/src/app/[locale]/logout/route.ts
Normal file
10
apps/web/src/app/[locale]/logout/route.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { resetUser } from "@/app/guest";
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
export async function GET() {
|
||||
const res = NextResponse.redirect(absoluteUrl());
|
||||
await resetUser(res);
|
||||
return res;
|
||||
}
|
107
apps/web/src/app/guest.ts
Normal file
107
apps/web/src/app/guest.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { randomid } from "@rallly/backend/utils/nanoid";
|
||||
import type { TimeFormat } from "@rallly/database";
|
||||
import { unsealData } from "iron-session/edge";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { encode, JWT } from "next-auth/jwt";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
function getCookieSettings() {
|
||||
const secure = absoluteUrl().startsWith("https://");
|
||||
const prefix = secure ? "__Secure-" : "";
|
||||
const name = `${prefix}next-auth.session-token`;
|
||||
return {
|
||||
secure,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
async function setCookie(res: NextResponse, jwt: JWT) {
|
||||
const { name, secure } = getCookieSettings();
|
||||
|
||||
const token = await encode({
|
||||
token: jwt,
|
||||
secret: process.env.SECRET_PASSWORD,
|
||||
});
|
||||
|
||||
res.cookies.set({
|
||||
name,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetUser(res: NextResponse) {
|
||||
// resets to a new guest user
|
||||
const jwt: JWT = {
|
||||
sub: `user-${randomid()}`,
|
||||
email: null,
|
||||
};
|
||||
|
||||
await setCookie(res, jwt);
|
||||
}
|
||||
|
||||
export async function initGuest(req: NextRequest, res: NextResponse) {
|
||||
const { name } = getCookieSettings();
|
||||
|
||||
if (req.cookies.has(name)) {
|
||||
// already has a session token
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO (Luke Vella) [2023-11-07]: Remove this after 30 days (Date: 2023-12-07)
|
||||
const legacyJwt = await getLegacyToken(req, res);
|
||||
|
||||
const jwt: JWT = legacyJwt || {
|
||||
sub: `user-${randomid()}`,
|
||||
email: null,
|
||||
};
|
||||
|
||||
await setCookie(res, jwt);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
async function getLegacyToken(req: NextRequest, res: NextResponse) {
|
||||
/**
|
||||
* We moved from a bespoke session implementation to next-auth.
|
||||
* This middleware looks for the old session cookie and moves it to
|
||||
* a temporary cookie accessible to the client which will exchange it
|
||||
* for a new session token with the legacy-token provider.
|
||||
*/
|
||||
const legacyToken = req.cookies.get("rallly-session");
|
||||
if (legacyToken) {
|
||||
// delete old cookie
|
||||
res.cookies.delete("rallly-session");
|
||||
|
||||
// make sure old cookie isn't expired
|
||||
const payload = await unsealData<{
|
||||
user: {
|
||||
id: string;
|
||||
isGuest: boolean;
|
||||
preferences?: {
|
||||
weekStart?: number;
|
||||
timeZone?: string;
|
||||
timeFormat?: TimeFormat;
|
||||
};
|
||||
};
|
||||
}>(legacyToken.value, {
|
||||
password: process.env.SECRET_PASSWORD,
|
||||
});
|
||||
// if it's not expired, write it to a new cookie that we
|
||||
// can read from the client
|
||||
if (Object.keys(payload).length > 0 && payload?.user?.isGuest) {
|
||||
const jwt: JWT = {
|
||||
sub: payload.user.id,
|
||||
email: null,
|
||||
...payload.user.preferences,
|
||||
};
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -22,7 +22,6 @@ import {
|
|||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
import { LoginLink } from "@/components/login-link";
|
||||
import { RegisterLink } from "@/components/register-link";
|
||||
|
@ -147,25 +146,25 @@ export const UserDropdown = () => {
|
|||
<Trans i18nKey="createAnAccount" defaults="Register" />
|
||||
</RegisterLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-x-2"
|
||||
onSelect={() =>
|
||||
signOut({
|
||||
redirect: false,
|
||||
})
|
||||
}
|
||||
asChild
|
||||
className="flex text-destructive items-center gap-x-2"
|
||||
>
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
<Trans i18nKey="forgetMe" />
|
||||
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
||||
<a href="/logout">
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
<Trans i18nKey="forgetMe" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</IfGuest>
|
||||
<IfAuthenticated>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-x-2"
|
||||
onSelect={() => signOut()}
|
||||
>
|
||||
<LogOutIcon className="h-4 w-4" />
|
||||
<Trans i18nKey="logout" />
|
||||
<DropdownMenuItem asChild className="flex items-center gap-x-2">
|
||||
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
||||
<a href="/logout">
|
||||
<LogOutIcon className="h-4 w-4" />
|
||||
<Trans i18nKey="logout" />
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</IfAuthenticated>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
import Cookies from "js-cookie";
|
||||
import { Session } from "next-auth";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -59,27 +58,7 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
|
|||
};
|
||||
|
||||
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||
const session = useSession({
|
||||
required: true,
|
||||
onUnauthenticated() {
|
||||
// Begin: Legacy token migration
|
||||
const legacyToken = Cookies.get("legacy-token");
|
||||
// It's important to remove the token from the cookies,
|
||||
// otherwise when the user signs out.
|
||||
if (legacyToken) {
|
||||
Cookies.remove("legacy-token");
|
||||
signIn("legacy-token", {
|
||||
token: legacyToken,
|
||||
redirect: false,
|
||||
});
|
||||
} else {
|
||||
// End: Legacy token migration
|
||||
signIn("guest", {
|
||||
redirect: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const session = useSession();
|
||||
|
||||
const user = session.data?.user;
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { randomid } from "@rallly/backend/utils/nanoid";
|
||||
import languages from "@rallly/languages";
|
||||
import languageParser from "accept-language-parser";
|
||||
import { unsealData } from "iron-session/edge";
|
||||
import { NextResponse } from "next/server";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import withAuth from "next-auth/middleware";
|
||||
|
||||
import { initGuest } from "@/app/guest";
|
||||
|
||||
const supportedLocales = Object.keys(languages);
|
||||
|
||||
export default withAuth(
|
||||
export const middleware = withAuth(
|
||||
async function middleware(req) {
|
||||
const { headers, nextUrl } = req;
|
||||
const newUrl = nextUrl.clone();
|
||||
|
@ -39,54 +38,7 @@ export default withAuth(
|
|||
|
||||
const res = NextResponse.rewrite(newUrl);
|
||||
|
||||
if (!req.nextauth.token) {
|
||||
/**
|
||||
* We moved from a bespoke session implementation to next-auth.
|
||||
* This middleware looks for the old session cookie and moves it to
|
||||
* a temporary cookie accessible to the client which will exchange it
|
||||
* for a new session token with the legacy-token provider.
|
||||
*/
|
||||
const legacyToken = req.cookies.get("rallly-session");
|
||||
if (legacyToken) {
|
||||
// delete old cookie
|
||||
res.cookies.delete("rallly-session");
|
||||
// make sure old cookie isn't expired
|
||||
const payload = await unsealData(legacyToken.value, {
|
||||
password: process.env.SECRET_PASSWORD,
|
||||
});
|
||||
// if it's not expired, write it to a new cookie that we
|
||||
// can read from the client
|
||||
if (Object.keys(payload).length > 0) {
|
||||
res.cookies.set({
|
||||
name: "legacy-token",
|
||||
value: legacyToken.value,
|
||||
httpOnly: false,
|
||||
});
|
||||
} else {
|
||||
// Create new guest user
|
||||
const newUser = `user-${randomid()}`;
|
||||
const token = await encode({
|
||||
token: {
|
||||
sub: newUser,
|
||||
email: null,
|
||||
},
|
||||
secret: process.env.SECRET_PASSWORD,
|
||||
});
|
||||
const secure = process.env.NODE_ENV === "production";
|
||||
const prefix = secure ? "__Secure-" : "";
|
||||
const name = `${prefix}next-auth.session-token`;
|
||||
|
||||
res.cookies.set({
|
||||
name,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await initGuest(req, res);
|
||||
|
||||
return res;
|
||||
},
|
||||
|
|
|
@ -18,7 +18,6 @@ import CredentialsProvider from "next-auth/providers/credentials";
|
|||
import EmailProvider from "next-auth/providers/email";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider";
|
||||
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
|
||||
import { emailClient } from "@/utils/emails";
|
||||
|
||||
|
@ -30,7 +29,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
|||
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({
|
||||
|
@ -139,7 +137,8 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
|||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (user.email) {
|
||||
// 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]);
|
||||
|
@ -198,8 +197,8 @@ type GetServerSessionParams =
|
|||
| [NextApiRequest, NextApiResponse]
|
||||
| [];
|
||||
|
||||
export function getServerSession(...args: GetServerSessionParams) {
|
||||
return getServerSessionWithOptions(...args, getAuthOptions(...args));
|
||||
export async function getServerSession(...args: GetServerSessionParams) {
|
||||
return await getServerSessionWithOptions(...args, getAuthOptions(...args));
|
||||
}
|
||||
|
||||
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import { decryptToken } from "@rallly/backend/session";
|
||||
import { prisma, TimeFormat } from "@rallly/database";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
|
||||
/**
|
||||
* This provider allows us to login with a token from an older session created with
|
||||
* iron-session.
|
||||
*
|
||||
* We should keep this provider available for at least 30 days in production to allow returning
|
||||
* users to keep their existing sessions.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const LegacyTokenProvider = CredentialsProvider({
|
||||
id: "legacy-token",
|
||||
name: "Legacy Token",
|
||||
credentials: {
|
||||
token: {
|
||||
label: "Token",
|
||||
type: "text",
|
||||
},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (credentials?.token) {
|
||||
const session = await decryptToken<{
|
||||
user: {
|
||||
id: string;
|
||||
isGuest: boolean;
|
||||
preferences?: {
|
||||
weekStart?: number;
|
||||
timeZone?: string;
|
||||
timeFormat?: TimeFormat;
|
||||
};
|
||||
};
|
||||
}>(credentials.token);
|
||||
|
||||
if (session?.user) {
|
||||
if (session.user.isGuest) {
|
||||
return {
|
||||
id: session.user.id,
|
||||
email: null,
|
||||
weekStart: session.user.preferences?.weekStart,
|
||||
timeZone: session.user.preferences?.timeZone,
|
||||
timeFormat: session.user.preferences?.timeFormat,
|
||||
};
|
||||
} else {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
Loading…
Add table
Reference in a new issue