├── .env.example ├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── README.md ├── docker-compose.yml ├── next.config.mjs ├── package.json ├── prisma ├── migrations │ ├── 20240829155609_init │ │ └── migration.sql │ ├── 20240829161751_ │ │ └── migration.sql │ ├── 20240830061537_init │ │ └── migration.sql │ └── migration_lock.toml └── schema │ ├── account.prisma │ ├── schema.prisma │ ├── session.prisma │ └── user.prisma ├── public ├── example.png ├── example1.png ├── example2.png ├── example3.png ├── github-intro.png ├── ic-facebook.png ├── ic-github.png ├── ic-google.png ├── ic-line.png ├── logo.png ├── next.svg ├── og-image.png └── vercel.svg ├── src ├── app │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── register │ │ │ └── route.ts │ │ ├── users │ │ │ ├── avatar │ │ │ │ └── route.ts │ │ │ ├── resetPassword │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── sendResetPasswordEmail │ │ │ │ └── route.ts │ │ └── verify │ │ │ └── route.ts │ ├── favicon.ico │ ├── forgotPassword │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── register │ │ └── page.tsx │ ├── repository │ │ ├── user.ts │ │ └── verification.ts │ ├── resetPassword │ │ └── page.tsx │ ├── service │ │ ├── email │ │ │ ├── index.ts │ │ │ ├── resetPassword.ts │ │ │ ├── verify.ts │ │ │ └── welcome.ts │ │ ├── resetPassword │ │ │ └── index.ts │ │ └── verification │ │ │ └── index.ts │ ├── signin │ │ └── page.tsx │ └── verification │ │ ├── Verify.tsx │ │ └── page.tsx ├── auth.ts ├── components │ ├── client │ │ ├── Button │ │ │ └── index.tsx │ │ ├── FacebookSigninButton.tsx │ │ ├── GithubSigninButton.tsx │ │ ├── GoogleSigninButton.tsx │ │ ├── LineSigninButton.tsx │ │ ├── RegisterForm.tsx │ │ ├── ResetPasswordForm.tsx │ │ ├── SendForgotPasswordLinkForm.tsx │ │ ├── SigninForm.tsx │ │ ├── SignoutButton.tsx │ │ ├── TextField │ │ │ └── index.tsx │ │ ├── Title.tsx │ │ ├── UploadAvatarDialog.tsx │ │ ├── UserAvatar.tsx │ │ └── UserList.tsx │ └── email │ │ ├── ResetPassword.tsx │ │ ├── SigninWelcome.tsx │ │ └── Verify.tsx ├── hooks │ └── useDisclosure.ts ├── providers │ ├── Provider.tsx │ └── constants.tsx └── theme.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # GOOGLE SIGNIN 2 | AUTH_GOOGLE_ID= 3 | AUTH_GOOGLE_SECRET= 4 | 5 | # GITHUB SIGNIN 6 | AUTH_GITHUB_ID= 7 | AUTH_GITHUB_SECRET= 8 | 9 | # FACEBOOK SIGNIN 10 | AUTH_FACEBOOK_ID= 11 | AUTH_FACEBOOK_SECRET= 12 | 13 | #LINE SIGNIN 14 | AUTH_LINE_ID= 15 | AUTH_LINE_SECRET= 16 | 17 | # JWT 18 | JWT_SECRET= 19 | 20 | # DB 21 | DB_USER=postgres 22 | DB_PASSWORD=postgres 23 | POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public 24 | 25 | # API URL 26 | NEXT_PUBLIC_API_URL=http://localhost:3000/api 27 | 28 | # Web URL 29 | URL=http://localhost:3000 30 | 31 | # Email API Key 32 | RESEND_API_KEY= 33 | 34 | # Vercel Blob Token 35 | BLOB_READ_WRITE_TOKEN= 36 | -------------------------------------------------------------------------------- /.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 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.4.0](https://github.com/Dannyisadog/nextjs-authjs-template/compare/v0.2.0...v0.4.0) (2024-09-02) 6 | 7 | 8 | ### Features 9 | 10 | * **api:** add update user avatar api ([a2a1113](https://github.com/Dannyisadog/nextjs-authjs-template/commit/a2a1113c2aeb303b56f0ca5d98c2d62f62dd6e82)) 11 | * **api:** add verify email ([5573a84](https://github.com/Dannyisadog/nextjs-authjs-template/commit/5573a84962420e46aaa1f4af22b3607a9d1c3bac)) 12 | * **api:** send welcome email when register successfully ([ae76da1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ae76da120549b4efe7a713e032706f6a31242e1f)) 13 | * **app:** adjust layout metadata ([e852828](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e8528287fc19be28434d0b2e2cd932ab041206f6)) 14 | * **auth:** add prisma adapter ([705adc9](https://github.com/Dannyisadog/nextjs-authjs-template/commit/705adc9e05bd569c92b9e0c67f194334f510f2ef)) 15 | * **auth:** adjust session type ([ae20616](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ae20616a86d37f3f16cf23f7c7ff430a508e9776)) 16 | * **component:** add upload avatar dialog ([872ab3e](https://github.com/Dannyisadog/nextjs-authjs-template/commit/872ab3ea9effb8c5d551635f64a582b6810d2864)) 17 | * **component:** add upload user avatar dialog in user avatar ([da79164](https://github.com/Dannyisadog/nextjs-authjs-template/commit/da79164237dfb95a6196b24514f58681ec51de47)) 18 | * **component:** get users from provider ([76e9361](https://github.com/Dannyisadog/nextjs-authjs-template/commit/76e93618e8644f571fb40811a39e04a62b6db8e2)) 19 | * **email:** add email verification template ([e9aa759](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e9aa759c6f4e2e13ce86b807d9c58e58d4bd75b7)) 20 | * **email:** send email verification when register & first login ([b5201c5](https://github.com/Dannyisadog/nextjs-authjs-template/commit/b5201c5675a188aa1e2c81f0cbb9955da5f23790)) 21 | * **hook:** add useDisclosure hook ([3d373e9](https://github.com/Dannyisadog/nextjs-authjs-template/commit/3d373e9d4e155656f6f96a794c80715945298933)) 22 | * **middleware:** remove middleware for auth.js bug ([276c1f1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/276c1f15c016dca7dd5badb871d49aca00b211bd)) 23 | * **package:** add jwt package ([e2f4631](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e2f4631b8a47ef9632131d6ccc93ae04f316538c)) 24 | * **package:** add prisma adapter package ([dcb91ab](https://github.com/Dannyisadog/nextjs-authjs-template/commit/dcb91abadd4fd6e41dd00c2a5de1ac3e41356e3a)) 25 | * **package:** update prisma & add vercel/blob package ([0f06435](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0f064359acfa035d890586426c60a4f131a3b4e3)) 26 | * **page:** add verification page ([9d44dc7](https://github.com/Dannyisadog/nextjs-authjs-template/commit/9d44dc77304284d56a2d9a2a1b147cf9c5c6a5c2)) 27 | * **page:** display error message when verify failed ([7027a43](https://github.com/Dannyisadog/nextjs-authjs-template/commit/7027a43f56e377e938bb62466a68d35ca6dd39b7)) 28 | * **page:** display user verification status ([6885888](https://github.com/Dannyisadog/nextjs-authjs-template/commit/688588801cd143e808901012e288730ea55ef7d6)) 29 | * **prisma:** add & update schemas and migrations ([abeda08](https://github.com/Dannyisadog/nextjs-authjs-template/commit/abeda081232e6cd332d41f00e5c7ef918e09a1de)) 30 | * **provider:** add global provider ([68da421](https://github.com/Dannyisadog/nextjs-authjs-template/commit/68da4210c995e5976788a6f829bb52c74c6250fd)) 31 | * **repository:** add update function in user repository ([0f538f1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0f538f1ca1da12b8d694256e391d791dcc86d349)) 32 | * **repository:** add verification repository ([cfd624a](https://github.com/Dannyisadog/nextjs-authjs-template/commit/cfd624a09d0d71fb276c3e9ed7f41f218d81fcae)) 33 | * **repository:** adjust user update for emailVerified ([5776a95](https://github.com/Dannyisadog/nextjs-authjs-template/commit/5776a95221a09494f71b27dabb5e10b06d2a2fca)) 34 | * **service:** add base email service ([ac7663c](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ac7663cd42d03b0779cf0eb89354308eb862eb90)) 35 | * **service:** add send welcome email service ([be26376](https://github.com/Dannyisadog/nextjs-authjs-template/commit/be26376cf3882c64d4fcce63bc563441e85bb674)) 36 | * **service:** add service to send verification email ([e5052e4](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e5052e47d9322bf9bebc9e42187f9d10ed346878)) 37 | * **service:** add verification service ([ebe99c2](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ebe99c22a7ea4067bd177da392936baac5783e94)) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **api:** return error response when exception happened ([2f8a614](https://github.com/Dannyisadog/nextjs-authjs-template/commit/2f8a614b8b8b1fd5759dd12479778bcce0e542b1)) 43 | * **repository:** remove condition to check user exist ([0aa67c0](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0aa67c05636a888ad1fc7f2d2985104f1a8904c0)) 44 | * **service:** throw error when verify token failed ([cf1b1e7](https://github.com/Dannyisadog/nextjs-authjs-template/commit/cf1b1e7672a0c042c292b8d0e248b16420d62c10)) 45 | 46 | ## [0.3.0](https://github.com/Dannyisadog/nextjs-authjs-template/compare/v0.2.0...v0.3.0) (2024-08-30) 47 | 48 | 49 | ### Features 50 | 51 | * **api:** add update user avatar api ([a2a1113](https://github.com/Dannyisadog/nextjs-authjs-template/commit/a2a1113c2aeb303b56f0ca5d98c2d62f62dd6e82)) 52 | * **auth:** add prisma adapter ([705adc9](https://github.com/Dannyisadog/nextjs-authjs-template/commit/705adc9e05bd569c92b9e0c67f194334f510f2ef)) 53 | * **component:** add upload avatar dialog ([872ab3e](https://github.com/Dannyisadog/nextjs-authjs-template/commit/872ab3ea9effb8c5d551635f64a582b6810d2864)) 54 | * **component:** add upload user avatar dialog in user avatar ([da79164](https://github.com/Dannyisadog/nextjs-authjs-template/commit/da79164237dfb95a6196b24514f58681ec51de47)) 55 | * **component:** get users from provider ([76e9361](https://github.com/Dannyisadog/nextjs-authjs-template/commit/76e93618e8644f571fb40811a39e04a62b6db8e2)) 56 | * **hook:** add useDisclosure hook ([3d373e9](https://github.com/Dannyisadog/nextjs-authjs-template/commit/3d373e9d4e155656f6f96a794c80715945298933)) 57 | * **middleware:** remove middleware for auth.js bug ([276c1f1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/276c1f15c016dca7dd5badb871d49aca00b211bd)) 58 | * **package:** add prisma adapter package ([dcb91ab](https://github.com/Dannyisadog/nextjs-authjs-template/commit/dcb91abadd4fd6e41dd00c2a5de1ac3e41356e3a)) 59 | * **package:** update prisma & add vercel/blob package ([0f06435](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0f064359acfa035d890586426c60a4f131a3b4e3)) 60 | * **prisma:** add & update schemas and migrations ([abeda08](https://github.com/Dannyisadog/nextjs-authjs-template/commit/abeda081232e6cd332d41f00e5c7ef918e09a1de)) 61 | * **provider:** add global provider ([68da421](https://github.com/Dannyisadog/nextjs-authjs-template/commit/68da4210c995e5976788a6f829bb52c74c6250fd)) 62 | * **repository:** add update function in user repository ([0f538f1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0f538f1ca1da12b8d694256e391d791dcc86d349)) 63 | 64 | ## 0.1.0 (2024-08-28) 65 | 66 | 67 | ### Features 68 | 69 | * **api:** add register api ([5a9ce72](https://github.com/Dannyisadog/nextjs-authjs-template/commit/5a9ce723e8248c345a501daab5e776755f0338bb)) 70 | * **api:** add users crud api ([36a0a56](https://github.com/Dannyisadog/nextjs-authjs-template/commit/36a0a5680a4a06f266ecb177398c01ae3a91fcab)) 71 | * **api:** protect get users api ([0a05164](https://github.com/Dannyisadog/nextjs-authjs-template/commit/0a05164db84cd63f9b92bc12089e1f0a1cdf8d7b)) 72 | * **api:** remove create user api ([ec83269](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ec8326981827ac2dd67fd328d3bd02a59ce6c46e)) 73 | * **app:** add og title & image ([a280fa6](https://github.com/Dannyisadog/nextjs-authjs-template/commit/a280fa6c46e3765520926bc029281c266a96c10b)) 74 | * **auth:** add credential signin ([59240e1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/59240e119230e607afcb84f770be6e39dcd24a43)) 75 | * **auth:** add Github provider in auth providers ([9ee1c58](https://github.com/Dannyisadog/nextjs-authjs-template/commit/9ee1c5802f874fc422131b5e4a2f15713addc3da)) 76 | * **auth:** add image when first signin create user ([fcece58](https://github.com/Dannyisadog/nextjs-authjs-template/commit/fcece587591d62a8d17e774f492c9d09e4d912d0)) 77 | * **auth:** send welcome email when first signin ([fbc945b](https://github.com/Dannyisadog/nextjs-authjs-template/commit/fbc945bfe27e1c8e9c59165abc1604fde24a579a)) 78 | * **component:** add button loading & disable status ([d782632](https://github.com/Dannyisadog/nextjs-authjs-template/commit/d782632dcba6750f18b73821a4c5a27d16d11721)) 79 | * **component:** add common button component ([fbe1ead](https://github.com/Dannyisadog/nextjs-authjs-template/commit/fbe1ead59e346a656fc483d50ac49696ce848483)) 80 | * **component:** add common textfield component ([8093a21](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8093a21ac7fb65d8ba6fffef8d1ab7df86214c9b)) 81 | * **component:** add github link title component ([63b0e59](https://github.com/Dannyisadog/nextjs-authjs-template/commit/63b0e598024c4e392ccdd0523aa25be30b9b03ef)) 82 | * **component:** add github signin button ([cb9f118](https://github.com/Dannyisadog/nextjs-authjs-template/commit/cb9f118141ec6948120ef8fbc9720f53914b070e)) 83 | * **component:** add goBack option in title component ([72e4818](https://github.com/Dannyisadog/nextjs-authjs-template/commit/72e48184d7f9d99f0ef3d6ea0d0cffad289938ce)) 84 | * **component:** add google signin button component ([835ff5b](https://github.com/Dannyisadog/nextjs-authjs-template/commit/835ff5be8247faaef5ae9936a29613791ee8ac5d)) 85 | * **component:** add loading status in google signin & signout button ([232068a](https://github.com/Dannyisadog/nextjs-authjs-template/commit/232068a92085dd1e875377cd148463c682bd0845)) 86 | * **component:** add register link in signin form ([bfcd943](https://github.com/Dannyisadog/nextjs-authjs-template/commit/bfcd9431d322d5cb250333f5d772b658e2ad677c)) 87 | * **component:** add signin button status ([d002649](https://github.com/Dannyisadog/nextjs-authjs-template/commit/d002649ee09910a3ce3bed33c04556bdc7b2482e)) 88 | * **component:** add signin form component ([82c38cb](https://github.com/Dannyisadog/nextjs-authjs-template/commit/82c38cbc6a2459b078ee17261586e6c33536de12)) 89 | * **component:** add signin welcome email template ([8421a05](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8421a05ea9349f9c7605d519b29b74fdebdface0)) 90 | * **component:** add title component ([2a2bbe6](https://github.com/Dannyisadog/nextjs-authjs-template/commit/2a2bbe6b4be74e289c1188640e81f4248dcbac33)) 91 | * **component:** add user list ([da8ce21](https://github.com/Dannyisadog/nextjs-authjs-template/commit/da8ce2105408b1d03b5965d21309395c7005c3a2)) 92 | * **component:** adjust components style ([835bf72](https://github.com/Dannyisadog/nextjs-authjs-template/commit/835bf72ecbdc81b80575138fad75df54db382def)) 93 | * **component:** adjust error message style ([dabda98](https://github.com/Dannyisadog/nextjs-authjs-template/commit/dabda98dab169867e41cfdfdc1f8f8e11ee3f924)) 94 | * **component:** adjust title height ([5d9cc0e](https://github.com/Dannyisadog/nextjs-authjs-template/commit/5d9cc0ee53a9c556f56d0950bdf9a87a08ee2993)) 95 | * **component:** display github icon conditionally ([2c797d5](https://github.com/Dannyisadog/nextjs-authjs-template/commit/2c797d5965360984a9cf76eb9cfddde4ee0f13bc)) 96 | * **component:** display user list ([22868c6](https://github.com/Dannyisadog/nextjs-authjs-template/commit/22868c6be99d0bfb76f4724c9bdd21fe659ad0f2)) 97 | * **component:** move user list to client component ([543530c](https://github.com/Dannyisadog/nextjs-authjs-template/commit/543530ca26b3d1ed57d0c621c2d08c61661059b1)) 98 | * **component:** restyle error message ([833a8b4](https://github.com/Dannyisadog/nextjs-authjs-template/commit/833a8b4e8e8676fba7cc70d79279c70ac2cdabac)) 99 | * **component:** set register link color to primary ([57ce0ce](https://github.com/Dannyisadog/nextjs-authjs-template/commit/57ce0ce622afb75bec4d88ce977d30cc082670ea)) 100 | * **component:** validate signin form data in frontend ([e36e044](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e36e044bed7994c724689a362e9781876d740a49)) 101 | * **docker:** add docker-compose for postgres db ([3fb2f1f](https://github.com/Dannyisadog/nextjs-authjs-template/commit/3fb2f1fb9d83a4cf4b68a51bf5614f7ae1aa3616)) 102 | * **env:** add resend api key in env example ([1d83342](https://github.com/Dannyisadog/nextjs-authjs-template/commit/1d833428c172afa217aeda2c407ae071746d1a7a)) 103 | * **env:** update env example for github signin ([b2c7771](https://github.com/Dannyisadog/nextjs-authjs-template/commit/b2c7771cf5401c08bdf200b22da5a1b25b5dbcfe)) 104 | * **favicon:** update favicon ([39c5f53](https://github.com/Dannyisadog/nextjs-authjs-template/commit/39c5f53fdf019c344f34966b2df528e448d23ea5)) 105 | * **head:** adjust title & description ([f0290ff](https://github.com/Dannyisadog/nextjs-authjs-template/commit/f0290ff4ae11edaf7e315176f0ca1e0b2953dd1e)) 106 | * **layout:** add basic layout ([9f12ae1](https://github.com/Dannyisadog/nextjs-authjs-template/commit/9f12ae11913633cd07656f00f35c3505e2f08775)) 107 | * **layout:** add mui theme ([7eb6d8e](https://github.com/Dannyisadog/nextjs-authjs-template/commit/7eb6d8e8d7c3249b522777840641deb24de5d321)) 108 | * **layout:** adjust layout padding top ([475b653](https://github.com/Dannyisadog/nextjs-authjs-template/commit/475b653a85b64d130d9e53998f281fd5e2404148)) 109 | * **layout:** adjust layout padding y ([ddc1275](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ddc1275cb45cafc6c6c1f3fbac1efb8f6a8c4e7b)) 110 | * **layout:** inject vercel analytics into app layout ([4bc64a8](https://github.com/Dannyisadog/nextjs-authjs-template/commit/4bc64a8417dc4076501ac01105a48d46d9dbf863)) 111 | * **layout:** inject vercel speed insights into app layout ([cea4b7c](https://github.com/Dannyisadog/nextjs-authjs-template/commit/cea4b7c038aa4c96e085eae20ec04b2549b29cb5)) 112 | * **layout:** move viewport into head section ([1d1d1f2](https://github.com/Dannyisadog/nextjs-authjs-template/commit/1d1d1f218e43c43c27dceffdd06d12e740ab68ab)) 113 | * **meta:** adjust meta data ([8ed876d](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8ed876d4840aafb2231121b51d2d7dde037f987e)) 114 | * **package:** add bcrypt & prisma package ([c5cb695](https://github.com/Dannyisadog/nextjs-authjs-template/commit/c5cb695766e720c3a31e7b3ffe75b6a690b10771)) 115 | * **package:** add mui icon package ([10e4f33](https://github.com/Dannyisadog/nextjs-authjs-template/commit/10e4f3300f8fb2714e04ae9502e6dda6ed269ca8)) 116 | * **package:** add resend & react-email packages ([67ee4b6](https://github.com/Dannyisadog/nextjs-authjs-template/commit/67ee4b6fb80ffbaec2fd2e2872da28322a290029)) 117 | * **package:** add standard-version package ([4909ab2](https://github.com/Dannyisadog/nextjs-authjs-template/commit/4909ab2c7bdc7b4e9472405738f0a28afa8557f9)) 118 | * **package:** add vercel analytics package ([260bfe8](https://github.com/Dannyisadog/nextjs-authjs-template/commit/260bfe81769ab1f8adc915a6a26b3d861456ef4a)) 119 | * **package:** add vercel speed insights package ([6d6a141](https://github.com/Dannyisadog/nextjs-authjs-template/commit/6d6a141a1c211829e203f0cba827d6065eba4dbd)) 120 | * **package:** install mui related packages ([e00e0ac](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e00e0ac50dcc48f12792c3fad0526a0e893356ab)) 121 | * **package:** remove @react-email/components package ([4f4741e](https://github.com/Dannyisadog/nextjs-authjs-template/commit/4f4741e48b5874c375fbc2216db70ff1a947358f)) 122 | * **page:** add github signin button into signin page ([6127901](https://github.com/Dannyisadog/nextjs-authjs-template/commit/61279012d7b93091db72003d42b8dbb7cd945571)) 123 | * **page:** add register form ([aedb59a](https://github.com/Dannyisadog/nextjs-authjs-template/commit/aedb59a40253ce2f3d5136b692428180ad98449e)) 124 | * **page:** add signin page ([ba16c68](https://github.com/Dannyisadog/nextjs-authjs-template/commit/ba16c689a1ad03c35f3e4c55a6f8048f69fc53cd)) 125 | * **page:** adjust api base url ([a7014e2](https://github.com/Dannyisadog/nextjs-authjs-template/commit/a7014e2adbf06eed02ddacf0ce76eb2f7dd82825)) 126 | * **page:** adjust home page ([8743c63](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8743c63591a4e7cdee43bc4ea97bdccb9d314035)) 127 | * **page:** adjust page style ([6a12a93](https://github.com/Dannyisadog/nextjs-authjs-template/commit/6a12a93c89e16beb072f4e42598de7438793c05b)) 128 | * **page:** adjust pages style ([dc5a1f8](https://github.com/Dannyisadog/nextjs-authjs-template/commit/dc5a1f808f71bb13dd50380f3eccc77779ff2562)) 129 | * **page:** remoe useless stuff ([3e2df11](https://github.com/Dannyisadog/nextjs-authjs-template/commit/3e2df117b49d655a2e894c5b4555341b1b034a8a)) 130 | * **prisma:** add prisma schema & model ([5cd84ed](https://github.com/Dannyisadog/nextjs-authjs-template/commit/5cd84ed10cce770f209c99517ddf9533925d416c)) 131 | * **prisma:** split user schema ([56049a7](https://github.com/Dannyisadog/nextjs-authjs-template/commit/56049a7d7fd9d46bf3af36e5229fe4bc49ed5bec)) 132 | * **repository:** add email param to get user ([8ee1ccc](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8ee1cccec24a2d7f7a60a4821dc94d368606c567)) 133 | * **repository:** add image field when create user ([a1136d9](https://github.com/Dannyisadog/nextjs-authjs-template/commit/a1136d9bfa331c5761d5ce47f75c842925b695c1)) 134 | * **repository:** add user create with password ([8804b02](https://github.com/Dannyisadog/nextjs-authjs-template/commit/8804b02a9ed60b5232cad1dc7760a514de47ddc0)) 135 | * **repository:** add user repository ([b77b053](https://github.com/Dannyisadog/nextjs-authjs-template/commit/b77b0533faaad2fa81ac165d4bc9f8117a41eb8f)) 136 | * **repository:** adjust get user condition when deleted is false ([e89d494](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e89d49411981948e3ffb3e711b4c29bfc3b27972)) 137 | * **service:** add send email service ([e96c539](https://github.com/Dannyisadog/nextjs-authjs-template/commit/e96c53962e6de43846593a0b8e563c9ed8f46b1a)) 138 | * **style:** adjust global & page css ([3648605](https://github.com/Dannyisadog/nextjs-authjs-template/commit/364860556af90d8adbf561f89c83d9df15a44d51)) 139 | * **style:** remove page css ([1b84388](https://github.com/Dannyisadog/nextjs-authjs-template/commit/1b84388d929157068ce377b027757136d6fe1bfe)) 140 | * **style:** reset global css ([07befe8](https://github.com/Dannyisadog/nextjs-authjs-template/commit/07befe8bc59f61c212b1aa0a9b8bd07c03375467)) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * **component:** adjust email template using pure html tsx ([fab77ee](https://github.com/Dannyisadog/nextjs-authjs-template/commit/fab77ee1125bec54e8ee3de643dbd5055b97b131)) 146 | * **prisma:** split prisma schema ([b5ed7bb](https://github.com/Dannyisadog/nextjs-authjs-template/commit/b5ed7bb9dc061d635313eb07b2ccb972628bdad0)) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Next.js Auth Template](https://nextauth.dannyisadog.com) 2 | 3 | ![Github Intro Image](https://nextauth.dannyisadog.com/github-intro.png) 4 | 5 | The nextjs-authjs-template is a robust starter template for building modern web applications with Next.js and Auth.js (formerly NextAuth.js). This template is designed to streamline the development process by providing a pre-configured setup that integrates user authentication and authorization seamlessly into your Next.js project. 6 | 7 | ## Features: 8 | - FullStack [Next.js](https://nextjs.org/) application with built-in authentication. 9 | - User authentication with email, Google, and GitHub. 10 | - ORM with [Prisma](https://www.prisma.io/) for database management. 11 | - PostgreSQL database with Docker for easy setup. 12 | - Email service with [Resend](https://resend.com/) API. 13 | - [MUI](https://mui.com/) for styling and responsive design. 14 | 15 | 16 | ## Use Cases: 17 | - Rapidly prototype and deploy Next.js applications with built-in authentication. 18 | - Start new projects with a solid foundation, reducing the boilerplate code required for user management. 19 | 20 | ## Getting Started 21 | 22 | 1. Create a `.env` file in the root of the project and add the following 23 | 24 | ```bash 25 | cp .env.example .env 26 | ``` 27 | 28 | 2. Generate auth secret key 29 | 30 | ```bash 31 | npx auth secret 32 | ``` 33 | 34 | 3. Fill up your google & github client id & secret in `.env` 35 | 36 | ```bash 37 | ... 38 | AUTH_GOOGLE_ID=xxxx 39 | AUTH_GOOGLE_SECRET=xxxx 40 | 41 | AUTH_GITHUB_ID=xxxx 42 | AUTH_GITHUB_SECRET=xxxx 43 | ... 44 | ``` 45 | 46 | 4. Create your [Resend api key](https://resend.com/api-keys) for email service in `.env` 47 | 48 | ```bash 49 | ... 50 | RESEND_API_KEY=xxxx 51 | ... 52 | ``` 53 | 54 | 5. Install dependencies 55 | 56 | ```bash 57 | yarn install 58 | ``` 59 | 60 | 6. Start postgres database 61 | 62 | ```bash 63 | docker-compose up -d 64 | ``` 65 | 66 | 7. Run the development server: 67 | 68 | ```bash 69 | yarn dev 70 | ``` 71 | 72 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 73 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | container_name: postgres_db 6 | image: postgres 7 | environment: 8 | POSTGRES_USER: ${DB_USER} 9 | POSTGRES_PASSWORD: ${DB_PASSWORD} 10 | PGDATA: /data/postgres 11 | volumes: 12 | - postgres:/data/postgres 13 | ports: 14 | - "5432:5432" 15 | restart: unless-stopped 16 | 17 | volumes: 18 | postgres: 19 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "**", 8 | }, 9 | ], 10 | } 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-authentication-template", 3 | "version": "0.4.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate", 11 | "release": "standard-version" 12 | }, 13 | "dependencies": { 14 | "@auth/prisma-adapter": "^2.4.2", 15 | "@emotion/react": "^11.13.3", 16 | "@emotion/styled": "^11.13.0", 17 | "@mui/icons-material": "^5.16.7", 18 | "@mui/material": "^5.16.7", 19 | "@multiavatar/multiavatar": "^1.0.7", 20 | "@prisma/client": "^5.19.0", 21 | "@vercel/analytics": "^1.3.1", 22 | "@vercel/blob": "^0.23.4", 23 | "@vercel/speed-insights": "^1.0.12", 24 | "jsonwebtoken": "^9.0.2", 25 | "next": "14.2.5", 26 | "next-auth": "^5.0.0-beta.20", 27 | "react": "^18", 28 | "react-dom": "^18", 29 | "resend": "^4.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/bcryptjs": "^2.4.6", 33 | "@types/jsonwebtoken": "^9.0.6", 34 | "@types/node": "^20", 35 | "@types/react": "^18", 36 | "@types/react-dom": "^18", 37 | "bcryptjs": "^2.4.3", 38 | "eslint": "^8", 39 | "eslint-config-next": "14.2.5", 40 | "prisma": "^5.19.0", 41 | "standard-version": "^9.5.0", 42 | "typescript": "^5" 43 | } 44 | } -------------------------------------------------------------------------------- /prisma/migrations/20240829155609_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "image" TEXT, 7 | "password" TEXT, 8 | "is_active" BOOLEAN NOT NULL DEFAULT false, 9 | "is_deleted" BOOLEAN NOT NULL DEFAULT false, 10 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updated_at" TIMESTAMP(3), 12 | 13 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 18 | -------------------------------------------------------------------------------- /prisma/migrations/20240829161751_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "email_verified" TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240830061537_init/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `email_verified` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" DROP COLUMN "email_verified", 9 | ADD COLUMN "emailVerified" TIMESTAMP(3); 10 | 11 | -- CreateTable 12 | CREATE TABLE "Account" ( 13 | "userId" INTEGER NOT NULL, 14 | "type" TEXT NOT NULL, 15 | "provider" TEXT NOT NULL, 16 | "providerAccountId" TEXT NOT NULL, 17 | "refresh_token" TEXT, 18 | "access_token" TEXT, 19 | "expires_at" INTEGER, 20 | "token_type" TEXT, 21 | "scope" TEXT, 22 | "id_token" TEXT, 23 | "session_state" TEXT, 24 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 25 | "updatedAt" TIMESTAMP(3) NOT NULL, 26 | 27 | CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId") 28 | ); 29 | 30 | -- CreateTable 31 | CREATE TABLE "Session" ( 32 | "sessionToken" TEXT NOT NULL, 33 | "userId" INTEGER NOT NULL, 34 | "expires" TIMESTAMP(3) NOT NULL, 35 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 36 | "updatedAt" TIMESTAMP(3) NOT NULL 37 | ); 38 | 39 | -- CreateIndex 40 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 41 | 42 | -- AddForeignKey 43 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 44 | 45 | -- AddForeignKey 46 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 47 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema/account.prisma: -------------------------------------------------------------------------------- 1 | model Account { 2 | userId Int 3 | type String 4 | provider String 5 | providerAccountId String 6 | refresh_token String? 7 | access_token String? 8 | expires_at Int? 9 | token_type String? 10 | scope String? 11 | id_token String? 12 | session_state String? 13 | 14 | createdAt DateTime @default(now()) 15 | updatedAt DateTime @updatedAt 16 | 17 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 18 | 19 | @@id([provider, providerAccountId]) 20 | } 21 | -------------------------------------------------------------------------------- /prisma/schema/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | previewFeatures = ["prismaSchemaFolder"] 10 | } 11 | 12 | datasource db { 13 | provider = "postgresql" 14 | url = env("POSTGRES_URL") 15 | } 16 | -------------------------------------------------------------------------------- /prisma/schema/session.prisma: -------------------------------------------------------------------------------- 1 | model Session { 2 | sessionToken String @unique 3 | userId Int 4 | expires DateTime 5 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 6 | 7 | createdAt DateTime @default(now()) 8 | updatedAt DateTime @updatedAt 9 | } 10 | -------------------------------------------------------------------------------- /prisma/schema/user.prisma: -------------------------------------------------------------------------------- 1 | model User { 2 | id Int @id @default(autoincrement()) 3 | name String 4 | email String @unique 5 | emailVerified DateTime? 6 | image String? 7 | password String? 8 | is_active Boolean @default(false) 9 | is_deleted Boolean @default(false) 10 | created_at DateTime @default(now()) 11 | updated_at DateTime? @updatedAt 12 | accounts Account[] 13 | sessions Session[] 14 | } 15 | -------------------------------------------------------------------------------- /public/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/example.png -------------------------------------------------------------------------------- /public/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/example1.png -------------------------------------------------------------------------------- /public/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/example2.png -------------------------------------------------------------------------------- /public/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/example3.png -------------------------------------------------------------------------------- /public/github-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/github-intro.png -------------------------------------------------------------------------------- /public/ic-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/ic-facebook.png -------------------------------------------------------------------------------- /public/ic-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/ic-github.png -------------------------------------------------------------------------------- /public/ic-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/ic-google.png -------------------------------------------------------------------------------- /public/ic-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/ic-line.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/logo.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/public/og-image.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "auth" 2 | export const { GET, POST } = handlers -------------------------------------------------------------------------------- /src/app/api/register/route.ts: -------------------------------------------------------------------------------- 1 | import { create } from "app/repository/user"; 2 | import { sendVerificationEmail } from "app/service/email/verify"; 3 | import { sendWelcomeEmail } from "app/service/email/welcome"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | 6 | export async function POST(req: NextRequest) { 7 | const { name, email, password, passwordConfirm } = await req.json(); 8 | 9 | await create({ 10 | name, 11 | email, 12 | password, 13 | passwordConfirm, 14 | }); 15 | 16 | sendWelcomeEmail(name, email); 17 | sendVerificationEmail(name, email); 18 | 19 | return NextResponse.json({ message: "Create user" }); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/api/users/avatar/route.ts: -------------------------------------------------------------------------------- 1 | import { put } from "@vercel/blob"; 2 | import { get, update } from "app/repository/user"; 3 | import { auth } from "auth"; 4 | 5 | export const POST = auth(async (req) => { 6 | if (!req.auth || !req.auth.user) { 7 | return Response.json({ message: "Unauthorized" }, { status: 401 }); 8 | } 9 | 10 | const { user } = req.auth; 11 | 12 | const dbUser = await get({ email: user.email as string }); 13 | 14 | const formData = await req.formData(); 15 | 16 | const image = formData.get("image"); 17 | 18 | const { name: filename } = image as File; 19 | 20 | if (!image) { 21 | return Response.json({ error: "Image is required" }, { status: 400 }); 22 | } 23 | 24 | if (!req.body) { 25 | return Response.json({ message: "Body is required" }, { status: 400 }); 26 | } 27 | 28 | const buffer = Buffer.from(await (image as Blob).arrayBuffer()); 29 | 30 | try { 31 | const blob = await put(`avatars/${dbUser.id}-${filename}`, buffer, { 32 | access: "public", 33 | }); 34 | return Response.json({ 35 | url: blob.url, 36 | }); 37 | } catch (e) { 38 | console.log(e); 39 | return Response.json( 40 | { message: "Fail to upload user avatar using vercel/blob" }, 41 | { status: 500 } 42 | ); 43 | } 44 | }); 45 | 46 | export const PUT = auth(async (req) => { 47 | if (!req.auth || !req.auth.user) { 48 | return Response.json({ message: "Unauthorized" }, { status: 401 }); 49 | } 50 | 51 | const { user } = req.auth; 52 | 53 | const dbUser = await get({ email: user.email as string }); 54 | 55 | const { image } = await req.json(); 56 | 57 | if (!image) { 58 | return Response.json({ error: "Image url is required" }, { status: 400 }); 59 | } 60 | 61 | try { 62 | await update({ 63 | id: dbUser.id, 64 | image, 65 | }); 66 | return Response.json({ message: "User updated" }, { status: 201 }); 67 | } catch (e) { 68 | console.log(e); 69 | return Response.json({ message: "Fail to update user" }, { status: 500 }); 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /src/app/api/users/resetPassword/route.ts: -------------------------------------------------------------------------------- 1 | import { resetPassword } from "app/repository/user"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export const POST = async (req: NextRequest) => { 5 | const { token, password, passwordConfirm } = await req.json(); 6 | 7 | try { 8 | await resetPassword({ 9 | token, 10 | password, 11 | passwordConfirm, 12 | }); 13 | 14 | return NextResponse.json({ message: "Password reset" }); 15 | } catch (e) { 16 | return NextResponse.json( 17 | { message: (e as Error).message }, 18 | { status: 400 } 19 | ); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/api/users/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { list } from "app/repository/user"; 3 | import { auth } from "auth"; 4 | 5 | export const GET = auth(async (req) => { 6 | if (req.auth) { 7 | const users = await list(); 8 | return NextResponse.json(users); 9 | } 10 | 11 | return Response.json({ message: "Not authenticated" }, { status: 401 }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/api/users/sendResetPasswordEmail/route.ts: -------------------------------------------------------------------------------- 1 | import { sendResetPasswordEmail } from "app/service/email/resetPassword"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export const POST = async (req: NextRequest) => { 5 | const { email } = await req.json(); 6 | 7 | await sendResetPasswordEmail(email); 8 | 9 | return NextResponse.json({ message: "Email sent" }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/api/verify/route.ts: -------------------------------------------------------------------------------- 1 | import { verifyEmailToken } from "app/repository/verification"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export const POST = async (req: NextRequest) => { 5 | const { token } = await req.json(); 6 | 7 | try { 8 | await verifyEmailToken(token); 9 | 10 | return NextResponse.json({ message: "Verified user" }); 11 | } catch (e) { 12 | return NextResponse.json( 13 | { message: (e as Error).message }, 14 | { status: 400 } 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dannyisadog/nextjs-authentication-template/25c90b3b0ae18fcef0b9db3c20871c8519d16158/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/forgotPassword/page.tsx: -------------------------------------------------------------------------------- 1 | import SendForgotPasswordLinkForm from "components/client/SendForgotPasswordLinkForm"; 2 | import Title from "components/client/Title"; 3 | 4 | export default async function Signin() { 5 | return ( 6 | <> 7 | 8 | <SendForgotPasswordLinkForm /> 9 | </> 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | max-width: 100vw; 10 | overflow-x: hidden; 11 | color: #0c254c; 12 | } 13 | 14 | a { 15 | color: inherit; 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { ThemeProvider } from "@mui/material/styles"; 4 | import { theme } from "theme"; 5 | import { Stack } from "@mui/material"; 6 | import { SpeedInsights } from "@vercel/speed-insights/next"; 7 | import { Analytics } from "@vercel/analytics/react"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Next.js Auth Template", 11 | description: 12 | "A robust template for Next.js with integrated authentication and authorization.", 13 | keywords: 14 | "Next.js, Auth.js, Authentication, Authorization, Template, Web Development", 15 | openGraph: { 16 | type: "website", 17 | url: "https://nextauth.dannyisadog.com", 18 | title: "Next.js Auth Template", 19 | description: 20 | "A robust template for Next.js with integrated authentication and authorization.", 21 | images: [ 22 | { 23 | url: "/og-image.png", 24 | width: 1200, 25 | height: 630, 26 | alt: "Next.js Auth Template", 27 | }, 28 | ], 29 | }, 30 | alternates: { 31 | canonical: "https://nextauth.dannyisadog.com", 32 | }, 33 | }; 34 | 35 | export default function RootLayout({ 36 | children, 37 | }: Readonly<{ 38 | children: React.ReactNode; 39 | }>) { 40 | return ( 41 | <html lang="en"> 42 | <meta charSet="utf-8" /> 43 | <meta 44 | name="viewport" 45 | content="width=device-width, initial-scale=1, user-scalable=no" 46 | /> 47 | <body> 48 | <ThemeProvider theme={theme}> 49 | <Stack 50 | spacing={2} 51 | sx={{ 52 | maxWidth: 400, 53 | margin: "auto", 54 | px: 2, 55 | py: 4, 56 | }} 57 | justifyContent="center" 58 | > 59 | {children} 60 | <SpeedInsights /> 61 | <Analytics /> 62 | </Stack> 63 | </ThemeProvider> 64 | </body> 65 | </html> 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import SignoutButton from "components/client/SignoutButton"; 2 | import { auth, CustomSession } from "auth"; 3 | import { redirect } from "next/navigation"; 4 | import { Alert, AlertTitle, Stack, Typography } from "@mui/material"; 5 | import Title from "components/client/Title"; 6 | import UserList from "components/client/UserList"; 7 | import UserAvatar from "components/client/UserAvatar"; 8 | import Provider from "providers/Provider"; 9 | 10 | export default async function Home() { 11 | const session = (await auth()) as CustomSession; 12 | 13 | if (!session || !session.user || !session.user?.email) { 14 | redirect("/signin"); 15 | } 16 | 17 | const emailVerified = !!session.authUser.emailVerified; 18 | 19 | return ( 20 | <Provider session={session}> 21 | <Title text="Basic Info" /> 22 | <UserAvatar /> 23 | <Stack direction="row" spacing={2}> 24 | <Typography textAlign="center">Account:</Typography> 25 | <Typography textAlign="center">{session.authUser.email}</Typography> 26 | </Stack> 27 | <Stack direction="row" spacing={2}> 28 | <Typography textAlign="center">Name:</Typography> 29 | <Typography textAlign="center">{session.authUser.name}</Typography> 30 | </Stack> 31 | <Stack direction="row" spacing={2}> 32 | <Typography textAlign="center">Status:</Typography> 33 | <Typography textAlign="center"> 34 | {emailVerified ? "Verified" : "Not Verified"} 35 | </Typography> 36 | </Stack> 37 | <SignoutButton /> 38 | <Title text="Users" showGithub={false} /> 39 | <UserList /> 40 | </Provider> 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/register/page.tsx: -------------------------------------------------------------------------------- 1 | import RegisterForm from "components/client/RegisterForm"; 2 | import Title from "components/client/Title"; 3 | 4 | export default async function Signin() { 5 | return ( 6 | <> 7 | <Title text="Register" hasGoBack /> 8 | <RegisterForm /> 9 | </> 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/repository/user.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, User } from "@prisma/client"; 2 | import { verifyResetPasswordToken } from "app/service/resetPassword"; 3 | import bcrypt from "bcryptjs"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | const saltRounds = 10; 8 | 9 | export const get = async ({ 10 | id, 11 | email, 12 | }: { 13 | id?: number; 14 | email?: string; 15 | }): Promise<User> => { 16 | if (!id && !email) { 17 | throw new Error("User id or email is required"); 18 | } 19 | 20 | const user = await prisma.user.findUnique({ 21 | where: { 22 | id, 23 | email, 24 | is_deleted: false, 25 | }, 26 | }); 27 | 28 | if (!user) { 29 | throw new Error("User not found"); 30 | } 31 | 32 | return user; 33 | }; 34 | export const list = async (): Promise<User[]> => { 35 | const users = await prisma.user.findMany(); 36 | return users; 37 | }; 38 | export const create = async ({ 39 | name, 40 | email, 41 | password, 42 | passwordConfirm, 43 | image, 44 | }: { 45 | name: string; 46 | email: string; 47 | password?: string; 48 | passwordConfirm?: string; 49 | image?: string; 50 | }) => { 51 | let userExists = false; 52 | 53 | try { 54 | await get({ email }); 55 | userExists = true; 56 | } catch (e) { 57 | userExists = false; 58 | } 59 | 60 | if (userExists) { 61 | throw new Error("User already exists"); 62 | } 63 | 64 | if (!name) { 65 | throw new Error("User name is required"); 66 | } 67 | 68 | if (!email) { 69 | throw new Error("User email is required"); 70 | } 71 | 72 | if (password && password !== passwordConfirm) { 73 | throw new Error("Passwords do not match"); 74 | } 75 | 76 | if (password) { 77 | bcrypt.hash(password, saltRounds, async function (err, hash) { 78 | if (err) { 79 | throw new Error("Error hashing password"); 80 | } 81 | 82 | await prisma.user.create({ 83 | data: { 84 | name, 85 | email, 86 | password: hash, 87 | image, 88 | }, 89 | }); 90 | }); 91 | } else { 92 | await prisma.user.create({ 93 | data: { 94 | name, 95 | email, 96 | image, 97 | }, 98 | }); 99 | } 100 | }; 101 | 102 | export const remove = async (id: number) => { 103 | if (!id) { 104 | throw new Error("User ID is required"); 105 | } 106 | 107 | const user = await prisma.user.findUnique({ 108 | where: { 109 | id, 110 | }, 111 | }); 112 | 113 | if (!user) { 114 | throw new Error("User not found"); 115 | } 116 | 117 | await prisma.user.update({ 118 | where: { 119 | id, 120 | }, 121 | data: { 122 | is_deleted: true, 123 | }, 124 | }); 125 | }; 126 | 127 | export const update = async ({ 128 | id, 129 | name, 130 | email, 131 | image, 132 | emailVerified, 133 | password, 134 | }: { 135 | id: number; 136 | name?: string; 137 | email?: string; 138 | image?: string; 139 | emailVerified?: Date; 140 | password?: string; 141 | }) => { 142 | if (!id) { 143 | throw new Error("User ID is required"); 144 | } 145 | 146 | const user = await prisma.user.findUnique({ 147 | where: { 148 | id, 149 | }, 150 | }); 151 | 152 | if (!user) { 153 | throw new Error("User not found"); 154 | } 155 | 156 | await prisma.user.update({ 157 | where: { 158 | id, 159 | }, 160 | data: { 161 | name, 162 | email, 163 | image, 164 | emailVerified, 165 | password, 166 | }, 167 | }); 168 | }; 169 | 170 | interface ResetPasswordParams { 171 | token: string; 172 | password: string; 173 | passwordConfirm: string; 174 | } 175 | 176 | export const resetPassword = async (params: ResetPasswordParams) => { 177 | const { token, password, passwordConfirm } = params; 178 | 179 | const { data } = verifyResetPasswordToken(token); 180 | 181 | const { email } = data; 182 | 183 | if (!token) { 184 | throw new Error("Token is required"); 185 | } 186 | 187 | if (!password) { 188 | throw new Error("Password is required"); 189 | } 190 | 191 | if (password.length < 8) { 192 | throw new Error("Password must be at least 8 characters"); 193 | } 194 | 195 | if (!passwordConfirm) { 196 | throw new Error("Password confirmation is required"); 197 | } 198 | 199 | if (password !== passwordConfirm) { 200 | throw new Error("Passwords do not match"); 201 | } 202 | 203 | const user = await get({ email }); 204 | 205 | bcrypt.hash(password, saltRounds, async function (err, hash) { 206 | if (err) { 207 | throw new Error("Error hashing password"); 208 | } 209 | 210 | await update({ 211 | id: user.id, 212 | password: hash, 213 | }); 214 | }); 215 | }; 216 | -------------------------------------------------------------------------------- /src/app/repository/verification.ts: -------------------------------------------------------------------------------- 1 | import { VerifyToken, verifyToken } from "app/service/verification"; 2 | import { NextResponse } from "next/server"; 3 | import { get, update } from "./user"; 4 | 5 | export const verifyEmailToken = async (token: string) => { 6 | if (!token) { 7 | return NextResponse.json( 8 | { message: "Token is required" }, 9 | { 10 | status: 400, 11 | } 12 | ); 13 | } 14 | 15 | const { data } = verifyToken(token) as VerifyToken; 16 | const { email } = data; 17 | 18 | const user = await get({ email }); 19 | 20 | await update({ 21 | id: user.id, 22 | emailVerified: new Date(), 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/resetPassword/page.tsx: -------------------------------------------------------------------------------- 1 | import ResetPasswordForm from "components/client/ResetPasswordForm"; 2 | import Title from "components/client/Title"; 3 | import { Suspense } from "react"; 4 | 5 | export default function ResetPasswordPage() { 6 | return ( 7 | <> 8 | <Title text="Reset password" /> 9 | <Suspense fallback={<></>}> 10 | <ResetPasswordForm /> 11 | </Suspense> 12 | </> 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/service/email/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateEmailResponseSuccess, Resend } from "resend"; 2 | 3 | interface EmailParams { 4 | from?: string; 5 | to: string[]; 6 | subject: string; 7 | template: JSX.Element; 8 | } 9 | 10 | const resend = new Resend(process.env.RESEND_API_KEY); 11 | 12 | export const sendEmail = async ( 13 | params: EmailParams 14 | ): Promise<CreateEmailResponseSuccess | null> => { 15 | const { 16 | from = "Next Auth Template <no-reply@nextauth.dannyisadog.com>", 17 | to, 18 | subject, 19 | template, 20 | } = params; 21 | 22 | const { data, error } = await resend.emails.send({ 23 | from, 24 | to, 25 | subject, 26 | react: template, 27 | }); 28 | 29 | if (error) { 30 | throw new Error(error.message); 31 | } 32 | 33 | return data; 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/service/email/resetPassword.ts: -------------------------------------------------------------------------------- 1 | import ResetPasswordEmail from "components/email/ResetPassword"; 2 | import { sendEmail } from "."; 3 | import { generateResetPasswordToken } from "../resetPassword"; 4 | import { get } from "app/repository/user"; 5 | 6 | export const sendResetPasswordEmail = async (email: string) => { 7 | const subject = "Reset your password"; 8 | 9 | const resetPasswordToken = generateResetPasswordToken(email); 10 | 11 | const user = await get({ email }); 12 | 13 | await sendEmail({ 14 | to: [email], 15 | subject, 16 | template: ResetPasswordEmail({ 17 | firstName: user.name, 18 | token: resetPasswordToken, 19 | }), 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/service/email/verify.ts: -------------------------------------------------------------------------------- 1 | import VerifyEmail from "components/email/Verify"; 2 | import { sendEmail } from "."; 3 | import { generateVerificationToken } from "../verification"; 4 | import { get, update } from "app/repository/user"; 5 | 6 | export const sendVerificationEmail = async ( 7 | firstName: string, 8 | email: string 9 | ) => { 10 | const subject = "Verify your email"; 11 | 12 | const token = generateVerificationToken(email); 13 | 14 | try { 15 | sendEmail({ 16 | to: [email], 17 | subject, 18 | template: VerifyEmail({ 19 | firstName, 20 | token, 21 | }), 22 | }); 23 | } catch (error) { 24 | console.error(error); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/service/email/welcome.ts: -------------------------------------------------------------------------------- 1 | import SigninWelcomeEmail from "components/email/SigninWelcome"; 2 | import { sendEmail } from "."; 3 | 4 | export const sendWelcomeEmail = async (firstName: string, email: string) => { 5 | const subject = "Welcome to Next Auth Template"; 6 | 7 | try { 8 | sendEmail({ 9 | to: [email], 10 | subject, 11 | template: SigninWelcomeEmail({ firstName }), 12 | }); 13 | } catch (error) { 14 | console.error(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/service/resetPassword/index.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const JWT_SECRET = process.env.JWT_SECRET; 4 | const EXP = Math.floor(Date.now() / 1000) + 60 * 10; 5 | 6 | export interface VerifyResetPasswordToken { 7 | data: { 8 | email: string; 9 | }; 10 | exp: number; 11 | } 12 | 13 | export const generateResetPasswordToken = (email: string) => { 14 | return jwt.sign( 15 | { 16 | exp: EXP, 17 | data: { 18 | email, 19 | }, 20 | }, 21 | JWT_SECRET as string 22 | ); 23 | }; 24 | 25 | export const verifyResetPasswordToken = ( 26 | token: string 27 | ): VerifyResetPasswordToken => { 28 | try { 29 | return jwt.verify(token, JWT_SECRET as string) as VerifyResetPasswordToken; 30 | } catch (e) { 31 | throw new Error("Token is invalid or expired"); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/service/verification/index.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const JWT_SECRET = process.env.JWT_SECRET; 4 | const EXP = Math.floor(Date.now() / 1000) + 60 * 60; 5 | 6 | export interface VerifyToken { 7 | data: { 8 | email: string; 9 | }; 10 | exp: number; 11 | } 12 | 13 | export const generateVerificationToken = (email: string): string => { 14 | const token = jwt.sign( 15 | { 16 | exp: EXP, 17 | data: { 18 | email, 19 | }, 20 | }, 21 | JWT_SECRET as string 22 | ); 23 | return token; 24 | }; 25 | 26 | export const verifyToken = (token: string): VerifyToken | null => { 27 | try { 28 | return jwt.verify(token, JWT_SECRET as string) as VerifyToken; 29 | } catch (error) { 30 | throw new Error("Invalid token or token expired"); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "auth"; 2 | import GoogleSigninButton from "components/client/GoogleSigninButton"; 3 | import { SigninForm } from "components/client/SigninForm"; 4 | import { redirect } from "next/navigation"; 5 | import Title from "components/client/Title"; 6 | import GithubSigninButton from "components/client/GithubSigninButton"; 7 | import FacebookSigninButton from "components/client/FacebookSigninButton"; 8 | import LineSigninButton from "components/client/LineSigninButton"; 9 | 10 | export default async function Signin() { 11 | const session = await auth(); 12 | 13 | if (session) { 14 | redirect("/"); 15 | } 16 | 17 | return ( 18 | <> 19 | <Title text="Next.js Auth Template" /> 20 | <SigninForm /> 21 | <GoogleSigninButton /> 22 | <GithubSigninButton /> 23 | <FacebookSigninButton /> 24 | <LineSigninButton /> 25 | </> 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/verification/Verify.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter, useSearchParams } from "next/navigation"; 4 | import { useCallback, useEffect } from "react"; 5 | 6 | interface VerifyProps { 7 | setError: (error: string) => void; 8 | } 9 | 10 | export default function Verify(props: VerifyProps) { 11 | const { setError } = props; 12 | 13 | const searchParams = useSearchParams(); 14 | const router = useRouter(); 15 | 16 | const token = searchParams.get("token"); 17 | 18 | if (!token) { 19 | router.push("/signin"); 20 | } 21 | 22 | const verify = useCallback(async () => { 23 | const response = await fetch("/api/verify", { 24 | method: "POST", 25 | body: JSON.stringify({ token }), 26 | }); 27 | 28 | if (response.ok) { 29 | router.push("/signin"); 30 | } else { 31 | const data = await response.json(); 32 | setError(data.message); 33 | } 34 | }, [router, token, setError]); 35 | 36 | useEffect(() => { 37 | verify(); 38 | }, [token, verify]); 39 | 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/verification/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Alert, CircularProgress, Stack } from "@mui/material"; 4 | import Title from "components/client/Title"; 5 | import Verify from "./Verify"; 6 | import { Suspense, useState } from "react"; 7 | 8 | export default function VerificationPage() { 9 | const [error, setError] = useState(""); 10 | return ( 11 | <> 12 | <Title text="Verifying..." /> 13 | <Stack 14 | direction="row" 15 | justifyContent="center" 16 | sx={{ 17 | pt: 4, 18 | }} 19 | > 20 | {!error && <CircularProgress />} 21 | {error && <Alert severity="error">{error}</Alert>} 22 | </Stack> 23 | <Suspense fallback={<></>}> 24 | <Verify setError={setError} /> 25 | </Suspense> 26 | </> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { 2 | NextAuthConfig, 3 | Session, 4 | User as NextAuthUser, 5 | } from "next-auth"; 6 | import Google from "next-auth/providers/google"; 7 | import GitHub from "next-auth/providers/github"; 8 | import Faacebook from "next-auth/providers/facebook"; 9 | import Line from "next-auth/providers/line"; 10 | import Credentials from "next-auth/providers/credentials"; 11 | import bcrypt from "bcryptjs"; 12 | import { create as createUser, get as getUser } from "app/repository/user"; 13 | import { PrismaAdapter } from "@auth/prisma-adapter"; 14 | import { PrismaClient, User as PrismaUser } from "@prisma/client"; 15 | import { sendWelcomeEmail } from "app/service/email/welcome"; 16 | import { sendVerificationEmail } from "app/service/email/verify"; 17 | 18 | const prisma = new PrismaClient(); 19 | 20 | export type CustomSession = { 21 | authUser: PrismaUser; 22 | } & Session; 23 | 24 | export const { handlers, signIn, signOut, auth } = NextAuth({ 25 | adapter: PrismaAdapter(prisma), 26 | session: { strategy: "jwt" }, 27 | pages: { 28 | signIn: "/signin", 29 | }, 30 | providers: [ 31 | Google({ 32 | allowDangerousEmailAccountLinking: true, 33 | }), 34 | GitHub({ 35 | allowDangerousEmailAccountLinking: true, 36 | }), 37 | Faacebook({ 38 | allowDangerousEmailAccountLinking: true, 39 | }), 40 | Line({ 41 | allowDangerousEmailAccountLinking: true, 42 | checks: ["state"], 43 | }), 44 | Credentials({ 45 | async authorize({ email, password }) { 46 | const user = await getUser({ 47 | email: email as string, 48 | }); 49 | 50 | if (!user || !user.password) { 51 | return null; 52 | } 53 | 54 | const match = await bcrypt.compare(password as string, user.password); 55 | 56 | if (match) { 57 | return user as unknown as NextAuthUser; 58 | } 59 | 60 | return null; 61 | }, 62 | }), 63 | ], 64 | callbacks: { 65 | async session({ session }) { 66 | const dbUser = await getUser({ email: session.user.email as string }); 67 | 68 | const newSession = session as unknown as CustomSession; 69 | newSession.authUser = dbUser; 70 | 71 | return session; 72 | }, 73 | async signIn({ user }) { 74 | if (!user) { 75 | return false; 76 | } 77 | 78 | let userExists = null; 79 | 80 | try { 81 | userExists = await getUser({ email: user.email as string }); 82 | } catch (e) { 83 | console.error(e); 84 | } 85 | 86 | if (userExists) { 87 | return true; 88 | } 89 | 90 | await createUser({ 91 | email: user.email as string, 92 | name: user.name as string, 93 | image: user.image as string, 94 | }); 95 | 96 | sendWelcomeEmail(user.name as string, user.email as string); 97 | sendVerificationEmail(user.name as string, user.email as string); 98 | 99 | return true; 100 | }, 101 | }, 102 | } satisfies NextAuthConfig); 103 | -------------------------------------------------------------------------------- /src/components/client/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button as MuiButton, 3 | ButtonProps as MuiButtonProps, 4 | } from "@mui/material"; 5 | 6 | import CircularProgress from "@mui/material/CircularProgress"; 7 | 8 | type ButtonProps = { 9 | isLoading?: boolean; 10 | } & MuiButtonProps; 11 | 12 | export default function Button(props: ButtonProps) { 13 | const { children, isLoading, ...rest } = props; 14 | 15 | return ( 16 | <MuiButton 17 | sx={{ 18 | minHeight: 48, 19 | }} 20 | disabled={isLoading} 21 | {...rest} 22 | > 23 | {isLoading && <CircularProgress size={20} thickness={6} />} 24 | {!isLoading && children} 25 | </MuiButton> 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/client/FacebookSigninButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import Button from "./Button"; 5 | import { Stack, Typography } from "@mui/material"; 6 | import { useState } from "react"; 7 | import Image from "next/image"; 8 | 9 | export default function FacebookSigninButton() { 10 | const [loading, setLoading] = useState(false); 11 | 12 | return ( 13 | <Button 14 | isLoading={loading} 15 | fullWidth 16 | variant="outlined" 17 | onClick={async () => { 18 | setLoading(true); 19 | await signIn("facebook"); 20 | }} 21 | > 22 | <Stack direction="row" spacing={1}> 23 | <Image 24 | src="/ic-facebook.png" 25 | alt="Facebook" 26 | width={24} 27 | height={24} 28 | priority 29 | /> 30 | <Typography>Sign in with Facebook</Typography> 31 | </Stack> 32 | </Button> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/client/GithubSigninButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import Button from "./Button"; 5 | import { Stack, Typography } from "@mui/material"; 6 | import { useState } from "react"; 7 | import Image from "next/image"; 8 | 9 | export default function GithubSigninButton() { 10 | const [loading, setLoading] = useState(false); 11 | 12 | return ( 13 | <Button 14 | isLoading={loading} 15 | fullWidth 16 | variant="outlined" 17 | onClick={async () => { 18 | setLoading(true); 19 | await signIn("github"); 20 | }} 21 | > 22 | <Stack direction="row" spacing={1}> 23 | <Image 24 | src="/ic-github.png" 25 | alt="Github" 26 | width={24} 27 | height={24} 28 | priority 29 | /> 30 | <Typography>Sign in with Github</Typography> 31 | </Stack> 32 | </Button> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/client/GoogleSigninButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import Button from "./Button"; 5 | import { Stack, Typography } from "@mui/material"; 6 | import { useState } from "react"; 7 | import Image from "next/image"; 8 | 9 | export default function GoogleSigninButton() { 10 | const [loading, setLoading] = useState(false); 11 | 12 | return ( 13 | <Button 14 | isLoading={loading} 15 | fullWidth 16 | variant="outlined" 17 | onClick={async () => { 18 | setLoading(true); 19 | await signIn("google"); 20 | }} 21 | > 22 | <Stack direction="row" spacing={1}> 23 | <Image 24 | src="/ic-google.png" 25 | alt="Google" 26 | width={24} 27 | height={24} 28 | priority 29 | /> 30 | <Typography>Sign in with Google</Typography> 31 | </Stack> 32 | </Button> 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/client/LineSigninButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import Button from "./Button"; 5 | import { Stack, Typography } from "@mui/material"; 6 | import { useState } from "react"; 7 | import Image from "next/image"; 8 | 9 | export default function LineSigninButton() { 10 | const [loading, setLoading] = useState(false); 11 | 12 | return ( 13 | <Button 14 | isLoading={loading} 15 | fullWidth 16 | variant="outlined" 17 | onClick={async () => { 18 | setLoading(true); 19 | await signIn("line"); 20 | }} 21 | > 22 | <Stack direction="row" spacing={1}> 23 | <Image src="/ic-line.png" alt="Line" width={24} height={24} priority /> 24 | <Typography>Sign in with Line</Typography> 25 | </Stack> 26 | </Button> 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/client/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Alert, Box, Stack, Typography } from "@mui/material"; 4 | import TextField from "./TextField"; 5 | import Button from "./Button"; 6 | import { useState } from "react"; 7 | import CheckIcon from "@mui/icons-material/Check"; 8 | import Link from "next/link"; 9 | 10 | export default function RegisterForm() { 11 | const [username, setUsername] = useState(""); 12 | const [email, setEmail] = useState(""); 13 | const [password, setPassword] = useState(""); 14 | const [passwordConfirm, setPasswordConfirm] = useState(""); 15 | const [error, setError] = useState(""); 16 | const [loading, setLoading] = useState(false); 17 | const [success, setSuccess] = useState(false); 18 | 19 | return ( 20 | <form 21 | onSubmit={async (e) => { 22 | setLoading(true); 23 | e.preventDefault(); 24 | 25 | if (username.length < 3) { 26 | setError("Username must be at least 3 characters"); 27 | setLoading(false); 28 | return; 29 | } 30 | 31 | if (!email.includes("@")) { 32 | setError("Invalid email"); 33 | setLoading(false); 34 | return; 35 | } 36 | 37 | if (password.length < 8) { 38 | setError("Password must be at least 8 characters"); 39 | setLoading(false); 40 | return; 41 | } 42 | 43 | if (password !== passwordConfirm) { 44 | setError("Passwords do not match"); 45 | setLoading(false); 46 | return; 47 | } 48 | setError(""); 49 | 50 | const result = await fetch(`/api/register`, { 51 | method: "POST", 52 | body: JSON.stringify({ 53 | name: username, 54 | email, 55 | password, 56 | passwordConfirm, 57 | }), 58 | }); 59 | 60 | setLoading(false); 61 | 62 | if (result.ok) { 63 | setSuccess(true); 64 | setUsername(""); 65 | setEmail(""); 66 | setPassword(""); 67 | setPasswordConfirm(""); 68 | } else { 69 | setError("Error creating account"); 70 | } 71 | }} 72 | > 73 | <Stack spacing={2}> 74 | {error && <Alert severity="error">{error}</Alert>} 75 | {success && ( 76 | <Alert icon={<CheckIcon fontSize="inherit" />} severity="success"> 77 | <Stack direction="row" spacing={0.5}> 78 | <Typography variant="body2"> 79 | Account created successfully 80 | </Typography> 81 | <Box color="primary.dark"> 82 | <Link href="/signin">/signin</Link> 83 | </Box> 84 | </Stack> 85 | </Alert> 86 | )} 87 | <TextField 88 | value={username} 89 | fullWidth 90 | type="text" 91 | name="username" 92 | placeholder="username" 93 | onChange={(e) => { 94 | setUsername(e.target.value); 95 | }} 96 | /> 97 | <TextField 98 | value={email} 99 | fullWidth 100 | type="email" 101 | name="email" 102 | placeholder="email" 103 | onChange={(e) => { 104 | setEmail(e.target.value); 105 | }} 106 | /> 107 | <TextField 108 | value={password} 109 | fullWidth 110 | type="password" 111 | name="password" 112 | placeholder="password" 113 | onChange={(e) => { 114 | setPassword(e.target.value); 115 | }} 116 | /> 117 | <TextField 118 | value={passwordConfirm} 119 | fullWidth 120 | type="password" 121 | name="password-confirm" 122 | placeholder="password-confirm" 123 | onChange={(e) => { 124 | setPasswordConfirm(e.target.value); 125 | }} 126 | /> 127 | <Button isLoading={loading} fullWidth type="submit" variant="contained"> 128 | <Typography>Register</Typography> 129 | </Button> 130 | </Stack> 131 | </form> 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /src/components/client/ResetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Alert, Box, Stack, TextField, Typography } from "@mui/material"; 4 | import { useState } from "react"; 5 | import Button from "./Button"; 6 | import { useSearchParams } from "next/navigation"; 7 | import CheckIcon from "@mui/icons-material/Check"; 8 | import Link from "next/link"; 9 | 10 | export default function ResetPasswordForm() { 11 | const [password, setPassword] = useState(""); 12 | const [passwordConfirm, setPasswordConfirm] = useState(""); 13 | const [loading, setLoading] = useState(false); 14 | const [error, setError] = useState(""); 15 | const [success, setSuccess] = useState(false); 16 | const searchParams = useSearchParams(); 17 | 18 | const token = searchParams.get("token"); 19 | 20 | return ( 21 | <form 22 | onSubmit={async (e) => { 23 | e.preventDefault(); 24 | 25 | if (!password) { 26 | setError("password is required"); 27 | return; 28 | } 29 | 30 | if (password.length < 8) { 31 | setError("Password must be at least 8 characters"); 32 | setLoading(false); 33 | return; 34 | } 35 | 36 | if (!passwordConfirm) { 37 | setError("password-confirm is required"); 38 | return; 39 | } 40 | 41 | if (password !== passwordConfirm) { 42 | setError("Passwords do not match"); 43 | return; 44 | } 45 | 46 | setError(""); 47 | setLoading(true); 48 | 49 | const res = await fetch("/api/users/resetPassword", { 50 | method: "POST", 51 | body: JSON.stringify({ 52 | token, 53 | password, 54 | passwordConfirm, 55 | }), 56 | }); 57 | 58 | if (res.ok) { 59 | setSuccess(true); 60 | setLoading(false); 61 | } else { 62 | const data = await res.json(); 63 | setError(data.message); 64 | setLoading(false); 65 | } 66 | }} 67 | > 68 | <Stack spacing={2}> 69 | {error && ( 70 | <Alert severity="error"> 71 | <Stack direction="row" spacing={0.5}> 72 | <Typography variant="body2">{error}</Typography> 73 | <Box color="primary.dark"> 74 | <Link href="/forgotPassword">/resend</Link> 75 | </Box> 76 | </Stack> 77 | </Alert> 78 | )} 79 | {success && ( 80 | <Alert icon={<CheckIcon fontSize="inherit" />} severity="success"> 81 | <Stack direction="row" spacing={0.5}> 82 | <Typography variant="body2"> 83 | Password reset successfully 84 | </Typography> 85 | <Box color="primary.dark"> 86 | <Link href="/signin">/signin</Link> 87 | </Box> 88 | </Stack> 89 | </Alert> 90 | )} 91 | <TextField 92 | value={password} 93 | fullWidth 94 | type="password" 95 | name="password" 96 | placeholder="password" 97 | onChange={(e) => { 98 | setPassword(e.target.value); 99 | }} 100 | /> 101 | <TextField 102 | value={passwordConfirm} 103 | fullWidth 104 | type="password" 105 | name="password-confirm" 106 | placeholder="password-confirm" 107 | onChange={(e) => { 108 | setPasswordConfirm(e.target.value); 109 | }} 110 | /> 111 | <Button isLoading={loading} fullWidth type="submit" variant="contained"> 112 | <Typography>Reset Password</Typography> 113 | </Button> 114 | </Stack> 115 | </form> 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/components/client/SendForgotPasswordLinkForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Alert, Stack, Typography } from "@mui/material"; 4 | import TextField from "./TextField"; 5 | import { useState } from "react"; 6 | import Button from "./Button"; 7 | 8 | export default function SendForgotPasswordLinkForm() { 9 | const [email, setEmail] = useState(""); 10 | const [loading, setLoading] = useState(false); 11 | const [success, setSuccess] = useState(false); 12 | const [error, setError] = useState(""); 13 | 14 | return ( 15 | <form 16 | onSubmit={async (e) => { 17 | e.preventDefault(); 18 | 19 | if (!email) { 20 | setError("email is required"); 21 | return; 22 | } 23 | 24 | setLoading(true); 25 | 26 | const response = await fetch("/api/users/sendResetPasswordEmail", { 27 | method: "POST", 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | body: JSON.stringify({ email }), 32 | }); 33 | 34 | if (response.ok) { 35 | setError(""); 36 | setLoading(false); 37 | setEmail(""); 38 | setSuccess(true); 39 | } else { 40 | const data = await response.json(); 41 | setError(data.message); 42 | setLoading(false); 43 | } 44 | }} 45 | > 46 | <Stack spacing={2}> 47 | {error && <Alert severity="error">{error}</Alert>} 48 | {success && <Alert severity="success">Email sent</Alert>} 49 | <TextField 50 | fullWidth 51 | value={email} 52 | type="email" 53 | name="email" 54 | placeholder="email" 55 | onChange={(e) => { 56 | setEmail(e.target.value); 57 | }} 58 | /> 59 | <Button isLoading={loading} fullWidth type="submit" variant="contained"> 60 | <Typography>Send Reset Password Link</Typography> 61 | </Button> 62 | </Stack> 63 | </form> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/client/SigninForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import { useRouter } from "next/navigation"; 5 | import { useState } from "react"; 6 | import Button from "./Button"; 7 | import TextField from "./TextField"; 8 | import { Alert, Stack, Typography } from "@mui/material"; 9 | import Link from "next/link"; 10 | 11 | export function SigninForm() { 12 | const [email, setEmail] = useState(""); 13 | const [password, setPassword] = useState(""); 14 | const [error, setError] = useState(""); 15 | 16 | const router = useRouter(); 17 | 18 | const [loading, setLoading] = useState(false); 19 | 20 | return ( 21 | <form 22 | method="post" 23 | onSubmit={async (e) => { 24 | setLoading(true); 25 | e.preventDefault(); 26 | 27 | if (!email) { 28 | setError("email is required"); 29 | setLoading(false); 30 | return; 31 | } 32 | 33 | if (!email.includes("@")) { 34 | setError("email is not valid"); 35 | setLoading(false); 36 | return; 37 | } 38 | 39 | if (!password) { 40 | setError("password is required"); 41 | setLoading(false); 42 | return; 43 | } 44 | 45 | const result = await signIn("credentials", { 46 | callbackUrl: "/", 47 | email, 48 | password, 49 | redirect: false, 50 | }); 51 | 52 | if (result?.error) { 53 | setError("email or password is incorrect"); 54 | } 55 | 56 | if (!result?.error) { 57 | router.push("/"); 58 | } 59 | 60 | setLoading(false); 61 | }} 62 | > 63 | <Stack spacing={2}> 64 | {error && <Alert severity="error">{error}</Alert>} 65 | <TextField 66 | fullWidth 67 | type="email" 68 | name="email" 69 | placeholder="email" 70 | onChange={(e) => { 71 | setEmail(e.target.value); 72 | }} 73 | /> 74 | <TextField 75 | fullWidth 76 | type="password" 77 | name="password" 78 | placeholder="password" 79 | onChange={(e) => { 80 | setPassword(e.target.value); 81 | }} 82 | /> 83 | <Stack direction="row" justifyContent="space-between"> 84 | <Link href="/forgotPassword"> 85 | <Typography variant="body2" color="primary"> 86 | Forgot password? 87 | </Typography> 88 | </Link> 89 | <Link href="/register"> 90 | <Typography variant="body2" color="primary"> 91 | Register 92 | </Typography> 93 | </Link> 94 | </Stack> 95 | <Button isLoading={loading} fullWidth type="submit" variant="contained"> 96 | <Typography>Sign in</Typography> 97 | </Button> 98 | </Stack> 99 | </form> 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/client/SignoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { signOut } from "next-auth/react"; 3 | import Button from "./Button"; 4 | import { useState } from "react"; 5 | 6 | export default function SignoutButton() { 7 | const [loading, setLoading] = useState(false); 8 | return ( 9 | <Button 10 | isLoading={loading} 11 | variant="contained" 12 | onClick={() => { 13 | setLoading(true); 14 | signOut(); 15 | }} 16 | > 17 | Sign out 18 | </Button> 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/client/TextField/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | TextField as MuiTextField, 3 | TextFieldProps as MuiTextFieldProps, 4 | } from "@mui/material"; 5 | 6 | type TextFieldProps = MuiTextFieldProps; 7 | 8 | export default function TextField(props: TextFieldProps) { 9 | const { ...rest } = props; 10 | return ( 11 | <MuiTextField 12 | sx={{ 13 | "&.MuiTextField-root": { 14 | borderRadius: 1, 15 | backgroundColor: "transparent", 16 | }, 17 | }} 18 | {...rest} 19 | /> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/client/Title.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Stack, Typography } from "@mui/material"; 2 | import Link from "next/link"; 3 | import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; 4 | import GitHubIcon from "@mui/icons-material/GitHub"; 5 | 6 | interface TitleProps { 7 | text: string; 8 | hasGoBack?: boolean; 9 | showGithub?: boolean; 10 | } 11 | 12 | export default function Title(props: TitleProps) { 13 | const { text, hasGoBack, showGithub = true } = props; 14 | return ( 15 | <Stack justifyContent="center"> 16 | <Stack spacing={2}> 17 | <Stack direction="row" justifyContent="space-between"> 18 | {hasGoBack ? ( 19 | <Link href="/signin"> 20 | <ArrowBackIosNewIcon /> 21 | </Link> 22 | ) : ( 23 | <Box /> 24 | )} 25 | {showGithub && ( 26 | <Link 27 | href="https://github.com/Dannyisadog/nextjs-authjs-template" 28 | target="_blank" 29 | > 30 | <GitHubIcon /> 31 | </Link> 32 | )} 33 | </Stack> 34 | <Typography variant="h4">{text}</Typography> 35 | </Stack> 36 | </Stack> 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/client/UploadAvatarDialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | Input, 7 | } from "@mui/material"; 8 | import Button from "./Button"; 9 | import { useProvider } from "providers/Provider"; 10 | import Image from "next/image"; 11 | import multiavatar from "@multiavatar/multiavatar/esm"; 12 | import { useRef, useState } from "react"; 13 | 14 | interface UploadAvatarDialogProps { 15 | open: boolean; 16 | onClose: () => void; 17 | } 18 | 19 | export const UploadAvatarDialog = (props: UploadAvatarDialogProps) => { 20 | const { open, onClose } = props; 21 | 22 | const { session, updateSession, updateUsers } = useProvider(); 23 | const { user } = session; 24 | const [uploading, setUploading] = useState(false); 25 | const [saving, setSaving] = useState(false); 26 | 27 | const imageRef = useRef(null); 28 | const [image, setImage] = useState<string>(""); 29 | 30 | if (!user) { 31 | return null; 32 | } 33 | 34 | const close = () => { 35 | onClose(); 36 | setTimeout(() => { 37 | setImage(""); 38 | imageRef.current = null; 39 | }, 500); 40 | }; 41 | 42 | const upload = async () => { 43 | if (!imageRef.current) { 44 | return; 45 | } 46 | 47 | const file = (imageRef.current as any).files[0]; 48 | 49 | if (!file) { 50 | return; 51 | } 52 | 53 | setUploading(true); 54 | 55 | const formData = new FormData(); 56 | formData.append("image", file); 57 | 58 | const res = await fetch("/api/users/avatar", { 59 | method: "POST", 60 | body: formData, 61 | }); 62 | 63 | const data = await res.json(); 64 | setImage(data.url); 65 | 66 | setUploading(false); 67 | }; 68 | 69 | const updateUserAvatar = async () => { 70 | setSaving(true); 71 | await fetch("/api/users/avatar", { 72 | method: "PUT", 73 | body: JSON.stringify({ 74 | image, 75 | }), 76 | }); 77 | setSaving(false); 78 | close(); 79 | updateSession(); 80 | updateUsers(); 81 | }; 82 | 83 | return ( 84 | <Dialog 85 | open={open} 86 | onClose={close} 87 | sx={{ 88 | p: 2, 89 | }} 90 | > 91 | {!image && ( 92 | <> 93 | <DialogContent> 94 | {user.image && ( 95 | <Image 96 | src={user.image} 97 | width={240} 98 | height={240} 99 | alt={`Avatar for ${user.name}`} 100 | style={{ 101 | objectFit: "cover", 102 | borderRadius: "50%", 103 | }} 104 | /> 105 | )} 106 | {!user.image && ( 107 | <Box 108 | dangerouslySetInnerHTML={{ 109 | __html: multiavatar(user.email as string), 110 | }} 111 | width={240} 112 | height={240} 113 | /> 114 | )} 115 | </DialogContent> 116 | <DialogActions> 117 | <Button onClick={close}>Cancel</Button> 118 | <Button 119 | variant="contained" 120 | component="label" 121 | autoFocus 122 | isLoading={uploading} 123 | > 124 | Upload 125 | <input ref={imageRef} type="file" hidden onChange={upload} /> 126 | </Button> 127 | </DialogActions> 128 | </> 129 | )} 130 | {image && ( 131 | <> 132 | <DialogContent> 133 | <Image 134 | src={image} 135 | alt="New Avatar" 136 | width={240} 137 | height={240} 138 | style={{ 139 | objectFit: "cover", 140 | borderRadius: "50%", 141 | }} 142 | /> 143 | </DialogContent> 144 | <DialogActions> 145 | <Button onClick={close}>Cancel</Button> 146 | <Button 147 | variant="outlined" 148 | component="label" 149 | autoFocus 150 | isLoading={uploading} 151 | > 152 | Reselect 153 | <input ref={imageRef} type="file" hidden onChange={upload} /> 154 | </Button> 155 | <Button 156 | variant="contained" 157 | onClick={updateUserAvatar} 158 | isLoading={saving} 159 | > 160 | Save 161 | </Button> 162 | </DialogActions> 163 | </> 164 | )} 165 | </Dialog> 166 | ); 167 | }; 168 | -------------------------------------------------------------------------------- /src/components/client/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Box } from "@mui/material"; 4 | import multiavatar from "@multiavatar/multiavatar/esm"; 5 | import { User } from "next-auth"; 6 | import Image from "next/image"; 7 | import EditIcon from "@mui/icons-material/Edit"; 8 | import { useDisclosure } from "hooks/useDisclosure"; 9 | import { UploadAvatarDialog } from "./UploadAvatarDialog"; 10 | import { useProvider } from "providers/Provider"; 11 | 12 | const UserAvatar = () => { 13 | const { session } = useProvider(); 14 | 15 | const { authUser } = session; 16 | 17 | const { isOpen, onClose, onOpen } = useDisclosure(); 18 | 19 | if (!authUser) { 20 | return null; 21 | } 22 | 23 | const svgCode = multiavatar(authUser.email as string); 24 | 25 | return ( 26 | <Box position="relative" width={64}> 27 | {!authUser.image && ( 28 | <Box 29 | dangerouslySetInnerHTML={{ __html: svgCode }} 30 | width={64} 31 | height={64} 32 | /> 33 | )} 34 | {authUser.image && ( 35 | <Box width={64} height={64} borderRadius={100} overflow="hidden"> 36 | <Image 37 | src={authUser.image} 38 | width={64} 39 | height={64} 40 | alt={`Avatar for ${authUser.name}`} 41 | style={{ 42 | objectFit: "cover", 43 | }} 44 | /> 45 | </Box> 46 | )} 47 | <Box 48 | sx={{ 49 | width: 24, 50 | height: 24, 51 | borderRadius: 100, 52 | position: "absolute", 53 | bottom: 0, 54 | right: 0, 55 | cursor: "pointer", 56 | backgroundColor: "#333333aa", 57 | display: "flex", 58 | justifyContent: "center", 59 | alignItems: "center", 60 | }} 61 | onClick={onOpen} 62 | > 63 | <EditIcon 64 | sx={{ 65 | fontSize: 14, 66 | color: "white", 67 | }} 68 | /> 69 | </Box> 70 | <UploadAvatarDialog open={isOpen} onClose={onClose} /> 71 | </Box> 72 | ); 73 | }; 74 | 75 | export default UserAvatar; 76 | -------------------------------------------------------------------------------- /src/components/client/UserList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Avatar, 5 | Box, 6 | List, 7 | ListItem, 8 | ListItemAvatar, 9 | ListItemText, 10 | } from "@mui/material"; 11 | import multiavatar from "@multiavatar/multiavatar/esm"; 12 | import { useProvider } from "providers/Provider"; 13 | 14 | export default function UserList() { 15 | const { users } = useProvider(); 16 | 17 | return ( 18 | <List> 19 | {users.map((user) => { 20 | const svgCode = multiavatar(user.email as string); 21 | return ( 22 | <ListItem key={user.id}> 23 | <ListItemAvatar> 24 | {!user.image && ( 25 | <Avatar> 26 | <Box 27 | dangerouslySetInnerHTML={{ __html: svgCode }} 28 | width="100%" 29 | height="100%" 30 | /> 31 | </Avatar> 32 | )} 33 | {user.image && <Avatar src={user.image} />} 34 | </ListItemAvatar> 35 | <ListItemText primary={user.name} secondary={user.email} /> 36 | </ListItem> 37 | ); 38 | })} 39 | </List> 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/email/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | interface ResetPasswordEmailProps { 3 | firstName: string; 4 | token: string; 5 | } 6 | 7 | const url = process.env.URL; 8 | 9 | export const ResetPasswordEmail = (props: ResetPasswordEmailProps) => { 10 | const { firstName, token } = props; 11 | return ( 12 | <html> 13 | <body style={main}> 14 | <div style={container}> 15 | <img 16 | src={`https://nextauth.dannyisadog.com/logo.png`} 17 | width="50" 18 | height="50" 19 | alt="nextjs-authjs-template" 20 | style={logo} 21 | /> 22 | <p style={paragraph}>Hi {firstName},</p> 23 | <p style={paragraph}> 24 | We received a request to reset your password. If you didn{"'"}t make 25 | the request, you can ignore this email. 26 | </p> 27 | <section style={btnContainer}> 28 | <a style={button} href={`${url}/resetPassword?token=${token}`}> 29 | Reset password 30 | </a> 31 | </section> 32 | <p style={paragraph}> 33 | Best, 34 | <br /> 35 | Nextjs Authjs Template Team 36 | </p> 37 | </div> 38 | </body> 39 | </html> 40 | ); 41 | }; 42 | 43 | ResetPasswordEmail.PreviewProps = { 44 | firstName: "", 45 | } as ResetPasswordEmailProps; 46 | 47 | export default ResetPasswordEmail; 48 | 49 | const main = { 50 | backgroundColor: "#ffffff", 51 | fontFamily: 52 | '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', 53 | }; 54 | 55 | const container = { 56 | maxWidth: "37.5em", 57 | margin: "0 auto", 58 | padding: "20px 0 48px", 59 | }; 60 | 61 | const logo = { 62 | margin: "0 auto", 63 | }; 64 | 65 | const paragraph = { 66 | fontSize: "16px", 67 | lineHeight: "26px", 68 | }; 69 | 70 | const btnContainer = { 71 | textAlign: "center" as const, 72 | }; 73 | 74 | const button = { 75 | backgroundColor: "#0773f7", 76 | borderRadius: "3px", 77 | color: "#fff", 78 | fontSize: "16px", 79 | textDecoration: "none", 80 | textAlign: "center" as const, 81 | display: "block", 82 | padding: "12px", 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/email/SigninWelcome.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | interface SigninWelcomeEmailProps { 3 | firstName: string; 4 | } 5 | 6 | export const SigninWelcomeEmail = (props: SigninWelcomeEmailProps) => { 7 | const { firstName } = props; 8 | return ( 9 | <html> 10 | <body style={main}> 11 | <div style={container}> 12 | <img 13 | src={`https://nextauth.dannyisadog.com/logo.png`} 14 | width="50" 15 | height="50" 16 | alt="nextjs-authjs-template" 17 | style={logo} 18 | /> 19 | <p style={paragraph}>Hi {firstName},</p> 20 | <p style={paragraph}> 21 | Welcome to the Next.js Auth.js Template! 22 | <br /> 23 | We{"'"}re thrilled to have you with us! 24 | </p> 25 | <section style={btnContainer}> 26 | <a 27 | style={button} 28 | href="https://github.com/Dannyisadog/nextjs-authjs-template" 29 | > 30 | Get started 31 | </a> 32 | </section> 33 | <p style={paragraph}> 34 | Best, 35 | <br /> 36 | Nextjs Authjs Template Team 37 | </p> 38 | </div> 39 | </body> 40 | </html> 41 | ); 42 | }; 43 | 44 | SigninWelcomeEmail.PreviewProps = { 45 | firstName: "", 46 | } as SigninWelcomeEmailProps; 47 | 48 | export default SigninWelcomeEmail; 49 | 50 | const main = { 51 | backgroundColor: "#ffffff", 52 | fontFamily: 53 | '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', 54 | }; 55 | 56 | const container = { 57 | maxWidth: "37.5em", 58 | margin: "0 auto", 59 | padding: "20px 0 48px", 60 | }; 61 | 62 | const logo = { 63 | margin: "0 auto", 64 | }; 65 | 66 | const paragraph = { 67 | fontSize: "16px", 68 | lineHeight: "26px", 69 | }; 70 | 71 | const btnContainer = { 72 | textAlign: "center" as const, 73 | }; 74 | 75 | const button = { 76 | backgroundColor: "#0773f7", 77 | borderRadius: "3px", 78 | color: "#fff", 79 | fontSize: "16px", 80 | textDecoration: "none", 81 | textAlign: "center" as const, 82 | display: "block", 83 | padding: "12px", 84 | }; 85 | -------------------------------------------------------------------------------- /src/components/email/Verify.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | interface VerifyEmailProps { 3 | firstName: string; 4 | token: string; 5 | } 6 | 7 | export const VerifyEmail = (props: VerifyEmailProps) => { 8 | const { firstName, token } = props; 9 | const url = process.env.URL; 10 | return ( 11 | <html> 12 | <body style={main}> 13 | <div style={container}> 14 | <img 15 | src={`https://nextauth.dannyisadog.com/logo.png`} 16 | width="50" 17 | height="50" 18 | alt="nextjs-authjs-template" 19 | style={logo} 20 | /> 21 | <p style={paragraph}>Hi {firstName},</p> 22 | <p style={paragraph}> 23 | Welcome to the Next.js Auth.js Template! 24 | <br /> 25 | Please verify your email by clicking the button below. 26 | </p> 27 | <section style={btnContainer}> 28 | <a style={button} href={`${url}/verification?token=${token}`}> 29 | Verify email 30 | </a> 31 | </section> 32 | <p style={paragraph}> 33 | Best, 34 | <br /> 35 | Next.js Authentication Template Team 36 | </p> 37 | </div> 38 | </body> 39 | </html> 40 | ); 41 | }; 42 | 43 | VerifyEmail.PreviewProps = { 44 | firstName: "", 45 | } as VerifyEmailProps; 46 | 47 | export default VerifyEmail; 48 | 49 | const main = { 50 | backgroundColor: "#ffffff", 51 | fontFamily: 52 | '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', 53 | }; 54 | 55 | const container = { 56 | maxWidth: "37.5em", 57 | margin: "0 auto", 58 | padding: "20px 0 48px", 59 | }; 60 | 61 | const logo = { 62 | margin: "0 auto", 63 | }; 64 | 65 | const paragraph = { 66 | fontSize: "16px", 67 | lineHeight: "26px", 68 | }; 69 | 70 | const btnContainer = { 71 | textAlign: "center" as const, 72 | }; 73 | 74 | const button = { 75 | backgroundColor: "#0773f7", 76 | borderRadius: "3px", 77 | color: "#fff", 78 | fontSize: "16px", 79 | textDecoration: "none", 80 | textAlign: "center" as const, 81 | display: "block", 82 | padding: "12px", 83 | }; 84 | -------------------------------------------------------------------------------- /src/hooks/useDisclosure.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useDisclosure = () => { 4 | const [isOpen, setIsOpen] = useState(false); 5 | 6 | const onOpen = () => setIsOpen(true); 7 | const onClose = () => setIsOpen(false); 8 | const onToggle = () => setIsOpen(!isOpen); 9 | 10 | return { 11 | isOpen, 12 | onOpen, 13 | onClose, 14 | onToggle, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/providers/Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext, useEffect, useState } from "react"; 4 | import { ProviderContextType } from "./constants"; 5 | import { Session, User } from "next-auth"; 6 | import { getSession } from "next-auth/react"; 7 | import { CustomSession } from "auth"; 8 | 9 | interface ProviderProps { 10 | children: React.ReactNode; 11 | session: CustomSession; 12 | } 13 | 14 | const ProviderContext = createContext<ProviderContextType>( 15 | {} as ProviderContextType 16 | ); 17 | 18 | const apiUrl = process.env.NEXT_PUBLIC_API_URL; 19 | 20 | const Provider = (props: ProviderProps) => { 21 | const { children, session } = props; 22 | 23 | const [currentSession, setCurrentSession] = useState<CustomSession>(session); 24 | 25 | const [users, setUsers] = useState<User[]>([]); 26 | 27 | const updateSession = async () => { 28 | const newSession = (await getSession()) as CustomSession; 29 | 30 | if (!newSession) return; 31 | 32 | setCurrentSession(newSession); 33 | }; 34 | 35 | const updateUsers = async () => { 36 | const response = await fetch(`${apiUrl}/users`); 37 | const data = await response.json(); 38 | setUsers(data); 39 | }; 40 | 41 | useEffect(() => { 42 | updateUsers(); 43 | }, []); 44 | 45 | return ( 46 | <ProviderContext.Provider 47 | value={{ 48 | session: currentSession, 49 | updateSession, 50 | users, 51 | updateUsers, 52 | }} 53 | > 54 | {children} 55 | </ProviderContext.Provider> 56 | ); 57 | }; 58 | 59 | export const useProvider = () => { 60 | const context = useContext(ProviderContext); 61 | 62 | if (!context) { 63 | throw new Error("useProvider must be used within a Provider"); 64 | } 65 | 66 | return context; 67 | }; 68 | 69 | export default Provider; 70 | -------------------------------------------------------------------------------- /src/providers/constants.tsx: -------------------------------------------------------------------------------- 1 | import { CustomSession } from "auth"; 2 | import { User } from "next-auth"; 3 | 4 | export interface ProviderContextType { 5 | session: CustomSession; 6 | updateSession: () => void; 7 | users: User[]; 8 | updateUsers: () => void; 9 | } 10 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createTheme } from "@mui/material"; 4 | import { Poppins } from "next/font/google"; 5 | 6 | const poppins = Poppins({ 7 | subsets: ["latin"], 8 | display: 'swap', 9 | variable: '--font-poppins', 10 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] 11 | }); 12 | 13 | const { fontFamily } = poppins.style; 14 | 15 | export const theme = createTheme({ 16 | palette: { 17 | primary: { 18 | main: "#0773f7", 19 | }, 20 | }, 21 | typography: { 22 | fontFamily, 23 | h4: { 24 | fontSize: "2rem", 25 | fontWeight: 600, 26 | }, 27 | h5: { 28 | fontWeight: 600, 29 | } 30 | }, 31 | components: { 32 | MuiButton: { 33 | styleOverrides: { 34 | root: { 35 | textTransform: "none", 36 | }, 37 | }, 38 | }, 39 | }, 40 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "baseUrl": "./src" 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } --------------------------------------------------------------------------------