🐣🥚🔰🐣🥚とりあえずログイン機能を作ってみたい（laravelとnext.js）🐣🥚🔰🐣🥚

# はじめに
社内ではlaravelとnuxt.jsや、laravelとnext.jsを組み合わせた開発が行われています。
ただ、私自身業務でcodeigniterを使って開発しているので知見がありません。
そこでログイン機能を作って知見を深めたいと思いました。

また、laravelとnext.jsでの組み合わせで調べると記事が少ないような気がしたので、少しでも貢献できれば嬉しいです☺️

# 環境構築
macOS Ventura 13.4.1で開発します。

## 事前作業
### 事前にインストールが必要なもの
docker Version4.19.0
node.js v18.17.x（next.js）
php 8.x
composer 2.6.x（laravel 10.x）

### 事前に作成が必要なもの
事前にdockerのコンテナを作成しておいてください。
また、この中で以下のようにnodeをインストールしておいてください。
```dockerfile:dockerfile
# node install
RUN curl -fsSL https://rpm.nodesource.com/setup_18.x | bash -
RUN yum install -y nodejs
```

## next.jsの環境構築
next.jsをローカル環境にインストールしたいと思います。
公式の通りにコマンドを実行してみます
```
$ npx create-next-app@latest
Need to install the following packages:
create-next-app@14.0.4
Ok to proceed? (y)
✔ What is your project named? … src
✔ 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
```
アプリ名をsrcにしたこと以外はデフォルト設定です。

さて、コンテナに入ってnextアプリが立ち上がることを確認します
```
$ docker container exec -it app bash
# npm run dev

> src@0.1.0 dev
> next dev

   ▲ Next.js 14.0.4
   - Local:        http://localhost:3000

 ✓ Ready in 4.4s
```

ブラウザで確認してみます
![スクリーンショット 2023-12-12 17.12.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450442/a09e60c3-d91b-efce-942d-05139b23c2ec.png)

表示できました🥳

## laravelの環境構築
laravelをローカル環境にインストールしたいと思います。
公式の通りにコマンドを実行してみます
```
$ composer create-project laravel/laravel src
```
アプリ名をsrcにしたこと以外はデフォルト設定です。

アプリが立ち上がることを確認します
![スクリーンショット 2023-12-20 3.52.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450442/0f7aa39c-3860-fa8b-0058-64ce0be910dc.png)


表示できました🥳

# 認証機能の実装

## laravel側
sanctumを使って認証システムを作りたいと思います。
変更点は以下です
```php:routes/api.php
+ use App\Http\Controllers\API\AuthController;

+ Route::post('register', [AuthController::class, 'register']);
+ Route::get('login', [AuthController::class, 'login'])->name('login');
+ 
+ Route::middleware('auth:sanctum')->group(function() {
+     Route::post('logout', [AuthController::class, 'logout']);
+ });
```

```php:config/cors.php
- 'supports_credentials' => false,
+ 'supports_credentials' => true,
```

```php:app/Http/Kernel.php
- // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
+ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
```

```php:app/Http/Controllers/API/AuthController.php
<?php

namespace App\Http\Controllers\API;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Validator;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name'=>'required|max:191',
            'email'=>'required|email|max:191|unique:users,email',
            'password'=>'required|min:8',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'validation_errors' => $validator->messages(),
            ]);
        } else {
            $user = User::create([
                'name' => $request->name,
                'email' => $request->email,
                'password' => Hash::make($request->password),
            ]);

            return response()->json([
                'status' => 200,
                'username' => $user->name,
                'message' => 'register success',
            ]);
        }
    }

    public function login(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required',
            'password' => 'required',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'validation_errors' => $validator->messages(),
            ]);
        } else {
            $user = User::where('email', $request->email)->first();
            if (!$user || !Hash::check($request->password, $user->password)) {
                return response()->json([
                    'status' => 401,
                    'message' => '入力情報が不正です',
                ]);
            } else {
                $token = $user->createToken($user->email . '_Token')->plainTextToken;

                return response()->json([
                    'status' => 200,
                    'username' => $user->name,
                    'token' => $token,
                    'message' => 'ログインにせいこうしました。',
                ]);
            }
        }
    }

    public function logout()
    {
        auth()->user()->currentAccessToken()->delete();
        return response()->json([
            'status' => 200,
            'message' => 'ログアウトせいこう',
        ]);
    }
```

insomniaで認証できているか確認してみます
アカウント登録
![スクリーンショット 2023-12-20 5.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450442/3b6a3dac-5ee2-3e8f-ef05-0ca81116738c.png)

ログイン
![スクリーンショット 2023-12-20 5.49.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450442/cbd44a4f-e892-3844-e1f9-3ea31c025b35.png)

ログアウト
![スクリーンショット 2023-12-20 5.50.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/450442/689a6d24-bdd8-65e9-8ff5-93fbced2f4d2.png)

laravel側の認証機能はできていそうです🥳

## next側
フロント側で行うことは以下です。
- localstorageにlaravelで発行したapiトークンを保存する
- api通信時にはトークンを設定したaxiosを使う

```typescript: app/login/page.tsx
'use client'
import axios from "@/app/api/axios"

const clickSignUp = () => {
    const email = document.querySelector<HTMLInputElement>('input[name="email"]')
    const password = document.querySelector<HTMLInputElement>('input[name="password"]')

    if (email && password) {
        const data = {
            email: email.value,
            password: password.value,
        }

        axios.post('api/register', data).then(res => {
            if (res.data.status !== 200) {
                alert('アカウント登録しっぱい')
            }
        })
    } else {
        alert('emailかpasswordが存在してません')
    }
}

const clickLogin = () => {
    const email = document.querySelector<HTMLInputElement>('input[name="email"]')
    const password = document.querySelector<HTMLInputElement>('input[name="password"]')

    if (email && password) {
        const data = {
            email: email.value,
            password: password.value,
        }

        const urlSearchParam = new URLSearchParams(data).toString()

        axios.get('api/login?' + urlSearchParam).then(res => {
            if (res.data.status === 200) {
                console.log('000000000000000000')
                console.log(res.data)
                localStorage.setItem('auth_token', res.data.token);
            } else {
                alert('ログインしっぱい')
            }
        })
    } else {
        alert('emailかpasswordが存在してません')
    }
}

const clickLogout = () => {
    axios.post('api/logout').then(res => {
        if (res.data.status === 200) {
            localStorage.removeItem('auth_token')
            console.log('aaaaaaaaaaa')
        } else {
            alert('ログアウトしっぱい')
        }
    })
}

const login = () => {
    return (
        <>
            <header>
                <nav>
                    <ul>
                        <li>
                            <button onClick={clickSignUp}>新規登録</button>
                        </li>
                        <li>
                            <button onClick={clickLogin}>ログイン</button>
                        </li>
                        <li>
                            <button onClick={clickLogout}>ログアウト</button>
                        </li>
                    </ul>
                </nav>
            </header>
            <main>
                <div className='login_form'>
                    <label>
                        メールアドレス：
                        <input type='input' className='login_form__input' name='email' />
                    </label>
                    <label>
                        パスワード：
                        <input type='input' className='login_form__input' name='password' />
                    </label>
                </div>
            </main>
        </>
    )
}

export default login
```

```typescript: app/api/axios.tsx
import axios from 'axios'

const new_axios = axios.create({
  baseURL: 'http://localhost',
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  }
})

new_axios.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('auth_token')
    config.headers.Authorization = token ? 'Bearer ' + token : ''
    return config
  }
)

export default new_axios
```

```css: app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

header nav {
  max-width: 1200px;
  margin: 0 auto;
  padding: 30px;
}

header nav ul {
  display: flex;
}

header nav ul li {
  &:not(:last-child) {
    margin-right: 20px;
  }
}

header nav ul li button {
  width: 150px;
  height: 40px;
  border: solid 1px;
  border-radius: 30px;
  cursor: pointer;
}

.login_form {
  margin: 0 auto;
  padding: 0 30px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 1200px;
}

.login_form .login_form__input {
  border: solid 1px black;
}
```

# 課題
## nextauth.jsを使い、認証をセッションで管理したい🐣
cookieだとセキュリティ的に値が丸見えなため、insomniaなどのツールからAPIトークンを誰でも簡単に設定して値を取ってこれるので、セッション管理したいと思いました。

# おわり
laravelとnext.jsに興味があったので今回勉強するための良いきっかけになったと思います。引き続き情報を収集して面白そうなことがあったら自分の環境で開発してみるのも良いのではないかと思いました。