Skip to content

Commit

Permalink
feat: adding multiple middleware and locales
Browse files Browse the repository at this point in the history
  • Loading branch information
hhertout committed Jan 24, 2024
1 parent 39122f8 commit 3464d37
Show file tree
Hide file tree
Showing 16 changed files with 203 additions and 28 deletions.
6 changes: 6 additions & 0 deletions i18n.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const i18n = {
defaultLocale: 'en',
locales: ['en', 'fr']
} as const

export type Locale = (typeof i18n)['locales'][number]
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test": "vitest --dir tests/specs"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
Expand All @@ -18,6 +19,7 @@
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"negotiator": "^0.6.3",
"next": "14.0.4",
"react": "^18",
"react-dom": "^18",
Expand All @@ -26,6 +28,7 @@
},
"devDependencies": {
"@playwright/test": "^1.41.1",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
File renamed without changes.
File renamed without changes.
7 changes: 5 additions & 2 deletions src/app/layout.tsx → src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './globals.css'
import {cn} from "@/lib/utils";
import { Inter as FontSans } from "next/font/google"
import UserContextProvider from "@/context/UserContext";
import {Locale} from "../../../i18n.config";

const fontSans = FontSans({
subsets: ["latin"],
Expand All @@ -17,11 +18,13 @@ export const metadata: Metadata = {

export default function RootLayout({
children,
params
}: {
children: React.ReactNode
children: React.ReactNode,
params: {lang: Locale}
}) {
return (
<html lang="en">
<html lang={params.lang}>
<UserContextProvider>
<body className={cn(
"min-h-screen bg-background font-sans antialiased",
Expand Down
10 changes: 7 additions & 3 deletions src/app/page.tsx → src/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import LoginForm from "@/components/LoginForm";
import Container from "@/components/ui/container";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import NextIcon from "@/components/icons/NextIcon";
import {getLocale} from "@/lib/i18n";
import {Locale} from "../../../i18n.config";

const Login = async ({params}: {params: {lang: Locale}}) => {
const t = await getLocale(params.lang)

const Login = () => {
return (
<div className={"w-screen h-screen flex justify-center items-center"}>
<Container type={"lg"}>
Expand All @@ -14,11 +18,11 @@ const Login = () => {
<NextIcon size={70}/>
</div>
<CardTitle className={"font-extrabold text-xl"}>
Login
{t.login.title}
</CardTitle>
</CardHeader>
<CardContent>
<LoginForm/>
<LoginForm />
</CardContent>
</Card>
</Container>
Expand Down
22 changes: 20 additions & 2 deletions src/components/nav/LanguagesSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
'use client'

import React from 'react';
import {Button} from "@/components/ui/button";
import {i18n, Locale} from "../../../i18n.config";
import {useRouter} from "next/navigation";

const LanguagesSwitcher = () => {
const router = useRouter();

const handleLocaleChange = (lang: Locale) => {
document.cookie = `i18n_locale=${lang}; Path=/`;
const location = window.location.pathname
i18n.locales.forEach(locale => {
if (location.startsWith(`/${locale}`)) {
router.push(`/${lang}${location.slice(locale.length + 1)}`)
}
})
}

return (
<>
<div className={'text-sm opacity-60'}>Languages :</div>
<div className={'columns-2 my-1'}>
<div className={"flex justify-center"}>
<Button variant={"outline"} size={'icon'} className={'w-full text-lg'}>🇺🇸</Button>
<Button variant={"outline"} size={'icon'} className={'w-full text-sm'}
onClick={() => handleLocaleChange('en')}>en</Button>
</div>
<div className={"flex justify-center"}>
<Button variant={"outline"} size={'icon'} className={'w-full text-lg'}>🇫🇷</Button>
<Button variant={"outline"} size={'icon'} className={'w-full text-sm'}
onClick={() => handleLocaleChange('fr')}>fr</Button>
</div>
</div>
</>
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"login": {
"title": "Login",
"email": "Email",
"password": "Password"
}
}
7 changes: 7 additions & 0 deletions src/i18n/fr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"login": {
"title": "Connexion",
"email": "Email",
"password": "Mot de passe"
}
}
9 changes: 9 additions & 0 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'server-only'
import {Locale} from "../../i18n.config";

const dictionaries = {
en: async () => await import('@/i18n/en.json').then(module => module.default),
fr: async () => await import('@/i18n/fr.json').then(module => module.default)
}

export const getLocale = async (locale: Locale) => await dictionaries[locale]()
30 changes: 9 additions & 21 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import {NextRequest, NextResponse} from "next/server";
import {stackMiddlewares} from "@/middleware/stackMiddleware";
import {withAuth} from "@/middleware/auth.middleware";
import {withLocale} from "@/middleware/locale.middleware";

export default async function middleware(request: NextRequest) {
const cookie = request.cookies.get('Authorization')
if (!cookie) return NextResponse.redirect(new URL("/", request.url))
try {
const res = await fetch(`${process.env.BACKEND_URL}/api/auth/check-cookie`, {
credentials: 'include',
headers: {
cookie: Object.values(cookie).join("=")
}
})
if (res.status !== 200) {
return NextResponse.redirect(new URL("/", request.url))
}
return NextResponse.next()
} catch (err) {
console.error(err)
}
}

const middlewares = [withLocale, withAuth];
export default stackMiddlewares(middlewares);

export const config = {
matcher: "/admin/:path*",
};
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
39 changes: 39 additions & 0 deletions src/middleware/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {NextFetchEvent, NextRequest, NextResponse} from "next/server";
import {i18n} from "../../i18n.config";

const middlewarePath = ["/admin"]

export const withAuth = (next: any) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
const pathname = getPathWithoutLocale(request.nextUrl.pathname);

if (middlewarePath?.some((path) => pathname.startsWith(path))) {
const cookie = request.cookies.get('Authorization')
if (!cookie) return NextResponse.redirect(new URL("/", request.url))
try {
const res = await fetch(`${process.env.BACKEND_URL}/api/auth/check-cookie`, {
credentials: 'include',
headers: {
cookie: Object.values(cookie).join("=")
}
})
if (res.status !== 200) {
return NextResponse.redirect(new URL("/", request.url))
}
return NextResponse.next()
} catch (err) {
console.error(err)
}
}
return next(request, _next);
}
}

const getPathWithoutLocale = (path: string): string => {
for (const locale of i18n.locales) {
if (path.startsWith(`/${locale}`)) {
return path.slice(locale.length + 1)
}
}
return path
}
47 changes: 47 additions & 0 deletions src/middleware/locale.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {NextFetchEvent, NextRequest} from 'next/server'
import {NextResponse} from 'next/server'

import {match} from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
import {i18n} from "../../i18n.config";

function getLocale(request: NextRequest): string {
const i18nCookie = request.cookies.get("i18n_locale")
if (i18nCookie) {
return i18nCookie.value
} else {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))

// @ts-ignore locales are readonly
const locales: string[] = i18n.locales
const languages: string[] = new Negotiator({headers: negotiatorHeaders}).languages()
return match(languages, locales, i18n.defaultLocale)
}
}

export const withLocale = (next: any) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = i18n.locales.every(
locale => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request)
const url = new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
const response = NextResponse.redirect(url)
response.cookies.set({
name: 'i18n_locale',
value: locale,
path: '/',
})
return response
}

return next(request, _next);
}
}
5 changes: 5 additions & 0 deletions src/middleware/middleware.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {
NextMiddleware
} from "next/server";

export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware;
14 changes: 14 additions & 0 deletions src/middleware/stackMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
NextMiddleware,
NextResponse
} from "next/server";
import {MiddlewareFactory} from "@/middleware/middleware";

export function stackMiddlewares(functions: MiddlewareFactory[] = [], index = 0): NextMiddleware {
const current = functions[index];
if (current) {
const next = stackMiddlewares(functions, index + 1);
return current(next);
}
return () => NextResponse.next();
}

0 comments on commit 3464d37

Please sign in to comment.