├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── [locale] │ └── (root) │ │ ├── (auth) │ │ ├── _components │ │ │ ├── error-card.tsx │ │ │ ├── new-password-form.tsx │ │ │ ├── new-verification-form.tsx │ │ │ ├── reset-form.tsx │ │ │ ├── signin-form.tsx │ │ │ └── signup-form.tsx │ │ ├── error │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── new-password │ │ │ └── page.tsx │ │ ├── new-verification │ │ │ └── page.tsx │ │ ├── reset │ │ │ └── page.tsx │ │ ├── signin │ │ │ └── page.tsx │ │ └── signup │ │ │ └── page.tsx │ │ ├── (protected) │ │ ├── _components │ │ │ └── settings-form.tsx │ │ ├── layout.tsx │ │ └── settings │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx └── api │ ├── auth │ └── [...nextauth] │ │ └── route.ts │ ├── test │ └── email-preview │ │ └── route.ts │ ├── twofac │ ├── [id] │ │ └── route.ts │ └── user │ │ └── [userId] │ │ └── route.ts │ └── user │ ├── [id] │ └── route.ts │ ├── oauth │ └── route.ts │ └── route.ts ├── auth.config.ts ├── auth.ts ├── components.json ├── components ├── shared │ ├── button │ │ ├── back-button.tsx │ │ ├── locale-select.tsx │ │ ├── locale-switcher.tsx │ │ ├── mode-toggle.tsx │ │ ├── signin-button.tsx │ │ ├── signout-button.tsx │ │ ├── social-button.tsx │ │ └── user-button.tsx │ ├── footer.tsx │ ├── form │ │ ├── form-error.tsx │ │ ├── form-header.tsx │ │ ├── form-success.tsx │ │ └── form-wrapper.tsx │ ├── main-nav.tsx │ ├── navbar.tsx │ └── spinner.tsx └── ui │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── switch.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── use-toast.ts ├── constants ├── auth-error.ts └── nav-links.ts ├── hooks └── use-session.ts ├── i18n ├── request.ts └── routing.ts ├── lib ├── actions │ └── auth │ │ ├── new-password.ts │ │ ├── new-verification.ts │ │ ├── reset-password.ts │ │ ├── settings.ts │ │ ├── signIn.ts │ │ ├── signin-with-credentials.ts │ │ ├── signout.ts │ │ └── signup-with-credentials.ts ├── api-client │ ├── twofac.ts │ └── user.ts ├── database │ ├── db.ts │ ├── models │ │ └── auth.model.ts │ ├── services │ │ ├── create-one.ts │ │ ├── index.ts │ │ └── update-model-field.ts │ └── types.ts ├── fetcher.ts ├── mail │ ├── nodemailer.ts │ └── resend.ts ├── session.ts ├── token.ts ├── utils.ts └── validations │ └── auth.ts ├── messages ├── en.json └── zh.json ├── middleware.ts ├── middleware ├── chain.ts ├── internal-auth.ts ├── with-Intl-middleware.ts └── with-auth-middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── providers └── theme-provider.tsx ├── public ├── next.svg └── vercel.svg ├── routes.ts ├── tailwind.config.ts ├── tsconfig.json └── types └── next-auth.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Next.js 14 Fullstack NextAuth.js & i18n Example (with MongoDB + TypeScript) 2 | A full-stack authentication demo built with Next.js 14, NextAuth.js v5, MongoDB (via Mongoose), and TypeScript. 3 | 4 | Supports both Google OAuth and email/password login, with internationalization (i18n) and comprehensive authentication flows. 5 | 6 | ## ✨ Features 7 | - Google OAuth Login – Authenticate users via their Google accounts. 8 | 9 | - Email & Password Authentication: Sign-in with email and password. 10 | 11 | - Email Verification: Ensure account validity by requiring users to confirm their email address. 12 | 13 | - Forgot Password Flow: Allow users to reset their password through a email link. 14 | 15 | - Two-Factor Authentication (2FA): Login requires a digit verification code sent via email. 16 | 17 | - User Account Settings: Let users update their username, change passwords, and enable or disable 2FA. 18 | 19 | - Internationalization (i18n) with Auth Integration: Built-in locale routing with authentication middleware support. 20 | 21 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 22 | 23 | ## 🚀 Getting Started 24 | 25 | Run the development server: 26 | ```bash 27 | npm run dev 28 | # or 29 | yarn dev 30 | # or 31 | pnpm dev 32 | # or 33 | bun dev 34 | ``` 35 | 36 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 37 | 38 | ## ⚙️ Environment Variables 39 | Create a .env file in the project root and include the following: 40 | 41 | ```env 42 | NEXT_PUBLIC_APP_URL="http://localhost:3000" 43 | 44 | AUTH_SECRET="YOUR_AUTH_SECRET" 45 | INTERNAL_API_KEY="YOUR_INTERNAL_API_KEY" 46 | INTERNAL_API_SECRET="YOUR_INTERNAL_API_SECRET" 47 | NEXT_PUBLIC_INTERNAL_API_SECRET="YOUR_NEXT_PUBLIC_INTERNAL_API_SECRET" 48 | 49 | MONGODB_URI="YOUR_MONGODB_URI" 50 | 51 | GOOGLE_CLIENT_ID="YOUR_GOOGLE_CLIENT_ID" 52 | GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_CLIENT_SECRET" 53 | 54 | TOKEN_SECRET="YOUR_TOKEN_SECRET" 55 | 56 | # Resend 57 | RESEND_API_KEY="YOUR_RESEND_API_KEY" 58 | RESEND_EMAIL_URL="YOUR_RESEND_EMAIL_URL" 59 | 60 | # OR 61 | 62 | # Nodemailer 63 | EMAIL_USER="YOUR_EMAIL_USER" 64 | EMAIL_PASSWORD="YOUR_EMAIL_PASSWORD" 65 | ``` 66 | 67 | ## 🔐 Getting Google OAuth Credentials 68 | 69 | How to obtain GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET 70 | 71 | - Go to [Google Cloud Console](https://console.cloud.google.com/) . 72 | 73 | - Create a new project. 74 | 75 | - Navigate to APIs & Services → Credentials. 76 | 77 | - Click Create Credentials → OAuth client ID. 78 | 79 | - Choose Web application. 80 | 81 | - Under Authorized JavaScript origins, add: http://localhost:3000 . 82 | 83 | - Under Authorized redirect URIs, add: http://localhost:3000/api/auth/callback/google. 84 | 85 | - Configure your OAuth consent screen and publish the app. 86 | 87 | ## 📚 Learn More 88 | 89 | Explore these official resources to learn more: 90 | 91 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 92 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 93 | - [NextAuth.js Documentation](https://next-auth.js.org) - Learn how to implement secure authentication with OAuth, credentials, JWT, and more. 94 | - [next-intl Documentation](https://next-intl.dev) - A internationalization solution for Next.js. 95 | 96 | ## ☁️ Deploy to Vercel 97 | 98 | 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. 99 | 100 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 101 | -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/error-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useSearchParams } from "next/navigation" 4 | import { useTranslations } from "next-intl" 5 | import { AuthErrorCode } from "@/constants/auth-error" 6 | 7 | import { FiAlertTriangle } from "react-icons/fi" 8 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 9 | 10 | export const ErrorCard = () => { 11 | const searchParams = useSearchParams() 12 | const errorCode = searchParams.get("error") as AuthErrorCode 13 | const tUi = useTranslations("ErrorCard.ui") 14 | 15 | const errorHeaders: Record = { 16 | [AuthErrorCode.ACCESS_DENIED]: tUi("headerAccessDenied"), 17 | [AuthErrorCode.PROVIDER_MISMATCH]: tUi("headerProviderMismatch"), 18 | [AuthErrorCode.EMAIL_UNVERIFIED]: tUi("headerEmailUnverified"), 19 | [AuthErrorCode.UNKNOWN]: tUi("headerUnknownError"), 20 | } 21 | 22 | const errorMessages: Record = { 23 | [AuthErrorCode.ACCESS_DENIED]: tUi("accessDenied"), 24 | [AuthErrorCode.PROVIDER_MISMATCH]: tUi("providerMismatch"), 25 | [AuthErrorCode.EMAIL_UNVERIFIED]: tUi("emailUnverified"), 26 | [AuthErrorCode.UNKNOWN]: tUi("unknownError"), 27 | } 28 | 29 | const headerLabel = errorHeaders[errorCode] ?? errorHeaders[AuthErrorCode.UNKNOWN] 30 | const errorMessage = errorMessages[errorCode] ?? errorMessages[AuthErrorCode.UNKNOWN] 31 | 32 | return ( 33 | 38 |
39 | 40 |

{errorMessage}

41 |
42 |
43 | ) 44 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/new-password-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useTransition } from "react" 4 | import { useForm } from "react-hook-form" 5 | import { useTranslations } from "next-intl" 6 | import { useSearchParams } from "next/navigation" 7 | import { zodResolver } from "@hookform/resolvers/zod" 8 | import { 9 | NewPasswordFormValues, 10 | getNewPasswordFormSchema 11 | } from "@/lib/validations/auth" 12 | import { newPassword } from "@/lib/actions/auth/new-password" 13 | 14 | import { Button } from "@/components/ui/button" 15 | import { 16 | Form, 17 | FormControl, 18 | FormField, 19 | FormItem, 20 | FormLabel, 21 | FormMessage, 22 | } from "@/components/ui/form" 23 | import { Input } from "@/components/ui/input" 24 | import { FormError } from "@/components/shared/form/form-error" 25 | import { FormSuccess } from "@/components/shared/form/form-success" 26 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 27 | 28 | export const NewPasswordForm = () => { 29 | const searchParams = useSearchParams() 30 | const token = searchParams.get("token") 31 | 32 | const [error, setError] = useState("") 33 | const [success, setSuccess] = useState("") 34 | const [isPending, startTransition] = useTransition() 35 | 36 | const tUi = useTranslations("NewPasswordForm.ui") 37 | const tValidation = useTranslations("NewPasswordForm.validation") 38 | const tError = useTranslations("Common.error") 39 | 40 | const form = useForm({ 41 | resolver: zodResolver(getNewPasswordFormSchema(tValidation)), 42 | defaultValues: { 43 | newPassword: "", 44 | confirmPassword: "" 45 | } 46 | }) 47 | 48 | async function onSubmit(values: NewPasswordFormValues) { 49 | // console.log(values) 50 | setError("") 51 | setSuccess("") 52 | 53 | startTransition(() => { 54 | newPassword(values, token) 55 | .then((data) => { 56 | if (data?.error) { 57 | setError(data.error) 58 | } else if (data?.success) { 59 | setSuccess(data.success) 60 | } 61 | }) 62 | .catch(() => setError(tError("generic"))) 63 | }) 64 | } 65 | 66 | return ( 67 | 72 |
73 | 74 |
75 | ( 79 | 80 | {tUi("newPassword")} 81 | 82 | 89 | 90 | 91 | 92 | )} 93 | /> 94 | ( 98 | 99 | {tUi("confirmPassword")} 100 | 101 | 108 | 109 | 110 | 111 | )} 112 | /> 113 |
114 | 115 | 116 | 124 | 125 | 126 |
127 | ) 128 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/new-verification-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useCallback, useEffect, useState } from "react" 4 | import { useSearchParams } from "next/navigation" 5 | import { useTranslations } from "next-intl" 6 | import { newVerification } from "@/lib/actions/auth/new-verification" 7 | 8 | import { FormError } from "@/components/shared/form/form-error" 9 | import { FormSuccess } from "@/components/shared/form/form-success" 10 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 11 | import { Spinner } from "@/components/shared/spinner" 12 | 13 | export const NewVerificationForm = () => { 14 | const searchParams = useSearchParams() 15 | const [error, setError] = useState("") 16 | const [success, setSuccess] = useState("") 17 | 18 | const tUi = useTranslations("NewVerificationForm.ui") 19 | const tError = useTranslations("Common.error") 20 | 21 | const token = searchParams.get("token") 22 | 23 | const onSubmit = useCallback(() => { 24 | // console.log(token) 25 | if (success || error) return 26 | 27 | if (!token) { 28 | setError(tError("missingToken")) 29 | return 30 | } 31 | 32 | newVerification(token) 33 | .then((data) => { 34 | if (data?.error) { 35 | setError(data.error) 36 | } else if (data?.success) { 37 | setSuccess(data.success) 38 | } 39 | }) 40 | .catch(() => setError(tError("generic"))) 41 | }, [token, success, error, tError]) 42 | 43 | useEffect(() => { 44 | onSubmit() 45 | }, [onSubmit]) 46 | 47 | return ( 48 | 53 |
54 | {!success && !error && ( 55 | 56 | )} 57 | 58 | {!success && ( 59 | 60 | )} 61 |
62 |
63 | ) 64 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/reset-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useTransition } from "react" 4 | import { useForm } from "react-hook-form" 5 | import { useTranslations } from "next-intl" 6 | import { zodResolver } from "@hookform/resolvers/zod" 7 | import { 8 | ResetPasswordFormValues, 9 | getResetPasswordFormSchema 10 | } from "@/lib/validations/auth" 11 | import { resetPassword } from "@/lib/actions/auth/reset-password" 12 | 13 | import { Button } from "@/components/ui/button" 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@/components/ui/form" 22 | import { Input } from "@/components/ui/input" 23 | import { FormError } from "@/components/shared/form/form-error" 24 | import { FormSuccess } from "@/components/shared/form/form-success" 25 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 26 | 27 | export const ResetForm = () => { 28 | const [error, setError] = useState("") 29 | const [success, setSuccess] = useState("") 30 | const [isPending, startTransition] = useTransition() 31 | 32 | const tUi = useTranslations("ResetForm.ui") 33 | const tValidation = useTranslations("ResetForm.validation") 34 | const tError = useTranslations("Common.error") 35 | 36 | const form = useForm({ 37 | resolver: zodResolver(getResetPasswordFormSchema(tValidation)), 38 | defaultValues: { 39 | email: "" 40 | } 41 | }) 42 | 43 | async function onSubmit(values: ResetPasswordFormValues) { 44 | // console.log(values) 45 | setError("") 46 | setSuccess("") 47 | 48 | startTransition(() => { 49 | resetPassword(values) 50 | .then((data) => { 51 | if (data?.error) { 52 | setError(data.error) 53 | } else if (data?.success) { 54 | setSuccess(data.success) 55 | } 56 | }) 57 | .catch(() => setError(tError("generic"))) 58 | }) 59 | } 60 | 61 | return ( 62 | 67 |
68 | 69 |
70 | ( 74 | 75 | {tUi("email")} 76 | 77 | 83 | 84 | 85 | 86 | )} 87 | /> 88 |
89 | 90 | 91 | 99 | 100 | 101 |
102 | ) 103 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/signin-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useTransition } from "react" 4 | import { useSearchParams } from "next/navigation" 5 | import { useForm } from "react-hook-form" 6 | import { useTranslations } from "next-intl" 7 | import { Link } from "@/i18n/routing" 8 | import { zodResolver } from "@hookform/resolvers/zod" 9 | import { 10 | SignInFormValues, 11 | getSignInFormSchema 12 | } from "@/lib/validations/auth" 13 | import { signInWithCredentials } from "@/lib/actions/auth/signin-with-credentials" 14 | 15 | import { Button } from "@/components/ui/button" 16 | import { 17 | Form, 18 | FormControl, 19 | FormField, 20 | FormItem, 21 | FormLabel, 22 | FormMessage, 23 | } from "@/components/ui/form" 24 | import { Input } from "@/components/ui/input" 25 | import { FormError } from "@/components/shared/form/form-error" 26 | import { FormSuccess } from "@/components/shared/form/form-success" 27 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 28 | 29 | export const SignInForm = () => { 30 | const searchParams = useSearchParams() 31 | const callbackUrl = searchParams.get("callbackUrl") 32 | const [error, setError] = useState("") 33 | const [success, setSuccess] = useState("") 34 | const [showTwoFactor, setShowTwoFactor] = useState(false) 35 | const [isPending, startTransition] = useTransition() 36 | 37 | const tUi = useTranslations("SignInForm.ui") 38 | const tValidation = useTranslations("SignInForm.validation") 39 | const tError = useTranslations("Common.error") 40 | 41 | const form = useForm({ 42 | resolver: zodResolver(getSignInFormSchema(tValidation)), 43 | defaultValues: { 44 | email: "", 45 | password: "", 46 | code: "", 47 | } 48 | }) 49 | 50 | async function onSubmit(values: SignInFormValues) { 51 | // console.log(values) 52 | setError("") 53 | setSuccess("") 54 | 55 | startTransition(() => { 56 | signInWithCredentials(values, callbackUrl) 57 | .then((data) => { 58 | if (data?.error) { 59 | setError(data.error) 60 | } else if (data?.success) { 61 | setSuccess(data.success) 62 | } else if (data?.url) { 63 | window.location.assign(data?.url) 64 | } 65 | 66 | if (data?.twoFactor) { 67 | setShowTwoFactor(true) 68 | } 69 | }) 70 | .catch(() => setError(tError("generic"))) 71 | }) 72 | } 73 | 74 | return ( 75 | 81 |
82 | 83 |
84 | {showTwoFactor && ( 85 | ( 89 | 90 | {tUi("code")} 91 | 92 | 98 | 99 | 100 | 101 | )} 102 | /> 103 | )} 104 | {!showTwoFactor && ( 105 | <> 106 | ( 110 | 111 | {tUi("email")} 112 | 113 | 119 | 120 | 121 | 122 | )} 123 | /> 124 | ( 128 | 129 | {tUi("password")} 130 | 131 | 138 | 139 | 149 | 150 | 151 | )} 152 | /> 153 | 154 | )} 155 |
156 | 157 | 158 | 166 | 167 | 168 |
169 | ) 170 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/_components/signup-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useTransition } from "react" 4 | import { useForm } from "react-hook-form" 5 | import { useTranslations } from "next-intl" 6 | import { zodResolver } from "@hookform/resolvers/zod" 7 | import { 8 | SignUpFormValues, 9 | getSignUpFormSchema 10 | } from "@/lib/validations/auth" 11 | import { signUpWithCredentials } from "@/lib/actions/auth/signup-with-credentials" 12 | 13 | import { Button } from "@/components/ui/button" 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@/components/ui/form" 22 | import { Input } from "@/components/ui/input" 23 | import { FormError } from "@/components/shared/form/form-error" 24 | import { FormSuccess } from "@/components/shared/form/form-success" 25 | import { FormWrapper } from "@/components/shared/form/form-wrapper" 26 | 27 | export const SignUpForm = () => { 28 | const [error, setError] = useState("") 29 | const [success, setSuccess] = useState("") 30 | const [isPending, startTransition] = useTransition() 31 | 32 | const tUi = useTranslations("SignUpForm.ui") 33 | const tValidation = useTranslations("SignUpForm.validation") 34 | const tError = useTranslations("Common.error") 35 | 36 | const form = useForm({ 37 | resolver: zodResolver(getSignUpFormSchema(tValidation)), 38 | defaultValues: { 39 | name: "", 40 | email: "", 41 | password: "", 42 | confirmPassword: "", 43 | } 44 | }) 45 | 46 | async function onSubmit(values: SignUpFormValues) { 47 | // console.log(values) 48 | setError("") 49 | setSuccess("") 50 | 51 | startTransition(() => { 52 | signUpWithCredentials(values) 53 | .then((data) => { 54 | if (data?.error) { 55 | setError(data.error) 56 | } else if (data?.success) { 57 | setSuccess(data.success) 58 | } 59 | }) 60 | .catch(() => setError(tError("generic"))) 61 | }) 62 | } 63 | 64 | return ( 65 | 71 |
72 | 73 |
74 | ( 78 | 79 | {tUi("name")} 80 | 81 | 87 | 88 | 89 | 90 | )} 91 | /> 92 | ( 96 | 97 | {tUi("email")} 98 | 99 | 105 | 106 | 107 | 108 | )} 109 | /> 110 | ( 114 | 115 | {tUi("password")} 116 | 117 | 124 | 125 | 126 | 127 | )} 128 | /> 129 | ( 133 | 134 | {tUi("confirmPassword")} 135 | 136 | 143 | 144 | 145 | 146 | )} 147 | /> 148 |
149 | 150 | 151 | 159 | 160 | 161 |
162 | ) 163 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/error/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ErrorCard } from "../_components/error-card" 4 | 5 | const AuthErrorPage = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default AuthErrorPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default AuthLayout -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/new-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewPasswordForm } from "../_components/new-password-form" 2 | 3 | const NewPasswordPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default NewPasswordPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewVerificationForm } from "../_components/new-verification-form" 2 | 3 | const NewVerificationPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default NewVerificationPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetForm } from "../_components/reset-form" 2 | 3 | const ResetPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default ResetPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignInForm } from "../_components/signin-form" 2 | 3 | const SignInPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default SignInPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from "../_components/signup-form" 2 | 3 | const SignUpPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default SignUpPage -------------------------------------------------------------------------------- /app/[locale]/(root)/(protected)/_components/settings-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useTransition } from "react" 4 | import { useForm } from "react-hook-form" 5 | import { useTranslations } from "next-intl" 6 | import { zodResolver } from "@hookform/resolvers/zod" 7 | import { useSession } from "next-auth/react" 8 | import { UserRole, UserProvider } from "@/lib/database/types" 9 | import { 10 | SettingsFormValues, 11 | getSettingsFormSchema 12 | } from "@/lib/validations/auth" 13 | import { settings } from "@/lib/actions/auth/settings" 14 | 15 | import { 16 | Card, 17 | CardContent, 18 | CardHeader 19 | } from "@/components/ui/card" 20 | import { 21 | Form, 22 | FormControl, 23 | FormField, 24 | FormItem, 25 | FormLabel, 26 | FormMessage, 27 | FormDescription, 28 | } from "@/components/ui/form" 29 | import { Input } from "@/components/ui/input" 30 | import { Button } from "@/components/ui/button" 31 | import { Switch } from "@/components/ui/switch" 32 | import { FormError } from "@/components/shared/form/form-error" 33 | import { FormSuccess } from "@/components/shared/form/form-success" 34 | import { Skeleton } from "@/components/ui/skeleton" 35 | 36 | export const SettingsForm = () => { 37 | const { data: session, status, update } = useSession({ required: true }) 38 | const user = session?.user 39 | // console.log({user}) 40 | 41 | const [error, setError] = useState("") 42 | const [success, setSuccess] = useState("") 43 | const [isPending, startTransition] = useTransition() 44 | 45 | const tUi = useTranslations("SettingsForm.ui") 46 | const tValidation = useTranslations("SettingsForm.validation") 47 | const tError = useTranslations("Common.error") 48 | 49 | const form = useForm({ 50 | resolver: zodResolver(getSettingsFormSchema(tValidation)), 51 | defaultValues: { 52 | name: user?.name || "", 53 | email: user?.email || "", 54 | password: "", 55 | newPassword: "", 56 | role: user?.role || UserRole.USER, 57 | isTwoFactorEnabled: user?.isTwoFactorEnabled || false 58 | } 59 | }) 60 | 61 | async function onSubmit(values: SettingsFormValues) { 62 | // console.log(values) 63 | setError("") 64 | setSuccess("") 65 | 66 | startTransition(() => { 67 | settings(values) 68 | .then((data) => { 69 | if (data?.error) { 70 | setError(data.error) 71 | } else if (data?.success) { 72 | update() 73 | setSuccess(data.success) 74 | } 75 | }) 76 | .catch(() => setError(tError("generic"))) 77 | }) 78 | } 79 | 80 | if (status === "loading") { 81 | return 82 | } 83 | 84 | return ( 85 | 86 | 87 |

88 | {tUi("header")} 89 |

90 |
91 | 92 |
93 | 94 |
95 | ( 99 | 100 | {tUi("name")} 101 | 102 | 108 | 109 | 110 | 111 | )} 112 | /> 113 | ( 117 | 118 | {tUi("email")} 119 | 120 | 126 | 127 | 128 | 129 | )} 130 | /> 131 | {user?.provider === UserProvider.CREDENTIALS && ( 132 | <> 133 | ( 137 | 138 | {tUi("password")} 139 | 140 | 147 | 148 | 149 | 150 | )} 151 | /> 152 | ( 156 | 157 | {tUi("newPassword")} 158 | 159 | 166 | 167 | 168 | 169 | )} 170 | /> 171 | 172 | )} 173 | {user?.provider === UserProvider.CREDENTIALS && ( 174 | ( 178 | 179 |
180 | {tUi("isTwoFactorEnabled")} 181 | 182 | {tUi("isTwoFactorEnabledDescription")} 183 | 184 |
185 | 186 | 191 | 192 |
193 | )} 194 | /> 195 | )} 196 |
197 | 198 | 199 | 207 | 208 | 209 |
210 |
211 | ) 212 | } 213 | 214 | SettingsForm.Skeleton = function SkeletonSettingsForm() { 215 | return ( 216 | 217 | 218 | 219 | 220 | 221 |
222 | 223 | 224 | 225 | 226 |
227 |
228 |
229 | ) 230 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | const ProtectedLayout = ({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default ProtectedLayout -------------------------------------------------------------------------------- /app/[locale]/(root)/(protected)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsForm } from "../_components/settings-form" 2 | 3 | const SettingsPage = async() => { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | 11 | export default SettingsPage 12 | -------------------------------------------------------------------------------- /app/[locale]/(root)/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wei30172/nextauth-v5-mongodb-typescript-example/9a5ae3232058d6d22b455e2f3bc26021bf422eb2/app/[locale]/(root)/favicon.ico -------------------------------------------------------------------------------- /app/[locale]/(root)/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | :root { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --muted: 210 40% 96.1%; 29 | --muted-foreground: 215.4 16.3% 46.9%; 30 | 31 | --accent: 210 40% 96.1%; 32 | --accent-foreground: 222.2 47.4% 11.2%; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 210 40% 98%; 36 | 37 | --border: 214.3 31.8% 91.4%; 38 | --input: 214.3 31.8% 91.4%; 39 | --ring: 222.2 84% 4.9%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 222.2 84% 4.9%; 46 | --foreground: 210 40% 98%; 47 | 48 | --card: 222.2 84% 4.9%; 49 | --card-foreground: 210 40% 98%; 50 | 51 | --popover: 222.2 84% 4.9%; 52 | --popover-foreground: 210 40% 98%; 53 | 54 | --primary: 210 40% 98%; 55 | --primary-foreground: 222.2 47.4% 11.2%; 56 | 57 | --secondary: 217.2 32.6% 17.5%; 58 | --secondary-foreground: 210 40% 98%; 59 | 60 | --muted: 217.2 32.6% 17.5%; 61 | --muted-foreground: 215 20.2% 65.1%; 62 | 63 | --accent: 217.2 32.6% 17.5%; 64 | --accent-foreground: 210 40% 98%; 65 | 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --border: 217.2 32.6% 17.5%; 70 | --input: 217.2 32.6% 17.5%; 71 | --ring: 212.7 26.8% 83.9%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-background text-foreground; 81 | } 82 | } 83 | 84 | /* ====== ANIMATION ====== */ 85 | .laoding-animation svg { 86 | animation: blink-animation 3s infinite both; 87 | } 88 | 89 | @keyframes blink-animation { 90 | 0% { opacity: 1; } 91 | 50% { opacity: 0; } 92 | 100% { opacity: 1; } 93 | } -------------------------------------------------------------------------------- /app/[locale]/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { NextIntlClientProvider } from "next-intl" 3 | import { getMessages } from "next-intl/server" 4 | import { notFound } from "next/navigation" 5 | import { Inter } from "next/font/google" 6 | import { SessionProvider } from "next-auth/react" 7 | import { Locale, routing } from "@/i18n/routing" 8 | import { auth } from '@/auth' 9 | import "./globals.css" 10 | 11 | import ThemeProvider from "@/providers/theme-provider" 12 | import { Navbar } from "@/components/shared/navbar" 13 | import { Footer } from "@/components/shared/footer" 14 | import { Toaster } from "@/components/ui/toaster" 15 | 16 | const inter = Inter({ subsets: ["latin"] }) 17 | 18 | export const metadata: Metadata = { 19 | title: "Nextjs fullstack Authentication", 20 | description: "Sign-Up and Sign-In with Nextjs", 21 | } 22 | 23 | export default async function AppLayout({ 24 | children, 25 | params 26 | }: { 27 | children: React.ReactNode 28 | params: { locale: Locale } 29 | }) { 30 | const { locale } = await params 31 | if (!routing.locales.includes(locale as Locale)) { 32 | notFound() 33 | } 34 | 35 | const messages = await getMessages() 36 | const session = await auth() 37 | 38 | // console.log({locale, messages}) 39 | 40 | return ( 41 | 42 | 43 | 44 | 50 | 51 | 52 |
53 | {children} 54 |
55 |