From 5af2d73023230536d10b89027f875e415bcc5fca Mon Sep 17 00:00:00 2001 From: rharkor Date: Thu, 10 Aug 2023 18:23:55 +0200 Subject: [PATCH 01/11] feat: secure redirect --- src/lib/utils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5bfac87e..4616e9ee 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -135,3 +135,12 @@ export const getTimeBetween = ( // Construct and return the time elapsed string return `${timeValue} ${timeUnit}${timeValue !== 1 ? "s" : ""}` } + +// Function to ensure an url is relative to the current domain +export const ensureRelativeUrl = (url: string | undefined) => { + if (url && url.startsWith("http")) { + const urlObject = new URL(url) + return urlObject.pathname + } + return url +} From c965470a97f74f52ff2e4d5da5a08f615f69e31b Mon Sep 17 00:00:00 2001 From: rharkor Date: Thu, 10 Aug 2023 20:01:24 +0200 Subject: [PATCH 02/11] feat: add translations --- i18n-config.ts | 6 +++ package-lock.json | 18 ++++++- package.json | 3 ++ .../profile/see-details-toggle.tsx | 19 ------- src/app/{ => [lang]}/(not-protected)/page.tsx | 20 +++++-- src/app/{ => [lang]}/(protected)/layout.tsx | 0 .../{ => [lang]}/(protected)/profile/page.tsx | 19 +++++-- .../profile/see-details-toggle.tsx | 32 +++++++++++ .../(sys-auth)/sign-in/layout.tsx | 0 .../{ => [lang]}/(sys-auth)/sign-in/page.tsx | 24 ++++++--- .../(sys-auth)/sign-up/credentials/page.tsx | 12 ++++- .../(sys-auth)/sign-up/layout.tsx | 0 .../{ => [lang]}/(sys-auth)/sign-up/page.tsx | 25 +++++---- .../[...not-found]/page.tsx} | 16 ++++-- src/app/{ => [lang]}/layout.tsx | 11 ++-- src/components/auth/login-user-auth-form.tsx | 4 +- src/components/auth/sign-out-button.tsx | 4 +- src/components/locale-switcher.tsx | 29 ++++++++++ src/components/profile/profile-details.tsx | 19 +++++-- .../profile/sessions/sessions-table.tsx | 23 +++++--- .../profile/sessions/user-active-sessions.tsx | 23 ++++++-- src/components/profile/update-account.tsx | 15 ++++-- src/langs/en.json | 47 ++++++++++++++++ src/langs/fr.json | 47 ++++++++++++++++ src/lib/langs.ts | 13 +++++ src/middleware.ts | 54 +++++++++++++++++++ 26 files changed, 406 insertions(+), 77 deletions(-) create mode 100644 i18n-config.ts delete mode 100644 src/app/(protected)/profile/see-details-toggle.tsx rename src/app/{ => [lang]}/(not-protected)/page.tsx (71%) rename src/app/{ => [lang]}/(protected)/layout.tsx (100%) rename src/app/{ => [lang]}/(protected)/profile/page.tsx (63%) create mode 100644 src/app/[lang]/(protected)/profile/see-details-toggle.tsx rename src/app/{ => [lang]}/(sys-auth)/sign-in/layout.tsx (100%) rename src/app/{ => [lang]}/(sys-auth)/sign-in/page.tsx (79%) rename src/app/{ => [lang]}/(sys-auth)/sign-up/credentials/page.tsx (73%) rename src/app/{ => [lang]}/(sys-auth)/sign-up/layout.tsx (100%) rename src/app/{ => [lang]}/(sys-auth)/sign-up/page.tsx (78%) rename src/app/{not-found.tsx => [lang]/[...not-found]/page.tsx} (50%) rename src/app/{ => [lang]}/layout.tsx (71%) create mode 100644 src/components/locale-switcher.tsx create mode 100644 src/langs/en.json create mode 100644 src/langs/fr.json create mode 100644 src/lib/langs.ts create mode 100644 src/middleware.ts diff --git a/i18n-config.ts b/i18n-config.ts new file mode 100644 index 00000000..dc0ace05 --- /dev/null +++ b/i18n-config.ts @@ -0,0 +1,6 @@ +export const i18n = { + defaultLocale: "en", + locales: ["en", "fr"], +} as const + +export type Locale = (typeof i18n)["locales"][number] diff --git a/package-lock.json b/package-lock.json index 579ea8ac..b608667a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^1.0.1", + "@formatjs/intl-localematcher": "^0.4.0", "@hookform/resolvers": "^3.2.0", "@next-auth/prisma-adapter": "^1.0.7", "@next/bundle-analyzer": "^13.4.13", @@ -52,6 +53,7 @@ "lodash": "^4.17.21", "lodash.isequal": "^4.5.0", "lucide-react": "^0.263.0", + "negotiator": "^0.6.3", "next": "^13.4.13", "next-auth": "^4.22.5", "next-compose-plugins": "^2.2.1", @@ -86,6 +88,7 @@ "@types/bcryptjs": "^2.4.2", "@types/crypto-js": "^4.1.1", "@types/lodash.isequal": "^4.5.6", + "@types/negotiator": "^0.6.1", "@types/node": "^20.4.9", "@types/react": "^18.2.19", "@types/react-dom": "^18.2.7", @@ -3125,6 +3128,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.0.tgz", "integrity": "sha512-ZSlli/beGZdvoqT3/Y9oOW79XSEpBfxt8UY6vjyWJW0B8d/M+MKlkQ3kBzLKDXaSsB84IVj6QntQfHLzesB4mA==" }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", + "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.8.20", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.20.tgz", @@ -11743,6 +11754,12 @@ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" }, + "node_modules/@types/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.4.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz", @@ -27100,7 +27117,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "engines": { "node": ">= 0.6" } diff --git a/package.json b/package.json index 400941cf..9cd6b251 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@auth/prisma-adapter": "^1.0.1", + "@formatjs/intl-localematcher": "^0.4.0", "@hookform/resolvers": "^3.2.0", "@next-auth/prisma-adapter": "^1.0.7", "@next/bundle-analyzer": "^13.4.13", @@ -68,6 +69,7 @@ "lodash": "^4.17.21", "lodash.isequal": "^4.5.0", "lucide-react": "^0.263.0", + "negotiator": "^0.6.3", "next": "^13.4.13", "next-auth": "^4.22.5", "next-compose-plugins": "^2.2.1", @@ -102,6 +104,7 @@ "@types/bcryptjs": "^2.4.2", "@types/crypto-js": "^4.1.1", "@types/lodash.isequal": "^4.5.6", + "@types/negotiator": "^0.6.1", "@types/node": "^20.4.9", "@types/react": "^18.2.19", "@types/react-dom": "^18.2.7", diff --git a/src/app/(protected)/profile/see-details-toggle.tsx b/src/app/(protected)/profile/see-details-toggle.tsx deleted file mode 100644 index 6d5dadea..00000000 --- a/src/app/(protected)/profile/see-details-toggle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import ProfileDetails from "@/components/profile/profile-details" -import UserActiveSessions from "@/components/profile/sessions/user-active-sessions" -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" - -export default function SeeDetailsToggle() { - return ( -
- - - See details - - - - - - -
- ) -} diff --git a/src/app/(not-protected)/page.tsx b/src/app/[lang]/(not-protected)/page.tsx similarity index 71% rename from src/app/(not-protected)/page.tsx rename to src/app/[lang]/(not-protected)/page.tsx index 2f5ca0dc..cad4abf3 100644 --- a/src/app/(not-protected)/page.tsx +++ b/src/app/[lang]/(not-protected)/page.tsx @@ -2,27 +2,37 @@ import Link from "next/link" import { ThemeSwitch } from "@/components/theme/theme-switch" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" +import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" + +export default async function Home({ + params: { lang }, +}: { + params: { + lang: Locale + } +}) { + const dictionary = await getDictionary(lang) -export default function Home() { return ( <>
-

Hello World

+

{dictionary.homePage.title}

) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx new file mode 100644 index 00000000..df42c757 --- /dev/null +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -0,0 +1,32 @@ +import ProfileDetails from "@/components/profile/profile-details" +import UserActiveSessions from "@/components/profile/sessions/user-active-sessions" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" +import { TDictionary } from "@/lib/langs" + +export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictionary }) { + return ( +
+ + + + {dictionary.profilePage.profileDetails.toggle} + + + + + + + +
+ ) +} diff --git a/src/app/(sys-auth)/sign-in/layout.tsx b/src/app/[lang]/(sys-auth)/sign-in/layout.tsx similarity index 100% rename from src/app/(sys-auth)/sign-in/layout.tsx rename to src/app/[lang]/(sys-auth)/sign-in/layout.tsx diff --git a/src/app/(sys-auth)/sign-in/page.tsx b/src/app/[lang]/(sys-auth)/sign-in/page.tsx similarity index 79% rename from src/app/(sys-auth)/sign-in/page.tsx rename to src/app/[lang]/(sys-auth)/sign-in/page.tsx index f39b8fc2..ae999538 100644 --- a/src/app/(sys-auth)/sign-in/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-in/page.tsx @@ -4,13 +4,21 @@ import GithubSignIn from "@/components/auth/github-sign-in" import { LoginUserAuthForm } from "@/components/auth/login-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" +import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" +import { Locale } from "i18n-config" export default async function SignInPage({ searchParams, + params: { lang }, }: { searchParams: { [key: string]: string | string[] | undefined } + params: { + lang: Locale + } }) { + const dictionary = await getDictionary(lang) + const providers = await getProviders() return ( @@ -27,8 +35,8 @@ export default async function SignInPage({
-

Login to your account

-

Enter your details below.

+

{dictionary.signInPage.loginToYourAccount}

+

{dictionary.signInPage.enterDetails}

@@ -37,19 +45,19 @@ export default async function SignInPage({
- Or continue with + {dictionary.auth.orContinueWith}
{providers?.github && }

- By clicking continue, you agree to our{" "} + {dictionary.auth.clickingAggreement}{" "} - Terms of Service - {" "} - and{" "} + {dictionary.auth.termsOfService} + + {dictionary.and} - Privacy Policy + {dictionary.auth.privacyPolicy} .

diff --git a/src/app/(sys-auth)/sign-up/credentials/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx similarity index 73% rename from src/app/(sys-auth)/sign-up/credentials/page.tsx rename to src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx index b455d298..d58b83db 100644 --- a/src/app/(sys-auth)/sign-up/credentials/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx @@ -2,12 +2,20 @@ import { redirect } from "next/navigation" import { RegisterUserAuthForm } from "@/components/auth/register-user-auth-form" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { authRoutes } from "@/lib/auth/constants" +import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" -export default function SignupByCredentials({ +export default async function SignupByCredentials({ searchParams, + params: { lang }, }: { searchParams: { [key: string]: string | string[] | undefined } + params: { + lang: Locale + } }) { + const dictionary = await getDictionary(lang) + //? If there is no email in the search params, redirect to the sign-up page if (!searchParams?.email) { redirect(authRoutes.signUp[0]) @@ -17,7 +25,7 @@ export default function SignupByCredentials({
- Create an account + {dictionary.signUpPage.createAnAccount} diff --git a/src/app/(sys-auth)/sign-up/layout.tsx b/src/app/[lang]/(sys-auth)/sign-up/layout.tsx similarity index 100% rename from src/app/(sys-auth)/sign-up/layout.tsx rename to src/app/[lang]/(sys-auth)/sign-up/layout.tsx diff --git a/src/app/(sys-auth)/sign-up/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/page.tsx similarity index 78% rename from src/app/(sys-auth)/sign-up/page.tsx rename to src/app/[lang]/(sys-auth)/sign-up/page.tsx index 411bc0bf..39735e7c 100644 --- a/src/app/(sys-auth)/sign-up/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -4,13 +4,20 @@ import GithubSignIn from "@/components/auth/github-sign-in" import { RegisterUserAuthForm } from "@/components/auth/register-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" +import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" +import { Locale } from "i18n-config" export default async function SignUpPage({ searchParams, + params: { lang }, }: { searchParams: { [key: string]: string | string[] | undefined } + params: { + lang: Locale + } }) { + const dictionary = await getDictionary(lang) const providers = await getProviders() return ( @@ -19,7 +26,7 @@ export default async function SignUpPage({ href={authRoutes.signIn[0]} className={cn(buttonVariants({ variant: "ghost" }), "absolute right-4 top-4 md:right-8 md:top-8")} > - Login + {dictionary.login}
@@ -27,8 +34,8 @@ export default async function SignUpPage({
-

Create an account

-

Enter your email below to create your account

+

{dictionary.signUpPage.createAnAccount}

+

{dictionary.signUpPage.enterEmail}

@@ -37,19 +44,19 @@ export default async function SignUpPage({
- Or continue with + {dictionary.auth.orContinueWith}
{providers?.github && }

- By clicking continue, you agree to our{" "} + {dictionary.auth.clickingAggreement}{" "} - Terms of Service - {" "} - and{" "} + {dictionary.auth.termsOfService} + + {dictionary.and} - Privacy Policy + {dictionary.auth.privacyPolicy} .

diff --git a/src/app/not-found.tsx b/src/app/[lang]/[...not-found]/page.tsx similarity index 50% rename from src/app/not-found.tsx rename to src/app/[lang]/[...not-found]/page.tsx index 3a49b635..63c3014a 100644 --- a/src/app/not-found.tsx +++ b/src/app/[lang]/[...not-found]/page.tsx @@ -1,13 +1,23 @@ import Link from "next/link" import React from "react" import { buttonVariants } from "@/components/ui/button" +import { getDictionary } from "@/lib/langs" +import { Locale } from "i18n-config" + +export default async function Page404({ + params: { lang }, +}: { + params: { + lang: Locale + } +}) { + const dictionary = await getDictionary(lang) -export default function Page404() { return (
-

Page not found

+

{dictionary.notFound}

- Home + {dictionary.home}
) diff --git a/src/app/layout.tsx b/src/app/[lang]/layout.tsx similarity index 71% rename from src/app/layout.tsx rename to src/app/[lang]/layout.tsx index 7a639a44..d5b85b03 100644 --- a/src/app/layout.tsx +++ b/src/app/[lang]/layout.tsx @@ -1,19 +1,24 @@ import { Metadata } from "next" import React from "react" -import "./globals.css" +import "../globals.css" import { NextAuthProvider } from "@/components/auth/provider" import { ThemeProvider } from "@/components/theme/theme-provider" import { Toaster } from "@/components/ui/toaster" import QueryClientProvider from "@/contexts/query-provider" +import { i18n } from "i18n-config" export const metadata: Metadata = { title: "Home", description: "Welcome to Next.js boilerplate", } -export default function RootLayout({ children }: { children: React.ReactNode }) { +export async function generateStaticParams() { + return i18n.locales.map((locale) => ({ lang: locale })) +} + +export default function RootLayout({ children, params }: { children: React.ReactNode; params: { lang: string } }) { return ( - + diff --git a/src/components/auth/login-user-auth-form.tsx b/src/components/auth/login-user-auth-form.tsx index d328516f..5ce9613d 100644 --- a/src/components/auth/login-user-auth-form.tsx +++ b/src/components/auth/login-user-auth-form.tsx @@ -6,7 +6,7 @@ import * as React from "react" import { useForm } from "react-hook-form" import * as z from "zod" import { handleSignError, handleSignIn } from "@/lib/auth/handle-sign" -import { cn } from "@/lib/utils" +import { cn, ensureRelativeUrl } from "@/lib/utils" import { signInSchema } from "@/types/auth" import { Button } from "../ui/button" import { Form } from "../ui/form" @@ -24,7 +24,7 @@ export type IForm = z.infer export function LoginUserAuthForm({ searchParams, ...props }: UserAuthFormProps) { const router = useRouter() - const callbackUrl = searchParams?.callbackUrl?.toString() || "/profile" + const callbackUrl = ensureRelativeUrl(searchParams?.callbackUrl?.toString()) || "/profile" const error = searchParams?.error?.toString() const [isLoading, setIsLoading] = React.useState(false) diff --git a/src/components/auth/sign-out-button.tsx b/src/components/auth/sign-out-button.tsx index a63a69b5..b60f2148 100644 --- a/src/components/auth/sign-out-button.tsx +++ b/src/components/auth/sign-out-button.tsx @@ -3,10 +3,10 @@ import { signOut } from "next-auth/react" import { Button } from "../ui/button" -export default function SignoutButton() { +export default function SignoutButton({ children }: { children: React.ReactNode }) { return ( ) } diff --git a/src/components/locale-switcher.tsx b/src/components/locale-switcher.tsx new file mode 100644 index 00000000..2c21fe3f --- /dev/null +++ b/src/components/locale-switcher.tsx @@ -0,0 +1,29 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { i18n } from "i18n-config" + +export default function LocaleSwitcher() { + const pathName = usePathname() + const redirectedPathName = (locale: string) => { + if (!pathName) return "/" + const segments = pathName.split("/") + segments[1] = locale + return segments.join("/") + } + + return ( +
+
    + {i18n.locales.map((locale) => { + return ( +
  • + {locale} +
  • + ) + })} +
+
+ ) +} diff --git a/src/components/profile/profile-details.tsx b/src/components/profile/profile-details.tsx index c67361fb..c1fdf1ea 100644 --- a/src/components/profile/profile-details.tsx +++ b/src/components/profile/profile-details.tsx @@ -1,13 +1,24 @@ import UpdateAccount from "./update-account" -export default function ProfileDetails() { +export default function ProfileDetails({ + dictionary, +}: { + dictionary: { + updateAccount: string + updateAccountDescription: string + username: { + label: string + placeholder: string + } + } +}) { return (
-

Update your account

-

You can update your account details here.

+

{dictionary.updateAccount}

+

{dictionary.updateAccountDescription}

- +
) } diff --git a/src/components/profile/sessions/sessions-table.tsx b/src/components/profile/sessions/sessions-table.tsx index ddacf295..09cada2a 100644 --- a/src/components/profile/sessions/sessions-table.tsx +++ b/src/components/profile/sessions/sessions-table.tsx @@ -23,7 +23,18 @@ import SessionRow from "./session-row" const itemsPerPageInitial = 5 -export default function SessionsTable() { +export default function SessionsTable({ + dictionary, +}: { + dictionary: { + areYouAbsolutelySure: string + deleteLoggedDevice: { + description: string + } + cancel: string + continue: string + } +}) { const { data: curSession } = useSession() const router = useRouter() const apiFetch = useApiStore((state) => state.apiFetch(router)) @@ -100,14 +111,12 @@ export default function SessionsTable() { /> - Are you absolutely sure? - - This action will disconnect the device connected to this session. - + {dictionary.areYouAbsolutelySure} + {dictionary.deleteLoggedDevice.description} - Cancel - Continue + {dictionary.cancel} + {dictionary.continue} diff --git a/src/components/profile/sessions/user-active-sessions.tsx b/src/components/profile/sessions/user-active-sessions.tsx index 7879ac1b..82bce1da 100644 --- a/src/components/profile/sessions/user-active-sessions.tsx +++ b/src/components/profile/sessions/user-active-sessions.tsx @@ -1,13 +1,28 @@ import SessionsTable from "./sessions-table" -export default function UserActiveSessions() { +export default function UserActiveSessions({ + dictionary, +}: { + dictionary: { + loggedDevices: string + loggedDevicesDescription: string + sessionTable: { + areYouAbsolutelySure: string + deleteLoggedDevice: { + description: string + } + cancel: string + continue: string + } + } +}) { return (
-

Logged in devices

-

Manage your logged in devices here.

+

{dictionary.loggedDevices}

+

{dictionary.loggedDevicesDescription}

- +
) } diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index 18ed71e5..c2d14d46 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -19,7 +19,16 @@ export const nonSensibleSchema = UpdateUserSchema export type INonSensibleForm = z.infer -export default function UpdateAccount() { +export default function UpdateAccount({ + dictionary, +}: { + dictionary: { + username: { + label: string + placeholder: string + } + } +}) { const { data: curSession, update } = useSession() const router = useRouter() const apiFetch = useApiStore((state) => state.apiFetch(router)) @@ -77,8 +86,8 @@ export default function UpdateAccount() {
import("../langs/en.json").then((module) => module.default), + fr: () => import("../langs/fr.json").then((module) => module.default), +} + +export const getDictionary = async (locale: Locale) => dictionaries[locale]?.() ?? dictionaries.en() + +export type TDictionary = Awaited> diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 00000000..e16f6a14 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,54 @@ +import { match as matchLocale } from "@formatjs/intl-localematcher" +import Negotiator from "negotiator" +import { NextResponse } from "next/server" +import type { NextRequest } from "next/server" + +import { i18n } from "../i18n-config" + +function getLocale(request: NextRequest): string | undefined { + // Negotiator expects plain object so we need to transform headers + const negotiatorHeaders: Record = {} + request.headers.forEach((value, key) => (negotiatorHeaders[key] = value)) + + const locales: string[] = i18n.locales as unknown as string[] + + // Use negotiator and intl-localematcher to get best locale + const languages = new Negotiator({ headers: negotiatorHeaders }).languages(locales) + + const locale = matchLocale(languages, locales, i18n.defaultLocale) + + return locale +} + +export function middleware(request: NextRequest) { + const pathname = request.nextUrl.pathname + + // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually. + // If you have one + if ( + [ + "/favicon.ico", + // Your other files in `public` + ].includes(pathname) + ) + return + + // Check if there is any supported locale in the pathname + const pathnameIsMissingLocale = i18n.locales.every( + (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` + ) + + // Redirect if there is no locale + if (pathnameIsMissingLocale) { + const locale = getLocale(request) + + // e.g. incoming request is /products + // The new URL is now /en-US/products + return NextResponse.redirect(new URL(`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`, request.url)) + } +} + +export const config = { + // Matcher ignoring `/_next/` and `/api/` + matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], +} From 9796af5768996aaa66483bc56f2d2cf64dc4e49e Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 11:33:56 +0200 Subject: [PATCH 03/11] refactor: api call erorr message translation for sessions --- .../profile/see-details-toggle.tsx | 6 +++ .../profile/sessions/sessions-table.tsx | 48 ++++++++++++++++--- .../profile/sessions/user-active-sessions.tsx | 6 +++ src/langs/en.json | 12 ++++- src/langs/fr.json | 12 ++++- src/lib/utils.ts | 12 +++++ 6 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx index df42c757..3b6c141a 100644 --- a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -20,6 +20,12 @@ export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictiona cancel: dictionary.cancel, continue: dictionary.continue, deleteLoggedDevice: dictionary.profilePage.profileDetails.deleteLoggedDevice, + session: dictionary.profilePage.profileDetails.session, + sessions: dictionary.profilePage.profileDetails.sessions, + error: dictionary.error, + delete: dictionary.delete, + fetch: dictionary.fetch, + couldNotMessage: dictionary.couldNotMessage, }, }} /> diff --git a/src/components/profile/sessions/sessions-table.tsx b/src/components/profile/sessions/sessions-table.tsx index 09cada2a..3b8c4371 100644 --- a/src/components/profile/sessions/sessions-table.tsx +++ b/src/components/profile/sessions/sessions-table.tsx @@ -4,7 +4,7 @@ import { Prisma } from "@prisma/client" import { useQuery } from "@tanstack/react-query" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" -import { useState } from "react" +import { useEffect, useState } from "react" import { AlertDialog, AlertDialogAction, @@ -19,6 +19,7 @@ import Pagination from "@/components/ui/pagination" import { toast } from "@/components/ui/use-toast" import { useApiStore } from "@/contexts/api.store" import { IJsonApiResponse, jsonApiQuery } from "@/lib/json-api" +import { formatCouldNotMessage } from "@/lib/utils" import SessionRow from "./session-row" const itemsPerPageInitial = 5 @@ -33,6 +34,12 @@ export default function SessionsTable({ } cancel: string continue: string + session: string + sessions: string + error: string + delete: string + fetch: string + couldNotMessage: string } }) { const { data: curSession } = useSession() @@ -43,7 +50,11 @@ export default function SessionsTable({ const [currentPage, setCurrentPage] = useState(1) const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageInitial) - const { data: sessions, refetch } = useQuery({ + const { + data: sessionsFromFetch, + refetch, + isFetched: isSessionFetched, + } = useQuery({ queryKey: ["session", curSession, currentPage, itemsPerPage], queryFn: async () => { const res = (await apiFetch( @@ -56,8 +67,12 @@ export default function SessionsTable({ ), () => { toast({ - title: "Error", - description: "Could not fetch sessions. Please try again later.", + title: dictionary.error, + description: formatCouldNotMessage({ + couldNotMessage: dictionary.couldNotMessage, + action: dictionary.fetch, + subject: dictionary.sessions, + }), variant: "destructive", }) } @@ -66,13 +81,32 @@ export default function SessionsTable({ }, enabled: !!curSession?.user?.id, }) + const [sessions, setSessions] = useState(sessionsFromFetch) + + useEffect(() => { + if (!sessionsFromFetch) return + setSessions(sessionsFromFetch) + }, [sessionsFromFetch]) const deleteSession = async () => { if (!selectedSession) return + //? Delete from UI + setSessions((prev) => { + if (!prev) return prev + return { + ...prev, + data: prev.data?.filter((session) => session.id !== selectedSession), + } + }) + //? Delete from DB const res = await apiFetch(fetch(`/api/sessions/${selectedSession}`, { method: "DELETE" }), () => { toast({ - title: "Error", - description: "Could not delete session. Please try again later.", + title: dictionary.error, + description: formatCouldNotMessage({ + couldNotMessage: dictionary.couldNotMessage, + action: dictionary.delete, + subject: dictionary.session, + }), variant: "destructive", }) }) @@ -99,7 +133,7 @@ export default function SessionsTable({ return (
- {sessions ? rows : skelRows} + {isSessionFetched || sessions ? rows : skelRows} { } return url } + +export const formatCouldNotMessage = async ({ + couldNotMessage, + action, + subject, +}: { + couldNotMessage: string + action: string + subject: string +}) => { + return couldNotMessage.replace("{action}", action).replace("{subject}", subject) +} From 00fee80be9ef2f81781b7115e27124bb34f7fc93 Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 11:37:40 +0200 Subject: [PATCH 04/11] refactor: error translation in update profile --- src/app/[lang]/(protected)/profile/see-details-toggle.tsx | 7 ++++++- src/components/profile/profile-details.tsx | 1 + src/components/profile/update-account.tsx | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx index 3b6c141a..0b85d3f0 100644 --- a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -29,7 +29,12 @@ export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictiona }, }} /> - + diff --git a/src/components/profile/profile-details.tsx b/src/components/profile/profile-details.tsx index c1fdf1ea..8b25d8f1 100644 --- a/src/components/profile/profile-details.tsx +++ b/src/components/profile/profile-details.tsx @@ -10,6 +10,7 @@ export default function ProfileDetails({ label: string placeholder: string } + error: string } }) { return ( diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index c2d14d46..bc715a77 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -27,6 +27,7 @@ export default function UpdateAccount({ label: string placeholder: string } + error: string } }) { const { data: curSession, update } = useSession() @@ -68,7 +69,7 @@ export default function UpdateAccount({ }), (err) => { toast({ - title: "Error", + title: dictionary.error, description: err, variant: "destructive", }) From b00bc71804f32a2c91277cfc2256ca2d0372eff0 Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 11:44:46 +0200 Subject: [PATCH 05/11] refactor: translate need save popup --- .../(protected)/profile/see-details-toggle.tsx | 3 +++ src/components/need-save-popup.tsx | 18 ++++++++---------- src/components/profile/profile-details.tsx | 3 +++ src/components/profile/update-account.tsx | 5 +++++ src/langs/en.json | 5 ++++- src/langs/fr.json | 5 ++++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx index 0b85d3f0..44cb121a 100644 --- a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -33,6 +33,9 @@ export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictiona dictionary={{ ...dictionary.profilePage.profileDetails, error: dictionary.error, + needSavePopup: dictionary.needSavePopup, + reset: dictionary.reset, + saveChanges: dictionary.saveChanges, }} /> diff --git a/src/components/need-save-popup.tsx b/src/components/need-save-popup.tsx index 97cde89e..820b47ba 100644 --- a/src/components/need-save-popup.tsx +++ b/src/components/need-save-popup.tsx @@ -5,17 +5,15 @@ export type INeedSavePopupProps = { show: boolean onReset?: () => void onSave?: () => void - text?: string + text: string isSubmitting?: boolean + dictionary: { + reset: string + saveChanges: string + } } -export default function NeedSavePopup({ - show, - onReset, - onSave, - text = "Be careful, there are still unsaved changes!", - isSubmitting, -}: INeedSavePopupProps) { +export default function NeedSavePopup({ show, onReset, onSave, text, isSubmitting, dictionary }: INeedSavePopupProps) { return (
{text}

diff --git a/src/components/profile/profile-details.tsx b/src/components/profile/profile-details.tsx index 8b25d8f1..8ceac545 100644 --- a/src/components/profile/profile-details.tsx +++ b/src/components/profile/profile-details.tsx @@ -11,6 +11,9 @@ export default function ProfileDetails({ placeholder: string } error: string + needSavePopup: string + reset: string + saveChanges: string } }) { return ( diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index bc715a77..18b5aca1 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -28,6 +28,9 @@ export default function UpdateAccount({ placeholder: string } error: string + needSavePopup: string + reset: string + saveChanges: string } }) { const { data: curSession, update } = useSession() @@ -99,6 +102,8 @@ export default function UpdateAccount({ show={isNotSensibleInformationsUpdated} onReset={resetForm} isSubmitting={form.formState.isSubmitting} + text={dictionary.needSavePopup} + dictionary={dictionary} /> diff --git a/src/langs/en.json b/src/langs/en.json index 19790f46..e4b4e93d 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -51,5 +51,8 @@ "fetch": "fetch", "create": "create", "update": "update", - "delete": "delete" + "delete": "delete", + "needSavePopup": "Be careful, there are still unsaved changes!", + "reset": "Reset", + "saveChanges": "Save changes" } diff --git a/src/langs/fr.json b/src/langs/fr.json index 88ea8aa5..cb8dd62f 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -51,5 +51,8 @@ "fetch": "récupérer", "create": "créer", "update": "mettre à jour", - "delete": "supprimer" + "delete": "supprimer", + "needSavePopup": "Vous avez des modifications non enregistrées !", + "reset": "Réinitialiser", + "saveChanges": "Sauvegarder" } From b24a7675b05e80067ac88bddaddc9b531557665a Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 12:44:42 +0200 Subject: [PATCH 06/11] refactor: translate zod schema --- .../profile/see-details-toggle.tsx | 10 +--- src/app/[lang]/(sys-auth)/sign-in/page.tsx | 2 +- .../(sys-auth)/sign-up/credentials/page.tsx | 2 +- src/app/[lang]/(sys-auth)/sign-up/page.tsx | 2 +- src/app/api/me/route.ts | 2 +- src/app/api/register/route.ts | 6 ++- src/components/auth/login-user-auth-form.tsx | 8 +-- .../auth/register-user-auth-form.tsx | 49 ++++++++++--------- src/components/profile/profile-details.tsx | 24 +++------ src/components/profile/update-account.tsx | 24 +++------ src/langs/en.json | 20 +++++++- src/langs/fr.json | 20 +++++++- src/lib/auth/handle-sign.ts | 6 +-- src/lib/auth/index.ts | 21 +++++++- src/types/api.ts | 8 +-- src/types/auth.ts | 25 +++++----- src/types/constants.ts | 36 ++++++++++---- 17 files changed, 159 insertions(+), 106 deletions(-) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx index 44cb121a..92e4b011 100644 --- a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -29,15 +29,7 @@ export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictiona }, }} /> - + diff --git a/src/app/[lang]/(sys-auth)/sign-in/page.tsx b/src/app/[lang]/(sys-auth)/sign-in/page.tsx index ae999538..d1134991 100644 --- a/src/app/[lang]/(sys-auth)/sign-in/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-in/page.tsx @@ -39,7 +39,7 @@ export default async function SignInPage({

{dictionary.signInPage.enterDetails}

- +
diff --git a/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx index d58b83db..0bbea9e0 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/credentials/page.tsx @@ -28,7 +28,7 @@ export default async function SignupByCredentials({ {dictionary.signUpPage.createAnAccount} - +
diff --git a/src/app/[lang]/(sys-auth)/sign-up/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/page.tsx index 39735e7c..315537f0 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -38,7 +38,7 @@ export default async function SignUpPage({

{dictionary.signUpPage.enterEmail}

- +
diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts index 5fe319d4..c03daeb1 100644 --- a/src/app/api/me/route.ts +++ b/src/app/api/me/route.ts @@ -39,7 +39,7 @@ export async function PATCH(request: Request) { if (!success) return throttlerErrorResponse const body = await request.json() - const bodyParsedResult = UpdateUserSchema.safeParse(body) + const bodyParsedResult = UpdateUserSchema().safeParse(body) if (!bodyParsedResult.success) return ApiError(bodyParsedResult.error.message, { status: 400 }) const bodyParsed = bodyParsedResult.data diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 43c67dad..5591a8ee 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -9,8 +9,10 @@ import { signUpSchema } from "@/types/auth" export async function POST(req: Request) { try { - const data = (await req.json()) as z.infer - const { username, email, password } = signUpSchema.parse(data) + const reqData = (await req.json()) as z.infer> + const bodyParsedResult = signUpSchema().safeParse(reqData) + if (!bodyParsedResult.success) return ApiError(bodyParsedResult.error.message, { status: 400 }) + const { email, password, username } = bodyParsedResult.data const hashedPassword = await hash(password, 12) const user = await prisma.user.create({ diff --git a/src/components/auth/login-user-auth-form.tsx b/src/components/auth/login-user-auth-form.tsx index 5ce9613d..52486954 100644 --- a/src/components/auth/login-user-auth-form.tsx +++ b/src/components/auth/login-user-auth-form.tsx @@ -6,6 +6,7 @@ import * as React from "react" import { useForm } from "react-hook-form" import * as z from "zod" import { handleSignError, handleSignIn } from "@/lib/auth/handle-sign" +import { TDictionary } from "@/lib/langs" import { cn, ensureRelativeUrl } from "@/lib/utils" import { signInSchema } from "@/types/auth" import { Button } from "../ui/button" @@ -14,14 +15,15 @@ import FormField from "../ui/form-field" import { Label } from "../ui/label" type UserAuthFormProps = React.HTMLAttributes & { + dictionary: TDictionary searchParams: { [key: string]: string | string[] | undefined } } export const formSchema = signInSchema -export type IForm = z.infer +export type IForm = z.infer> -export function LoginUserAuthForm({ searchParams, ...props }: UserAuthFormProps) { +export function LoginUserAuthForm({ dictionary, searchParams, ...props }: UserAuthFormProps) { const router = useRouter() const callbackUrl = ensureRelativeUrl(searchParams?.callbackUrl?.toString()) || "/profile" @@ -36,7 +38,7 @@ export function LoginUserAuthForm({ searchParams, ...props }: UserAuthFormProps) } const form = useForm({ - resolver: zodResolver(formSchema), + resolver: zodResolver(formSchema(dictionary)), defaultValues: { email: "", password: "", diff --git a/src/components/auth/register-user-auth-form.tsx b/src/components/auth/register-user-auth-form.tsx index f3cd9acc..51aa689e 100644 --- a/src/components/auth/register-user-auth-form.tsx +++ b/src/components/auth/register-user-auth-form.tsx @@ -9,6 +9,7 @@ import * as z from "zod" import { useApiStore } from "@/contexts/api.store" import { authRoutes } from "@/lib/auth/constants" import { handleSignError, handleSignUp } from "@/lib/auth/handle-sign" +import { TDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" import { signUpSchema } from "@/types/auth" import { Button, buttonVariants } from "../ui/button" @@ -17,35 +18,39 @@ import FormField from "../ui/form-field" import { Label } from "../ui/label" type UserAuthFormProps = React.HTMLAttributes & { + dictionary: TDictionary isMinimized?: boolean searchParams?: { [key: string]: string | string[] | undefined } } -export const formSchema = signUpSchema - .extend({ - confirmPassword: z.string(), +export const formSchema = (dictionary: TDictionary) => + signUpSchema(dictionary) + .extend({ + confirmPassword: z.string(), + }) + .superRefine((data, ctx) => { + if (data.password !== data.confirmPassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: dictionary.errors.password.dontMatch, + path: ["confirmPassword"], + fatal: true, + }) + } + }) + +export const formMinizedSchema = (dictionary: TDictionary) => + signUpSchema(dictionary).pick({ + email: true, }) - .superRefine((data, ctx) => { - if (data.password !== data.confirmPassword) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Passwords don't match", - path: ["confirmPassword"], - fatal: true, - }) - } - }) - -export const formMinizedSchema = signUpSchema.pick({ - email: true, -}) -export const getFormSchema = (isMinimized?: boolean) => (isMinimized ? formMinizedSchema : formSchema) +export const getFormSchema = ({ dictionary, isMinimized }: { dictionary: TDictionary; isMinimized?: boolean }) => + isMinimized ? formMinizedSchema(dictionary) : formSchema(dictionary) -export type IForm = z.infer -export type IFormMinimized = z.infer +export type IForm = z.infer> +export type IFormMinimized = z.infer> -export function RegisterUserAuthForm({ isMinimized, searchParams, ...props }: UserAuthFormProps) { +export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, ...props }: UserAuthFormProps) { const router = useRouter() const apiFetch = useApiStore((state) => state.apiFetch(router)) @@ -60,7 +65,7 @@ export function RegisterUserAuthForm({ isMinimized, searchParams, ...props }: Us const [errorDisplayed, setErrorDisplayed] = React.useState(null) const form = useForm({ - resolver: zodResolver(getFormSchema(isMinimized)), + resolver: zodResolver(getFormSchema({ isMinimized, dictionary })), defaultValues: { email: emailFromSearchParam || "", username: "", diff --git a/src/components/profile/profile-details.tsx b/src/components/profile/profile-details.tsx index 8ceac545..6b773c3f 100644 --- a/src/components/profile/profile-details.tsx +++ b/src/components/profile/profile-details.tsx @@ -1,26 +1,14 @@ +import { TDictionary } from "@/lib/langs" import UpdateAccount from "./update-account" -export default function ProfileDetails({ - dictionary, -}: { - dictionary: { - updateAccount: string - updateAccountDescription: string - username: { - label: string - placeholder: string - } - error: string - needSavePopup: string - reset: string - saveChanges: string - } -}) { +export default function ProfileDetails({ dictionary }: { dictionary: TDictionary }) { return (
-

{dictionary.updateAccount}

-

{dictionary.updateAccountDescription}

+

{dictionary.profilePage.profileDetails.updateAccount}

+

+ {dictionary.profilePage.profileDetails.updateAccountDescription} +

diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index 18b5aca1..19992a38 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useState } from "react" import { useForm } from "react-hook-form" import * as z from "zod" import { useApiStore } from "@/contexts/api.store" +import { TDictionary } from "@/lib/langs" import { logger } from "@/lib/logger" import { UpdateUserSchema } from "@/types/api" import NeedSavePopup from "../need-save-popup" @@ -17,22 +18,9 @@ import { toast } from "../ui/use-toast" //? Put only the fields you can update withou password confirmation export const nonSensibleSchema = UpdateUserSchema -export type INonSensibleForm = z.infer +export type INonSensibleForm = z.infer> -export default function UpdateAccount({ - dictionary, -}: { - dictionary: { - username: { - label: string - placeholder: string - } - error: string - needSavePopup: string - reset: string - saveChanges: string - } -}) { +export default function UpdateAccount({ dictionary }: { dictionary: TDictionary }) { const { data: curSession, update } = useSession() const router = useRouter() const apiFetch = useApiStore((state) => state.apiFetch(router)) @@ -40,7 +28,7 @@ export default function UpdateAccount({ const [isNotSensibleInformationsUpdated, setIsNotSensibleInformationsUpdated] = useState(false) const form = useForm({ - resolver: zodResolver(nonSensibleSchema), + resolver: zodResolver(nonSensibleSchema(dictionary)), defaultValues: { username: curSession?.user?.name || "", }, @@ -90,8 +78,8 @@ export default function UpdateAccount({
{ } export const handleSignIn = async ( - data: z.infer, + data: z.infer>, callbackUrl: string, router: AppRouterInstance ) => { @@ -81,8 +81,8 @@ export const handleSignUp = async ({ apiFetch, loginOnSignUp = true, }: { - data: z.infer - form: UseFormReturn> + data: z.infer> + form: UseFormReturn>> router: AppRouterInstance loginOnSignUp: boolean apiFetch: ReturnType diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 926df382..9e093c8d 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -6,12 +6,18 @@ import requestIp from "request-ip" import { randomUUID } from "crypto" import { signInSchema } from "@/types/auth" import { env } from "env.mjs" +import { i18n, Locale } from "i18n-config" import { authRoutes, JWT_MAX_AGE } from "./constants" import { bcryptCompare } from "../bcrypt" +import { getDictionary, TDictionary } from "../langs" import { logger } from "../logger" import { prisma } from "../prisma" +import { ensureRelativeUrl } from "../utils" -export const nextAuthOptions: NextAuthOptions = { +export const nextAuthOptions: NextAuthOptions & { + loadedDictionary: Map +} = { + loadedDictionary: new Map(), secret: env.NEXTAUTH_SECRET, adapter: PrismaAdapter(prisma), //? Require to use database providers: [ @@ -26,7 +32,18 @@ export const nextAuthOptions: NextAuthOptions = { password: { label: "Password", type: "password" }, }, authorize: async (credentials, req) => { - const creds = await signInSchema.parseAsync(credentials) + const referer = (req.headers?.referer as string) ?? "" + const refererUrl = ensureRelativeUrl(referer) ?? "" + const lang = i18n.locales.find((locale) => refererUrl.startsWith(`/${locale}/`)) ?? i18n.defaultLocale + const dictionary = + nextAuthOptions.loadedDictionary.get(lang) ?? + (await (async () => { + logger.debug("Loading dictionary in auth", lang) + const dictionary = await getDictionary(lang) + nextAuthOptions.loadedDictionary.set(lang, dictionary) + return dictionary + })()) + const creds = await signInSchema(dictionary).parseAsync(credentials) if (!creds.email || !creds.password) { logger.debug("Missing credentials", creds) diff --git a/src/types/api.ts b/src/types/api.ts index b883eb9f..62f97fcd 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,4 +1,5 @@ import * as z from "zod" +import { TDictionary } from "@/lib/langs" import { usernameSchema } from "./constants" export type IApiError = { @@ -6,6 +7,7 @@ export type IApiError = { message: string } -export const UpdateUserSchema = z.object({ - username: usernameSchema, -}) +export const UpdateUserSchema = (dictionary?: TDictionary) => + z.object({ + username: usernameSchema(dictionary), + }) diff --git a/src/types/auth.ts b/src/types/auth.ts index f9beeb1b..7dd2c138 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -1,19 +1,22 @@ import { DefaultSession } from "next-auth" import * as z from "zod" -import { passwordSchema, passwordSchemaWithRegex, usernameSchema } from "./constants" +import { TDictionary } from "@/lib/langs" +import { emailSchema, passwordSchema, passwordSchemaWithRegex, usernameSchema } from "./constants" -export const signInSchema = z.object({ - email: z.string().email(), - password: passwordSchema, -}) +export const signInSchema = (dictionary?: TDictionary) => + z.object({ + email: emailSchema(dictionary), + password: passwordSchema(dictionary), + }) -export const signUpSchema = signInSchema.extend({ - username: usernameSchema, - password: passwordSchemaWithRegex, -}) +export const signUpSchema = (dictionary?: TDictionary) => + signInSchema(dictionary).extend({ + username: usernameSchema(dictionary), + password: passwordSchemaWithRegex(dictionary), + }) -export type ISignIn = z.infer -export type ISignUp = z.infer +export type ISignIn = (dictionary: TDictionary) => z.infer> +export type ISignUp = z.infer> export type Session = { id?: unknown diff --git a/src/types/constants.ts b/src/types/constants.ts index ba9bee49..2c81269c 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -1,13 +1,31 @@ import * as z from "zod" +import { TDictionary } from "@/lib/langs" -export const passwordSchema = z - .string() - .min(4, "Password must be at least 4 characters long") - .max(16, "Password must be at most 16 characters long") +export const passwordSchema = (dictionary?: TDictionary) => + z + .string({ + required_error: dictionary && dictionary.errors.password.required, + }) + .min(4, dictionary && dictionary.errors.password.min4) + .max(25, dictionary && dictionary.errors.password.max25) -export const passwordSchemaWithRegex = passwordSchema.regex( - /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,}$/, - "Password must contain at least one uppercase letter, one lowercase letter, and one number" -) +export const passwordSchemaWithRegex = (dictionary?: TDictionary) => + passwordSchema(dictionary).regex( + /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,}$/, + dictionary && dictionary.errors.password.regex + ) -export const usernameSchema = z.string().min(3).max(30) +export const usernameSchema = (dictionary?: TDictionary) => + z + .string({ + required_error: dictionary && dictionary.errors.username.required, + }) + .min(3, dictionary && dictionary.errors.username.min3) + .max(30, dictionary && dictionary.errors.username.max30) + +export const emailSchema = (dictionary?: TDictionary) => + z + .string({ + required_error: dictionary && dictionary.errors.email.required, + }) + .email(dictionary && dictionary.errors.email.invalid) From 74c4c3aaf291637021942fd8a610b4744e1b3bbb Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 12:47:17 +0200 Subject: [PATCH 07/11] refactor: privacy acceptance spaces --- .../[lang]/(sys-auth)/privacy-acceptance.tsx | 18 ++++++++++++++++++ src/app/[lang]/(sys-auth)/sign-in/page.tsx | 13 ++----------- src/app/[lang]/(sys-auth)/sign-up/page.tsx | 13 ++----------- 3 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 src/app/[lang]/(sys-auth)/privacy-acceptance.tsx diff --git a/src/app/[lang]/(sys-auth)/privacy-acceptance.tsx b/src/app/[lang]/(sys-auth)/privacy-acceptance.tsx new file mode 100644 index 00000000..d9f96aab --- /dev/null +++ b/src/app/[lang]/(sys-auth)/privacy-acceptance.tsx @@ -0,0 +1,18 @@ +import Link from "next/link" +import { TDictionary } from "@/lib/langs" + +export default function PrivacyAcceptance({ dictionary }: { dictionary: TDictionary }) { + return ( +

+ {dictionary.auth.clickingAggreement}{" "} + + {dictionary.auth.termsOfService} + {" "} + {dictionary.and}{" "} + + {dictionary.auth.privacyPolicy} + + . +

+ ) +} diff --git a/src/app/[lang]/(sys-auth)/sign-in/page.tsx b/src/app/[lang]/(sys-auth)/sign-in/page.tsx index d1134991..9b553d3c 100644 --- a/src/app/[lang]/(sys-auth)/sign-in/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-in/page.tsx @@ -7,6 +7,7 @@ import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" import { Locale } from "i18n-config" +import PrivacyAcceptance from "../privacy-acceptance" export default async function SignInPage({ searchParams, @@ -50,17 +51,7 @@ export default async function SignInPage({
{providers?.github && }
-

- {dictionary.auth.clickingAggreement}{" "} - - {dictionary.auth.termsOfService} - - {dictionary.and} - - {dictionary.auth.privacyPolicy} - - . -

+
diff --git a/src/app/[lang]/(sys-auth)/sign-up/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/page.tsx index 315537f0..bded01b2 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -7,6 +7,7 @@ import { authRoutes } from "@/lib/auth/constants" import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" import { Locale } from "i18n-config" +import PrivacyAcceptance from "../privacy-acceptance" export default async function SignUpPage({ searchParams, @@ -49,17 +50,7 @@ export default async function SignUpPage({ {providers?.github && } -

- {dictionary.auth.clickingAggreement}{" "} - - {dictionary.auth.termsOfService} - - {dictionary.and} - - {dictionary.auth.privacyPolicy} - - . -

+ From f049b9e56bd5e5fa4140a82c8113ab7698533be7 Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 13:31:30 +0200 Subject: [PATCH 08/11] refactor: providers component --- src/app/[lang]/(sys-auth)/providers.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/app/[lang]/(sys-auth)/providers.tsx diff --git a/src/app/[lang]/(sys-auth)/providers.tsx b/src/app/[lang]/(sys-auth)/providers.tsx new file mode 100644 index 00000000..d3bd0f11 --- /dev/null +++ b/src/app/[lang]/(sys-auth)/providers.tsx @@ -0,0 +1,9 @@ +import { getProviders } from "next-auth/react" +import GithubSignIn from "@/components/auth/github-sign-in" +import { TDictionary } from "@/lib/langs" + +export default async function Providers({ dictionary }: { dictionary: TDictionary }) { + const providers = await getProviders() + + return <>{providers?.github && } +} From 5b1a38bdb8d78b94f343aa17bb6d42092873da4f Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 13:31:36 +0200 Subject: [PATCH 09/11] refactor: error translation --- .../profile/see-details-toggle.tsx | 18 +------ src/app/[lang]/(sys-auth)/sign-in/page.tsx | 7 +-- src/app/[lang]/(sys-auth)/sign-up/page.tsx | 6 +-- src/components/auth/github-sign-in.tsx | 17 +++++-- src/components/auth/login-user-auth-form.tsx | 4 +- .../auth/register-user-auth-form.tsx | 4 +- .../profile/sessions/sessions-table.tsx | 31 ++++------- .../profile/sessions/user-active-sessions.tsx | 49 +++++++++--------- src/components/profile/update-account.tsx | 1 + src/contexts/api.store.tsx | 25 ++++++--- src/langs/en.json | 11 ++-- src/langs/fr.json | 11 ++-- src/lib/auth/handle-sign.ts | 51 ++++++++++++------- src/lib/auth/index.ts | 3 +- src/lib/utils.ts | 6 ++- 15 files changed, 127 insertions(+), 117 deletions(-) diff --git a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx index 92e4b011..8fbd965c 100644 --- a/src/app/[lang]/(protected)/profile/see-details-toggle.tsx +++ b/src/app/[lang]/(protected)/profile/see-details-toggle.tsx @@ -12,23 +12,7 @@ export default function SeeDetailsToggle({ dictionary }: { dictionary: TDictiona {dictionary.profilePage.profileDetails.toggle} - + diff --git a/src/app/[lang]/(sys-auth)/sign-in/page.tsx b/src/app/[lang]/(sys-auth)/sign-in/page.tsx index 9b553d3c..1b2dd01e 100644 --- a/src/app/[lang]/(sys-auth)/sign-in/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-in/page.tsx @@ -1,6 +1,4 @@ import Link from "next/link" -import { getProviders } from "next-auth/react" -import GithubSignIn from "@/components/auth/github-sign-in" import { LoginUserAuthForm } from "@/components/auth/login-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" @@ -8,6 +6,7 @@ import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" import { Locale } from "i18n-config" import PrivacyAcceptance from "../privacy-acceptance" +import Providers from "../providers" export default async function SignInPage({ searchParams, @@ -20,8 +19,6 @@ export default async function SignInPage({ }) { const dictionary = await getDictionary(lang) - const providers = await getProviders() - return (
{dictionary.auth.orContinueWith} - {providers?.github && } + diff --git a/src/app/[lang]/(sys-auth)/sign-up/page.tsx b/src/app/[lang]/(sys-auth)/sign-up/page.tsx index bded01b2..7157da62 100644 --- a/src/app/[lang]/(sys-auth)/sign-up/page.tsx +++ b/src/app/[lang]/(sys-auth)/sign-up/page.tsx @@ -1,6 +1,4 @@ import Link from "next/link" -import { getProviders } from "next-auth/react" -import GithubSignIn from "@/components/auth/github-sign-in" import { RegisterUserAuthForm } from "@/components/auth/register-user-auth-form" import { buttonVariants } from "@/components/ui/button" import { authRoutes } from "@/lib/auth/constants" @@ -8,6 +6,7 @@ import { getDictionary } from "@/lib/langs" import { cn } from "@/lib/utils" import { Locale } from "i18n-config" import PrivacyAcceptance from "../privacy-acceptance" +import Providers from "../providers" export default async function SignUpPage({ searchParams, @@ -19,7 +18,6 @@ export default async function SignUpPage({ } }) { const dictionary = await getDictionary(lang) - const providers = await getProviders() return (
@@ -48,7 +46,7 @@ export default async function SignUpPage({ {dictionary.auth.orContinueWith} - {providers?.github && } + diff --git a/src/components/auth/github-sign-in.tsx b/src/components/auth/github-sign-in.tsx index 2bc99f15..313133a5 100644 --- a/src/components/auth/github-sign-in.tsx +++ b/src/components/auth/github-sign-in.tsx @@ -2,12 +2,19 @@ import { ClientSafeProvider, signIn } from "next-auth/react" import { useState } from "react" +import { TDictionary } from "@/lib/langs" import { logger } from "@/lib/logger" import { Icons } from "../icons" import { Button } from "../ui/button" import { toast } from "../ui/use-toast" -export default function GithubSignIn({ provider }: { provider: ClientSafeProvider }) { +export default function GithubSignIn({ + provider, + dictionary, +}: { + provider: ClientSafeProvider + dictionary: TDictionary +}) { const [isLoading, setIsLoading] = useState(false) async function handleSignIn() { @@ -21,21 +28,21 @@ export default function GithubSignIn({ provider }: { provider: ClientSafeProvide if (res?.error) { if (res?.error === "OAuthAccountNotLinked") { } else { - throw new Error("Invalid credentials. Please try again.") + throw new Error(dictionary.errors.unknownError) } } } catch (error) { logger.error(error) if (error instanceof Error) { toast({ - title: "Error", + title: dictionary.error, description: error.message, variant: "destructive", }) } else { toast({ - title: "Error", - description: "An unknown error occurred", + title: dictionary.error, + description: dictionary.errors.unknownError, variant: "destructive", }) } diff --git a/src/components/auth/login-user-auth-form.tsx b/src/components/auth/login-user-auth-form.tsx index 52486954..2453e31f 100644 --- a/src/components/auth/login-user-auth-form.tsx +++ b/src/components/auth/login-user-auth-form.tsx @@ -34,7 +34,7 @@ export function LoginUserAuthForm({ dictionary, searchParams, ...props }: UserAu if (error && (!errorDisplayed || errorDisplayed !== error)) { setErrorDisplayed(error) - handleSignError(error) + handleSignError(error, dictionary) } const form = useForm({ @@ -47,7 +47,7 @@ export function LoginUserAuthForm({ dictionary, searchParams, ...props }: UserAu async function onSubmit(data: IForm) { setIsLoading(true) - const isPushingRoute = await handleSignIn(data, callbackUrl, router) + const isPushingRoute = await handleSignIn({ data, callbackUrl, router, dictionary }) //? If isPushingRoute is true, it means that the user is being redirected to the callbackUrl if (!isPushingRoute) setIsLoading(false) } diff --git a/src/components/auth/register-user-auth-form.tsx b/src/components/auth/register-user-auth-form.tsx index 51aa689e..a1f9f79f 100644 --- a/src/components/auth/register-user-auth-form.tsx +++ b/src/components/auth/register-user-auth-form.tsx @@ -86,7 +86,7 @@ export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, .. if (error && (!errorDisplayed || errorDisplayed !== error)) { setErrorDisplayed(error) - handleSignError(error) + handleSignError(error, dictionary) } async function onSubmitMinimized(data: IFormMinimized) { @@ -100,7 +100,7 @@ export function RegisterUserAuthForm({ dictionary, isMinimized, searchParams, .. async function onSubmit(data: IForm) { setIsLoading(true) - const isPushingRoute = await handleSignUp({ data, form, router, loginOnSignUp: true, apiFetch }) + const isPushingRoute = await handleSignUp({ data, form, router, loginOnSignUp: true, apiFetch, dictionary }) //? If isPushingRoute is true, it means that the user is being redirected to the callbackUrl if (!isPushingRoute) setIsLoading(false) } diff --git a/src/components/profile/sessions/sessions-table.tsx b/src/components/profile/sessions/sessions-table.tsx index 3b8c4371..c2e41225 100644 --- a/src/components/profile/sessions/sessions-table.tsx +++ b/src/components/profile/sessions/sessions-table.tsx @@ -19,29 +19,13 @@ import Pagination from "@/components/ui/pagination" import { toast } from "@/components/ui/use-toast" import { useApiStore } from "@/contexts/api.store" import { IJsonApiResponse, jsonApiQuery } from "@/lib/json-api" +import { TDictionary } from "@/lib/langs" import { formatCouldNotMessage } from "@/lib/utils" import SessionRow from "./session-row" const itemsPerPageInitial = 5 -export default function SessionsTable({ - dictionary, -}: { - dictionary: { - areYouAbsolutelySure: string - deleteLoggedDevice: { - description: string - } - cancel: string - continue: string - session: string - sessions: string - error: string - delete: string - fetch: string - couldNotMessage: string - } -}) { +export default function SessionsTable({ dictionary }: { dictionary: TDictionary }) { const { data: curSession } = useSession() const router = useRouter() const apiFetch = useApiStore((state) => state.apiFetch(router)) @@ -65,13 +49,14 @@ export default function SessionsTable({ sort: ["-lastUsedAt"], })}` ), + dictionary, () => { toast({ title: dictionary.error, description: formatCouldNotMessage({ couldNotMessage: dictionary.couldNotMessage, action: dictionary.fetch, - subject: dictionary.sessions, + subject: dictionary.profilePage.profileDetails.sessions, }), variant: "destructive", }) @@ -99,13 +84,13 @@ export default function SessionsTable({ } }) //? Delete from DB - const res = await apiFetch(fetch(`/api/sessions/${selectedSession}`, { method: "DELETE" }), () => { + const res = await apiFetch(fetch(`/api/sessions/${selectedSession}`, { method: "DELETE" }), dictionary, () => { toast({ title: dictionary.error, description: formatCouldNotMessage({ couldNotMessage: dictionary.couldNotMessage, action: dictionary.delete, - subject: dictionary.session, + subject: dictionary.profilePage.profileDetails.session, }), variant: "destructive", }) @@ -146,7 +131,9 @@ export default function SessionsTable({ {dictionary.areYouAbsolutelySure} - {dictionary.deleteLoggedDevice.description} + + {dictionary.profilePage.profileDetails.deleteLoggedDevice.description} + {dictionary.cancel} diff --git a/src/components/profile/sessions/user-active-sessions.tsx b/src/components/profile/sessions/user-active-sessions.tsx index 152b89b6..086a2a97 100644 --- a/src/components/profile/sessions/user-active-sessions.tsx +++ b/src/components/profile/sessions/user-active-sessions.tsx @@ -1,34 +1,33 @@ +import { TDictionary } from "@/lib/langs" import SessionsTable from "./sessions-table" -export default function UserActiveSessions({ - dictionary, -}: { - dictionary: { - loggedDevices: string - loggedDevicesDescription: string - sessionTable: { - areYouAbsolutelySure: string - deleteLoggedDevice: { - description: string - } - cancel: string - continue: string - session: string - sessions: string - error: string - delete: string - fetch: string - couldNotMessage: string - } - } -}) { +/* +...dictionary.profilePage.profileDetails, + sessionTable: { + areYouAbsolutelySure: dictionary.areYouAbsolutelySure, + cancel: dictionary.cancel, + continue: dictionary.continue, + deleteLoggedDevice: dictionary.profilePage.profileDetails.deleteLoggedDevice, + session: dictionary.profilePage.profileDetails.session, + sessions: dictionary.profilePage.profileDetails.sessions, + error: dictionary.error, + delete: dictionary.delete, + fetch: dictionary.fetch, + couldNotMessage: dictionary.couldNotMessage, + }, + }} +*/ + +export default function UserActiveSessions({ dictionary }: { dictionary: TDictionary }) { return (
-

{dictionary.loggedDevices}

-

{dictionary.loggedDevicesDescription}

+

{dictionary.profilePage.profileDetails.loggedDevices}

+

+ {dictionary.profilePage.profileDetails.loggedDevicesDescription} +

- +
) } diff --git a/src/components/profile/update-account.tsx b/src/components/profile/update-account.tsx index 19992a38..790a76f6 100644 --- a/src/components/profile/update-account.tsx +++ b/src/components/profile/update-account.tsx @@ -58,6 +58,7 @@ export default function UpdateAccount({ dictionary }: { dictionary: TDictionary method: "PATCH", body: JSON.stringify(data), }), + dictionary, (err) => { toast({ title: dictionary.error, diff --git a/src/contexts/api.store.tsx b/src/contexts/api.store.tsx index b854cb74..cc3eae4b 100644 --- a/src/contexts/api.store.tsx +++ b/src/contexts/api.store.tsx @@ -1,5 +1,6 @@ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context" import { create } from "zustand" +import { TDictionary } from "@/lib/langs" import { logger } from "@/lib/logger" import { handleFetch } from "@/lib/utils" @@ -13,11 +14,15 @@ export type IFullResponseOptions = { export interface IApiState { apiFetch: ( router: AppRouterInstance - ) => (fetchRequest: Promise, responseOptions?: IOnError | IFullResponseOptions) => Promise + ) => ( + fetchRequest: Promise, + dictionary: TDictionary, + responseOptions?: IOnError | IFullResponseOptions + ) => Promise } export const useApiStore = create(() => ({ - apiFetch: (router) => async (fetchRequest, responseOptions) => { + apiFetch: (router) => async (fetchRequest, dictionary, responseOptions) => { let onError: IOnError if (typeof responseOptions === "function") { onError = responseOptions @@ -39,12 +44,16 @@ export const useApiStore = create(() => ({ redirectOnUnauthorized = true } - const res = await handleFetch(fetchRequest, { - onError, - onResponse, - redirectOnUnauthorized, - router, - }) + const res = await handleFetch( + fetchRequest, + { + onError, + onResponse, + redirectOnUnauthorized, + router, + }, + dictionary + ) return res }, })) diff --git a/src/langs/en.json b/src/langs/en.json index 10219b57..065640b9 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -59,7 +59,8 @@ "username": { "required": "Username is required.", "min3": "Username must be at least 3 characters long.", - "max30": "Username must be at most 30 characters long." + "max30": "Username must be at most 30 characters long.", + "exist": "Username already exists." }, "password": { "regex": "Password must contain at least one uppercase letter, one lowercase letter, and one number", @@ -70,7 +71,11 @@ }, "email": { "required": "Email is required.", - "invalid": "Email is invalid." - } + "invalid": "Email is invalid.", + "exist": "Email already exists." + }, + "unknownError": "An unknown error has occurred. Please try again later.", + "wrongProvider": "You already have an account with a different provider. Please sign in with it.", + "invalidCredentials": "Invalid credentials. Please try again." } } diff --git a/src/langs/fr.json b/src/langs/fr.json index 4ff84f8e..b6ff6a1e 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -59,7 +59,8 @@ "username": { "required": "Nom d'utilisateur requis", "min3": "Le nom d'utilisateur doit être au moins 3 caractères long.", - "max30": "Le nom d'utilisateur doit être au maximum 30 caractères long." + "max30": "Le nom d'utilisateur doit être au maximum 30 caractères long.", + "exist": "Le nom d'utilisateur existe déjà." }, "password": { "regex": "Le mot de passe doit contenir au moins une lettre majuscule, une lettre minuscule et un chiffre.", @@ -70,7 +71,11 @@ }, "email": { "required": "Email requis", - "invalid": "Email invalide" - } + "invalid": "Email invalide", + "exist": "L'email existe déjà." + }, + "unknownError": "Une erreur inconnue s'est produite. Veuillez ressayer plus tard.", + "wrongProvider": "Vous avez déjà un compte avec un autre provider. Veuillez vous connecter avec celui-ci.", + "invalidCredentials": "Les informations d'identification sont invalides." } } diff --git a/src/lib/auth/handle-sign.ts b/src/lib/auth/handle-sign.ts index a1ac1260..9c88afae 100644 --- a/src/lib/auth/handle-sign.ts +++ b/src/lib/auth/handle-sign.ts @@ -8,29 +8,36 @@ import { formSchema as registerFormSchema } from "@/components/auth/register-use import { toast } from "@/components/ui/use-toast" import { IApiState } from "@/contexts/api.store" import { signInSchema, signUpSchema } from "@/types/auth" +import { TDictionary } from "../langs" import { logger } from "../logger" -export const handleSignError = (error: string) => { +export const handleSignError = (error: string, dictionary: TDictionary) => { if (error == "OAuthAccountNotLinked") { toast({ - title: "Error", - description: "You already have an account. Please sign in with your provider.", + title: dictionary.error, + description: dictionary.errors.wrongProvider, variant: "destructive", }) } else { toast({ - title: "Error", + title: dictionary.error, description: error, variant: "destructive", }) } } -export const handleSignIn = async ( - data: z.infer>, - callbackUrl: string, +export const handleSignIn = async ({ + data, + callbackUrl, + router, + dictionary, +}: { + data: z.infer> + callbackUrl: string router: AppRouterInstance -) => { + dictionary: TDictionary +}) => { return new Promise(async (resolve) => { logger.debug("Signing in with credentials", data) try { @@ -49,22 +56,22 @@ export const handleSignIn = async ( } else { console.error(res.error) if (typeof res.error === "string") { - if (res.error === "You signed up with a provider, please sign in with it") throw new Error(res.error) + if (res.error === dictionary.errors.wrongProvider) throw new Error(res.error) } - throw new Error("Invalid credentials. Please try again.") + throw new Error(dictionary.errors.invalidCredentials) } } catch (error) { logger.error(error) if (error instanceof Error) { toast({ - title: "Error", + title: dictionary.error, description: error.message, variant: "destructive", }) } else { toast({ - title: "Error", - description: "An unknown error occurred", + title: dictionary.error, + description: dictionary.errors.unknownError, variant: "destructive", }) } @@ -79,13 +86,15 @@ export const handleSignUp = async ({ form, router, apiFetch, + dictionary, loginOnSignUp = true, }: { data: z.infer> form: UseFormReturn>> router: AppRouterInstance - loginOnSignUp: boolean apiFetch: ReturnType + dictionary: TDictionary + loginOnSignUp: boolean }) => { return new Promise(async (resolve) => { logger.debug("Signing up with credentials", data) @@ -95,21 +104,22 @@ export const handleSignUp = async ({ headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }), + dictionary, { onError: (error) => { if (error === "Email already exists") { return form.setError("email", { type: "manual", - message: "Email already exists", + message: dictionary.errors.email.exist, }) } else if (error === "Username already exists") { return form.setError("username", { type: "manual", - message: "Username already exists", + message: dictionary.errors.username.exist, }) } toast({ - title: "Error", + title: dictionary.error, description: error, variant: "destructive", }) @@ -120,7 +130,12 @@ export const handleSignUp = async ({ logger.debug("Sign up successful") if (loginOnSignUp) { logger.debug("Logging in after sign up") - return handleSignIn({ email: data.email, password: data.password }, "/profile", router) + return handleSignIn({ + data: { email: data.email, password: data.password }, + callbackUrl: "/profile", + router, + dictionary, + }) } else { logger.debug("Pushing to profile page") router.push("/profile") diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 9e093c8d..529f66ff 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -43,6 +43,7 @@ export const nextAuthOptions: NextAuthOptions & { nextAuthOptions.loadedDictionary.set(lang, dictionary) return dictionary })()) + const creds = await signInSchema(dictionary).parseAsync(credentials) if (!creds.email || !creds.password) { @@ -61,7 +62,7 @@ export const nextAuthOptions: NextAuthOptions & { if (!user.password) { //? this should happen if the user signed up with a provider - throw new Error("You signed up with a provider, please sign in with it") + throw new Error(dictionary.errors.wrongProvider) } const isValidPassword = await bcryptCompare(creds.password, user.password) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1f4c3c3f..a05664b4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,6 +3,7 @@ import { AppRouterInstance } from "next/dist/shared/lib/app-router-context" import { NextResponse } from "next/server" import { twMerge } from "tailwind-merge" import { IApiError } from "@/types/api" +import { TDictionary } from "./langs" import { logger } from "./logger" export function cn(...inputs: ClassValue[]) { @@ -20,7 +21,8 @@ export async function handleFetch( onResponse?: (response: Response) => void router: AppRouterInstance redirectOnUnauthorized: boolean - } + }, + dictionary: TDictionary ): Promise { try { const response = await fetch @@ -55,7 +57,7 @@ export async function handleFetch( if (error instanceof Error) { responseOptions.onError(error.message) } else { - responseOptions.onError("An unknown error occurred") + responseOptions.onError(dictionary.errors.unknownError) } } } From 0dc2e5dafe44b8752b5baa2d129c8a6b8c55db73 Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 13:38:22 +0200 Subject: [PATCH 10/11] fix: middleware redirection with params --- src/middleware.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index e16f6a14..06dd8b8d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,6 +3,7 @@ import Negotiator from "negotiator" import { NextResponse } from "next/server" import type { NextRequest } from "next/server" +import { logger } from "./lib/logger" import { i18n } from "../i18n-config" function getLocale(request: NextRequest): string | undefined { @@ -44,7 +45,10 @@ export function middleware(request: NextRequest) { // e.g. incoming request is /products // The new URL is now /en-US/products - return NextResponse.redirect(new URL(`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}`, request.url)) + const params = new URLSearchParams(request.nextUrl.search) + const redirectUrl = new URL(`/${locale}${pathname.startsWith("/") ? "" : "/"}${pathname}?${params}`, request.url) + logger.debug("Redirecting to locale", { from: request.url, to: redirectUrl.href }) + return NextResponse.redirect(redirectUrl) } } From 7f761de49b7d14a5280728ee3006bead94aae5ee Mon Sep 17 00:00:00 2001 From: rharkor Date: Fri, 11 Aug 2023 13:58:48 +0200 Subject: [PATCH 11/11] refactor: renovate bot base branch --- renovate.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 4d9c8353..f97b2145 100644 --- a/renovate.json +++ b/renovate.json @@ -4,8 +4,9 @@ "dependencyDashboard": true, "packageRules": [ { - "updateTypes": ["minor", "patch"], + "matchDepTypes": ["devDependencies"], "automerge": true } - ] + ], + "baseBranches": ["dev"] }