Next.jsを利用したログイン機能の実装

個人開発するにしても、どんなサービス世にdeployするとしても、データベース設計と認証機能の実装は必須だよねってことで、今回はNext.jsを使ったログイン機能の実装をアウトプットしたいと思います。

今回のログイン機能を学習するにあたって、以下の動画が大変参考になりました。ありがとうございました。

https://www.youtube.com/watch?v=2xexm8VXwj8


## 実行環境
```
node: v18.20.5
npm: v10.8.2
next: 15.1.5
```
macbook proを利用した構築になります。

## Next.jsをCreate
```
npx create-next-app@latest
```
npmではなくてnpxで実行してね。

## プロジェクトの起動
```
npm run dev
```
でローカルホストを立ち上げます。
ポートはデフォルトだと3000になります。
以下画面にアクセスできればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524273/21233c46-9015-bdb2-39a2-cb66d3dbe34d.png)

この状態でTailwindcssもRecommend通りやってれば入っているはずなので、そのまま進めていきます。

### Package.json
```json
"dependencies": {
    "@radix-ui/react-avatar": "^1.1.2",
    "@radix-ui/react-dropdown-menu": "^2.1.4",
    "@radix-ui/react-navigation-menu": "^1.2.3",
    "@radix-ui/react-slot": "^1.1.1",
    "@types/bcrypt": "^5.0.2",
    "bcrypt": "^5.1.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "jsonwebtoken": "^9.0.2",
    "lucide-react": "^0.473.0",
    "next": "15.1.5",
    "next-auth": "^5.0.0-beta.25",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tailwind-merge": "^2.6.0",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/jsonwebtoken": "^9.0.7",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.1.5",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
```
うまくいくまでにいろいろ試したので不要なものも結構入ってます（zodとか）

## ページの作成
ページ構成は簡単にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524273/b229ab9f-2a74-d3b3-f3ce-d3272b30d84b.png)
サインインボタンを用意し、サインインすればサインアウトボタンが表示されるようになるというものです。
動画を参考に、セッションデータから情報を取得してくるのも画面では含めています。



## ログイン認証についての考え方
基本はNext-authを使って認証周りを作っていくことにしました。個人開発、ということを念頭に置いていると規模感としては1万ユーザー集まるプロジェクト作れれば御の字だと自分の感覚では考えているので、その規模感で耐えられるようにと考えると、next-authがシンプルだし十分な機能かなと。
いろんなプロバイダーにも対応しているのでやりやすいですね。

サインインをクリックすることで、定義したプロバイダーが別画面で表示されログインを試すことができます。
具体的にはNextAuth関数に含まれるsignIn関数についてConfig（引数）を渡して実行することで認証周りの挙動が実装されます。

フォームを作って、バリデーションして、登録されたデータがあればその情報を取得してログイン後のページを表示させるといったAPIの作成をしなくても実装できるのが拍子抜けというかすごく感動ものでした。

Next.jsのルートディレクトリに`auth.ts`ファイルを作成し、そこでNextAuth関数に渡す引数の定義をしていきます。
```typescript:/auth.ts
import NextAuth, { NextAuthConfig } from "next-auth";
import Line from "next-auth/providers/line"
import github from "next-auth/providers/github";

export const config: NextAuthConfig = {
    theme: {
        colorScheme: "light"
    },
    providers: [
        Line({
            clientId: process.env.AUTH_LINE_ID as string,
            clientSecret: process.env.AUTH_LINE_SECRET,
            checks: ["state"]
        }),
        github({
            clientId: process.env.AUTH_GITHUB_ID,
            clientSecret: process.env.AUTH_GITHUB_SECRET,
            style: {
                text: "#24292f",
                bg: "#fff",
            }
        }),
    ],
    basePath: "/api/auth",
    callbacks: {
        authorized({request, auth}){
            try{
                const { pathname } = request.nextUrl;
                if(pathname === "/serverpage") return !!auth;
                return true
            } catch(err){
                console.log(err)
            }
        },
        jwt({token, trigger, session}) {
            if(trigger === "update") token.name  = session.user.name;
            return token;
        },
    }
}

export const {handlers, signIn, signOut, auth} = NextAuth(config)
```
そしてここでexportした関数たちを`auth-components`として呼び出します。
```typescript:app/components/auth-components.tsx
import React from "react";
import { Button } from "./ui/button";
import { signIn, signOut } from "@/auth";

export function SignIn({
    // provider,
    ...props
    }: { provider?: string } & React.ComponentPropsWithRef<typeof Button>) {
    return (
        <form
        action={async () => {
            "use server";
            await signIn();
        }}
        >
        <Button {...props} className="bg-blue-400 hover:bg-white hover:text-blue-400">サインイン</Button>
        </form>
    );
}

export function SignOut({
  // provider,
  ...props
}: { provider?: string } & React.ComponentPropsWithRef<typeof Button>) {
  return (
    <form
      action={async () => {
        "use server";
        await signOut();
      }}
      className="w-full"
    >
      <Button variant="ghost" className="w-full p-0" {...props}>
        ログアウト
      </Button>
    </form>
  );
}
```

そしてこのコンポーネントを`page.tsx`の中で呼び出せばOKです。
```typescript:app/page.tsx
import { auth } from "@/auth";
import { SignIn, SignOut } from "../components/auth-components";
// import CustomLink from "@/components/custom-link";

export default async function Home(){
  const session = await auth();

  return (
    <div className="w-8/12 flex flex-col items-center justify-center p-4 mx-auto text-center bg-gray-300 rounded-lg min-h-60">
      {!session?.user ? ( 
        <SignIn className="w-full"/> 
      ) : (
        <SignOut /> )}
    </div>
  )
}
```

でも画面で用意しているサーバー側からの見た目は、ログインしないと見られないようにしているわけですが、これはmiddlewareの機能として作成しています。
具体的には任意のパスにマッチするものをアクセスから除外するというような書き方になるわけですが、ルートディレクトリに直接`middleware.ts`を作成し以下のように記述しています。
```typescript:/middleware.ts
export { auth as middleware } from "@/auth";

export const config = {
    matcher: ['/serverpage/:path*']
}
```

クライアント側からの見た目と書いたページはサーバーアクションを経由しないので、ログインしてもしなくても見られるのですが、ログインしていない状態でサーバー側からの見た目というページにアクセスすると以下のページにリダイレクトされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524273/afa43c6a-3dd7-dc16-d5a6-edb86921c1bd.png)


ということで実際に今回はLINEとGithubでプロバイダーを作成したわけですが、ログインが完了すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524273/1bf947c3-58b8-4fa2-1083-518a73dca5e0.png)

この画面になります。
そしてサーバー側からの見た目ページにいくとセッションデータが取得できているのがわかるはずです。

https://private-nextjs-login.vercel.app/