@@ -2,7 +2,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next | |||||
## Setting Up the Environment | ## 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 | ```bash | ||||
nvm use | nvm use | ||||
@@ -21,21 +23,10 @@ npm run dev | |||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | 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/icons-material": "^5.15.0", | ||||
"@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
"@mui/material-nextjs": "^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": "14.0.4", | ||||
"next-auth": "^4.24.5", | "next-auth": "^4.24.5", | ||||
"react": "^18", | "react": "^18", | ||||
"react-dom": "^18", | "react-dom": "^18", | ||||
"react-hook-form": "^7.49.2" | |||||
"react-hook-form": "^7.49.2", | |||||
"react-i18next": "^13.5.0" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/node": "^20", | "@types/node": "^20", | ||||
@@ -1068,6 +1072,43 @@ | |||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", | "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", | ||||
"dev": true | "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": { | "node_modules/acorn": { | ||||
"version": "8.11.2", | "version": "8.11.2", | ||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", | "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", | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" | "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": { | "node_modules/human-signals": { | ||||
"version": "4.3.1", | "version": "4.3.1", | ||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", | ||||
@@ -3012,6 +3061,44 @@ | |||||
"node": ">=14.18.0" | "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": { | "node_modules/ignore": { | ||||
"version": "5.3.0", | "version": "5.3.0", | ||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", | ||||
@@ -3610,6 +3697,16 @@ | |||||
"url": "https://github.com/sponsors/sindresorhus" | "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": { | "node_modules/lodash.merge": { | ||||
"version": "4.6.2", | "version": "4.6.2", | ||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", | ||||
@@ -4422,6 +4519,27 @@ | |||||
"react": "^16.8.0 || ^17 || ^18" | "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": { | "node_modules/react-is": { | ||||
"version": "18.2.0", | "version": "18.2.0", | ||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", | "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", | ||||
@@ -5087,6 +5205,14 @@ | |||||
"uuid": "dist/bin/uuid" | "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": { | "node_modules/watchpack": { | ||||
"version": "2.4.0", | "version": "2.4.0", | ||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", | ||||
@@ -17,11 +17,15 @@ | |||||
"@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
"@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
"@mui/material-nextjs": "^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": "14.0.4", | ||||
"next-auth": "^4.24.5", | "next-auth": "^4.24.5", | ||||
"react": "^18", | "react": "^18", | ||||
"react-dom": "^18", | "react-dom": "^18", | ||||
"react-hook-form": "^7.49.2" | |||||
"react-hook-form": "^7.49.2", | |||||
"react-i18next": "^13.5.0" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/node": "^20", | "@types/node": "^20", | ||||
@@ -1,4 +1,4 @@ | |||||
const Dashboard: React.FC = () => { | |||||
const Dashboard: React.FC = async () => { | |||||
return "Dashboard (protected)"; | return "Dashboard (protected)"; | ||||
}; | }; | ||||
@@ -1,5 +1,5 @@ | |||||
import type { Metadata } from "next"; | import type { Metadata } from "next"; | ||||
import { detectLanguage } from "@/i18n"; | |||||
import ThemeRegistry from "@/theme/ThemeRegistry"; | import ThemeRegistry from "@/theme/ThemeRegistry"; | ||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
@@ -7,13 +7,15 @@ export const metadata: Metadata = { | |||||
description: "Generated by create next app", | description: "Generated by create next app", | ||||
}; | }; | ||||
export default function RootLayout({ | |||||
export default async function RootLayout({ | |||||
children, | children, | ||||
}: { | }: { | ||||
children: React.ReactNode; | children: React.ReactNode; | ||||
}) { | }) { | ||||
const lang = await detectLanguage(); | |||||
return ( | return ( | ||||
<html lang="en"> | |||||
<html lang={lang}> | |||||
<body> | <body> | ||||
<ThemeRegistry>{children}</ThemeRegistry> | <ThemeRegistry>{children}</ThemeRegistry> | ||||
</body> | </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 { getServerSession } from "next-auth"; | ||||
import { redirect } from "next/navigation"; | import { redirect } from "next/navigation"; | ||||
import { authOptions } from "@/config/authConfig"; | import { authOptions } from "@/config/authConfig"; | ||||
import { I18nProvider } from "@/i18n"; | |||||
import LoginPage from "@/components/LoginPage/LoginPage"; | |||||
const Login: React.FC = async () => { | const Login: React.FC = async () => { | ||||
const session = await getServerSession(authOptions); | const session = await getServerSession(authOptions); | ||||
@@ -12,16 +11,9 @@ const Login: React.FC = async () => { | |||||
} | } | ||||
return ( | 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 Stack from "@mui/material/Stack"; | ||||
import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import { TFunction } from "i18next"; | |||||
import { signIn } from "next-auth/react"; | import { signIn } from "next-auth/react"; | ||||
import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||
import { useState } from "react"; | import { useState } from "react"; | ||||
import { SubmitHandler, useForm } from "react-hook-form"; | import { SubmitHandler, useForm } from "react-hook-form"; | ||||
import { useTranslation } from "react-i18next"; | |||||
type LoginFields = { | type LoginFields = { | ||||
username: string; | username: string; | ||||
@@ -16,17 +18,21 @@ type LoginFields = { | |||||
}; | }; | ||||
// Error codes in https://next-auth.js.org/configuration/pages#sign-in-page | // 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) { | switch (serverError) { | ||||
case "CredentialsSignin": | case "CredentialsSignin": | ||||
return "Invalid username or password."; | |||||
return t("Invalid username or password."); | |||||
case "Default": | case "Default": | ||||
default: | default: | ||||
return "Something went wrong. Please try again later."; | |||||
return t("Something went wrong. Please try again later."); | |||||
} | } | ||||
}; | }; | ||||
const LoginForm: React.FC = () => { | const LoginForm: React.FC = () => { | ||||
const { t } = useTranslation("login"); | |||||
const { | const { | ||||
register, | register, | ||||
handleSubmit, | handleSubmit, | ||||
@@ -60,23 +66,23 @@ const LoginForm: React.FC = () => { | |||||
component="form" | component="form" | ||||
onSubmit={handleSubmit(onSubmit)} | onSubmit={handleSubmit(onSubmit)} | ||||
> | > | ||||
<Typography variant="h1">Sign In</Typography> | |||||
<Typography variant="h1">{t("Sign In")}</Typography> | |||||
<TextField | <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)} | error={Boolean(errors.username)} | ||||
helperText={errors.username?.message} | helperText={errors.username?.message} | ||||
/> | /> | ||||
<TextField | <TextField | ||||
label="Password" | |||||
label={t("Password")} | |||||
type="password" | type="password" | ||||
{...register("password", { required: "Please enter a password" })} | |||||
{...register("password", { required: t("Please enter a password") })} | |||||
error={Boolean(errors.password)} | error={Boolean(errors.password)} | ||||
helperText={errors.password?.message} | helperText={errors.password?.message} | ||||
/> | /> | ||||
{serverError && ( | {serverError && ( | ||||
<FormHelperText error> | <FormHelperText error> | ||||
{getHumanFriendlyErrorMessage(serverError)} | |||||
{getHumanFriendlyErrorMessage(t, serverError)} | |||||
</FormHelperText> | </FormHelperText> | ||||
)} | )} | ||||
<Button | <Button | ||||
@@ -85,7 +91,7 @@ const LoginForm: React.FC = () => { | |||||
type="submit" | type="submit" | ||||
disabled={isSubmitting} | disabled={isSubmitting} | ||||
> | > | ||||
Login | |||||
{t("Login")} | |||||
</Button> | </Button> | ||||
</Stack> | </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 CredentialsProvider from "next-auth/providers/credentials"; | ||||
import { LOGIN_API_PATH } from "./api"; | import { LOGIN_API_PATH } from "./api"; | ||||
interface SessionWithTokens extends Session { | |||||
accessToken?: string; | |||||
refreshToken?: string; | |||||
} | |||||
export const authOptions: AuthOptions = { | export const authOptions: AuthOptions = { | ||||
debug: process.env.NODE_ENV === "development", | debug: process.env.NODE_ENV === "development", | ||||
providers: [ | providers: [ | ||||
@@ -33,10 +38,21 @@ export const authOptions: AuthOptions = { | |||||
}, | }, | ||||
callbacks: { | callbacks: { | ||||
jwt(params) { | 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 { 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, | 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 | |||||
} |