無償版PaaSだけでGithubでログインするNext.jsアプリを作る

最近next.jsでいろいろアプリを作っているのですが、そのときにnextauthというライブラリで認証機能をさくっと作れることを知りまして、作ってみようと思いました。

自分のGithubのレポジトリはこちらです。

https://github.com/greenteabiscuit/jamstack

## やりたいこと

- Next.jsでウェブアプリを作る
- Githubで認証できるようにする
- データベースにログイン情報を保存できるようにする
- すべて無料サービスで行う

## 使う技術構成

- Next.js
- Typescript
- nextauth
- mysql
- Docker
- Prisma

## 使うサービス

- Github App
- Vercel
- Heroku ClearDB

## ファイル構成

```
tree -I node_modules
.
├── README.md
├── app
│   ├── LICENSE
│   ├── README.md
│   ├── components
│   │   ├── access-denied.tsx
│   │   ├── footer.module.css
│   │   ├── footer.tsx
│   │   ├── header.module.css
│   │   ├── header.tsx
│   │   └── layout.tsx
│   ├── global.d.ts
│   ├── next-env.d.ts
│   ├── package.json
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── api
│   │   │   ├── auth
│   │   │   │   └── [...nextauth].ts
│   │   │   └── examples
│   │   │       ├── jwt.ts
│   │   │       ├── protected.ts
│   │   │       └── session.ts
│   │   ├── api-example.tsx
│   │   ├── client.tsx
│   │   ├── index.tsx
│   │   ├── policy.tsx
│   │   ├── protected.tsx
│   │   ├── server.tsx
│   │   └── styles.css
│   ├── prisma
│   │   ├── migrations
│   │   │   ├── 20210521153533_20210521
│   │   │   │   └── migration.sql
│   │   │   └── migration_lock.toml
│   │   └── schema.prisma
│   ├── tsconfig.json
│   ├── types
│   │   ├── environment.d.ts
│   │   ├── next-auth.d.ts
│   │   └── next.d.ts
│   └── yarn.lock
├── docker-compose.yml
└── mysql
    ├── Dockerfile
    └── my.cnf
```

## フォルダ作成

typescriptバージョンのnext-authのサンプルを使って修正していきます。
こちらのテンプレートをそのまま使わせていただきます。

https://github.com/nextauthjs/next-auth-typescript-example

自分は`jamstack app`と名付けました。

```
$ mkdir jamstack
$ cd jamstack
$ mkdir mysql
$ git clone git@github.com:nextauthjs/next-auth-typescript-example.git app
```

app 内のgitの紐付けも外し、yarn installでパッケージをインストールしておきます。

```
$ cd app
$ rm -rf .git
$ yarn install
```

## Prismaのインストール

Prismaをインストールして初期化します。

```
$ yarn add prisma --dev
$ yarn add @prisma/client

$ npx prisma init
```

これで`.env`ファイルが生成されるので、ここの環境変数を編集します。
基本的にコンテナの中からアクセスするので、mysqlコンテナの3306ポートを指定します。
（外側からの場合は `127.0.0.1:13306`となります。）

```.env
DATABASE_URL="mysql://root:root@mysql:3306/jamstack_db"
```

## docker-compose.ymlの作成

ルートディレクトリに `docker-compose.yml`を置きます。
mysqlとnext.js用のコンテナを2つ用意します。

```docker-compose.yml
version: '3'
services:

    next:
      image: node:15.0.1
      volumes:
        - ./app:/home/app
        - node_modules_volume:/home/app/node_modules
      ports:
        - 3000:3000
      working_dir: /home/app
      command: [bash, -c, yarn upgrade --no-progress --network-timeout 1000000 && yarn run dev]

    mysql:
        build: ./mysql
        environment:
            TZ: Asia/Tokyo
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: jamstack_db
        ports:
            - 13306:3306
        volumes:
            - mysql_volume:/var/lib/mysql
    
volumes:
    mysql_volume:
    node_modules_volume:
```

mysqlの設定ファイルは以下です。

```mysql.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character_set_server=utf8mb4
collation_server=utf8mb4_bin

# タイムゾーンの設定
default_time_zone=SYSTEM
log_timestamps=SYSTEM

# デフォルト認証プラグインの設定
default_authentication_plugin=mysql_native_password

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default_character_set=utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default_character_set=utf8mb4
```

mysqlのDockerfileです。

```Dockerfile
FROM mysql:8.0.21
# FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca

RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql &&  mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql

COPY ./my.cnf /etc/mysql/conf.d/my.cnf
```

## Githubの認証の設定

今回はGithubアカウントを使って認証をしたいので、Github Appを作ることになります。Settingsを開きます。

![Screen Shot 2021-05-21 at 23.49.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/b02dc02b-2371-5c59-8337-f4a1675cfb22.png)

下までスクロールして Developer settingsをクリックします。

![Screen Shot 2021-05-21 at 23.49.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/47181a8c-b191-b80e-8f3b-0f13576d8ca5.png)

ここで New Github Appを作ります。

![Screen Shot 2021-05-21 at 23.49.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/0d988985-92ed-652f-5627-a91ebc080b79.png)

appの名前は`jamstack-app`として、homepage URLはとりあえずgithubのレポジトリのリンクにしておきます。

![Screen Shot 2021-05-21 at 23.52.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/a053e4b6-ae53-9a82-e778-20b1092c742a.png)

`Callback URL`は`http://localhost:3000`としておきます。Vercelなどでデプロイしたらまた変更します。

![Screen Shot 2021-05-21 at 23.53.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/245455e3-77f3-3d3d-65ff-a608d5a0b291.png)

これで `Create App`とします。すると次の画面に到達するので、client secretをgenerateします。`client secret`と`Client ID`は保存しておきます。

![Screen Shot 2021-05-21 at 23.56.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/d9783519-a5bd-97d4-6412-8ebe00f4eb69.png)


## 環境変数の設定

クローンしたレポジトリに`.env.local.example`があるはずなので、内容をコピーして `.env`に追加します。先ほどゲットした変数もペーストしておきます。

```.env
NEXTAUTH_URL=http://localhost:3000
GITHUB_ID=[Client ID]
GITHUB_SECRET=[Client secret]
```

## prisma周辺の設定

prismaをインポートするように`[...nextauth].ts`を編集します。

```[...nextauth].ts
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
// 以下を追加
import { PrismaClient } from "@prisma/client";
import Adapters from "next-auth/adapters";
let prisma;

if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  if (!global.prisma) {
    global.prisma = new PrismaClient();
  }
  prisma = global.prisma;
}

...
// adapterを追加
  database: process.env.DATABASE_URL,
  adapter: Adapters.Prisma.Adapter({ prisma }),
```

このままだとtypescriptに怒られるので、global.d.tsにprismaをanyとして定義しておきます。（もっといい方法を探す余裕が今回ありませんでした、すみません。）

```global.d.ts
export {};
declare global {
    namespace NodeJS {
        interface Global {
            prisma: any;
        }
    }
}
```

## マイグレーションする

マイグレーションするときにPrismaのスキーマファイルをここに書きます。

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

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

generator client {
  provider = "prisma-client-js"
}

model Account {
  id                 Int       @id @default(autoincrement())
  compoundId         String    @unique @map(name: "compound_id")
  userId             Int       @map(name: "user_id")
  providerType       String    @map(name: "provider_type")
  providerId         String    @map(name: "provider_id")
  providerAccountId  String    @map(name: "provider_account_id")
  refreshToken       String?   @map(name: "refresh_token")
  accessToken        String?   @map(name: "access_token")
  accessTokenExpires DateTime? @map(name: "access_token_expires")
  createdAt          DateTime  @default(now()) @map(name: "created_at")
  updatedAt          DateTime  @default(now()) @map(name: "updated_at")
  @@index([providerAccountId], name: "providerAccountId")
  @@index([providerId], name: "providerId")
  @@index([userId], name: "userId")
  @@map(name: "accounts")
}
model Session {
  id           Int      @id @default(autoincrement())
  userId       Int      @map(name: "user_id")
  expires      DateTime
  sessionToken String   @unique @map(name: "session_token")
  accessToken  String   @unique @map(name: "access_token")
  createdAt    DateTime @default(now()) @map(name: "created_at")
  updatedAt    DateTime @default(now()) @map(name: "updated_at")
  @@map(name: "sessions")
}
model User {
  id            Int       @id @default(autoincrement())
  name          String?
  email         String?   @unique
  emailVerified DateTime? @map(name: "email_verified")
  image         String?
  createdAt     DateTime  @default(now()) @map(name: "created_at")
  updatedAt     DateTime  @default(now()) @map(name: "updated_at")
  @@map(name: "users")
}
model VerificationRequest {
  id         Int      @id @default(autoincrement())
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime @default(now()) @map(name: "created_at")
  updatedAt  DateTime @default(now()) @map(name: "updated_at")
  @@map(name: "verification_requests")
}
```

マイグレーションを行う際には、コンテナに入って行いました。

```
$ dx jamstack_next_1 bash
root@ea23ae917a29:/home/app# npx prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "jamstack_db" at "127.0.0.1:13306"

✔ Enter a name for the new migration: … 20210521
The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20210521153533_20210521/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (2.23.0) to ./node_modules/@prisma/client in 158ms
```

これでSequel Proなどで確認してみると、dbが作成されているのがわかります。

![Screen Shot 2021-05-22 at 0.40.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/866656f7-7b00-cf2f-3a2b-697afd439e93.png)


## 立ち上げてみる

```
$ docker-compose up
```

localhost:3000にて、`You are not signed in`の状態でアクセスできるはずです。

![Screen Shot 2021-05-22 at 0.54.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/47f55348-cbdf-8f25-0be0-e6b452ffa4fd.png)

これでSign inボタンを押すと、いろいろ選択肢が出てくるはずです。今回はGithubの設定しかしていないので、`Sign in with Github`を選びます。

![Screen Shot 2021-05-22 at 0.54.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/7e697b42-5deb-9fd6-791f-04579a8c8f36.png)

そうすると無事ログインできているはずです。

![Screen Shot 2021-05-22 at 0.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/bad6676e-9a8e-2f32-aad7-ac32d45ff0bb.png)

# デプロイ

デプロイするときには二つのサービスを使います。

- Heroku ClearDB （ユーザー、セッションなどの管理）
- Vercel (Next.jsのアプリをデプロイする)

## HerokuでのDBのデプロイ

herokuで適当なアプリを作って、そこにcleardbのアドオンを追加します。そこで出てきた`CLEARDB_DATABASE_URL`をメモっておいてください。以下の5行でとりあえず終わりです。

```
$ heroku login
$ heroku git:remote --app database-provisioning
$ heroku apps:create database-provisioning
$ heroku addons:create cleardb:ignite
$ heroku config
CLEARDB_DATABASE_URL=......
```

## Vercelでのデプロイ

Vercelで最後にデプロイします。このときに`package.json`を修正して、マイグレーションを行う設定を追加します。あと、`npm-run-all`を走らせる設定も追加します。

```package.json
{
...........
  "homepage": "http://next-auth-typescript-example.now.sh",
  "main": "",
  "scripts": {
    // ココ変更！
    "migrate:deploy": "prisma migrate deploy",
    "dev": "next",
    "build": "npm-run-all migrate:deploy build-app",
    "build-app": "next build",
    "start": "next start",
    "types": "tsc --noEmit"
  },
...........
  // npm-run-all を走らせる
  "devDependencies": {
    "@types/node": "^14.14.41",
    "@types/react": "^17.0.3",
    "@types/react-dom": "^17.0.3",
    "prisma": "^2.23.0",
    "npm-run-all": "^4.1.5",
    "typescript": "^4.2.4"
  }
}
```

Vercelでデプロイする際には、`DATABASE_URL`の環境変数を追加しましょう。

![Screen Shot 2021-05-22 at 1.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/0ff04bca-f1c6-9f21-0585-b7f3bd488239.png)


vercelでもデプロイできています！

```
01:38:57.342  	$ npm-run-all migrate:deploy build-app
01:38:57.810  	$ prisma migrate deploy
01:38:58.325  	Prisma schema loaded from prisma/schema.prisma
01:38:58.358  	Datasource "db": MySQL database "heroku_819789418405824" at "us-cdbr-east-03.cleardb.com:3306"
01:39:00.084  	1 migration found in prisma/migrations
01:39:02.142  	The following migration have been applied:
01:39:02.143  	migrations/
01:39:02.143  	  └─ 20210521153533_20210521/
01:39:02.143  	    └─ migration.sql
01:39:02.143  	      
01:39:02.143  	All migrations have been successfully applied.
01:39:02.619  	$ next build
```

## 環境変数の追加

先ほどは追加し忘れた環境変数（`NEXTAUTH_URL`など）も追加しておきます。

![Screen Shot 2021-05-22 at 1.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/a42bf679-f431-c25f-28e6-9b4ca61f1c6e.png)

NEXTAUTH_URLはcanonical urlを貼るようにしてください。以下だと `jamstack-greenteabiscuit.vercel...`となります。

![Screen Shot 2021-05-22 at 2.01.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/21221917-9b41-b48a-e7f1-b6db512d9244.png)


GithubのCallback URLも更新しておきます。ローカルで開発するときとで分けないといけないのがちょっと面倒です。ローカル用のGithub Appと本番用のGithub Appで分けてもいいかもしれません。

![Screen Shot 2021-05-22 at 1.50.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/4db67905-4de7-c5a3-e552-6592a8386982.png)

## 参考

以下が参考になりました。ありがとうございました。

[Next.js + Prisma + NextAuth.js + React Query で作るフルスタックアプリケーションの新時代](https://yuyao.me/posts/next-prisma-auth-tutorial)