Skip to content

Commit

Permalink
docs: add example of next auth v5
Browse files Browse the repository at this point in the history
  • Loading branch information
Robin Louarn committed Feb 28, 2024
1 parent a4f30b4 commit 6d543c9
Show file tree
Hide file tree
Showing 23 changed files with 536 additions and 17 deletions.
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
1 change: 0 additions & 1 deletion examples/example-app-router-next-auth-v5/.env.local

This file was deleted.

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;
73 changes: 73 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,73 @@
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { NextResponse } from "next/server";
import { locales } from "./i18n";

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

const authPagesPathnameRegex = RegExp(
`^(/(${locales.join("|")}))?(${["/login"]
.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 = publicPagesPathnameRegex.test(pathname);
const isAuthPage = authPagesPathnameRegex.test(pathname);

if (!(isAuthenticated || isPublicPage))
return NextResponse.redirect(signInUrl);

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

return isAuthenticated || isPublicPage;
},
},
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}}"
}
}
19 changes: 19 additions & 0 deletions examples/example-app-router-next-auth-v5/src/messages/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"Navigation": {
"home": "Acceuil",
"profile": "Profile"
},
"SignIn": {
"label": "Se connecter"
},
"SignOut": {
"label": "Se déconnecter"
},
"Login": {
"title": "Connexion",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"submit": "Connexion",
"error": "{error, select, CredentialsSignin {Nom d'utilisateur ou mot de passe incorrect} other {Erreur inconnue}}"
}
}

0 comments on commit 6d543c9

Please sign in to comment.