@@ -2,7 +2,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next | |||
## Setting Up the Environment | |||
It is recommended to use the same node and npm versions for development. An easy way to do so would be to use `nvm`. After installing `nvm`, run: | |||
It is recommended to use the same node and npm versions for development. An easy way to do so would be to use `nvm` ([Linux/MacOS](https://github.com/nvm-sh/nvm), [Windows](https://github.com/coreybutler/nvm-windows)). | |||
After installing `nvm`, run: | |||
```bash | |||
nvm use | |||
@@ -21,21 +23,10 @@ npm run dev | |||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | |||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | |||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | |||
## Learn More | |||
To learn more about Next.js, take a look at the following resources: | |||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | |||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | |||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | |||
## Deploy on Vercel | |||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | |||
## References | |||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | |||
This project uses the following libraries: | |||
- [NextJS](https://nextjs.org/docs) | |||
- [Next-Auth](https://next-auth.js.org/getting-started/example) | |||
- [Material UI](https://mui.com/material-ui/getting-started/) | |||
- [i18next](https://www.i18next.com/overview/getting-started) |
@@ -16,11 +16,15 @@ | |||
"@mui/icons-material": "^5.15.0", | |||
"@mui/material": "^5.15.0", | |||
"@mui/material-nextjs": "^5.15.0", | |||
"@unly/universal-language-detector": "^2.0.3", | |||
"i18next": "^23.7.11", | |||
"i18next-resources-to-backend": "^1.2.0", | |||
"next": "14.0.4", | |||
"next-auth": "^4.24.5", | |||
"react": "^18", | |||
"react-dom": "^18", | |||
"react-hook-form": "^7.49.2" | |||
"react-hook-form": "^7.49.2", | |||
"react-i18next": "^13.5.0" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "^20", | |||
@@ -1068,6 +1072,43 @@ | |||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", | |||
"dev": true | |||
}, | |||
"node_modules/@unly/iso3166-1": { | |||
"version": "1.0.2", | |||
"resolved": "https://registry.npmjs.org/@unly/iso3166-1/-/iso3166-1-1.0.2.tgz", | |||
"integrity": "sha512-aL/7cmcfjpwOaKFr9XHcWP/Z7lQjKLm5NMcjncT96aeSJxfblmPLnH/8lnX0GrWWFA2/CseCxnA73u5eiZcClA==" | |||
}, | |||
"node_modules/@unly/universal-language-detector": { | |||
"version": "2.0.3", | |||
"resolved": "https://registry.npmjs.org/@unly/universal-language-detector/-/universal-language-detector-2.0.3.tgz", | |||
"integrity": "sha512-UMmZIQS2XfJko8/GOrJc9ojO4UIF9HJM59NyQgcH+Ncrb0WBHL2L7EZ/7iZ/YcdoRnioonRECUQmmUELWt9z5Q==", | |||
"dependencies": { | |||
"@unly/iso3166-1": "1.0.2", | |||
"@unly/utils": "1.0.3", | |||
"accept-language-parser": "1.5.0", | |||
"i18next": "19.0.2", | |||
"i18next-browser-languagedetector": "4.0.1", | |||
"lodash.get": "4.4.2", | |||
"lodash.includes": "4.3.0" | |||
} | |||
}, | |||
"node_modules/@unly/universal-language-detector/node_modules/i18next": { | |||
"version": "19.0.2", | |||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.0.2.tgz", | |||
"integrity": "sha512-fBa43Ann2udP1CQAz3IQpOZ1dGAkmi3mMfzisOhH17igneSRbvZ7P2RNbL+L1iRYKMufBmVwnC7G3gqcyviZ9g==", | |||
"dependencies": { | |||
"@babel/runtime": "^7.3.1" | |||
} | |||
}, | |||
"node_modules/@unly/utils": { | |||
"version": "1.0.3", | |||
"resolved": "https://registry.npmjs.org/@unly/utils/-/utils-1.0.3.tgz", | |||
"integrity": "sha512-QTRknIDX56FvzGcIpBum5D/oRSlX3dkZ+l1op1jsFlYCTd925OGUb991V7zsFv3ePcqFfvfqfR5cNVv+w4JAOw==" | |||
}, | |||
"node_modules/accept-language-parser": { | |||
"version": "1.5.0", | |||
"resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", | |||
"integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==" | |||
}, | |||
"node_modules/acorn": { | |||
"version": "8.11.2", | |||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", | |||
@@ -3003,6 +3044,14 @@ | |||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | |||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" | |||
}, | |||
"node_modules/html-parse-stringify": { | |||
"version": "3.0.1", | |||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", | |||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", | |||
"dependencies": { | |||
"void-elements": "3.1.0" | |||
} | |||
}, | |||
"node_modules/human-signals": { | |||
"version": "4.3.1", | |||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", | |||
@@ -3012,6 +3061,44 @@ | |||
"node": ">=14.18.0" | |||
} | |||
}, | |||
"node_modules/i18next": { | |||
"version": "23.7.11", | |||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.11.tgz", | |||
"integrity": "sha512-A/vOkw8vY99YHU9A1Td3I1dcTiYaPnwBWzrpVzfXUXSYgogK3cmBcmop/0cnXPc6QpUWIyqaugKNxRUEZVk9Nw==", | |||
"funding": [ | |||
{ | |||
"type": "individual", | |||
"url": "https://locize.com" | |||
}, | |||
{ | |||
"type": "individual", | |||
"url": "https://locize.com/i18next.html" | |||
}, | |||
{ | |||
"type": "individual", | |||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" | |||
} | |||
], | |||
"dependencies": { | |||
"@babel/runtime": "^7.23.2" | |||
} | |||
}, | |||
"node_modules/i18next-browser-languagedetector": { | |||
"version": "4.0.1", | |||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.0.1.tgz", | |||
"integrity": "sha512-RxSoX6mB8cab0CTIQ+klCS764vYRj+Jk621cnFVsINvcdlb/cdi3vQFyrPwmnowB7ReUadjHovgZX+RPIzHVQQ==", | |||
"dependencies": { | |||
"@babel/runtime": "^7.5.5" | |||
} | |||
}, | |||
"node_modules/i18next-resources-to-backend": { | |||
"version": "1.2.0", | |||
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", | |||
"integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", | |||
"dependencies": { | |||
"@babel/runtime": "^7.23.2" | |||
} | |||
}, | |||
"node_modules/ignore": { | |||
"version": "5.3.0", | |||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", | |||
@@ -3610,6 +3697,16 @@ | |||
"url": "https://github.com/sponsors/sindresorhus" | |||
} | |||
}, | |||
"node_modules/lodash.get": { | |||
"version": "4.4.2", | |||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", | |||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" | |||
}, | |||
"node_modules/lodash.includes": { | |||
"version": "4.3.0", | |||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", | |||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" | |||
}, | |||
"node_modules/lodash.merge": { | |||
"version": "4.6.2", | |||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", | |||
@@ -4422,6 +4519,27 @@ | |||
"react": "^16.8.0 || ^17 || ^18" | |||
} | |||
}, | |||
"node_modules/react-i18next": { | |||
"version": "13.5.0", | |||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz", | |||
"integrity": "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==", | |||
"dependencies": { | |||
"@babel/runtime": "^7.22.5", | |||
"html-parse-stringify": "^3.0.1" | |||
}, | |||
"peerDependencies": { | |||
"i18next": ">= 23.2.3", | |||
"react": ">= 16.8.0" | |||
}, | |||
"peerDependenciesMeta": { | |||
"react-dom": { | |||
"optional": true | |||
}, | |||
"react-native": { | |||
"optional": true | |||
} | |||
} | |||
}, | |||
"node_modules/react-is": { | |||
"version": "18.2.0", | |||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", | |||
@@ -5087,6 +5205,14 @@ | |||
"uuid": "dist/bin/uuid" | |||
} | |||
}, | |||
"node_modules/void-elements": { | |||
"version": "3.1.0", | |||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", | |||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", | |||
"engines": { | |||
"node": ">=0.10.0" | |||
} | |||
}, | |||
"node_modules/watchpack": { | |||
"version": "2.4.0", | |||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", | |||
@@ -17,11 +17,15 @@ | |||
"@mui/icons-material": "^5.15.0", | |||
"@mui/material": "^5.15.0", | |||
"@mui/material-nextjs": "^5.15.0", | |||
"@unly/universal-language-detector": "^2.0.3", | |||
"i18next": "^23.7.11", | |||
"i18next-resources-to-backend": "^1.2.0", | |||
"next": "14.0.4", | |||
"next-auth": "^4.24.5", | |||
"react": "^18", | |||
"react-dom": "^18", | |||
"react-hook-form": "^7.49.2" | |||
"react-hook-form": "^7.49.2", | |||
"react-i18next": "^13.5.0" | |||
}, | |||
"devDependencies": { | |||
"@types/node": "^20", | |||
@@ -1,4 +1,4 @@ | |||
const Dashboard: React.FC = () => { | |||
const Dashboard: React.FC = async () => { | |||
return "Dashboard (protected)"; | |||
}; | |||
@@ -1,5 +1,5 @@ | |||
import type { Metadata } from "next"; | |||
import { detectLanguage } from "@/i18n"; | |||
import ThemeRegistry from "@/theme/ThemeRegistry"; | |||
export const metadata: Metadata = { | |||
@@ -7,13 +7,15 @@ export const metadata: Metadata = { | |||
description: "Generated by create next app", | |||
}; | |||
export default function RootLayout({ | |||
export default async function RootLayout({ | |||
children, | |||
}: { | |||
children: React.ReactNode; | |||
}) { | |||
const lang = await detectLanguage(); | |||
return ( | |||
<html lang="en"> | |||
<html lang={lang}> | |||
<body> | |||
<ThemeRegistry>{children}</ThemeRegistry> | |||
</body> | |||
@@ -1,9 +1,8 @@ | |||
import Grid from "@mui/material/Grid"; | |||
import Paper from "@mui/material/Paper"; | |||
import LoginForm from "./LoginForm"; | |||
import { getServerSession } from "next-auth"; | |||
import { redirect } from "next/navigation"; | |||
import { authOptions } from "@/config/authConfig"; | |||
import { I18nProvider } from "@/i18n"; | |||
import LoginPage from "@/components/LoginPage/LoginPage"; | |||
const Login: React.FC = async () => { | |||
const session = await getServerSession(authOptions); | |||
@@ -12,16 +11,9 @@ const Login: React.FC = async () => { | |||
} | |||
return ( | |||
<Grid container height="100vh"> | |||
<Grid item sm> | |||
Hero | |||
</Grid> | |||
<Grid item xs={12} sm={8} lg={5}> | |||
<Paper square sx={{ height: "100%" }}> | |||
<LoginForm /> | |||
</Paper> | |||
</Grid> | |||
</Grid> | |||
<I18nProvider namespaces={["login"]}> | |||
<LoginPage /> | |||
</I18nProvider> | |||
); | |||
}; | |||
@@ -5,10 +5,12 @@ import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import TextField from "@mui/material/TextField"; | |||
import Typography from "@mui/material/Typography"; | |||
import { TFunction } from "i18next"; | |||
import { signIn } from "next-auth/react"; | |||
import { useRouter } from "next/navigation"; | |||
import { useState } from "react"; | |||
import { SubmitHandler, useForm } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
type LoginFields = { | |||
username: string; | |||
@@ -16,17 +18,21 @@ type LoginFields = { | |||
}; | |||
// Error codes in https://next-auth.js.org/configuration/pages#sign-in-page | |||
const getHumanFriendlyErrorMessage = (serverError: string): string => { | |||
const getHumanFriendlyErrorMessage = ( | |||
t: TFunction, | |||
serverError: string, | |||
): string => { | |||
switch (serverError) { | |||
case "CredentialsSignin": | |||
return "Invalid username or password."; | |||
return t("Invalid username or password."); | |||
case "Default": | |||
default: | |||
return "Something went wrong. Please try again later."; | |||
return t("Something went wrong. Please try again later."); | |||
} | |||
}; | |||
const LoginForm: React.FC = () => { | |||
const { t } = useTranslation("login"); | |||
const { | |||
register, | |||
handleSubmit, | |||
@@ -60,23 +66,23 @@ const LoginForm: React.FC = () => { | |||
component="form" | |||
onSubmit={handleSubmit(onSubmit)} | |||
> | |||
<Typography variant="h1">Sign In</Typography> | |||
<Typography variant="h1">{t("Sign In")}</Typography> | |||
<TextField | |||
label="Username" | |||
{...register("username", { required: "Please enter a username" })} | |||
label={t("Username")} | |||
{...register("username", { required: t("Please enter a username") })} | |||
error={Boolean(errors.username)} | |||
helperText={errors.username?.message} | |||
/> | |||
<TextField | |||
label="Password" | |||
label={t("Password")} | |||
type="password" | |||
{...register("password", { required: "Please enter a password" })} | |||
{...register("password", { required: t("Please enter a password") })} | |||
error={Boolean(errors.password)} | |||
helperText={errors.password?.message} | |||
/> | |||
{serverError && ( | |||
<FormHelperText error> | |||
{getHumanFriendlyErrorMessage(serverError)} | |||
{getHumanFriendlyErrorMessage(t, serverError)} | |||
</FormHelperText> | |||
)} | |||
<Button | |||
@@ -85,7 +91,7 @@ const LoginForm: React.FC = () => { | |||
type="submit" | |||
disabled={isSubmitting} | |||
> | |||
Login | |||
{t("Login")} | |||
</Button> | |||
</Stack> | |||
); |
@@ -0,0 +1,20 @@ | |||
import Grid from "@mui/material/Grid"; | |||
import Paper from "@mui/material/Paper"; | |||
import LoginForm from "./LoginForm"; | |||
const LoginPage = () => { | |||
return ( | |||
<Grid container height="100vh"> | |||
<Grid item sm> | |||
test | |||
</Grid> | |||
<Grid item xs={12} sm={8} lg={5}> | |||
<Paper square sx={{ height: "100%" }}> | |||
<LoginForm /> | |||
</Paper> | |||
</Grid> | |||
</Grid> | |||
); | |||
}; | |||
export default LoginPage; |
@@ -0,0 +1 @@ | |||
export { default } from "./LoginPage"; |
@@ -1,7 +1,12 @@ | |||
import { AuthOptions } from "next-auth"; | |||
import { AuthOptions, Session } from "next-auth"; | |||
import CredentialsProvider from "next-auth/providers/credentials"; | |||
import { LOGIN_API_PATH } from "./api"; | |||
interface SessionWithTokens extends Session { | |||
accessToken?: string; | |||
refreshToken?: string; | |||
} | |||
export const authOptions: AuthOptions = { | |||
debug: process.env.NODE_ENV === "development", | |||
providers: [ | |||
@@ -33,10 +38,21 @@ export const authOptions: AuthOptions = { | |||
}, | |||
callbacks: { | |||
jwt(params) { | |||
return params.token; | |||
// Add the data from user to the token | |||
const { token, user } = params; | |||
const newToken = { ...token, ...user }; | |||
return newToken; | |||
}, | |||
session(params) { | |||
return params.session; | |||
session({ session, token }) { | |||
const sessionWithToken: SessionWithTokens = { | |||
...session, | |||
// Add the data from the token to the session | |||
accessToken: token.accessToken as string | undefined, | |||
refreshToken: token.refreshToken as string | undefined, | |||
}; | |||
return sessionWithToken; | |||
}, | |||
}, | |||
}; |
@@ -0,0 +1,43 @@ | |||
"use client"; | |||
import { createInstance, i18n } from "i18next"; | |||
import React, { useMemo } from "react"; | |||
import { I18nextProvider } from "react-i18next"; | |||
interface Props { | |||
children: React.ReactNode; | |||
language: string; | |||
resources: { [ns: string]: any }; | |||
namespaces: string[]; | |||
} | |||
export const I18nContext = React.createContext(null); | |||
const I18nProvider: React.FC<Props> = ({ | |||
children, | |||
language, | |||
resources, | |||
namespaces, | |||
}) => { | |||
const i18n = useMemo<i18n>(() => { | |||
const instance = createInstance(); | |||
instance.init({ | |||
resources: { | |||
[language]: resources, | |||
}, | |||
fallbackLng: language, | |||
interpolation: { | |||
escapeValue: false, | |||
}, | |||
ns: namespaces, | |||
}); | |||
return instance as i18n; | |||
// No need to check dependencies since this | |||
// should only be created once from the server | |||
// eslint-disable-next-line react-hooks/exhaustive-deps | |||
}, []); | |||
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>; | |||
}; | |||
export default I18nProvider; |
@@ -0,0 +1 @@ | |||
{} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"test": "abc" | |||
} |
@@ -0,0 +1,92 @@ | |||
import { cookies, headers } from "next/headers"; | |||
import { createInstance, i18n, LanguageDetectorAsyncModule } from "i18next"; | |||
import resourcesToBackend from "i18next-resources-to-backend"; | |||
import { getServerSession } from "next-auth"; | |||
import { authOptions } from "@/config/authConfig"; | |||
import I18nClientProvider from "./I18nClientProvider"; | |||
import universalLanguageDetect from "@unly/universal-language-detector"; | |||
const FALLBACK_LANG = "en"; | |||
const SUPPORTED_LANGUAGES = ["en", "zh"]; | |||
export const detectLanguage = async (): Promise<string> => { | |||
// Logic to get language preference from cookies/headers/session | |||
const cookiesList = cookies(); | |||
const cookiesObj = cookiesList | |||
.getAll() | |||
.reduce<{ [name: string]: string }>( | |||
(acc, cookie) => ({ ...acc, [cookie.name]: cookie.value }), | |||
{}, | |||
); | |||
const headersList = headers(); | |||
const session = await getServerSession(authOptions); | |||
const lang = universalLanguageDetect({ | |||
supportedLanguages: SUPPORTED_LANGUAGES, | |||
fallbackLanguage: FALLBACK_LANG, | |||
acceptLanguageHeader: headersList.get("accept-language") || undefined, | |||
serverCookies: cookiesObj, | |||
}); | |||
return lang; | |||
}; | |||
const languageDetector: LanguageDetectorAsyncModule = { | |||
type: "languageDetector", | |||
detect: detectLanguage, | |||
async: true, | |||
}; | |||
const initI18next = async (namespaces: string[]): Promise<i18n> => { | |||
const i18nInstance = createInstance(); | |||
await i18nInstance | |||
.use(languageDetector) | |||
.use( | |||
resourcesToBackend((language: string, namespace: string) => { | |||
return import(`./${language}/${namespace}.json`); | |||
}), | |||
) | |||
.init({ | |||
fallbackLng: "en", | |||
interpolation: { | |||
escapeValue: false, | |||
}, | |||
ns: namespaces, | |||
}); | |||
return i18nInstance as i18n; | |||
}; | |||
export const getServerI18n = async (...namespaces: string[]) => { | |||
return initI18next(namespaces); | |||
}; | |||
interface Props { | |||
children: React.ReactNode; | |||
namespaces: string[]; | |||
} | |||
// Provides the resources for the client | |||
export const I18nProvider: React.FC<Props> = async ({ | |||
children, | |||
namespaces, | |||
}) => { | |||
const i18n = await getServerI18n(...namespaces); | |||
const language = i18n.language; | |||
const resources = namespaces.reduce<{ [ns: string]: any }>( | |||
(acc, ns) => ({ | |||
...acc, | |||
[ns]: i18n.getResourceBundle(language, ns), | |||
}), | |||
{}, | |||
); | |||
return ( | |||
<I18nClientProvider | |||
language={language} | |||
resources={resources} | |||
namespaces={namespaces} | |||
> | |||
{children} | |||
</I18nClientProvider> | |||
); | |||
}; |
@@ -0,0 +1,10 @@ | |||
{ | |||
"Invalid username or password.": "帳號或密碼錯誤。", | |||
"Something went wrong. Please try again later.": "出了些問題。請稍後再試。", | |||
"Username": "帳號", | |||
"Password": "密碼", | |||
"Please enter a username": "請輸入帳號", | |||
"Please enter a password": "請輸入密碼", | |||
"Login": "登入", | |||
"Sign In": "登入" | |||
} |
@@ -0,0 +1,3 @@ | |||
{ | |||
"test": "def" | |||
} |
@@ -1,8 +1,30 @@ | |||
import { withAuth } from "next-auth/middleware"; | |||
import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; | |||
import { authOptions } from "@/config/authConfig"; | |||
import { NextFetchEvent, NextResponse } from "next/server"; | |||
export default withAuth({ | |||
const PUBLIC_ROUTES = ["/login"]; | |||
const LANG_QUERY_PARAM = "lang"; | |||
const authMiddleware = withAuth({ | |||
pages: authOptions.pages, | |||
}); | |||
export const config = { matcher: ["/dashboard"] }; | |||
export default async function middleware( | |||
req: NextRequestWithAuth, | |||
event: NextFetchEvent, | |||
) { | |||
const langPref = req.nextUrl.searchParams.get(LANG_QUERY_PARAM); | |||
if (langPref) { | |||
// Redirect to same url without the lang query param + set cookies | |||
const newUrl = new URL(req.nextUrl); | |||
newUrl.searchParams.delete(LANG_QUERY_PARAM); | |||
const response = NextResponse.redirect(newUrl); | |||
response.cookies.set("i18next", langPref); | |||
return response; | |||
} | |||
// Matcher for using the auth middleware | |||
return PUBLIC_ROUTES.some((route) => req.nextUrl.pathname.startsWith(route)) | |||
? NextResponse.next() // Return normal response | |||
: await authMiddleware(req, event); // Let auth middleware handle response | |||
} |