diff --git a/examples/example-next-13-next-auth-v5/README.md b/examples/example-next-13-next-auth-v5/README.md new file mode 100644 index 000000000..5b534c074 --- /dev/null +++ b/examples/example-next-13-next-auth-v5/README.md @@ -0,0 +1,7 @@ +# example-next-13-next-auth-v5 + +An example that showcases the usage of `next-intl` together with Auth.js v5 in the `app` directory of Next.js 13. + +**Credentials**: admin / admin + +Many thanks to [narakhan](https://github.com/narakhan) for [sharing his middleware implementation](https://github.com/amannn/next-intl/pull/149#issuecomment-1509990635)! diff --git a/examples/example-next-13-next-auth-v5/auth.ts b/examples/example-next-13-next-auth-v5/auth.ts new file mode 100644 index 000000000..47642cc3e --- /dev/null +++ b/examples/example-next-13-next-auth-v5/auth.ts @@ -0,0 +1,24 @@ +import NextAuth from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; + +export const { 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; + }, + }), + ], +}); diff --git a/examples/example-next-13-next-auth-v5/package.json b/examples/example-next-13-next-auth-v5/package.json new file mode 100644 index 000000000..cbc6c0984 --- /dev/null +++ b/examples/example-next-13-next-auth-v5/package.json @@ -0,0 +1,29 @@ +{ + "name": "example-next-13-next-auth-v5", + "version": "2.14.3", + "private": true, + "scripts": { + "dev": "next dev", + "lint": "eslint src && tsc", + "test": "playwright test", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "14.0.1", + "next-auth": "^5.0.0-beta.3", + "next-intl": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.28.1", + "@types/lodash": "^4.14.176", + "@types/node": "^17.0.23", + "@types/react": "^18.2.5", + "eslint": "^8.46.0", + "eslint-config-molindo": "^7.0.0", + "eslint-config-next": "^13.4.0", + "typescript": "^5.0.0" + } +} diff --git a/examples/example-next-13-next-auth-v5/src/app/[locale]/Index.tsx b/examples/example-next-13-next-auth-v5/src/app/[locale]/Index.tsx new file mode 100644 index 000000000..b8b9388b0 --- /dev/null +++ b/examples/example-next-13-next-auth-v5/src/app/[locale]/Index.tsx @@ -0,0 +1,41 @@ +'use client'; + +import Link from 'next/link'; +import {Session} from 'next-auth'; +import {signOut} from 'auth'; +import {useLocale, useTranslations} from 'next-intl'; +import PageLayout from '../../components/PageLayout'; + +type Props = { + session: Session | null; +}; + +export default function Index({session}: Props) { + const t = useTranslations('Index'); + const locale = useLocale(); + + function onLogoutClick() { + signOut(); + } + + return ( + + {session ? ( + <> +

{t('loggedIn', {username: session.user?.name})}

+

+ {t('secret')} +

+ + + ) : ( + <> +

{t('loggedOut')}

+ {t('login')} + + )} +
+ ); +} diff --git a/examples/example-next-13-next-auth-v5/src/app/[locale]/login/page.tsx b/examples/example-next-13-next-auth-v5/src/app/[locale]/login/page.tsx new file mode 100644 index 000000000..386f6c74e --- /dev/null +++ b/examples/example-next-13-next-auth-v5/src/app/[locale]/login/page.tsx @@ -0,0 +1,58 @@ +'use client'; + +import {useRouter} from 'next/navigation'; +import {signIn} from 'auth'; +import {useLocale, useTranslations} from 'next-intl'; +import {FormEvent, useState} from 'react'; +import PageLayout from '../../../components/PageLayout'; + +export default function Login() { + const locale = useLocale(); + const t = useTranslations('Login'); + const [error, setError] = useState(); + const router = useRouter(); + + function onSubmit(event: FormEvent) { + event.preventDefault(); + if (error) setError(undefined); + + const formData = new FormData(event.currentTarget); + signIn('credentials', { + username: formData.get('username'), + password: formData.get('password'), + redirect: false + }).then((result) => { + if (result?.error) { + setError(result.error); + } else { + router.push('/' + locale); + } + }); + } + + return ( + +
+ + + {error &&

{t('error', {error})}

} + +
+
+ ); +} diff --git a/examples/example-next-13-next-auth-v5/src/app/[locale]/page.tsx b/examples/example-next-13-next-auth-v5/src/app/[locale]/page.tsx new file mode 100644 index 000000000..a181ae90b --- /dev/null +++ b/examples/example-next-13-next-auth-v5/src/app/[locale]/page.tsx @@ -0,0 +1,7 @@ +import auth from 'auth'; +import Index from './Index'; + +export default async function IndexPage() { + const session = await auth(); + return ; +} diff --git a/examples/example-next-13-next-auth-v5/src/app/api/auth/[...nextauth]/route.ts b/examples/example-next-13-next-auth-v5/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 000000000..77920d1de --- /dev/null +++ b/examples/example-next-13-next-auth-v5/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from 'next-auth'; +import auth from '../../../../../auth'; + +const handler = NextAuth(auth); + +export { handler as GET, handler as POST }; diff --git a/examples/example-next-13-next-auth-v5/src/middleware.tsx b/examples/example-next-13-next-auth-v5/src/middleware.tsx new file mode 100644 index 000000000..f316ccc4c --- /dev/null +++ b/examples/example-next-13-next-auth-v5/src/middleware.tsx @@ -0,0 +1,49 @@ +import {NextRequest} from 'next/server'; +import {auth} from 'auth'; +import createIntlMiddleware from 'next-intl/middleware'; + +const locales = ['en', 'de']; +const publicPages = [ + '/', + '/login' + // (/secret requires auth) +]; + +const intlMiddleware = createIntlMiddleware({ + locales, + defaultLocale: 'en' +}); + +const authMiddleware = auth( + // Note that this callback is only invoked if + // the `authorized` callback has returned `true` + // and not for pages listed in `pages`. + (req) => intlMiddleware(req), + { + callbacks: { + authorized: ({token}) => token != null + }, + pages: { + signIn: '/login' + } + } +); + +export default function middleware(req: NextRequest) { + const publicPathnameRegex = RegExp( + `^(/(${locales.join('|')}))?(${publicPages.join('|')})?/?$`, + 'i' + ); + const isPublicPage = publicPathnameRegex.test(req.nextUrl.pathname); + + if (isPublicPage) { + return intlMiddleware(req); + } else { + return (authMiddleware as any)(req); + } +} + +export const config = { + // Skip all paths that should not be internationalized + matcher: ['/((?!api|_next|.*\\..*).*)'] +}; diff --git a/examples/example-next-13-next-auth/src/app/[locale]/Index.tsx b/examples/example-next-13-next-auth/src/app/[locale]/Index.tsx index 48b5443da..b8b9388b0 100644 --- a/examples/example-next-13-next-auth/src/app/[locale]/Index.tsx +++ b/examples/example-next-13-next-auth/src/app/[locale]/Index.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import {Session} from 'next-auth'; -import {signOut} from 'next-auth/react'; +import {signOut} from 'auth'; import {useLocale, useTranslations} from 'next-intl'; import PageLayout from '../../components/PageLayout'; diff --git a/examples/example-next-13-next-auth/src/app/[locale]/login/page.tsx b/examples/example-next-13-next-auth/src/app/[locale]/login/page.tsx index d3f2b4e71..386f6c74e 100644 --- a/examples/example-next-13-next-auth/src/app/[locale]/login/page.tsx +++ b/examples/example-next-13-next-auth/src/app/[locale]/login/page.tsx @@ -1,7 +1,7 @@ 'use client'; import {useRouter} from 'next/navigation'; -import {signIn} from 'next-auth/react'; +import {signIn} from 'auth'; import {useLocale, useTranslations} from 'next-intl'; import {FormEvent, useState} from 'react'; import PageLayout from '../../../components/PageLayout'; diff --git a/examples/example-next-13-next-auth/src/app/[locale]/page.tsx b/examples/example-next-13-next-auth/src/app/[locale]/page.tsx index 2e87d95c6..a181ae90b 100644 --- a/examples/example-next-13-next-auth/src/app/[locale]/page.tsx +++ b/examples/example-next-13-next-auth/src/app/[locale]/page.tsx @@ -1,8 +1,7 @@ -import {getServerSession} from 'next-auth'; -import auth from '../../auth'; +import auth from 'auth'; import Index from './Index'; export default async function IndexPage() { - const session = await getServerSession(auth); + const session = await auth(); return ; } diff --git a/examples/example-next-13-next-auth/src/app/api/auth/[...nextauth]/route.ts b/examples/example-next-13-next-auth/src/app/api/auth/[...nextauth]/route.ts index 20ce19168..e64bb219f 100644 --- a/examples/example-next-13-next-auth/src/app/api/auth/[...nextauth]/route.ts +++ b/examples/example-next-13-next-auth/src/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ import NextAuth from 'next-auth'; -import auth from '../../../../auth'; +import auth from 'auth'; const handler = NextAuth(auth); diff --git a/examples/example-next-13-next-auth/src/middleware.tsx b/examples/example-next-13-next-auth/src/middleware.tsx index 21c8605fb..f316ccc4c 100644 --- a/examples/example-next-13-next-auth/src/middleware.tsx +++ b/examples/example-next-13-next-auth/src/middleware.tsx @@ -1,5 +1,5 @@ import {NextRequest} from 'next/server'; -import {withAuth} from 'next-auth/middleware'; +import {auth} from 'auth'; import createIntlMiddleware from 'next-intl/middleware'; const locales = ['en', 'de']; @@ -14,7 +14,7 @@ const intlMiddleware = createIntlMiddleware({ defaultLocale: 'en' }); -const authMiddleware = withAuth( +const authMiddleware = auth( // Note that this callback is only invoked if // the `authorized` callback has returned `true` // and not for pages listed in `pages`.