Laravel6.0で管理者用画面を作ったメモ

## 概要

[前回](https://qiita.com/hibohiboo/items/7de7ae7c76f081cbde50)の続き。
今回は、ログインユーザに管理者権限を与えて、管理者専用画面を表示する。
## データベース

### ER図
今回、DBは以下のようにする。
管理者クーポンが発行されたユーザが管理者権限を持つとする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7f1ebcfc-9b3d-95fc-e505-05d80c57b1d4.png)

### マイグレーション
以下のコマンドでマイグレーションファイルを作成できる。

```console
php artisan make:migration create_coupons_table
```

上記で作成したファイルに含まれる`$table->timestamps();`はnullableな`created_at`と`updated_at`を作成する。
DBに作ってもらいたかったので、作成時、更新時の時刻を挿入する設定にしている。

```php:whitemap/database/migrations/create_coupons_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCouponsTable extends Migration
{
    public function up()
    {
        Schema::create('coupons', function (Blueprint $table) {
            $table->string('id');
            $table->integer('type')->default(config('const.Coupons.TYPE_GET', 1))->comment('1:取得, 2:使用');
            $table->string('name');
            $table->boolean('is_display')->default(true);
            $table->timestamp('created_at')->useCurrent();
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
        });
    }
    public function down()
    {
        Schema::dropIfExists('coupons');
    }
}
```

クーポン種別は定数で定義してみた。

```php:whitemap/config/const.php
<?php

return [
    // Couponsで使う定数
    'Coupons' => [
        'TYPE_GET' => 1,
        'TYPE_USE' => 2,
    ],
];
```

ユーザとクーポンの紐づけテーブルでは外部キーを設定し、
ユーザテーブルやクーポンテーブルにないものは登録できないようにした。

```php:whitemap/database/create_user_coupons_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUserCouponsTable extends Migration
{
    public function up()
    {
        Schema::create('user_coupons', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('coupon_id')->comment('クーポンID');
            $table->integer('subscribe_user_id')->unsigned()->comment('受取ユーザID');
            $table->integer('publish_user_id')->unsigned()->comment('発行ユーザID');
            $table->dateTime('expire')->nullable()->comment('利用期限');
            $table->timestamp('created_at')->useCurrent();
            $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
            $table->softDeletes();

            $table->foreign('subscribe_user_id')->references('id')->on('users');
            $table->foreign('publish_user_id')->references('id')->on('users');
            $table->foreign('coupon_id')->references('id')->on('coupons');
        });
    }

    public function down()
    {
        Schema::dropIfExists('user_coupons');
    }
}
```

`php artisan migarete`でデータベース更新。

### シーダの設定

```console
php artisan make:seeder UsersTableSeeder 
```


システム用のユーザと初期ユーザを作成。

```php:whitemap/database/seeds/UserSeeder.php
<?php
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
    public function run()
    {
        $user = [
            'id' => 1,
            'firebase_uid' => 'system',
            'name' => 'システム管理者',
            'twitter_screen_name' => '',
            'twitter_profile_image_url_https' => '',
        ];
        DB::table('users')->insert($user);
        $user = [
            'id' => 2,
            'firebase_uid' => env('FIRST_USER_FIREBASE_UID'),
            'name' => env('FIRST_USER_NAME'),
            'twitter_screen_name' => env('FIRST_USER_TWITTER_SCREEN_NAME'),
            'twitter_profile_image_url_https' => env('FIRST_USER_TWITTER_PROFILE_IMAGE_URL'),
        ];
        DB::table('users')->insert($user);
    }
}

```

管理者クーポンを追加。

```php:whitemap/database/seeds/Couponseeder.php
<?php

use Illuminate\Database\Seeder;
use App\Enums\Coupon\CouponIds;

class CouponsSeeder extends Seeder
{
    public function run()
    {        
        $coupon = [
            'id' => CouponIds::ADMIN(),
            'name' => '管理者クーポン',
            'is_display' => false,
        ];
        DB::table('coupons')->insert($coupon);
    }
}
```

クーポンIDはEnumで登録してみた。

```console
composer require myclabs/php-enum
```

```php:whitemap/app/Enums/Coupon.php
<?php
namespace App\Enums\Coupon;
use MyCLabs\Enum\Enum;

class CouponIds extends Enum
{
    const ADMIN = 'admin';
}
```

システムユーザから初期ユーザに向けて管理者クーポンを発行

```php:whitemap/database/seeds/UserCouponsSeeder.php
<?php
use Illuminate\Database\Seeder;
use App\Enums\Coupon\CouponIds;

class UserCouponsSeeder extends Seeder
{
    public function run()
    {
        $user_coupon = [
            'id' => 1,
            'coupon_id' => CouponIds::ADMIN(),
            'subscribe_user_id' => 2,
            'publish_user_id' => 1,
        ];
        DB::table('user_coupons')->insert($user_coupon);
    }
}

```

実行するSeederを指定

```php:whitemap/database/seeds/DatabaseSeeder.php
<?php
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call([
            UserSeeder::class,
            CouponsSeeder::class,
            UserCouponsSeeder::class,
        ]);
    }
}
```

シーダの実行

```console
php artisan db:seed 
````

## アプリケーション

### モデルの設定

```console 
php artisan make:model Models/UserCoupon
```

```php:whitemap/app/Models//UserCoupon.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class UserCoupon extends Model
{
    protected $table = 'user_coupons';
}
```

`subscribe_user_id`でユーザクーポンとユーザを紐づける。

```diff:whitemap/app/User.php
<?php
namespace App;
use Laravel\Passport\HasApiTokens; // 追加
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use App\Enums\Coupon\CouponIds;

class User extends Authenticatable
{
    use Notifiable, HasApiTokens; // HasApiTokens を追加
    public function __construct(array $attributes = []){
    }

    protected $fillable = [
        'name', 'twitter_screen_name','twitter_profile_image_url_https', 'firebase_uid'
    ];
    protected $hidden = [
        'password', 'remember_token',
    ];
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];


+    public function userCoupons(){
+        return $this->hasMany('App\Models\UserCoupon',  'subscribe_user_id');
+    }
}
```


### ゲートの設定

```php:whitemap/app/Gate/AdminAccess
<?php 
namespace App\Gate;
use App\User;
use App\Enums\Coupon\CouponIds;

final class AdminAccess
{
    public function __invoke(User $user): bool 
    {
        // 管理者用のクーポンを持っているかDBに問い合わせる。
        return $user->userCoupons()->where('coupon_id',CouponIds::ADMIN())->exists();
    }
}
```

上記で設定したゲートを`admin-access`の名前で登録する。

```diff:whitemap/app/Providers/AuthServiceProvider.php
<?php
namespace App\Providers;
use App\Auth\MySessionGuard; 
use App\Auth\MyEloquentUserProvider; 
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Auth; 
use App\Gate\UserAccess; 
use App\Gate\AdminAccess; 
use \Psr\Log\LoggerInterface;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    public function boot(LoggerInterface $logger)
    {
        $this->registerPolicies();

        Auth::provider('my_eloquent', function($app, array $config) {
            return new MyEloquentUserProvider($app['hash'], $config['model']);
        });


+        // 認可
+        Gate::define('admin-access', new AdminAccess);

        // 認可の前にロギング
        Gate::before(function ($user, $ability) use ($logger) {
            // Log::info("Hello my log,");
            $logger->info($ability, ['firebase_uid'=>$user->getAuthIdentifier()]);
        });
    }
}
```

### ルーティング

```php:whitemap/routes/web.php
Route::group(['middleware' => ['auth', 'can:admin-access']], function () {
    // この中は管理者権限の場合のみルーティングされる
    Route::get('/admin', function (Illuminate\Http\Request $request) {
        return view('admin/dashboard');
    });
});
```

### 管理者ページへのリンク

管理者権限があるときだけ、管理者ページへのリンクを表示。

```diff:whitemap/resources/views/home.blade.php
@extends('layouts.app') 
@section('title') マイページ @endsection
@section('head-scripts')
@if(Auth::check())
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-auth.js"></script>
<script src="{{ mix('js/home/index.js') }}"></script>
@endif
@endsection

@section('content')
<main role="main" class="container">
<div class="starter-template">
    <h1>マイページ</h1>
    @if(Auth::check())
      <ul>
+        @can('admin-access')
+          <li><a href="/admin">管理者画面へ</a></li>
+        @endcan
        <li><a href="#" id="logout">ログアウト</a></li>
      </ul>
    @else 
      こんにちは！  ゲストさん <br />
      <a href="/login">ログイン</a>
    @endif
</div></main>
@endsection
```



## 参考
[Laravel で定数をつかうよ](https://qiita.com/clustfe/items/4fa169c6d5114a9b6f4e)
[Laravel で Enum を使う](https://qiita.com/nunulk/items/2366addc3aab59e6c81e)
[Laravel 6.0 Artisan コンソール](https://readouble.com/laravel/6.0/ja/artisan.html)
[全 68 種類！Laravel 5.6 の artisan コマンドまとめ](https://blog.capilano-fw.com/?p=768)
[【メモ】【Laravel】外部キー制約付き Migrate がさっぱり動かないときのチェック・ポイント（Mysql）](https://qiita.com/0w0/items/4a9cb7d27794bfb93d46)
[Laravel の DB migration で日付のデフォルトを指定](https://qiita.com/kuwana/items/ebf17c83c602ef36afb4)
[Laravel(Eloquent)の save メソッドを使ったら MySQL の timestamp 型で謎な挙動が発生した話](https://tech.innovator.jp.net/entry/2019/01/07/151803)
[管理者クーポンによるタグ画面の制御](https://github.com/hibohiboo/whitemap/tree/390ed235b867b20b70b0313d8a4ad3e2132653e1/laravel_docker/whitemap)
[blade テンプレートでの切替](https://github.com/hibohiboo/whitemap/tree/a0957fa0627a669f698af35fce40381f8e3aeb00/laravel_docker/whitemap)
[Laravel 6.0 基本のタスクリスト](https://qiita.com/ucan-lab/items/36f6e89abad26a68f69a)
[laravel](https://laravel.com/docs/6.x)
[readouble laravel](https://readouble.com/laravel/6.0/ja/passport.html)
[【Laravel】 認証や認可に関する補足資料](https://qiita.com/mpyw/items/8c5413b99b8e299f7002)
[Laravel 6.0 認可](https://readouble.com/laravel/6.0/ja/authorization.html)
[Laravel 6.0 ルーティング](https://readouble.com/laravel/6.0/ja/routing.html)
[Laravel 6.0 ミドルウェア](https://readouble.com/laravel/6.0/ja/middleware.html)
[Laravel の Gate(ゲート)機能で権限(ロール)によるアクセス制限を実装する](https://www.ritolab.com/entry/56)
[Laravel 6.0 バリデーション](https://readouble.com/laravel/6.0/ja/validation.html)