Next.jsとPrismaとNextAuthでログイン機能を作ってuseStateとuseEffectの使い方も知ったあとに公式のチュートリアルを踏破してみた

## はじめに
仕事のキャッチアップで急速錬成中。バックエンドばっかやってきたから急にフロントのフレームワークつかうことになるとつらーい！（でもチャンスだね）。今回はログイン画面と画面切り替えのあるものを作るぞ

- ログイン画面で、メールアドレスとパスワードを入力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/eab34a17-6621-1535-c79f-28c35412af13.png)

- Home のページが開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/d7b003e6-f569-21ef-12b2-5f91b87945a8.png)

- 保護されたページが開く（セッション削除してリロードするとログイン画面に戻る）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/f8746443-c670-93ad-eda8-d1e318c8c327.png)

- パスワードを忘れたらtokenつきメールを作って、パスワードを再設定できる（送信はしない）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/82105232-add5-8a1c-3c8a-72d518454db5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/52e4ecaa-9703-ba50-cb94-4dcf7e40a533.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/49e60b98-07e6-6876-f4b3-cf6f9aaac883.png)

ここまでできればかなりいろんなことができるようになるだろう。

## Reference
https://zenn.dev/kaikusakari/articles/98924270546642

## Node
:::note
いままで node は exe をダウンロードしてきてインストールしていたが、コマンドラインでできるらしい。pythonの venv みたいな感じ。nvmという
:::


https://github.com/coreybutler/nvm-windows/releases

（nvm-setup.exe をダウンロード。npmもインストールされる ※エディタの再起動必須）  

```console:console
nvm install 18.17.1
nvm use 18.17.1
nvm list
nvm uninstall 18.17.1
node -v
```

## openSSL
https://slproweb.com/products/Win32OpenSSL.html
Win64 OpenSSL vX.X.X Light
`C:\Program Files\OpenSSL-Win64\bin` を環境変数に登録

## Getting Started
First, run the development server:
```console:プロジェクトフォルダを作成
mkdir nextjs
cd nextjs
```
```console:console
npx create-next-app@latest .
  √ Would you like to use TypeScript? ... No / [Yes]
  √ Would you like to use ESLint? ... No / [Yes]
  √ Would you like to use Tailwind CSS? ... [No] / Yes
  √ Would you like to use `src/` directory? ... No / [Yes]
  √ Would you like to use App Router? (recommended) ... No / [Yes]
  √ Would you like to customize the default import alias (@/*)? ... [No] / Yes

npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/2434c275-85bb-98dc-7431-7bfd246f4402.png)

## .env.example
- `.env.example` を作ったあとに、`cp` コマンドで `.env` を作成する
- `NEXTAUTH_SECRET` は、`openssl rand -base64 32` をコンソールで打ち込んで、出てきた文字をペースト
```text:.env.example
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET= # openssl rand -base64 32
BASE_URL=http://localhost:3000
```
```console:console
cp .env.example .env
```

## .gitignore
```diff:.gitignore
    :
# local env files
+.env
.env*.local
```

## package.json
好きにインストールして
```diff_json:package.json
{
  "name": "nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
+   "format": "prettier --write ."
  },
  "dependencies": {
+   "@emotion/react": "^11.11.1",
+   "@emotion/styled": "^11.11.0",
+   "@mui/icons-material": "^5.14.16",
+   "@mui/material": "^5.14.17",
+   "@mui/x-data-grid": "^6.18.1",
+   "@prisma/client": "^5.5.2",
+   "dayjs": "^1.11.10",
-   "next": "14.0.1"
+   "next": "^13.5.6",
+   "next-auth": "^4.24.5",
    "react": "^18",
    "react-dom": "^18",
+   "react-hook-form": "^7.48.2",
+   "ts-node": "^10.9.1",
+   "zod": "^3.22.4"
  },
  "devDependencies": {
+   "@next/eslint-plugin-next": "^14.0.3",
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
+   "@typescript-eslint/eslint-plugin": "^6.13.1",
+   "@typescript-eslint/parser": "^6.13.1",
+   "dotenv": "^16.3.1",
    "eslint": "^8",
    "eslint-config-next": "14.0.2",
+   "eslint-config-prettier": "^9.0.0",
+   "jest": "^29.7.0",
+   "jest-environment-jsdom": "^29.7.0",
+   "prettier": "^3.1.0",
+   "prisma": "^5.5.2",
-   "typescript": "^5"
+   "typescript": "^5.3.2"
  }
}

```

## prettier
```js:prettier.config.js
/** @type {import('prettier').Config} */
module.exports = {
  semi: false,
  singleQuote: true,
  printWidth: 100,
  useTabs: false,
  tabWidth: 2,
  endOfLine: 'lf',
  jsxSingleQuote: true,
}
```

## prisma
https://www.prisma.io/docs/getting-started/quickstart

https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#mapping-model-names-to-tables-or-collections

```console:console
npx prisma init --datasource-provider mysql
```
```diff:.gitignore（最後に追記）
    :
# prisma
/prisma/migrations/
/src/generated/

```

```diff:prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
+ output   = "../src/generated/prismaClient"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

+ ここから下をコピーして追記
model Users {
  id             Int       @id @default(autoincrement())
  name           String?
  role           String    @default("user")
  email          String    @unique
  password       String
  passwordToken  String?   @map("password_token")
  tokenExpiredAt DateTime? @map("token_expired_at")
  createdAt      DateTime  @default(now()) @map("created_at")
  updatedAt      DateTime  @default(now()) @updatedAt @map("updated_at")

  @@map("users")
}

```
```console:.env
DATABASE_URL="mysql://your-user-name:your-password@localhost:3306/nextjs_db"
```

```console:console
npx prisma migrate dev --name init
  Environment variables loaded from .env
  Prisma schema loaded from prisma\schema.prisma
  Datasource "db": MySQL database "nextjs_db" at "localhost:3306"
  
  MySQL database nextjs_db created at localhost:3306
  
  Applying migration `20231103015547_init`
  
  The following migration(s) have been created and applied from new schema changes:
  
  migrations/
    └─ 20231103015547_init/
      └─ migration.sql
  
  Your database is now in sync with your schema.
  
  ✔ Generated Prisma Client (v5.5.2) to .\node_modules\@prisma\client in 197ms
  
```

## prisma clientに反映
新規で `schema.prisma` を作成した際、及び `schema.prisma` を編集した際には、以下のコマンドを実行して、 `prisma client` への反映を行う。このコマンドを実行してもgithub管理するコードの差分は発生しないが `node_modules` が更新されている。prismaがモデル情報を取り込んでメソッドとして使えるようになる、、ような感じかな。Djangoとかと比べると片手落ちというかいまいちな印象だね。

:::note
dbスキーマに変更が出たら `migrate` とセットで実行しろということ
:::

```console:console
npx prisma generate
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/6e2274b5-57cb-4ef2-40dc-74f89c2b2628.png)

## seeder
```console:console（package.jsonで入れていたら不要）
npm install ts-node
```
```diff_json:package.json（pycharmなら左のガターに再生ボタンが出るよ）
    :
"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "format": "prettier --write .",
+   "seed": "ts-node --project tsconfig.seeds.json -r tsconfig-paths/register ./prisma/seeds/seeder.ts"
  },
    :
```
```json:tsconfig.seeds.json
{
  "compilerOptions": {
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "CommonJS",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules"]
}

```
```ts:prisma/seeds/seeder.ts
import { PrismaClient } from '@/generated/prismaClient'

async function main() {
  const prisma = new PrismaClient()
  await prisma.users.deleteMany()
  await prisma.users.create({
    data: {
      name: '開発 太郎',
      email: 'test@example.com',
      password: 'hoge',
    },
  })
  prisma.$disconnect()
}
main().then(() => console.log('seeding ok.'))

```

## Route Handlers
https://nextjs.org/docs/app/building-your-application/routing/defining-routes

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

App Router という仕組みがあり、フォルダ構造がそのままいわば Djangoでいう `urls.py` のような役割になる :open_mouth:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c58916f4-6b5d-79b0-b32f-9d11da4a81ce.png)

urlベースで作ったフォルダ構造の下に決まったファイルを作ると `dashboard/page.tx` 、`http://localhost:3000/dashboard` でページが表示される :flushed:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ed7db276-7388-b99e-8862-78cbf3a98c1a.png)

さらに、`api` フォルダを噛ませてから、`route.tsx` というファイルを作って、HTTPメソッドをオーバーライドしてあげると `APIモード` になる。Djangoとかやってるやつにはかなり親和性が高い... :flushed: ようになってきたのは実はつい最近らしい

```react:src/app/api/users/route.tsx
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  return NextResponse.json({msg: "hello, api/users GET!"})
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/0f3b97e4-9c2c-982c-5ca3-01db0ad72c46.png)

ちなみに、以下のようなファイルをつくると、Postmanの疎通テストみたいなのができる（PyCharmでやってるけどVSCodeにもあるよ）
```text:src/app/api/users/route.http
GET http://localhost:3000/api/users
Accept: application/json

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/e676a26e-671e-4db7-1638-692a9f375491.png)

```diff:.gitignore（ゴリゴリログが溜まっていくので無視）
    :
+ # .idea
+ /.idea/httpRequests/
```

```powershell:console（削除）
# linuxだといわゆるこれ: rm -r /your/path/here
Remove-Item -Path "src/app/api/users" -Recurse -Force
```

## ログイン機能とパスワード再設定
どばーっと行くけど、作ってみるとなるほどね！となるはず。

### 自動生成ファイルの調整
- `app` ディレクトリの `layout.tsx` を加工
- `app` ディレクトリの `page.tsx` を加工

```tsx:src/app/layout.tsx（加工）
import React from 'react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  )
}

```
```tsx:src/app/page.tsx（加工）
import React from 'react'

export default function Page() {
  return (
    <div>
      <p>保護されていないページ</p>
      <p>This is Home</p>
      <p>
        <a href={'/admin/user-list'}>ユーザー管理ページにとぶ</a>
      </p>
    </div>
  )
}

```
```css:src/app/globals.css（削除）
```
```css:src/app/page.module.css（削除）
```

### forgot-password（コンポーネント）
- `forgot-password` ディレクトリの `layout.tsx` `page.tsx` を新規作成する
-  `(authenticated)` ディレクトリは単なるグループ分け用のフォルダ（言い換えると、ﾏﾙｶｯｺをつけることで `appRouter` から除外されるので保護ページとそうじゃないページを見やすくわけている）
-  同じような理由で先頭アンダーバー `_XXXX` ディレクトリは `appRouter` から無視される

```tsx:src/app/admin/(unauthenticated)/forgot-password/layout.tsx
import { ReactNode } from 'react'

export default async function Layout({ children }: { children: ReactNode }) {
  return <>{children}</>
}

```
```tsx:src/app/admin/(unauthenticated)/forgot-password/page.tsx
import React from 'react'
import { ForgotPasswordPage } from './_components/ForgotPasswordPage'

export default function Page() {
  // 無駄な処理に見えるけど、dbからデータをロードする必要があるときにまとめてデータを確保してコンポーネントに流し込むつくりにしよう
  return <ForgotPasswordPage />
}

```

```tsx:src/app/admin/(unauthenticated)/forgot-password/_components/ForgotPasswordPage.tsx
'use client'
import React, { useState } from 'react'
import { ForgotPasswordSent } from './ForgotPasswordSent'
import { ForgotPasswordEdit } from './ForgotPasswordEdit'

export function ForgotPasswordPage() {
  const [isSent, setIsSent] = useState(false)
  return isSent ? (
    <ForgotPasswordSent />
  ) : (
    <ForgotPasswordEdit onCompletion={() => setIsSent(true)} />
  )
}

```
```tsx:src/app/admin/(unauthenticated)/forgot-password/_components/ForgotPasswordEdit.tsx
import React, { FormEvent } from 'react'
import { Box, Button, TextField, Typography } from '@mui/material'

type Props = {
  onCompletion: () => void
}

export function ForgotPasswordEdit({ onCompletion }: Props) {
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const formData = new FormData(event.currentTarget)
    const data = {
      email: formData.get('email'),
    }
    fetch('/api/admin/forgot-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ to: data.email }),
    })
      .then((response) => response.json())
      .then((data) => {
        console.log(`data: ${JSON.stringify(data)}`)
        onCompletion()
      })
      .catch((error) => {
        console.error('Fetch error:', error)
      })
  }

  return (
    <Box
      component='main'
      sx={{
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <Typography variant='h5' textAlign='center' sx={{ mb: 1 }}>
        パスワードを忘れた場合
      </Typography>
      <Box
        component='form'
        sx={{ width: 345, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
        onSubmit={handleSubmit}
      >
        <TextField
          id='email'
          name='email'
          label='メールアドレス'
          variant='outlined'
          required
          sx={{ width: '100%' }}
        />
        <Box sx={{ m: 1 }} />
        <Button type='submit' variant='outlined'>
          送信
        </Button>
      </Box>
    </Box>
  )
}

```
```tsx:src/app/admin/(unauthenticated)/forgot-password/_components/ForgotPasswordSent.tsx
import React from 'react'
import { Box, Button, Typography } from '@mui/material'

export function ForgotPasswordSent() {
  return (
    <Box
      component='main'
      sx={{
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <Typography variant='h5' textAlign='center' sx={{ mb: 1 }}>
        送信ボタンが押されました（画面2）
      </Typography>
      <Box sx={{ width: 345, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
        <Typography sx={{ textAlign: 'center', width: 310, fontSize: '16px' }}>
          送信ボタンが押された結果、画面が遷移しました。ブラウザの開発者コンソールを開いてURLをクリックしてください
        </Typography>
        <Box sx={{ m: 1 }} />
        <Button variant='outlined' href='/admin/signin' sx={{ width: '100%' }}>
          ログイン画面へ
        </Button>
      </Box>
    </Box>
  )
}

```

### apiHandler
```ts: src/util/apiHandler.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

export async function validate<T>(req: NextRequest, schema: z.ZodType) {
  const { data, success, error } = schema.safeParse(await req.json()) as {
    data: JSON
    success: boolean
    error: { errors: z.ZodIssue[] }
  }
  if (!success) {
    throw new Error(JSON.stringify({ message: 'Invalid request', errors: error.errors }))
  }
  return data as T
}

export function requestError(e: unknown): NextResponse {
  if (e instanceof Error) {
    return NextResponse.json({ error: JSON.parse(e.message) }, { status: 400 })
  } else {
    console.error('Unexpected error type:', e)
    return NextResponse.json({ error: 'Unexpected error type' }, { status: 500 })
  }
}

export function serverError(e: unknown): NextResponse {
  if (e instanceof Error) {
    return NextResponse.json({ error: JSON.parse(e.message) }, { status: 500 })
  } else {
    console.error('Unexpected error type:', e)
    return NextResponse.json({ error: 'Unexpected error type' }, { status: 500 })
  }
}

```

### forgot-password（API）
```ts:src/util/date.ts
import dayjs, { Dayjs, ManipulateType } from 'dayjs'

export function dateOffset(
  n: number = 0,
  unit: ManipulateType = 'minute',
  base: Date = new Date(),
): Date {
  let shiftedNow: Dayjs
  if (n >= 0) {
    shiftedNow = dayjs(base).add(n, unit)
  } else {
    shiftedNow = dayjs(base).subtract(Math.abs(n), unit)
  }

  return new Date(shiftedNow.format())
}

```
```ts:src/util/token.ts
import { randomBytes } from 'crypto'

export function generateToken(): string {
  const buffer: Buffer = randomBytes(32)
  return buffer.toString('hex')
}

```
```ts:src/app/api/admin/forgot-password/route.ts
import { requestError, serverError, validate } from '@/util/apiHandler'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { PrismaClient } from '@/generated/prismaClient'
import { generateToken } from '@/util/token'
import { dateOffset } from '@/util/date'

export async function POST(req: NextRequest) {
  const schema = z.object({
    to: z.string().email(),
  })
  type RequestType = z.infer<typeof schema>
  let data: RequestType

  // 送信先メールアドレスのuserを抽出
  let userName: string
  const prisma = new PrismaClient()
  try {
    data = await validate<RequestType>(req, schema)
    const { name } = await prisma.users.findFirstOrThrow({ where: { email: data.to } })
    userName = name || ''
  } catch (error) {
    return requestError(error)
  }

  // TODO: メール送信
  const passwordToken = generateToken()
  const tokenExpiredAt: Date = dateOffset(30, 'minute')
  let message: object = {
    to: data.to,
    from: 'noreply@example.com',
    subject: 'パスワード再設定用のURLをくれてやろう',
    body: `${userName} さん、パスワード再設定用のURLです http://localhost:3000/admin/reset-password?token=${passwordToken}`,
  }
  try {
    await prisma.users.update({
      where: { email: data.to },
      data: {
        passwordToken,
        tokenExpiredAt,
      },
    })

    return NextResponse.json(message)
  } catch (error) {
    return serverError(error)
  } finally {
    await prisma.$disconnect()
  }
}

```
```http:src/app/api/admin/forgot-password/route.http
POST http://localhost:3000/api/admin/forgot-password
Content-Type: application/json

{"to":  "test@example.com"}

```

### reset-password（コンポーネント）
```tsx:src/app/admin/(unauthenticated)/reset-password/page.tsx
import React from 'react'
import { ResetPasswordEdit } from './_components/ResetPasswordEdit'

export default function Page() {
  // 無駄な処理に見えるけど、dbからデータをロードする必要があるときにまとめてデータを確保してコンポーネントに流し込むつくりにしよう
  return <ResetPasswordEdit />
}

```
```tsx:src/app/admin/(unauthenticated)/reset-password/_components/ResetPasswordEdit.tsx
'use client'

import { useRouter, useSearchParams } from 'next/navigation'
import React, { FormEvent, useEffect } from 'react'
import { Box, Button, TextField, Typography } from '@mui/material'

type ResetPasswordEditForm = {
  token: string
  password: string
  passwordConfirm: string
}

export function ResetPasswordEdit() {
  const router = useRouter()
  const forceRedirect = useSearchParams().size == 0
  const token = useSearchParams().get('token')

  // エフェクトは通常、コンポーネントを外部システム（＝外部コンポーネント）と同期させるのに使います。
  // see: https://ja.react.dev/learn/synchronizing-with-effects
  useEffect(() => {
    if (forceRedirect) {
      router.push('/admin/signin')
    }
  }, [forceRedirect, router])

  // クエリストリング（＝token）がない状態でたどりついたらねずみ返し
  if (forceRedirect) return <></>

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const formData = new FormData(event.currentTarget)
    const data = {
      password: formData.get('password'),
      passwordConfirm: formData.get('passwordConfirm'),
    }
    fetch('/api/admin/reset-password', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify({ password: data.password, passwordConfirm: data.passwordConfirm }),
    })
      .then(() => {
        router.push('/admin/signin')
      })
      .catch((error) => {
        console.error('Fetch error:', error)
      })
  }

  return (
    <Box
      component='main'
      sx={{
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <Typography variant='h5' textAlign='center' sx={{ mb: 1 }}>
        パスワード再設定
      </Typography>
      <Box
        component='form'
        sx={{ width: 345, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
        onSubmit={handleSubmit}
      >
        <TextField
          id='password'
          name='password'
          type='password'
          label='パスワード'
          variant='outlined'
          required
          sx={{ width: '100%' }}
        />
        <TextField
          id='passwordConfirm'
          name='passwordConfirm'
          type='password'
          label='パスワード再入力'
          variant='outlined'
          required
          sx={{ width: '100%' }}
        />
        <Box sx={{ m: 1 }} />
        <Button type='submit' variant='outlined'>
          送信
        </Button>
      </Box>
    </Box>
  )
}

```

### reset-password（API）
```ts:src/app/api/admin/reset-password/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { PrismaClient } from '@/generated/prismaClient'
import { requestError, serverError, validate } from '@/util/apiHandler'

export async function PUT(req: NextRequest) {
  const schema = z.object({
    password: z.string(),
    passwordConfirm: z.string(),
  })
  type RequestType = z.infer<typeof schema>
  let data: RequestType

  let userId: number
  const prisma = new PrismaClient()
  try {
    data = await validate<RequestType>(req, schema)

    let token = req.headers.get('Authorization')
    if (token && token.startsWith('Bearer ')) {
      token = token.slice(7)
    } else {
      throw new Error(JSON.stringify({ message: 'Invalid or missing Bearer token.' }))
    }
    const { id, tokenExpiredAt } = await prisma.users.findFirstOrThrow({
      where: { passwordToken: token },
    })
    userId = id

    if (tokenExpiredAt && new Date() > tokenExpiredAt) {
      throw new Error(JSON.stringify({ message: 'Token has expired.' }))
    }
    if (data.password != data.passwordConfirm) {
      throw new Error(
        JSON.stringify({
          message: 'The password and password confirmation do not match. Please try again.',
        }),
      )
    }
  } catch (error) {
    return requestError(error)
  }

  try {
    const { id } = await prisma.users.update({
      where: { id: userId },
      data: { password: data.password },
    })
    return NextResponse.json({ id })
  } catch (error) {
    return serverError(error)
  }
}

```
```http:src/app/api/admin/reset-password/route.http
### 200 OK
PUT http://localhost:3000/api/admin/reset-password
Content-Type: application/json
Authorization: Bearer 5698f13bcaa5b0208f031423a87f57a1229b6a8a31a390925a49927c28532b80

{
"password": "abc",
"passwordConfirm": "abc"
}

### 400 Bad Request: "Token has expired." or "The password and password confirmation do not match. Please try again."
PUT http://localhost:3000/api/admin/reset-password
Content-Type: application/json
Authorization: Bearer 5698f13bcaa5b0208f031423a87f57a1229b6a8a31a390925a49927c28532b80

{
"password": "a",
"passwordConfirm": "b"
}

### 400 Bad Request: Invalid or missing Bearer token.
PUT http://localhost:3000/api/admin/reset-password
Content-Type: application/json

{
"password": "a",
"passwordConfirm": "a"
}

```

### NextAuth
```ts:src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import options from '@/auth/authOptions'

const handler = NextAuth(options)
export { handler as GET, handler as POST }

```
```ts:src/app/api/auth/route.ts
import { requestError, validate } from '@/util/apiHandler'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { PrismaClient } from '@/generated/prismaClient'

export async function GET(req: NextRequest) {
  return NextResponse.json({ msg: 'hello, api/auth GET!' })
}

export async function POST(req: NextRequest) {
  const schema = z.object({
    email: z.string(),
    password: z.string(),
  })
  type RequestType = z.infer<typeof schema>
  let data: RequestType

  try {
    data = await validate<RequestType>(req, schema)
  } catch (e: unknown) {
    return requestError(e)
  }

  const prisma = new PrismaClient()
  try {
    const user = await prisma.users.findFirstOrThrow({
      where: {
        email: data.email,
        password: data.password,
      },
    })
    return NextResponse.json(user)
  } catch (error) {
    console.error(error)
  } finally {
    await prisma.$disconnect()
  }
}

```
```text:src/app/api/auth/route.http
POST http://localhost:3000/api/auth
Content-Type: application/json

{
  "email": "test@example.com",
  "password": "hoge"
}

###
GET http://localhost:3000/api/auth
Accept: application/json

```
```ts:src/auth/authOptions.ts
import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { UserSession } from '@/auth/UserSession'

const authOptions: NextAuthOptions = {
  session: {
    strategy: 'jwt',
  },
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
        },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }
        let userSession: UserSession
        try {
          const options = {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ email: credentials.email, password: credentials.password }),
          }
          const result = await fetch(`${process.env.BASE_URL}/api/auth`, options)
          userSession = await result.json()
        } catch (e) {
          return null
        }

        const user: UserSession = {
          id: userSession.id,
          name: userSession.name,
          email: userSession.email,
        }
        return user
      },
    }),
  ],
  callbacks: {
    jwt: ({ token, user, account }) => {
      if (user) {
        token.user = user
        token.id = user.id
      }
      if (account) {
        token.accessToken = account.access_token
      }
      return token
    },
    session: ({ session, token }) => {
      const tokenInUser: UserSession = token.user as UserSession
      return {
        ...session,
        user: {
          ...tokenInUser,
          ...session.user,
        },
      }
    },
  },
}

export default authOptions

```
```ts:src/auth/getUserSession.ts
import { UserSession } from '@/auth/UserSession'
import options from '@/auth/authOptions'
import { getServerSession } from 'next-auth'

export async function getUserSession(): Promise<UserSession | null> {
  const session = await getServerSession(options)
  if (!session) return null
  return session.user as UserSession
}

```
```ts:src/auth/UserSession.ts
export type UserSession = {
  id: string
  name: string
  email: string
}
```
```ts:src/auth/SessionProvider.ts
'use client'
export { SessionProvider } from 'next-auth/react'

```

### signin
```tsx:src/app/admin/signin/page.tsx
import { redirect } from 'next/navigation'
import SigninForm from '@/app/admin/signin/_components/SigninForm'
import { getUserSession } from '@/auth/getUserSession'
import React from 'react'

export default async function Page() {
  const userSession = await getUserSession()
  if (userSession) {
    redirect('/')
  }
  return <SigninForm />
}

```
```tsx:src/app/admin/signin/_components/SigninForm.tsx
'use client'

import React, { FormEvent } from 'react'
import { useRouter } from 'next/navigation'
import { signIn } from 'next-auth/react'
import { Box, Button, FormHelperText, Link, TextField, Typography } from '@mui/material'

export default function SigninForm() {
  const [errorText, setErrorText] = React.useState('')
  const router = useRouter()
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const formData = new FormData(event.currentTarget)
    const data = {
      email: formData.get('email'),
      password: formData.get('password'),
    }
    signIn('credentials', {
      redirect: false,
      ...data,
    })
      .then((res) => {
        if (!res || res?.error) {
          setErrorText('Authentication failed.')
          return
        }
        router.push('/')
      })
      .catch(() => {
        setErrorText('System error.')
      })
  }

  return (
    <Box
      component='main'
      sx={{
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}
    >
      <Box sx={{ width: 345, display: 'flex', flexDirection: 'column' }}>
        <Typography variant='h5' textAlign='center'>
          ログイン
        </Typography>
        <Box sx={{ m: 2 }} />
        <Box
          component='form'
          onSubmit={handleSubmit}
          sx={{ display: 'flex', flexDirection: 'column' }}
        >
          <TextField id='email' name='email' label='メールアドレス' variant='outlined' />
          <Box sx={{ m: 1 }} />
          <TextField
            id='password'
            name='password'
            label='パスワード'
            type='password'
            autoComplete='current-password'
          />
          <FormHelperText error>{errorText}</FormHelperText>
          <Box sx={{ m: 2 }} />
          <Button type='submit' variant='outlined'>
            サインイン
          </Button>
          <Link href={'/admin/forgot-password'}>パスワードを忘れました</Link>
        </Box>
      </Box>
    </Box>
  )
}

```

### user-list（コンポーネント）
```diff_tsx:src/util/date.ts
    :
+ ここから下を追記
/**
 * yyyy-mm-dd hh:mm:ss で出力
 */
export function dateFormatHelper(d: Date | null): string {
  if (d === null) {
    return ''
  }
  const options = {
    year: 'numeric' as const,
    month: '2-digit' as const,
    day: '2-digit' as const,
    hour: '2-digit' as const,
    minute: '2-digit' as const,
    second: '2-digit' as const,
    hour12: false,
  }

  return new Intl.DateTimeFormat('ja-JP', options).format(d).replace(/\//g, '-')
}

```

```tsx:src/app/admin/(authenticated)/user-list/layout.tsx
import { getServerSession } from 'next-auth'
import { redirect } from 'next/navigation'
import { ReactNode } from 'react'
import options from '@/auth/authOptions'

export default async function Layout({ children }: { children: ReactNode }) {
  const session = await getServerSession(options)
  if (!session) redirect('/admin/signin')
  return <>{children}</>
}

```
```tsx:src/app/admin/(authenticated)/user-list/page.tsx
import React from 'react'
import { UserListPage } from './_components/UserListPage'
import { PrismaClient } from '@/generated/prismaClient'

export default async function Page() {
  const prisma = new PrismaClient()
  const props = {
    users: await prisma.users.findMany(),
  }
  return <UserListPage {...props} />
}

```
```tsx:src/app/admin/(authenticated)/user-list/_components/UserListPage.tsx
'use client'
import { Users } from '@/generated/prismaClient'
import React from 'react'
import { Box, Typography } from '@mui/material'
import { DataGrid, GridColDef, GridRowsProp } from '@mui/x-data-grid'
import { dateFormatHelper } from '@/util/date'

type Props = {
  users: Users[]
}
export function UserListPage({ users }: Props) {
  if (!users || users.length === 0) {
    return <Typography variant='h5'>No data available.</Typography>
  }
  const rows: GridRowsProp = users.map((row) => ({
    id: row.id,
    name: row.name,
    role: row.role,
    email: row.email,
    password: row.password,
    passwordToken: row.passwordToken,
    tokenExpiredAt: dateFormatHelper(row.tokenExpiredAt),
    createdAt: dateFormatHelper(row.createdAt),
    updatedAt: dateFormatHelper(row.updatedAt),
  }))
  const columns: GridColDef[] = [
    { field: 'id', headerName: '#' },
    { field: 'name', headerName: '名前' },
    { field: 'role', headerName: '役割' },
    { field: 'email', headerName: 'メール' },
    { field: 'password', headerName: 'パスワード' },
    { field: 'passwordToken', headerName: 'トークン' },
    { field: 'tokenExpiredAt', headerName: 'トークン有効期限', width: 160 },
    { field: 'createdAt', headerName: '作成日時', width: 160 },
    { field: 'updatedAt', headerName: '更新日時', width: 160 },
  ]
  return (
    <>
      <p>保護されたページ</p>
      <Box width='100%'>
        <Typography variant='h6'>Users</Typography>
        <DataGrid columns={columns} rows={rows} />
      </Box>
    </>
  )
}

```

## Next.js のチュートリアル
https://nextjs.org/learn/dashboard-app

https://github.com/vercel/next-learn/tree/main/dashboard/starter-example/public

prismaを使うなどのアレンジを入れながら。公式が使ってるTailwind を剥いだり大変だったけどいい訓練になったわ。基本、できあがりをベタ貼りするから差分はじぶんで調べてくれ

### Chapter 2 CSS Styling
```diff_json:package.json
  "dependencies": {
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
+   "@heroicons/react": "^2.0.18",
    "@mui/icons-material": "^5.14.16",
        :
  },

```

```tsx:src/app/page.tsx
import React from 'react'
import { Box, Button, Grid, Link, Typography } from '@mui/material'
import { ArrowRightIcon } from '@heroicons/react/24/outline'

export default function Page() {
  return (
    <Box display='flex' flexDirection='column'>
      {/* 青い領域（ヘッダ） */}
      <Grid item xs={12}>
        <Box height={200} width='100%' bgcolor='primary.main'>
          <Typography sx={{ color: 'white' }}>保護されていないページ</Typography>
          <Typography variant='h5' sx={{ color: 'white', marginBottom: 2 }}>
            This is Header
          </Typography>
        </Box>
      </Grid>

      {/* ライトグレーの領域（コンテンツ領域） */}
      <Box display='flex' flexDirection='row'>
        <Box
          width={400}
          height={500}
          bgcolor='#f0f0f0'
          display='flex'
          flexDirection='column'
          padding='30px'
          justifyContent='center'
        >
          <Box display='flex' flexDirection='column'>
            <Typography variant='h5' sx={{ color: 'text.primary', marginBottom: 2 }}>
              <strong>Welcome to Acme.</strong> This is the example for the{' '}
              <Link href='https://nextjs.org/learn' color='primary'>
                Next.js Learn Course
              </Link>
              , brought to you by Vercel.
            </Typography>
            <Link href='/admin/signin' underline='none'>
              <Button
                variant='contained'
                color='primary'
                sx={{ width: '200px', borderRedius: 2, textTransform: 'none' }}
              >
                Log in
                <ArrowRightIcon
                  className='text-white'
                  style={{ width: '1rem', height: '1rem', paddingLeft: '10px' }}
                />
              </Button>
            </Link>
          </Box>
        </Box>

        {/* ホワイトの領域 */}
        <Box display='flex' alignItems='center' justifyContent='center'>
          {/*  ヒーローイメージを追加*/}
        </Box>
      </Box>
    </Box>
  )
}
```

### Chapter 3 Optimizing Fonts and Images
https://github.com/vercel/next-learn/tree/main/dashboard/starter-example/public

- 上記リンクから絵をダウンロードし、`public` に保存
    - `hero-desktop.png`
    - `hero-mobile.png`

```tsx:src/app/_component/fonts.ts
import { Inter, Lusitana } from 'next/font/google'

export const inter = Inter({ subsets: ['latin'] })

export const lusitana = Lusitana({
  weight: ['400', '700'],
  subsets: ['latin'],
})

```
```tsx:src/app/_component/acme-logo.tsx
import { Box, Typography } from '@mui/material'
import { Language } from '@mui/icons-material'
import { lusitana } from './fonts'
import React from 'react'

export default function AcmeLogo() {
  return (
    <Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', color: 'white' }}>
      <Language sx={{ fontSize: '3rem', transform: 'rotate(15deg)' }} />
      <Typography
        className={`${lusitana.className}`}
        variant='h2'
        sx={{ fontSize: '2.75rem', marginLeft: '8px' }}
      >
        Acme
      </Typography>
    </Box>
  )
}

```
```tsx:src/app/layout.tsx
import React from 'react'
import { inter } from './_component/fonts'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body className={`${inter.className} antialiasing`}>{children}</body>
    </html>
  )
}

```
```tsx:src/app/page.tsx
import React from 'react'
import { Box, Button, Grid, Hidden, Link, Typography } from '@mui/material'
import { ArrowRightIcon } from '@heroicons/react/24/outline'
import AcmeLogo from './_component/acme-logo'
import { lusitana } from './_component/fonts'
import Image from 'next/image'

export default function Page() {
  return (
    <Box display='flex' flexDirection='column'>
      {/* 青い領域（ヘッダ） */}
      <Grid item xs={12}>
        <Box height={200} width='100%' bgcolor='primary.main'>
          <Typography sx={{ color: 'white' }}>保護されていないページ</Typography>
          <Typography variant='h5' sx={{ color: 'white', marginBottom: 2 }}>
            <AcmeLogo />
          </Typography>
        </Box>
      </Grid>

      {/* ライトグレーの領域（コンテンツ領域） */}
      <Box display='flex' flexDirection='row'>
        <Box
          width={400}
          height={760}
          bgcolor='#f0f0f0'
          display='flex'
          flexDirection='column'
          padding='30px'
          justifyContent='center'
        >
          <Box display='flex' flexDirection='column'>
            <Typography
              className={`${lusitana.className}`}
              variant='h5'
              sx={{ color: 'text.primary', marginBottom: 2 }}
            >
              <strong>Welcome to Acme.</strong> This is the example for the{' '}
              <Link href='https://nextjs.org/learn' color='primary'>
                Next.js Learn Course
              </Link>
              , brought to you by Vercel.
            </Typography>
            <Link href='/admin/signin' underline='none'>
              <Button
                variant='contained'
                color='primary'
                sx={{ width: '200px', borderRedius: 2, textTransform: 'none' }}
              >
                Log in
                <ArrowRightIcon
                  className='text-white'
                  style={{ width: '1rem', height: '1rem', paddingLeft: '10px' }}
                />
              </Button>
            </Link>
          </Box>
        </Box>

        {/* ホワイトの領域 */}
        <Box display='flex' alignItems='center' justifyContent='center'>
          <Hidden mdDown>
            <Image
              src='/hero-desktop.png'
              width={1000}
              height={760}
              className='hidden md:block'
              alt='Screenshots of the dashboard project showing desktop version'
            />
          </Hidden>
          <Hidden mdUp>
            <Image
              src='/hero-mobile.png'
              width={560}
              height={620}
              className='block md:hidden'
              alt='Screenshots of the dashboard project showing mobile version'
            />
          </Hidden>
        </Box>
      </Box>
    </Box>
  )
}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/c21dc72b-51cd-a459-f2b7-542dea1af863.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/6dbb744f-e1db-124c-bf5a-bb903d6eb66f.png)


### Chapter 4 Creating Layouts and Pages
```tsx:src/app/_component/nav-links.tsx
'use client'
import { PresentationChartBarIcon, HomeIcon, UserGroupIcon } from '@heroicons/react/24/outline'
import { Box, Link, Typography } from '@mui/material'
import { usePathname } from 'next/navigation'
import React from 'react'

// いったん固定値で書くけど本来はdbから引っ張ってきてね
const links = [
  { name: 'Home', href: '/', icon: HomeIcon },
  { name: 'Dashboard', href: '/dashboard', icon: PresentationChartBarIcon },
  { name: 'Users', href: '/user-list', icon: UserGroupIcon },
]

export default function NavLinks() {
  const pathname = usePathname()
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon
        return (
          <Link key={link.name} href={link.href}>
            <Box display='flex' flexDirection='row' marginTop='20px'>
              <LinkIcon style={{ width: '2.5rem', height: '2.5rem', marginRight: '10px' }} />
              <Typography
                variant='h6'
                color={pathname === link.href ? 'primary.main' : 'text.primary'}
              >
                {link.name}
              </Typography>
            </Box>
          </Link>
        )
      })}
    </>
  )
}

```
```tsx:src/app/_component/sidenav.tsx
'use client'

import React, { FormEvent } from 'react'
import { Box, Link, Typography } from '@mui/material'
import { PowerIcon } from '@heroicons/react/24/outline'
import NavLinks from './nav-links'
import AcmeLogo from './acme-logo'
import { useRouter } from 'next/navigation'

export default function SideNav() {
  const router = useRouter()
  const handleLogout = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    router.push('/')
  }
  return (
    <Box display='flex' height='100%' flexDirection='column'>
      {/* ACMEロゴ */}
      <Box bgcolor='primary.main' height='200px'>
        <Link href='/'>
          <AcmeLogo />
        </Link>
      </Box>
      <div>
        {/* Home, Invoices, Customers のメニュー */}
        <Box height='600px'>
          <NavLinks />
        </Box>

        {/* サインアウト */}
        <form onSubmit={handleLogout}>
          <button type='submit'>
            <PowerIcon
              style={{ width: '2.5rem', height: '2.5rem', marginRight: '10px' }}
            ></PowerIcon>
            <Typography variant='h5'>Sign Out</Typography>
          </button>
        </form>
      </div>
    </Box>
  )
}

```
```tsx:src/app/layout.tsx
import React from 'react'
import { inter } from './_component/fonts'
import { Box } from '@mui/material'
import SideNav from './_component/sidenav'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang='en'>
      <body className={`${inter.className} antialiasing`}>
        <Box display='flex' height='100%' flexDirection='row' overflow='hidden'>
          {/* サイドバー表示部 */}
          <Box width='300px' border='1px solid blue' padding='10px'>
            <SideNav />
          </Box>

          {/* コンテンツ表示部 */}
          <Box>{children}</Box>
        </Box>
      </body>
    </html>
  )
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/460d857f-2d5f-1eb1-236c-7eb1a44b41d5.png)

### Chapter 5 Navigating Between Pages
```tsx:src/app/_component/nav-links.tsx
'use client'
import { DocumentDuplicateIcon, HomeIcon, UserGroupIcon } from '@heroicons/react/24/outline'
import { Box, Link, Typography } from '@mui/material'
import { usePathname } from 'next/navigation'
import React from 'react'

// いったん固定値で書くけど本来はdbから引っ張ってきてね
const links = [
  { name: 'Home', href: '/', icon: HomeIcon },
  { name: 'Invoices', href: '/invoices', icon: DocumentDuplicateIcon },
  { name: 'Customers', href: '/customers', icon: UserGroupIcon },
]

export default function NavLinks() {
  const pathname = usePathname()
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon
        return (
          <Link key={link.name} href={link.href}>
            <Box display='flex' flexDirection='row' marginTop='20px'>
              <LinkIcon style={{ width: '2.5rem', height: '2.5rem', marginRight: '10px' }} />
              <Typography
                variant='h6'
                color={pathname === link.href ? 'primary.main' : 'text.primary'}
              >
                {link.name}
              </Typography>
            </Box>
          </Link>
        )
      })}
    </>
  )
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/8552f72a-e949-a216-0539-4030008a4154.png)

### Chapter 7 Fetching Data
```prisma/schema.prisma（追記）
    :
model Revenue {
  id      Int    @id @default(autoincrement())
  month   String
  revenue Int

  @@map("revenue")
}

model Customer {
  id       Int       @id @default(autoincrement())
  invoices Invoice[]
  name     String
  email    String
  imageUrl String    @map("image_url")

  @@map("customer")
}

model Invoice {
  id         Int      @id @default(autoincrement())
  customer   Customer @relation(fields: [customerId], references: [id])
  customerId Int      @map("customer_id")
  amount     Int
  date       String
  status     String   @default("pending")

  @@map("invoice")
}

```
```console:console
npx prisma migrate dev --name dashboard
npx prisma generate
```

```ts:src/types/types.ts
export type Revenue = {
  month: string
  revenue: number
}

export type Invoice = {
  id: number
  amount: number
  customer: {
    name: string
    imageUrl: string
    email: string
  }
}

```

```tsx:src/app/dashboard/_component/InvoiceDataTable.tsx
'use client'

import React from 'react'
import { Box, Link, Typography } from '@mui/material'
import { DataGrid, GridColDef, GridRenderCellParams, GridRowsProp } from '@mui/x-data-grid'
import { Invoice } from '@/types/types'

export default function InvoiceDataTable({ invoice }: { invoice: Invoice[] }) {
  if (!invoice || invoice.length === 0) {
    return <Typography variant='h5'>No data available.</Typography>
  }

  const rows: GridRowsProp = invoice.map((row) => ({
    id: row.id,
    customerName: row.customer.name,
    amount: row.amount,
  }))

  const columns: GridColDef[] = [
    {
      field: 'id',
      headerName: '請求額No.',
      width: 100,
      renderCell: (params: GridRenderCellParams<any>) => (
        <>
          <Link href={`/invoices/${params.id}`}>{params.value}</Link>
        </>
      ),
    },
    { field: 'customerName', headerName: '名前', width: 150 },
    { field: 'amount', headerName: 'amount', width: 150 },
  ]

  return (
    <Box width='100%'>
      <Typography variant='h6'>Recent Invoice</Typography>
      <DataGrid rows={rows} columns={columns}></DataGrid>
    </Box>
  )
}

```
```tsx:src/app/dashboard/_component/RevenueDataTable.tsx
'use client'

import React from 'react'
import { Box, Typography } from '@mui/material'
import { DataGrid, GridColDef, GridRowsProp } from '@mui/x-data-grid'
import { Revenue } from '@/types/types'

export default function RevenueDataTable({ revenue }: { revenue: Revenue[] }) {
  if (!revenue || revenue.length === 0) {
    return <Typography variant='h5'>No data available.</Typography>
  }

  const rows: GridRowsProp = revenue
  const columns: GridColDef[] = [
    {
      field: 'month',
      headerName: '月',
      width: 150,
    },
    { field: 'revenue', headerName: '収益', width: 150 },
  ]

  return (
    <Box width='100%'>
      <Typography variant='h6'>Recent Revenue</Typography>
      <DataGrid rows={rows} columns={columns}></DataGrid>
    </Box>
  )
}

```

```tsx:src/app/dashboard/page.tsx
import React from 'react'
import { Box, Typography } from '@mui/material'
import InvoiceDataTable from './_component/InvoiceDataTable'
import RevenueDataTable from './_component/RevenueDataTable'

export default async function Page() {
  const response = await fetch('http://localhost:3000/api/dashboard')
  const { revenue, invoice } = await response.json()
  return (
    <Box display='flex' flexDirection='column'>
      <Typography variant='h4'>Dashboard</Typography>
      <Box>
        <RevenueDataTable revenue={revenue}></RevenueDataTable>
      </Box>
      <Box>
        <InvoiceDataTable invoice={invoice}></InvoiceDataTable>
      </Box>
    </Box>
  )
}

```

```ts:src/app/api/dashboard/route.ts
import { PrismaClient } from '@/generated/prismaClient'
import { NextRequest, NextResponse } from 'next/server'
import { serverError } from '@/util/apiHandler'

export async function GET(req: NextRequest) {
  const prisma = new PrismaClient()
  try {
    const invoice = await prisma.invoice.findMany({
      select: {
        amount: true,
        customer: {
          select: {
            name: true,
            imageUrl: true,
            email: true,
          },
        },
        id: true,
      },
      orderBy: {
        date: 'desc',
      },
      take: 5,
    })
    const revenue = await prisma.revenue.findMany()
    return NextResponse.json({ invoice, revenue })
  } catch (error) {
    return serverError(error)
  } finally {
    await prisma.$disconnect()
  }
}

```
```http:src/app/api/dashboard/route.http
### 200 OK
GET http://localhost:3000/api/dashboard
Accept: application/json

```

```prisma/seeds/seeder.ts
import { PrismaClient } from '@/generated/prismaClient'

async function main() {
  const prisma = new PrismaClient()
  await prisma.users.deleteMany()
  await prisma.users.create({
    data: {
      name: '開発 太郎',
      email: 'test@example.com',
      password: 'hoge',
    },
  })
  await prisma.customer.createMany({
    data: [
      {
        id: 1,
        name: 'Delba de Oliveira',
        email: 'delba@olivaira.com',
        imageUrl: '/customers/delba-de-oliveira.png',
      },
      {
        id: 2,
        name: 'Lee Robinson',
        email: 'lee@robinson.com',
        imageUrl: '/customers/lee-robinson.png',
      },
      {
        id: 3,
        name: 'Hector Simpson',
        email: 'hector@simpson.com',
        imageUrl: '/customers/hector-simpson.png',
      },
      {
        id: 4,
        name: 'Steven Tey',
        email: 'steven@tey.com',
        imageUrl: '/customers/steven-tey.png',
      },
      {
        id: 5,
        name: 'Steph Dietz',
        email: 'steph@dietz.com',
        imageUrl: '/customers/steph-diets.png',
      },
      {
        id: 6,
        name: 'Michael Novotny',
        email: 'michael@novotny',
        imageUrl: '/customers/michael-novotny.png',
      },
      {
        id: 7,
        name: 'Evil Rabbit',
        email: 'evil@rabbit.com',
        imageUrl: '/customers/evil-rabbit.png',
      },
      {
        id: 8,
        name: 'Emil komalski',
        email: 'emil@komalski.com',
        imageUrl: '/customers/emil-komalski.png',
      },
      {
        id: 9,
        name: 'Amy Burns',
        email: 'amy@burns.com',
        imageUrl: '/customers/amy-burns.png',
      },
      {
        id: 10,
        name: 'Balazs Orban',
        email: 'balazs@orban.com',
        imageUrl: '/customers/balazs-orban.png',
      },
    ],
  })
  await prisma.invoice.createMany({
    data: [
      {
        customerId: 1,
        amount: 15795,
        date: '2022-11-14',
        status: 'pending',
      },
      {
        customerId: 2,
        amount: 20348,
        date: '2022-11-14',
        status: 'pending',
      },
      {
        customerId: 5,
        amount: 3040,
        date: '2022-10-29',
        status: 'paid',
      },

      {
        customerId: 4,
        amount: 44000,
        date: '2023-09-10',
        status: 'paid',
      },
      {
        customerId: 6,
        amount: 34577,
        date: '2023-09-10',
        status: 'paid',
      },
      {
        customerId: 8,
        amount: 54246,
        date: '2023-07-16',
        status: 'pending',
      },
      {
        customerId: 7,
        amount: 666,
        date: '2023-06-27',
        status: 'pending',
      },
      {
        customerId: 4,
        amount: 32545,
        date: '2023-06-09',
        status: 'paid',
      },
      {
        customerId: 5,
        amount: 1250,
        date: '2023-06-17',
        status: 'paid',
      },
      {
        customerId: 6,
        amount: 8546,
        date: '2023-06-07',
        status: 'paid',
      },
      {
        customerId: 2,
        amount: 500,
        date: '2023-08-19',
        status: 'paid',
      },
      {
        customerId: 6,
        amount: 8945,
        date: '2023-06-03',
        status: 'paid',
      },
      {
        customerId: 3,
        amount: 8945,
        date: '2023-06-18',
        status: 'paid',
      },
      {
        customerId: 1,
        amount: 8945,
        date: '2023-10-04',
        status: 'paid',
      },
      {
        customerId: 3,
        amount: 1000,
        date: '2022-06-05',
        status: 'paid',
      },
    ],
  })
  await prisma.revenue.createMany({
    data: [
      {
        month: 'Jan',
        revenue: 2000,
      },
      {
        month: 'Feb',
        revenue: 1800,
      },
      {
        month: 'Mar',
        revenue: 2200,
      },
      {
        month: 'Apr',
        revenue: 2500,
      },
      {
        month: 'May',
        revenue: 2300,
      },
      {
        month: 'Jun',
        revenue: 3200,
      },
      {
        month: 'Jul',
        revenue: 3500,
      },
      {
        month: 'Aug',
        revenue: 3700,
      },
      {
        month: 'Sep',
        revenue: 2500,
      },
      {
        month: 'Oct',
        revenue: 2800,
      },
      {
        month: 'Nov',
        revenue: 3000,
      },
      {
        month: 'Dec',
        revenue: 4800,
      },
    ],
  })
  prisma.$disconnect()
}
main().then(() => console.log('seeding ok.'))
```

いったん休憩