├── .env ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── Header.tsx ├── Layout.tsx ├── Post.tsx └── auth │ └── form-passwordless.tsx ├── installation.md ├── lib ├── logger.ts └── prisma.ts ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── post │ │ ├── [id].ts │ │ └── index.ts │ ├── publish │ │ └── [id].ts │ └── user │ │ ├── [id].ts │ │ ├── check-credentials.ts │ │ └── create.ts ├── auth │ ├── signin.tsx │ ├── signout.tsx │ └── signup.tsx ├── create.tsx ├── drafts.tsx ├── index.tsx └── p │ └── [id].tsx ├── prisma ├── migrations │ ├── 20211210160636_initial_migration │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── theme ├── colors.ts ├── components.ts └── index.ts ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | SECRET=RANDOM_STRING 2 | 3 | NEXTAUTH_URL=YOUR_NEXTAUTH_URL 4 | 5 | # GitHub OAUTH 6 | GITHUB_ID=YOUR_GITHUB_ID 7 | GITHUB_SECRET=YOUR_GITHUB_SECRET -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | # prisma 4 | prisma/dev.db 5 | .env.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 70, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "fluid": false 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS - Prisma - NextAuth - Credentials & OAuth 2 | 3 | ## Introduction 4 | 5 | NextJS starter pack with 6 | - [Prisma](https://www.prisma.io/) 7 | - [Chakra-UI](https://chakra-ui.com/) 8 | - [NextAuthJS](https://next-auth.js.org/) 9 | - [Email (passwordless)](https://next-auth.js.org/providers/email) 10 | - [OAuth (Github)](https://next-auth.js.org/providers/github) 11 | - [Credentials (Email & Password)](https://next-auth.js.org/providers/credentials) 12 | 13 | This is a starter kit for authentication in NextJS. It is a fork from [rest-nextjs-api-routes-auth](https://github.com/prisma/prisma-examples/tree/latest/typescript/rest-nextjs-api-routes-auth), read more in the [installation instructions](installation.md) 14 | 15 | This repo illustrates user authentication logic with NextAuth v4 combined with credentials using the default configuration and the Prisma Adapter. No [callbacks](https://next-auth.js.org/configuration/callbacks) are used or needed for the flow to work - you can use both combined. 16 | 17 | # Install & run 18 | 19 | install 20 | ``` 21 | git clone https://github.com/mikemajara/nextjs-prisma-next-auth-credentials 22 | yarn install 23 | ``` 24 | 25 | copy environment and fill in with your data 26 | ``` 27 | cp .env .env.local 28 | ``` 29 | 30 | run 31 | ``` 32 | yarn dev 33 | ``` 34 | 35 | ## Motiviation 36 | Setting up credentials is generally [not recommended](https://github.com/nextauthjs/next-auth/discussions/3364) "_with your database because of the security implications most people aren't ware of._", but they are widely used and much needed for applications, specially at the start of a project. You don't want to start dealing with OAuth from the start, but need some user management. 37 | 38 | Nextauth has ~~very~~ **too** simple instructions and barely pays attention to credentials, so I decided to set up this project to experiment with the whole auth flow. **My key takeaways are**: 39 | - Default settings are enough to use both OAuth providers and your own credentials. 40 | - You should be able to authorize using any given API (this project uses NextJS API to check against the same prisma DB used with an [adapter](https://next-auth.js.org/adapters/overview)) 41 | - Session JWT is needed (as opposed to database strategy) 42 | 43 | ## Gotchas 44 | 45 | ### Using Gmail as your email provider 46 | 47 | If you are using Gmail to send e-mails for passwordless authentication, make sure you enable **Less secure app access**. Go to Google > Manage Account > Security > Less secure app access, turn it on. If you don't, google will reject your user & password accessed by `nodemailer`. -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import { signIn, signOut, useSession } from "next-auth/react"; 5 | import { logger } from "@lib/logger"; 6 | import { Button, HStack } from "@chakra-ui/react"; 7 | 8 | const Header: React.FC = () => { 9 | const router = useRouter(); 10 | const isActive: (pathname: string) => boolean = (pathname) => 11 | router.pathname === pathname; 12 | 13 | const { data: session, status } = useSession(); 14 | logger.debug(session); 15 | let left = ( 16 | 17 | 18 | 19 | Feed 20 | 21 | 22 | 23 | ); 24 | 25 | let right = null; 26 | 27 | if (status == "loading") { 28 | left = ( 29 |
30 | 31 | Feed 32 | 33 |
34 | ); 35 | right = ( 36 |
37 |

Validating session ...

38 | 43 |
44 | ); 45 | } 46 | 47 | if (!session) { 48 | right = ( 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | if (session) { 56 | left = ( 57 | 58 | 59 | Feed 60 | 61 | 62 | My drafts 63 | 64 | 65 | ); 66 | right = ( 67 | 68 |

69 | {session.user.name} ({session.user.email}) 70 |

71 | 72 | 75 | 76 | 84 |
85 | ); 86 | } 87 | 88 | return ( 89 | 100 | ); 101 | }; 102 | 103 | export default Header; 104 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/react"; 2 | import React, { ReactNode } from "react"; 3 | import Header from "./Header"; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | }; 8 | 9 | const Layout: React.FC = (props) => ( 10 | 11 |
12 | 13 | {props.children} 14 | 15 | 16 | ); 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /components/Post.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Router from "next/router"; 3 | import ReactMarkdown from "react-markdown"; 4 | import { Text, Heading, Box } from "@chakra-ui/react"; 5 | 6 | export type PostProps = { 7 | id: number; 8 | title: string; 9 | author: { 10 | name: string; 11 | email: string; 12 | } | null; 13 | content: string; 14 | published: boolean; 15 | }; 16 | 17 | const Post: React.FC<{ post: PostProps }> = ({ post }) => { 18 | const authorName = post.author 19 | ? post.author.name 20 | : "Unknown author"; 21 | return ( 22 | Router.push("/p/[id]", `/p/${post.id}`)} 28 | > 29 | {post.title} 30 | By {authorName} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Post; 37 | -------------------------------------------------------------------------------- /components/auth/form-passwordless.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useDisclosure, 3 | Button, 4 | Collapse, 5 | VStack, 6 | FormControl, 7 | FormLabel, 8 | Input, 9 | } from "@chakra-ui/react"; 10 | import { watch } from "fs"; 11 | import { signIn } from "next-auth/react"; 12 | import router, { useRouter } from "next/router"; 13 | import React from "react"; 14 | import { useForm } from "react-hook-form"; 15 | import { MdOutlineEmail } from "react-icons/md"; 16 | 17 | export default function FormPasswordlessEmail() { 18 | const { isOpen, onToggle } = useDisclosure(); 19 | const router = useRouter(); 20 | const { 21 | handleSubmit, 22 | register, 23 | watch, 24 | formState: { errors, isSubmitting }, 25 | } = useForm(); 26 | 27 | const onSubmit = (values) => { 28 | signIn("email", { 29 | ...values, 30 | callbackUrl: router.query.callbackUrl.toString(), 31 | }); 32 | }; 33 | 34 | return ( 35 | <> 36 | 43 | 44 |
45 | 46 | 50 | Email 51 | 55 | 56 | 65 | 66 |
67 |
68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /installation.md: -------------------------------------------------------------------------------- 1 | # Fullstack Authentication Example with Next.js and NextAuth.js 2 | 3 | This example shows how to implement a **fullstack app in TypeScript with [Next.js](https://nextjs.org/)** using [React](https://reactjs.org/) (frontend), [Next.js API routes](https://nextjs.org/docs/api-routes/introduction) and [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client) (backend). It also demonstrates how to implement authentication using [NextAuth.js](https://next-auth.js.org/). The example uses a SQLite database file with some initial dummy data which you can find at [`./prisma/dev.db`](./prisma/dev.db). 4 | 5 | Note that the app uses a mix of server-side rendering with `getServerSideProps` (SSR) and static site generation with `getStaticProps` (SSG). When possible, SSG is used to make database queries already at build-time (e.g. when fetching the [public feed](./pages/index.tsx)). Sometimes, the user requesting data needs to be authenticated, so SSR is being used to render data dynamically on the server-side (e.g. when viewing a user's [drafts](./pages/drafts.tsx)). 6 | 7 | ## Getting started 8 | 9 | ### 1. Download example and install dependencies 10 | 11 | ``` 12 | curl https://codeload.github.com/prisma/prisma-examples/tar.gz/latest | tar -xz --strip=2 prisma-examples-latest/typescript/rest-nextjs-api-routes-auth 13 | ``` 14 | 15 | Install npm dependencies: 16 | 17 | ``` 18 | cd rest-nextjs-api-routes-auth 19 | npm install 20 | ``` 21 | 22 |
Alternative: Clone the entire repo 23 | 24 | Clone this repository: 25 | 26 | ``` 27 | git clone git@github.com:prisma/prisma-examples.git --depth=1 28 | ``` 29 | 30 | Install npm dependencies: 31 | 32 | ``` 33 | cd prisma-examples/typescript/rest-nextjs-api-routes-auth 34 | npm install 35 | ``` 36 | 37 |
38 | 39 | ### 2. Create and seed the database 40 | 41 | Run the following command to create your SQLite database file. This also creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma): 42 | 43 | ``` 44 | npx prisma migrate dev --name init 45 | ``` 46 | 47 | Now, seed the database with the sample data in [`prisma/seed.ts`](./prisma/seed.ts) by running the following command: 48 | 49 | ``` 50 | npx prisma db seed 51 | ``` 52 | 53 | 54 | ### 3. Configuring your authentication provider 55 | 56 | In order to get this example to work, you need to configure the [GitHub](https://next-auth.js.org/providers/github) and/or [Email](https://next-auth.js.org/providers/email) authentication providers from NextAuth.js. 57 | 58 | #### Configuring the GitHub authentication provider 59 | 60 |
Expand to learn how you can configure the GitHub authentication provider 61 | 62 | First, log into your [GitHub](https://github.com/) account. 63 | 64 | Then, navigate to [**Settings**](https://github.com/settings/profile), then open to [**Developer Settings**](https://github.com/settings/apps), then switch to [**OAuth Apps**](https://github.com/settings/developers). 65 | 66 | ![](https://res.cloudinary.com/practicaldev/image/fetch/s--fBiGBXbE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/4eQrMAs.png) 67 | 68 | Clicking on the **Register a new application** button will redirect you to a registration form to fill out some information for your app. The **Authorization callback URL** should be the Next.js `/api/auth` route. 69 | 70 | An important thing to note here is that the **Authorization callback URL** field only supports a single URL, unlike e.g. Auth0, which allows you to add additional callback URLs separated with a comma. This means if you want to deploy your app later with a production URL, you will need to set up a new GitHub OAuth app. 71 | 72 | ![](https://res.cloudinary.com/practicaldev/image/fetch/s--v7s0OEs_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/tYtq5fd.png) 73 | 74 | Click on the **Register application** button, and then you will be able to find your newly generated **Client ID** and **Client Secret**. Copy and paste this info into the [`.env`](./env) file in the root directory. 75 | 76 | The resulting section in the `.env` file might look like this: 77 | 78 | ``` 79 | # GitHub oAuth 80 | GITHUB_ID=6bafeb321963449bdf51 81 | GITHUB_SECRET=509298c32faa283f28679ad6de6f86b2472e1bff 82 | ``` 83 | 84 |
85 | 86 | #### Configuring the Email authentication provider 87 | 88 | You can [follow the instructions in the NextAuth.js documentation](https://next-auth.js.org/providers/email#configuration) to configure the Email authentication provider. Once your email authentication provider is configured, you can set the environment variables in [`.env`](./env) accordingly. 89 | 90 | ### 4. Start the app 91 | 92 | ``` 93 | npm run dev 94 | ``` 95 | 96 | The app is now running, navigate to [`http://localhost:3000/`](http://localhost:3000/) in your browser to explore its UI. 97 | 98 | ## Evolving the app 99 | 100 | Evolving the application typically requires three steps: 101 | 102 | 1. Migrate your database using Prisma Migrate 103 | 1. Update your server-side application code 104 | 1. Build new UI features in React 105 | 106 | For the following example scenario, assume you want to add a "profile" feature to the app where users can create a profile and write a short bio about themselves. 107 | 108 | ### 1. Migrate your database using Prisma Migrate 109 | 110 | The first step is to add a new table, e.g. called `Profile`, to the database. You can do this by adding a new model to your [Prisma schema file](./prisma/schema.prisma) file and then running a migration afterwards: 111 | 112 | ```diff 113 | // schema.prisma 114 | 115 | model Post { 116 | id Int @default(autoincrement()) @id 117 | title String 118 | content String? 119 | published Boolean @default(false) 120 | author User? @relation(fields: [authorId], references: [id]) 121 | authorId Int 122 | } 123 | 124 | model User { 125 | id Int @default(autoincrement()) @id 126 | name String? 127 | email String @unique 128 | posts Post[] 129 | + profile Profile? 130 | } 131 | 132 | +model Profile { 133 | + id Int @default(autoincrement()) @id 134 | + bio String? 135 | + userId Int @unique 136 | + user User @relation(fields: [userId], references: [id]) 137 | +} 138 | ``` 139 | 140 | Once you've updated your data model, you can execute the changes against your database with the following command: 141 | 142 | ``` 143 | npx prisma migrate dev 144 | ``` 145 | 146 | ### 2. Update your application code 147 | 148 | You can now use your `PrismaClient` instance to perform operations against the new `Profile` table. Here are some examples: 149 | 150 | #### Create a new profile for an existing user 151 | 152 | ```ts 153 | const profile = await prisma.profile.create({ 154 | data: { 155 | bio: "Hello World", 156 | user: { 157 | connect: { email: "alice@prisma.io" }, 158 | }, 159 | }, 160 | }); 161 | ``` 162 | 163 | #### Create a new user with a new profile 164 | 165 | ```ts 166 | const user = await prisma.user.create({ 167 | data: { 168 | email: "john@prisma.io", 169 | name: "John", 170 | profile: { 171 | create: { 172 | bio: "Hello World", 173 | }, 174 | }, 175 | }, 176 | }); 177 | ``` 178 | 179 | #### Update the profile of an existing user 180 | 181 | ```ts 182 | const userWithUpdatedProfile = await prisma.user.update({ 183 | where: { email: "alice@prisma.io" }, 184 | data: { 185 | profile: { 186 | update: { 187 | bio: "Hello Friends", 188 | }, 189 | }, 190 | }, 191 | }); 192 | ``` 193 | 194 | 195 | ### 3. Build new UI features in React 196 | 197 | Once you have added a new endpoint to the API (e.g. `/api/profile` with `/POST`, `/PUT` and `GET` operations), you can start building a new UI component in React. It could e.g. be called `profile.tsx` and would be located in the `pages` directory. 198 | 199 | In the application code, you can access the new endpoint via `fetch` operations and populate the UI with the data you receive from the API calls. 200 | 201 | 202 | ## Switch to another database (e.g. PostgreSQL, MySQL, SQL Server, MongoDB) 203 | 204 | If you want to try this example with another database than SQLite, you can adjust the the database connection in [`prisma/schema.prisma`](./prisma/schema.prisma) by reconfiguring the `datasource` block. 205 | 206 | Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls). 207 | 208 |
Expand for an overview of example configurations with different databases 209 | 210 | ### PostgreSQL 211 | 212 | For PostgreSQL, the connection URL has the following structure: 213 | 214 | ```prisma 215 | datasource db { 216 | provider = "postgresql" 217 | url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA" 218 | } 219 | ``` 220 | 221 | Here is an example connection string with a local PostgreSQL database: 222 | 223 | ```prisma 224 | datasource db { 225 | provider = "postgresql" 226 | url = "postgresql://janedoe:mypassword@localhost:5432/notesapi?schema=public" 227 | } 228 | ``` 229 | 230 | ### MySQL 231 | 232 | For MySQL, the connection URL has the following structure: 233 | 234 | ```prisma 235 | datasource db { 236 | provider = "mysql" 237 | url = "mysql://USER:PASSWORD@HOST:PORT/DATABASE" 238 | } 239 | ``` 240 | 241 | Here is an example connection string with a local MySQL database: 242 | 243 | ```prisma 244 | datasource db { 245 | provider = "mysql" 246 | url = "mysql://janedoe:mypassword@localhost:3306/notesapi" 247 | } 248 | ``` 249 | 250 | ### Microsoft SQL Server 251 | 252 | Here is an example connection string with a local Microsoft SQL Server database: 253 | 254 | ```prisma 255 | datasource db { 256 | provider = "sqlserver" 257 | url = "sqlserver://localhost:1433;initial catalog=sample;user=sa;password=mypassword;" 258 | } 259 | ``` 260 | 261 | ### MongoDB 262 | 263 | Here is an example connection string with a local MongoDB database: 264 | 265 | ```prisma 266 | datasource db { 267 | provider = "mongodb" 268 | url = "mongodb://USERNAME:PASSWORD@HOST/DATABASE?authSource=admin&retryWrites=true&w=majority" 269 | } 270 | ``` 271 | Because MongoDB is currently in [Preview](https://www.prisma.io/docs/about/releases#preview), you need to specify the `previewFeatures` on your `generator` block: 272 | 273 | ``` 274 | generator client { 275 | provider = "prisma-client-js" 276 | previewFeatures = ["mongodb"] 277 | } 278 | ``` 279 |
280 | 281 | ## Next steps 282 | 283 | - Check out the [Prisma docs](https://www.prisma.io/docs) 284 | - Share your feedback in the [`prisma2`](https://prisma.slack.com/messages/CKQTGR6T0/) channel on the [Prisma Slack](https://slack.prisma.io/) 285 | - Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/) 286 | - Watch our biweekly "What's new in Prisma" livestreams on [Youtube](https://www.youtube.com/channel/UCptAHlN1gdwD89tFM3ENb6w) -------------------------------------------------------------------------------- /lib/logger.ts: -------------------------------------------------------------------------------- 1 | import log from "loglevel"; 2 | import chalk from "chalk"; 3 | import prefix from "loglevel-plugin-prefix"; 4 | 5 | const colors = { 6 | TRACE: chalk.magenta, 7 | DEBUG: chalk.cyan, 8 | INFO: chalk.blue, 9 | WARN: chalk.yellow, 10 | ERROR: chalk.red, 11 | }; 12 | 13 | if (process.env.NODE_ENV == "development") { 14 | log.setLevel("debug"); 15 | } 16 | 17 | prefix.reg(log); 18 | 19 | prefix.apply(log, { 20 | format(level, name, timestamp) { 21 | return `${chalk.gray(`[${timestamp}]`)} ${colors[ 22 | level.toUpperCase() 23 | ](level)} ${chalk.green(`${name}:`)}`; 24 | }, 25 | }); 26 | 27 | export { log as logger }; 28 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | // PrismaClient is attached to the `global` object in development to prevent 4 | // exhausting your database connection limit. 5 | // 6 | // Learn more: 7 | // https://pris.ly/d/help/next-js-best-practices 8 | 9 | let prisma: PrismaClient 10 | 11 | if (process.env.NODE_ENV === 'production') { 12 | prisma = new PrismaClient() 13 | } else { 14 | if (!global.prisma) { 15 | global.prisma = new PrismaClient() 16 | } 17 | prisma = global.prisma 18 | } 19 | export default prisma -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-next", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "scripts": { 9 | "dev": "next", 10 | "build": "next build", 11 | "start": "next start" 12 | }, 13 | "dependencies": { 14 | "@chakra-ui/icons": "^1.1.1", 15 | "@chakra-ui/react": "^1.7.2", 16 | "@emotion/react": "^11", 17 | "@emotion/styled": "^11", 18 | "@next-auth/prisma-adapter": "^1.0.1", 19 | "@prisma/client": "3.5.0", 20 | "chalk": "^5.0.0", 21 | "crypto-js": "^4.1.1", 22 | "framer-motion": "^4", 23 | "lodash": "^4.17.21", 24 | "loglevel": "^1.8.0", 25 | "loglevel-plugin-prefix": "^0.8.4", 26 | "next": "12.0.7", 27 | "next-auth": "^4.0.5", 28 | "nodemailer": "^6.7.2", 29 | "react": "17.0.2", 30 | "react-dom": "17.0.2", 31 | "react-hook-form": "^7.21.0", 32 | "react-icons": "^4.3.1", 33 | "react-markdown": "7.1.1" 34 | }, 35 | "devDependencies": { 36 | "@types/next-auth": "3.13.0", 37 | "@types/node": "16.11.12", 38 | "@types/react": "17.0.37", 39 | "prisma": "3.5.0", 40 | "ts-node": "10.4.0", 41 | "typescript": "4.5.2" 42 | }, 43 | "prisma": { 44 | "seed": "ts-node prisma/seed.ts" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { SessionProvider } from "next-auth/react"; 2 | import { AppProps } from "next/app"; 3 | import { ChakraProvider } from "@chakra-ui/provider"; 4 | 5 | import theme from "../theme"; 6 | 7 | const App = ({ Component, pageProps }: AppProps) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from "next"; 2 | import NextAuth, { NextAuthOptions } from "next-auth"; 3 | import Providers from "next-auth/providers"; 4 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 5 | import prisma from "../../../lib/prisma"; 6 | import { logger } from "../../../lib/logger"; 7 | import GitHubProvider from "next-auth/providers/github"; 8 | import EmailProvider from "next-auth/providers/email"; 9 | import CredentialsProvider from "next-auth/providers/credentials"; 10 | 11 | const options: NextAuthOptions = { 12 | debug: true, 13 | providers: [ 14 | GitHubProvider({ 15 | clientId: process.env.GITHUB_ID, 16 | clientSecret: process.env.GITHUB_SECRET, 17 | }), 18 | EmailProvider({ 19 | server: { 20 | host: process.env.SMTP_HOST, 21 | port: Number(process.env.SMTP_PORT), 22 | auth: { 23 | user: process.env.SMTP_USER, 24 | pass: process.env.SMTP_PASSWORD, 25 | }, 26 | }, 27 | from: process.env.EMAIL_FROM, 28 | }), 29 | CredentialsProvider({ 30 | // The name to display on the sign in form (e.g. 'Sign in with...') 31 | id: "credentials", 32 | name: "credentials", 33 | // The credentials is used to generate a suitable form on the sign in page. 34 | // You can specify whatever fields you are expecting to be submitted. 35 | // e.g. domain, username, password, 2FA token, etc. 36 | // You can pass any HTML attribute to the tag through the object. 37 | credentials: { 38 | username: { 39 | label: "Username", 40 | type: "text", 41 | placeholder: "jsmith", 42 | }, 43 | password: { label: "Password", type: "password" }, 44 | }, 45 | authorize: async (credentials, req) => { 46 | const user = await fetch( 47 | `${process.env.NEXTAUTH_URL}/api/user/check-credentials`, 48 | { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/x-www-form-urlencoded", 52 | accept: "application/json", 53 | }, 54 | body: Object.entries(credentials) 55 | .map((e) => e.join("=")) 56 | .join("&"), 57 | }, 58 | ) 59 | .then((res) => res.json()) 60 | .catch((err) => { 61 | return null; 62 | }); 63 | 64 | if (user) { 65 | return user; 66 | } else { 67 | return null; 68 | } 69 | }, 70 | }), 71 | ], 72 | // pages 73 | pages: { 74 | signIn: "/auth/signin", 75 | signOut: "/auth/signout", 76 | }, 77 | adapter: PrismaAdapter(prisma), 78 | secret: process.env.SECRET, 79 | logger: { 80 | error: (code, metadata) => { 81 | logger.error(code, metadata); 82 | }, 83 | warn: (code) => { 84 | logger.warn(code); 85 | }, 86 | debug: (code, metadata) => { 87 | logger.debug(code, metadata); 88 | }, 89 | }, 90 | session: { strategy: "jwt" }, 91 | // // callbacks 92 | // callbacks: { 93 | // signIn: async ({ 94 | // user, 95 | // account, 96 | // profile, 97 | // email, 98 | // credentials, 99 | // }) => { 100 | // logger.debug(`signIn:user`, user, "\n\n"); 101 | // logger.debug(`signIn:account`, account, "\n\n"); 102 | // logger.debug(`signIn:profile`, profile, "\n\n"); 103 | // return true; 104 | // }, 105 | // redirect: async ({ url, baseUrl }): Promise => { 106 | // logger.debug(`url, baseUrl`, url, baseUrl); 107 | // const params = new URLSearchParams(new URL(url).search); 108 | // const callbackUrl = params.get("callbackUrl"); 109 | // if (url.startsWith(baseUrl)) { 110 | // if (callbackUrl?.startsWith("/")) { 111 | // logger.debug("redirecting to", baseUrl + callbackUrl); 112 | // return baseUrl + callbackUrl; 113 | // } else if (callbackUrl?.startsWith(baseUrl)) { 114 | // logger.debug("redirecting to", callbackUrl); 115 | // return callbackUrl; 116 | // } 117 | // } else { 118 | // logger.debug("redirecting to", baseUrl); 119 | // return Promise.resolve(baseUrl); 120 | // } 121 | // // return Promise.resolve(url.startsWith(baseUrl) ? url : baseUrl); 122 | // }, 123 | // // Getting the JWT token from API response 124 | // jwt: async ({ token, user, account, profile, isNewUser }) => { 125 | // logger.debug(`jwt:token`, token, "\n\n"); 126 | // logger.debug(`jwt:user`, user, "\n\n"); 127 | // logger.debug(`jwt:account`, account, "\n\n"); 128 | // const isSigningIn = user ? true : false; 129 | // if (isSigningIn) { 130 | // token.jwt = user.access_token; 131 | // token.user = user; 132 | // } else { 133 | // logger.debug(`jwt:isSignIn: user is not logged in`, "\n\n"); 134 | // } 135 | // logger.debug(`resolving token`, token, "\n\n"); 136 | // return Promise.resolve(token); 137 | // }, 138 | // session: async ({ session, token }) => { 139 | // logger.debug(`session:session`, session, "\n\n"); 140 | // logger.debug(`session:token`, token, "\n\n"); 141 | // session.jwt = token.jwt; 142 | // session.user = token.user; 143 | // logger.debug(`resolving session`, session, "\n\n"); 144 | // return Promise.resolve(session); 145 | // }, 146 | // }, 147 | // // session 148 | // session: { 149 | // // Choose how you want to save the user session. 150 | // // The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie. 151 | // // If you use an `adapter` however, we default it to `"database"` instead. 152 | // // You can still force a JWT session by explicitly defining `"jwt"`. 153 | // // When using `"database"`, the session cookie will only contain a `sessionToken` value, 154 | // // which is used to look up the session in the database. 155 | // strategy: "jwt", 156 | 157 | // // Seconds - How long until an idle session expires and is no longer valid. 158 | // maxAge: 30 * 24 * 60 * 60, // 30 days 159 | 160 | // // Seconds - Throttle how frequently to write to database to extend a session. 161 | // // Use it to limit write operations. Set to 0 to always update the database. 162 | // // Note: This option is ignored if using JSON Web Tokens 163 | // updateAge: 24 * 60 * 60, // 24 hours 164 | // }, 165 | }; 166 | 167 | const authHandler: NextApiHandler = (req, res) => 168 | NextAuth(req, res, options); 169 | export default authHandler; 170 | -------------------------------------------------------------------------------- /pages/api/post/[id].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { getSession } from "next-auth/react"; 3 | import prisma from "../../../lib/prisma"; 4 | 5 | // DELETE /api/post/:id 6 | export default async function handle( 7 | req: NextApiRequest, 8 | res: NextApiResponse, 9 | ) { 10 | const postId = req.query.id; 11 | 12 | const session = await getSession({ req }); 13 | 14 | if (req.method === "DELETE") { 15 | if (session) { 16 | const post = await prisma.post.delete({ 17 | where: { id: Number(postId) }, 18 | }); 19 | res.json(post); 20 | } else { 21 | res.status(401).send({ message: "Unauthorized" }); 22 | } 23 | } else { 24 | throw new Error( 25 | `The HTTP ${req.method} method is not supported at this route.`, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pages/api/post/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | // POST /api/post 6 | // Required fields in body: title 7 | // Optional fields in body: content 8 | export default async function handle( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | const { title, content } = req.body; 13 | 14 | const session = await getSession({ req }); 15 | if (session) { 16 | const result = await prisma.post.create({ 17 | data: { 18 | title: title, 19 | content: content, 20 | author: { connect: { email: session?.user?.email } }, 21 | }, 22 | }); 23 | res.json(result); 24 | } else { 25 | res.status(401).send({ message: "Unauthorized" }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/api/publish/[id].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { getSession } from "next-auth/react"; 3 | import prisma from "../../../lib/prisma"; 4 | 5 | // PUT /api/publish/:id 6 | export default async function handle( 7 | req: NextApiRequest, 8 | res: NextApiResponse, 9 | ) { 10 | const postId = req.query.id; 11 | const session = await getSession({ req }); 12 | 13 | if (session) { 14 | const post = await prisma.post.update({ 15 | where: { id: Number(postId) }, 16 | data: { published: true }, 17 | }); 18 | res.json(post); 19 | } else { 20 | res.status(401).send({ message: "Unauthorized" }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/user/[id].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | 4 | export default async function handle( 5 | req: NextApiRequest, 6 | res: NextApiResponse, 7 | ) { 8 | const userId = req.query.id; 9 | 10 | if (req.method === "GET") { 11 | handleGET(userId, res); 12 | } else if (req.method === "POST") { 13 | handlePOST(userId, res, req); 14 | } else if (req.method === "DELETE") { 15 | handleDELETE(userId, res); 16 | } else { 17 | throw new Error( 18 | `The HTTP ${req.method} method is not supported at this route.`, 19 | ); 20 | } 21 | } 22 | 23 | // GET /api/user/:id 24 | async function handleGET(userId, res) { 25 | const user = await prisma.user.findUnique({ 26 | where: { id: Number(userId) }, 27 | // include: { id: true, name: true, email: true, image: true }, 28 | }); 29 | res.json(user); 30 | } 31 | 32 | // GET /api/user/:id 33 | async function handlePOST(userId, res, req) { 34 | const user = await prisma.user.update({ 35 | where: { id: Number(userId) }, 36 | data: { ...req.body }, 37 | }); 38 | return res.json(user); 39 | } 40 | 41 | // DELETE /api/user/:id 42 | async function handleDELETE(userId, res) { 43 | const user = await prisma.user.delete({ 44 | where: { id: Number(userId) }, 45 | }); 46 | res.json(user); 47 | } 48 | -------------------------------------------------------------------------------- /pages/api/user/check-credentials.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | import sha256 from "crypto-js/sha256"; 4 | import { logger } from "lib/logger"; 5 | import { omit } from "lodash"; 6 | 7 | export default async function handle( 8 | req: NextApiRequest, 9 | res: NextApiResponse, 10 | ) { 11 | if (req.method === "POST") { 12 | await handlePOST(res, req); 13 | } else { 14 | throw new Error( 15 | `The HTTP ${req.method} method is not supported at this route.`, 16 | ); 17 | } 18 | } 19 | 20 | const hashPassword = (password: string) => { 21 | return sha256(password).toString(); 22 | }; 23 | 24 | // POST /api/user 25 | async function handlePOST(res, req) { 26 | const user = await prisma.user.findUnique({ 27 | where: { email: req.body.username }, 28 | select: { 29 | id: true, 30 | name: true, 31 | email: true, 32 | image: true, 33 | password: true, 34 | }, 35 | }); 36 | if (user && user.password == hashPassword(req.body.password)) { 37 | logger.debug("password correct"); 38 | res.json(omit(user, "password")); 39 | } else { 40 | logger.debug("incorrect credentials"); 41 | res.status(400).end("Invalid credentials"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pages/api/user/create.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "../../../lib/prisma"; 3 | import sha256 from "crypto-js/sha256"; 4 | import { logger } from "@lib/logger"; 5 | 6 | export default async function handle( 7 | req: NextApiRequest, 8 | res: NextApiResponse, 9 | ) { 10 | if (req.method === "POST") { 11 | await handlePOST(res, req); 12 | } else { 13 | throw new Error( 14 | `The HTTP ${req.method} method is not supported at this route.`, 15 | ); 16 | } 17 | } 18 | 19 | const hashPassword = (password: string) => { 20 | return sha256(password).toString(); 21 | }; 22 | 23 | // POST /api/user 24 | async function handlePOST(res, req) { 25 | logger.debug("creating user", { 26 | ...req.body, 27 | password: hashPassword(req.body.password), 28 | }); 29 | const user = await prisma.user.create({ 30 | data: { ...req.body, password: hashPassword(req.body.password) }, 31 | }); 32 | res.json(user); 33 | } 34 | -------------------------------------------------------------------------------- /pages/auth/signin.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Flex, 4 | Box, 5 | FormControl, 6 | FormLabel, 7 | Input, 8 | Checkbox, 9 | Stack, 10 | Link, 11 | Button, 12 | Heading, 13 | Text, 14 | useColorModeValue, 15 | InputGroup, 16 | InputRightElement, 17 | VStack, 18 | FormErrorMessage, 19 | Divider, 20 | Collapse, 21 | useDisclosure, 22 | } from "@chakra-ui/react"; 23 | import { useForm } from "react-hook-form"; 24 | import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"; 25 | import { signIn, useSession } from "next-auth/react"; 26 | import { logger } from "@lib/logger"; 27 | import { each, forOwn, join } from "lodash"; 28 | import { useRouter } from "next/router"; 29 | 30 | //icons 31 | import { AiFillGithub, AiFillGoogleCircle } from "react-icons/ai"; 32 | import { MdOutlineEmail } from "react-icons/md"; 33 | import { BiLockAlt } from "react-icons/bi"; 34 | import FormPasswordlessEmail from "@components/auth/form-passwordless"; 35 | 36 | export default function SimpleCard() { 37 | const [showPassword, setShowPassword] = useState(false); 38 | const { isOpen: isOpenCollapse, onToggle: onToggleCollapse } = 39 | useDisclosure(); 40 | const { isOpen: isOpenEmail, onToggle: onToggleEmail } = 41 | useDisclosure(); 42 | const { data: session, status } = useSession(); 43 | const router = useRouter(); 44 | 45 | const { 46 | handleSubmit, 47 | register, 48 | watch, 49 | formState: { errors, isSubmitting }, 50 | } = useForm(); 51 | 52 | let defaultBody = { 53 | grant_type: "", 54 | username: "asdf@gmail.com", 55 | password: "asdf", 56 | scope: "", 57 | client_id: "", 58 | client_secret: "", 59 | }; 60 | 61 | async function onSubmit(values) { 62 | try { 63 | const body = { ...defaultBody, ...values }; 64 | console.log(`POSTing ${JSON.stringify(body, null, 2)}`); 65 | let res = await signIn("credentials", { 66 | ...body, 67 | callbackUrl: router.query.callbackUrl, 68 | }); 69 | logger.debug(`signing:onsubmit:res`, res); 70 | } catch (error) { 71 | logger.error(error); 72 | } 73 | } 74 | if (status === "authenticated") { 75 | router.push("/", { 76 | query: { 77 | callbackUrl: router.query.callbackUrl, 78 | }, 79 | }); 80 | } 81 | 82 | return ( 83 | 89 | 90 | 91 | Sign in to your account 92 | 93 | to enjoy all of our cool{" "} 94 | features ✌️ 95 | 96 | 97 | 103 | 104 | 105 | 116 | 123 | 124 | 125 |
126 | 127 | 132 | Email 133 | 134 | 135 | 140 | Password 141 | 142 | 146 | 147 | 163 | 164 | 165 | {router.query.error && 166 | router.query.error === "CredentialsSignin" && ( 167 | 168 | Invalid credentials 169 | 170 | )} 171 | 172 | 173 | 178 | Remember me 179 | Forgot password? 180 | 181 | 193 | 194 | 195 | 196 | Not a user yet?{" "} 197 | 205 | Register 206 | 207 | 208 | 209 | 210 |
211 |
212 |
213 |
214 |
215 | ); 216 | } 217 | -------------------------------------------------------------------------------- /pages/auth/signout.tsx: -------------------------------------------------------------------------------- 1 | import { logger } from "../../lib/logger"; 2 | import { GetStaticProps } from "next"; 3 | import { signOut } from "next-auth/react"; 4 | import React from "react"; 5 | 6 | interface Props { 7 | callbackUrl: string; 8 | } 9 | 10 | export default function logout({ callbackUrl }: Props) { 11 | logger.debug(`callbackUrl`); 12 | logger.debug(callbackUrl); 13 | signOut({ callbackUrl }); 14 | return
; 15 | } 16 | 17 | export const getStaticProps = async (context: GetStaticProps) => ({ 18 | props: { callbackUrl: process.env.NEXTAUTH_URL }, // will be passed to the page component as props 19 | }); 20 | -------------------------------------------------------------------------------- /pages/auth/signup.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Flex, 3 | Box, 4 | FormControl, 5 | FormLabel, 6 | Input, 7 | InputGroup, 8 | HStack, 9 | InputRightElement, 10 | Stack, 11 | Button, 12 | Heading, 13 | Text, 14 | useColorModeValue, 15 | Link, 16 | } from "@chakra-ui/react"; 17 | import { useState } from "react"; 18 | import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"; 19 | import { useForm } from "react-hook-form"; 20 | import { logger } from "lib/logger"; 21 | import { useRouter } from "next/router"; 22 | import { resetLevel } from "loglevel"; 23 | 24 | export default function SignupCard() { 25 | const router = useRouter(); 26 | const [showPassword, setShowPassword] = useState(false); 27 | const { 28 | handleSubmit, 29 | register, 30 | reset, 31 | formState: { errors, isSubmitting }, 32 | } = useForm(); 33 | 34 | async function onSubmit(values) { 35 | try { 36 | const body = { ...values }; 37 | console.log(`POSTing ${JSON.stringify(body, null, 2)}`); 38 | const res = await fetch(`/api/user/create`, { 39 | method: "POST", 40 | headers: { "Content-Type": "application/json" }, 41 | body: JSON.stringify(body), 42 | }); 43 | logger.debug(`res`, res); 44 | reset(); 45 | router.push( 46 | `signin${ 47 | router.query.callbackUrl 48 | ? `?callbackUrl=${router.query.callbackUrl}` 49 | : "" 50 | }`, 51 | ); 52 | } catch (error) { 53 | console.error(error); 54 | } 55 | } 56 | 57 | return ( 58 | 64 | 72 | 73 | 74 | Sign up 75 | 76 | 77 | to enjoy all of our cool features ✌️ 78 | 79 | 80 | 86 |
87 | 88 | 89 | 90 | Full name 91 | 92 | 93 | 94 | 95 | Email address 96 | 97 | 98 | 99 | Password 100 | 101 | 105 | 106 | 118 | 119 | 120 | 121 | 122 | 135 | 136 | 137 | 138 | Already a user?{" "} 139 | 140 | Sign in 141 | 142 | 143 | 144 | 145 |
146 |
147 |
148 |
149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /pages/create.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Layout from "../components/Layout"; 3 | import Router from "next/router"; 4 | 5 | const Draft: React.FC = () => { 6 | const [title, setTitle] = useState(""); 7 | const [content, setContent] = useState(""); 8 | 9 | const submitData = async (e: React.SyntheticEvent) => { 10 | e.preventDefault(); 11 | try { 12 | const body = { title, content }; 13 | await fetch(`http://localhost:3000/api/post`, { 14 | method: "POST", 15 | headers: { "Content-Type": "application/json" }, 16 | body: JSON.stringify(body), 17 | }); 18 | await Router.push("/drafts"); 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | }; 23 | 24 | return ( 25 | 26 |
27 |
28 |

New Draft

29 | setTitle(e.target.value)} 32 | placeholder="Title" 33 | type="text" 34 | value={title} 35 | /> 36 |