Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Upgrade to next-auth v5 #1558

Merged
merged 9 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ SECRET_PASSWORD=abcdef1234567890abcdef1234567890
# Example: https://example.com
NEXT_PUBLIC_BASE_URL=http://localhost:3000

# NEXTAUTH_URL should be the same as NEXT_PUBLIC_BASE_URL
NEXTAUTH_URL=http://localhost:3000

# A connection string to your Postgres database
DATABASE_URL="postgres://postgres:postgres@localhost:5450/rallly"

Expand Down
1 change: 0 additions & 1 deletion apps/web/.env.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
PORT=3002
NEXT_PUBLIC_BASE_URL=http://localhost:3002
NEXTAUTH_URL=$NEXT_PUBLIC_BASE_URL
SECRET_PASSWORD=abcdef1234567890abcdef1234567890
DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly
[email protected]
Expand Down
5 changes: 5 additions & 0 deletions apps/web/declarations/next-auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { TimeFormat } from "@rallly/database";
import type { NextRequest } from "next/server";
import type { DefaultSession, DefaultUser } from "next-auth";
import NextAuth from "next-auth";
import type { DefaultJWT } from "next-auth/jwt";
Expand All @@ -25,6 +26,10 @@ declare module "next-auth" {
timeFormat?: TimeFormat | null;
weekStart?: number | null;
}

interface NextAuthRequest extends NextRequest {
auth: Session | null;
}
}

declare module "next-auth/jwt" {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/i18next-scanner.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const typescriptTransform = require("i18next-scanner-typescript");

module.exports = {
input: ["src/**/*.{ts,tsx}", "!src/auth.ts"],
input: ["src/**/*.{ts,tsx}", "!src/next-auth*.ts"],
options: {
nsSeparator: false,
defaultNs: "app",
Expand Down
6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
"docker:start": "./scripts/docker-start.sh"
},
"dependencies": {
"@auth/prisma-adapter": "^1.0.3",
"@auth/prisma-adapter": "^2.7.4",
"@aws-sdk/client-s3": "^3.645.0",
"@aws-sdk/s3-request-presigner": "^3.645.0",
"@hookform/resolvers": "^3.3.1",
"@next/bundle-analyzer": "^12.3.4",
"@panva/hkdf": "^1.2.1",
"@radix-ui/react-slot": "^1.0.1",
"@radix-ui/react-switch": "^1.0.2",
"@rallly/billing": "*",
Expand Down Expand Up @@ -59,14 +60,15 @@
"ics": "^3.1.0",
"intl-messageformat": "^10.3.4",
"iron-session": "^6.3.1",
"jose": "^5.9.6",
"js-cookie": "^3.0.1",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash": "^4.17.21",
"lucide-react": "^0.387.0",
"micro": "^10.0.1",
"nanoid": "^5.0.9",
"next-auth": "^4.24.5",
"next-auth": "^5.0.0-beta.25",
"next-i18next": "^13.0.3",
"php-serialize": "^4.1.1",
"postcss": "^8.4.31",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,6 @@
"loginVerifyDescription": "Check your email for the verification code",
"createAccount": "Create Account",
"tooManyRequests": "Too many requests",
"tooManyRequestsDescription": "Please try again later."
"tooManyRequestsDescription": "Please try again later.",
"loginMagicLinkError": "This link is invalid or expired. Please request a new link."
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function DeleteAccountDialog({
onSuccess() {
posthog?.capture("delete account");
signOut({
callbackUrl: "/login",
redirectTo: "/login",
});
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import { Button } from "@rallly/ui/button";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import React from "react";

import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { useTranslation } from "@/i18n/client";
import { trpc } from "@/trpc/client";

type PageProps = { magicLink: string; email: string };

export const LoginPage = ({ magicLink, email }: PageProps) => {
const session = useSession();
const posthog = usePostHog();
const { t } = useTranslation();
const [error, setError] = React.useState<string | null>(null);

const magicLinkFetch = useMutation({
mutationFn: async () => {
const res = await fetch(magicLink);
Expand All @@ -31,9 +36,15 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
name: updatedSession.user.name,
});
}
router.push(data.url);
} else {
setError(
t("loginMagicLinkError", {
defaultValue:
"This link is invalid or expired. Please request a new link.",
}),
);
}

router.push(data.url);
},
});
const { data } = trpc.user.getByEmail.useQuery({ email });
Expand Down Expand Up @@ -72,6 +83,7 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
<Trans i18nKey="login" defaults="Login" />
</Button>
</div>
{error && <p className="text-destructive text-sm">{error}</p>}
</div>
</div>
);
Expand Down
22 changes: 14 additions & 8 deletions apps/web/src/app/[locale]/(auth)/login/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import { prisma } from "@rallly/database";
import { cookies } from "next/headers";

export async function setVerificationEmail(email: string) {
const count = await prisma.user.count({
const user = await prisma.user.findUnique({
where: {
email,
},
select: {
email: true,
},
});

cookies().set("verification-email", email, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60,
});
if (user) {
cookies().set("verification-email", user.email, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60,
});
return true;
}

return count > 0;
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ export function LoginWithEmailForm() {
if (doesExist) {
await signIn("email", {
email: identifier,
callbackUrl: searchParams?.get("callbackUrl") ?? undefined,
redirectTo: searchParams?.get("redirectTo") ?? undefined,
redirect: false,
});
// redirect to verify page with callbackUrl
// redirect to verify page with redirectTo
router.push(
`/login/verify?callbackUrl=${encodeURIComponent(
searchParams?.get("callbackUrl") ?? "",
`/login/verify?redirectTo=${encodeURIComponent(
searchParams?.get("redirectTo") ?? "",
)}`,
);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { Trans } from "@/components/trans";

export async function LoginWithOIDC({
name,
callbackUrl,
redirectTo,
}: {
name: string;
callbackUrl?: string;
redirectTo?: string;
}) {
return (
<Button
onClick={() => {
signIn("oidc", {
callbackUrl,
redirectTo,
});
}}
variant="link"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function SSOImage({ provider }: { provider: string }) {
);
}

if (provider === "azure-ad") {
if (provider === "microsoft-entra-id") {
return (
<Image
src="/static/microsoft.svg"
Expand All @@ -40,11 +40,11 @@ function SSOImage({ provider }: { provider: string }) {
export function SSOProvider({
providerId,
name,
callbackUrl,
redirectTo,
}: {
providerId: string;
name: string;
callbackUrl?: string;
redirectTo?: string;
}) {
const { t } = useTranslation();
return (
Expand All @@ -58,7 +58,7 @@ export function SSOProvider({
key={providerId}
onClick={() => {
signIn(providerId, {
callbackUrl,
redirectTo,
});
}}
>
Expand Down
38 changes: 18 additions & 20 deletions apps/web/src/app/[locale]/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Link from "next/link";
import { Trans } from "react-i18next/TransWithoutContext";

import { getOAuthProviders } from "@/auth";
import { GoogleProvider } from "@/auth/providers/google";
import { MicrosoftProvider } from "@/auth/providers/microsoft";
import { OIDCProvider } from "@/auth/providers/oidc";
import { getTranslation } from "@/i18n/server";

import {
Expand All @@ -22,20 +24,14 @@ export default async function LoginPage({
searchParams,
}: {
searchParams?: {
callbackUrl?: string;
redirectTo?: string;
};
}) {
const { t } = await getTranslation();
const oAuthProviders = getOAuthProviders();

const hasAlternateLoginMethods = oAuthProviders.length > 0;

const oidcProvider = oAuthProviders.find(
(provider) => provider.id === "oidc",
);
const socialProviders = oAuthProviders.filter(
(provider) => provider.id !== "oidc",
);
const oidcProvider = OIDCProvider();
const socialProviders = [GoogleProvider(), MicrosoftProvider()];
const hasAlternateLoginMethods = socialProviders.length > 0 || !!oidcProvider;

return (
<AuthPageContainer>
Expand All @@ -58,19 +54,21 @@ export default async function LoginPage({
{oidcProvider ? (
<LoginWithOIDC
name={oidcProvider.name}
callbackUrl={searchParams?.callbackUrl}
redirectTo={searchParams?.redirectTo}
/>
) : null}
{socialProviders ? (
<div className="grid gap-4">
{socialProviders.map((provider) => (
<SSOProvider
key={provider.id}
providerId={provider.id}
name={provider.name}
callbackUrl={searchParams?.callbackUrl}
/>
))}
{socialProviders.map((provider) =>
provider ? (
<SSOProvider
key={provider.id}
providerId={provider.id}
name={provider.options?.name || provider.name}
redirectTo={searchParams?.redirectTo}
/>
) : null,
)}
</div>
) : null}
</AuthPageContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function OTPForm({ email }: { email: string }) {
message: t("wrongVerificationCode"),
});
} else {
window.location.href = searchParams?.get("callbackUrl") ?? "/";
window.location.href = searchParams?.get("redirectTo") ?? "/";
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function OTPForm({ token }: { token: string }) {

signIn("registration-token", {
token,
callbackUrl: searchParams?.get("callbackUrl") ?? "/",
redirectTo: searchParams?.get("redirectTo") ?? "/",
});
});

Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import "../../style.css";
import { Toaster } from "@rallly/ui/toaster";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import { SessionProvider } from "next-auth/react";
import React from "react";

import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
import { Providers } from "@/app/providers";
import { getServerSession } from "@/auth";
import { SessionProvider } from "@/auth/session-provider";
import { auth } from "@/next-auth";

const inter = Inter({
subsets: ["latin"],
Expand All @@ -30,7 +30,7 @@ export default async function Root({
children: React.ReactNode;
params: { locale: string };
}) {
const session = await getServerSession();
const session = await auth();

return (
<html lang={locale} className={inter.className}>
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { withPosthog } from "@rallly/posthog/server";

import { handlers } from "@/next-auth";

export const GET = withPosthog(handlers.GET);
export const POST = withPosthog(handlers.POST);
4 changes: 2 additions & 2 deletions apps/web/src/app/api/notifications/unsubscribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import { getServerSession } from "@/auth";
import { auth } from "@/next-auth";
import type { DisableNotificationsPayload } from "@/trpc/types";
import { decryptToken } from "@/utils/session";

Expand All @@ -14,7 +14,7 @@ export const GET = async (req: NextRequest) => {
return NextResponse.redirect(new URL("/login", req.url));
}

const session = await getServerSession();
const session = await auth();

if (!session || !session.user?.email) {
return NextResponse.redirect(new URL("/login", req.url));
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/api/stripe/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";

import { getServerSession } from "@/auth";
import { auth } from "@/next-auth";

const inputSchema = z.object({
period: z.enum(["monthly", "yearly"]).optional(),
Expand All @@ -14,7 +14,7 @@ const inputSchema = z.object({
});

export async function POST(request: NextRequest) {
const userSession = await getServerSession();
const userSession = await auth();
const formData = await request.formData();
const { period = "monthly", return_path } = inputSchema.parse(
Object.fromEntries(formData.entries()),
Expand Down
Loading
Loading