mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-01 11:16:32 +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,
|
UserPlusIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { signOut } from "next-auth/react";
|
|
||||||
|
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
import { RegisterLink } from "@/components/register-link";
|
import { RegisterLink } from "@/components/register-link";
|
||||||
|
@ -147,25 +146,25 @@ export const UserDropdown = () => {
|
||||||
<Trans i18nKey="createAnAccount" defaults="Register" />
|
<Trans i18nKey="createAnAccount" defaults="Register" />
|
||||||
</RegisterLink>
|
</RegisterLink>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="flex items-center gap-x-2"
|
asChild
|
||||||
onSelect={() =>
|
className="flex text-destructive items-center gap-x-2"
|
||||||
signOut({
|
|
||||||
redirect: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<RefreshCcwIcon className="h-4 w-4" />
|
{/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
|
||||||
<Trans i18nKey="forgetMe" />
|
<a href="/logout">
|
||||||
|
<RefreshCcwIcon className="h-4 w-4" />
|
||||||
|
<Trans i18nKey="forgetMe" />
|
||||||
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</IfGuest>
|
</IfGuest>
|
||||||
<IfAuthenticated>
|
<IfAuthenticated>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem asChild className="flex items-center gap-x-2">
|
||||||
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. */}
|
||||||
onSelect={() => signOut()}
|
<a href="/logout">
|
||||||
>
|
<LogOutIcon className="h-4 w-4" />
|
||||||
<LogOutIcon className="h-4 w-4" />
|
<Trans i18nKey="logout" />
|
||||||
<Trans i18nKey="logout" />
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</IfAuthenticated>
|
</IfAuthenticated>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import Cookies from "js-cookie";
|
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
@ -59,27 +58,7 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
const session = useSession({
|
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 user = session.data?.user;
|
const user = session.data?.user;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { randomid } from "@rallly/backend/utils/nanoid";
|
|
||||||
import languages from "@rallly/languages";
|
import languages from "@rallly/languages";
|
||||||
import languageParser from "accept-language-parser";
|
import languageParser from "accept-language-parser";
|
||||||
import { unsealData } from "iron-session/edge";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { encode } from "next-auth/jwt";
|
|
||||||
import withAuth from "next-auth/middleware";
|
import withAuth from "next-auth/middleware";
|
||||||
|
|
||||||
|
import { initGuest } from "@/app/guest";
|
||||||
|
|
||||||
const supportedLocales = Object.keys(languages);
|
const supportedLocales = Object.keys(languages);
|
||||||
|
|
||||||
export default withAuth(
|
export const middleware = withAuth(
|
||||||
async function middleware(req) {
|
async function middleware(req) {
|
||||||
const { headers, nextUrl } = req;
|
const { headers, nextUrl } = req;
|
||||||
const newUrl = nextUrl.clone();
|
const newUrl = nextUrl.clone();
|
||||||
|
@ -39,54 +38,7 @@ export default withAuth(
|
||||||
|
|
||||||
const res = NextResponse.rewrite(newUrl);
|
const res = NextResponse.rewrite(newUrl);
|
||||||
|
|
||||||
if (!req.nextauth.token) {
|
await initGuest(req, res);
|
||||||
/**
|
|
||||||
* 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: "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,6 @@ import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
import EmailProvider from "next-auth/providers/email";
|
import EmailProvider from "next-auth/providers/email";
|
||||||
|
|
||||||
import { absoluteUrl } from "@/utils/absolute-url";
|
import { absoluteUrl } from "@/utils/absolute-url";
|
||||||
import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider";
|
|
||||||
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
|
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
|
||||||
import { emailClient } from "@/utils/emails";
|
import { emailClient } from "@/utils/emails";
|
||||||
|
|
||||||
|
@ -30,7 +29,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
},
|
},
|
||||||
providers: [
|
providers: [
|
||||||
LegacyTokenProvider,
|
|
||||||
// When a user registers, we don't want to go through the email verification process
|
// 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
|
// so this providers allows us exchange the registration token for a session token
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
|
@ -139,7 +137,8 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (user.email) {
|
||||||
|
// merge guest user into newly logged in user
|
||||||
const session = await getServerSession(...args);
|
const session = await getServerSession(...args);
|
||||||
if (session && session.user.email === null) {
|
if (session && session.user.email === null) {
|
||||||
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
await mergeGuestsIntoUser(user.id, [session.user.id]);
|
||||||
|
@ -198,8 +197,8 @@ type GetServerSessionParams =
|
||||||
| [NextApiRequest, NextApiResponse]
|
| [NextApiRequest, NextApiResponse]
|
||||||
| [];
|
| [];
|
||||||
|
|
||||||
export function getServerSession(...args: GetServerSessionParams) {
|
export async function getServerSession(...args: GetServerSessionParams) {
|
||||||
return getServerSessionWithOptions(...args, getAuthOptions(...args));
|
return await getServerSessionWithOptions(...args, getAuthOptions(...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
|
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