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

docs: add example of next auth v5 #899

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
},
"devDependencies": {
"@types/node": "^20.1.2",
"@types/react": "^18.2.29",
"@types/react": "^18.2.60",
"autoprefixer": "^10.4.0",
"eslint": "^8.54.0",
"eslint-config-molindo": "^7.0.0",
"eslint-config-next": "^14.0.3",
"next-sitemap": "^4.0.7",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
},
"funding": "https://github.com/amannn/next-intl?sponsor=1"
}
7 changes: 7 additions & 0 deletions docs/pages/examples/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ import Example from 'components/Example';
sourceLink="https://github.com/amannn/next-intl/tree/main/examples/example-app-router-next-auth"
/>

<Example
name="App Router Auth.js Beta"
hash="app-router-next-auth-v5"
description="An example that showcases the usage of next-intl together with Auth.js v5 and the App Router."
sourceLink="https://github.com/amannn/next-intl/tree/main/examples/example-app-router-next-auth-v5"
/>

<Example
name="App Router Playground"
hash="app-router-playground"
Expand Down
4 changes: 2 additions & 2 deletions examples/example-app-router-migration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"devDependencies": {
"@types/lodash": "^4.14.176",
"@types/node": "^20.1.2",
"@types/react": "^18.2.29",
"@types/react": "^18.2.60",
"eslint": "^8.54.0",
"eslint-config-molindo": "^7.0.0",
"eslint-config-next": "^14.0.3",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.vercel.app/32
20 changes: 20 additions & 0 deletions examples/example-app-router-next-auth-v5/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.DS_Store

node_modules/
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.yarn-integrity
.npm

.eslintcache

*.tsbuildinfo
next-env.d.ts

.next
.vercel
.env*.local
10 changes: 10 additions & 0 deletions examples/example-app-router-next-auth-v5/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import createNextIntlPlugin from "next-intl/plugin";

const withNextIntl = createNextIntlPlugin("src/i18n.ts");

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};

export default withNextIntl(nextConfig);
22 changes: 22 additions & 0 deletions examples/example-app-router-next-auth-v5/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "example-app-router-next-auth-v5",
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"next-auth": "beta",
"next-intl": "3.9.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "20.11.21",
"@types/react": "18.2.60",
"@types/react-dom": "18.2.19",
"typescript": "5.3.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { signIn, signOut } from "next-auth/react";
import { useTranslations } from "next-intl";
import { FC } from "react";

type SignInProps = {
provider?: string;
};

export const SignIn: FC<SignInProps> = ({ provider }) => {
const t = useTranslations("SignIn");
return <button onClick={() => signIn(provider)}>{t("label")}</button>;
};

export const SignOut: FC = () => {
const t = useTranslations("SignOut");
return <button onClick={() => signOut()}>{t("label")}</button>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { locales } from "@/i18n";
import { Link, usePathname } from "@/navigation";
import { FC } from "react";

export const LocaleSwitch: FC = () => {
const pathname = usePathname();
return (
<div style={{ display: "flex", gap: 5 }}>
{locales.map((locale) => (
<Link key={locale} href={pathname} locale={locale}>
{locale}
</Link>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useTranslations } from "next-intl";
import { Link } from "@/navigation";
import { FC } from "react";

const pages = {
"/": "home",
"/profile": "profile",
} as const;

export const MainNavigation: FC = () => {
const t = useTranslations("Navigation");

return (
<div style={{ display: "flex", gap: 5 }}>
{Object.entries(pages).map(([path, key]) => (
<Link key={path} href={path}>
{t(key)}
</Link>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { auth } from "@/auth";
import { PropsWithLocale, getMessages, timeZone } from "@/i18n";
import { SessionProvider } from "next-auth/react";
import { NextIntlClientProvider, useMessages } from "next-intl";
import { LocaleSwitch } from "./components/locale-switch";
import { MainNavigation } from "./components/main-navigation";
import { FC, PropsWithChildren } from "react";

type LocaleLayoutProps = PropsWithChildren<PropsWithLocale>;

const LocaleLayout: FC<LocaleLayoutProps> = async ({
children,
params: { locale },
}) => {
const session = await auth();
const messages = await getMessages(locale);

return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
<SessionProvider session={session}>
<main>
<MainNavigation />
<LocaleSwitch />
{children}
</main>
</SessionProvider>
</NextIntlClientProvider>
</body>
</html>
);
};

export default LocaleLayout;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { useSearchParams } from "next/navigation";
import { FormEvent, useState } from "react";

export default function Login() {
const t = useTranslations("Login");
const searchParams = useSearchParams();
const [error, setError] = useState<string>();

const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (error) setError(undefined);

const formData = new FormData(event.currentTarget);

const result = await signIn("credentials", {
username: formData.get("username"),
password: formData.get("password"),
callbackUrl: searchParams.get("callbackUrl") ?? undefined,
});

if (result?.error) setError(result.error);
};

return (
<form
onSubmit={onSubmit}
style={{
display: "flex",
flexDirection: "column",
gap: 10,
width: 300,
}}
>
<label style={{ display: "flex" }}>
<span style={{ display: "inline-block", flexGrow: 1, minWidth: 100 }}>
{t("username")}
</span>
<input name="username" type="text" />
</label>
<label style={{ display: "flex" }}>
<span style={{ display: "inline-block", flexGrow: 1, minWidth: 100 }}>
{t("password")}
</span>
<input name="password" type="password" />
</label>
{error && <p>{t("error", { error })}</p>}
<button type="submit">{t("submit")}</button>
</form>
);
}
11 changes: 11 additions & 0 deletions examples/example-app-router-next-auth-v5/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { auth } from "@/auth";
import { SignIn, SignOut } from "./components/auth-components";
import { FC } from "react";

const IndexPage: FC = async () => {
const session = await auth();

return <div>{session ? <SignOut /> : <SignIn />}</div>;
};

export default IndexPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { auth } from "@/auth";
import { FC } from "react";

const ProfilePage: FC = async () => {
const session = await auth();

return <pre>{JSON.stringify(session, null, 2)}</pre>;
};

export default ProfilePage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { GET, POST } from "@/auth";

export const runtime = "edge";
7 changes: 7 additions & 0 deletions examples/example-app-router-next-auth-v5/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FC, PropsWithChildren } from "react";

type RootLayoutProps = PropsWithChildren;

const RootLayout: FC<RootLayoutProps> = ({ children }) => <>{children}</>;

export default RootLayout;
67 changes: 67 additions & 0 deletions examples/example-app-router-next-auth-v5/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { NextResponse } from "next/server";
import { locales } from "./i18n";

const createPagesRegex = (pages: string[]) =>
RegExp(
`^(/(${locales.join("|")}))?(${pages
.flatMap((p) => (p === "/" ? ["", "/"] : p))
.join("|")})/?$`,
"i"
);

export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { type: "text" },
password: { type: "password" },
},
authorize(credentials) {
if (
credentials?.username === "admin" &&
credentials?.password === "admin"
) {
return { id: "1", name: "admin" };
}

return null;
},
}),
],
callbacks: {
authorized: ({
auth,
request: {
nextUrl: { pathname, origin, basePath, searchParams, href },
},
}) => {
const signInUrl = new URL("/login", origin);
signInUrl.searchParams.append("callbackUrl", href);

const isAuthenticated = !!auth;
const isPublicPage = createPagesRegex(["/", "/login"]).test(pathname);
const isAuthPage = createPagesRegex(["/login"]).test(pathname);
const idAuthorized = isAuthenticated || isPublicPage;

if (!idAuthorized) return NextResponse.redirect(signInUrl);

if (isAuthenticated && isAuthPage)
return NextResponse.redirect(
new URL(searchParams.get("callbackUrl") ?? new URL(origin), origin)
);

return idAuthorized;
},
},
pages: {
signIn: "/login",
},
});
20 changes: 20 additions & 0 deletions examples/example-app-router-next-auth-v5/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import messages from "@/messages/fr.json";
import { getRequestConfig } from "next-intl/server";

export const locales = ["fr", "en"] as const;
export const defaultLocale = locales[0];
export const timeZone = "Europe/Paris";

export type PropsWithLocale<T = unknown> = T & { params: { locale: string } };
export type Messages = typeof messages;
declare global {
interface IntlMessages extends Messages {}
}

export const getMessages = async (locale: string): Promise<Messages> =>
(await import(`@/messages/${locale}.json`)).default;

export default getRequestConfig(async ({ locale }) => ({
timeZone,
messages: await getMessages(locale),
}));
19 changes: 19 additions & 0 deletions examples/example-app-router-next-auth-v5/src/messages/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Navigation": {
"home": "Home",
"profile": "Profile"
},
"SignIn": {
"label": "Sign In"
},
"SignOut": {
"label": "Sign Out"
},
"Login": {
"title": "Login",
"username": "Username",
"password": "Password",
"submit": "Login",
"error": "{error, select, CredentialsSignin {Invalid username or password} other {Unknown error}}"
}
}