バージョン確認
PHP7.3
Laravel7.30
vue2.6.12
Dockerを使ってローカル環境構築済み
一般ユーザーの認証機能は実装済み
手順
- 一般ユーザーはusersテーブル、管理者画面はadminsテーブルを使う
- 管理者は
login_id
とpassword
でログイン、ログアウトできるところまでを実装 - 便宜上管理者の新規登録機能も実装し、実務ではマスター管理などに応用する
- app.tsに、常に管理者の認証状態をvuexで保持するよう記述する
- コンポーネント化とcssによるスタイリングは割愛
Laravel側の実装
/var/www/laravel
$ php artisan make:model Models/Admin -m
database/migrations/2021_03_31_120856_create_admins_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAdminsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('login_id');
$table->string('email');
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->unique(['email', 'deleted_at'], 'users_email_deleted_at_unique');
$table->unique(['login_id', 'deleted_at'], 'users_login_id_deleted_at_unique');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admins');
}
}
/var/www/laravel
$ php artisan migrate
config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 追加
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 追加
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
// 追加
'admins' => [
'provider' => 'admins',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
app/Models/Admin.php
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
class Admin extends Authenticatable
{
use Notifiable;
use SoftDeletes;
protected $table = 'admins';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'login_id', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
app/Http/Middleware/Authenticate.php
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Route; // 追加
class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
// 変更
if (Route::is('admin.*')) {
return route('admin.login');
} else {
return route('login');
}
}
}
}
app/Http/Middleware/RedirectIfAuthenticated.php
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
// 変更
if ($guard === 'user') {
return redirect()->route('currentUser');
} elseif ($guard === 'admin') {
return redirect()->route('currentAdmin');
}
}
return $next($request);
}
}
/var/www/laravel
$ mkdir -p app/Http/Controllers/Admin/Auth
$ cp app/Http/Controllers/Auth/{RegisterController,LoginController}.php app/Http/Controllers/Admin/Auth/
routes/api.php
Route::group(['middleware' => 'api'], function () {
Route::get('/current_user', function () {
return Auth::user();
})->name('currentUser');
Route::namespace('Auth')->group(function() {
Route::post('/register', 'RegisterController@register')->name('register');
Route::post('/login', 'LoginController@login')->name('login');
Route::middleware('auth')->group(function() {
Route::post('/logout', 'LoginController@logout')->name('logout');
});
});
// 追加
Route::get('/current_admin', function () {
return Auth::guard('admin')->user();
})->name('currentAdmin');
Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function () {
Route::post('register', 'Auth\RegisterController@register')->name('register');
Route::post('login', 'Auth\LoginController@login')->name('login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
Route::middleware('auth')->group(function () {
// 一般認証が必要なAPI
});
Route::middleware('auth:admin')->group(function () {
// 管理者認証が必要なAPI
});
});
app/Http/Controllers/Admin/Auth/RegisterController.php
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\Admin;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
protected function guard()
{
return Auth::guard('admin');
}
// 中略
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:admin');
}
/**
* Get a validator for an incoming registration request.
* 'users'を'admins'に変えるだけ
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'login_id' => ['required', 'regex:/^[\w\-\._]+$/i', 'min:8', 'max:20', Rule::unique('admins', 'login_id')->whereNull('deleted_at')],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('admins', 'email')->whereNull('deleted_at')],
'password' => ['required', 'string', 'min:8', 'max:20', 'confirmed'],
]);
}
/**
* @param Request $request
* @param Admin $admin
* @return Admin
*/
protected function registered(Request $request, $admin)
{
return $admin;
}
app/Http/Controllers/Admin/Auth/LoginController.php
<?php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Models\Admin;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
protected function guard()
{
return Auth::guard('admin');
}
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest:admin')->except('logout');
}
/**
* @return string
*/
public function username()
{
return 'login_id';
}
/**
* @param Request
* @param Admin $admin
* @return Admin
*/
protected function authenticated(Request $request, $admin)
{
return $admin;
}
/**
* @param Request $request
*/
protected function loggedOut(Request $request)
{
Auth::logout();
$request->session()->regenerate();
return response()->json();
}
}
Vue側の実装
~/workspace/myapp/resources/ts
$ cp types/Models/User.ts types/Models/Admin.ts \
> store/auth.ts store/admin.ts
resources/ts/types/Models/Admin.ts
- export type User = {
+ export type Admin = {
id: number
name: string
[k: string]: any
} | null
resources/ts/store/admin.ts
- import { User } from '@/types/Models/User'
+ import { Admin } from '@/types/Models/Admin'
import { RegisterRequest, LoginRequest } from '@/types/Auth'
import { UNPROCESSABLE_ENTITY } from '@/util'
import router from '@/router'
export type State = {
- user: User
+ admin: Admin
}
const state = {
- user: null,
+ admin: null,
}
const getters = {
- isLogin: (state: State): boolean => !!state.user,
- userId: (state: State): number | null => (state.user ? state.user.id : null),
- userName: (state: State): string => (state.user ? state.user.name : ''),
+ isLogin: (state: State): boolean => !!state.admin,
+ adminId: (state: State): number | null => (state.admin ? state.admin.id : null),
+ adminName: (state: State): string => (state.admin ? state.admin.name : ''),
}
const mutations = {
- setUser(state: State, user: User): void {
- state.user = user
+ setAdmin(state: State, admin: Admin): void {
+ state.admin = admin
},
}
const actions = {
async register(context, data: RegisterRequest): Promise<void> {
await window.axios
- .post('/api/register', data)
- .then((response) => {
- context.commit('setAdmin', response.data)
+ .post('/api/admin/register', data)
+ .then((response) => {
+ context.commit('setUser', response.data)
context.commit(
'error/setErrors',
{
messages: null,
status: null,
},
{ root: true }
)
- router.push('/mypage')
+ router.push('/admin/mypage')
})
.catch((err) => {
if (err.response.status === UNPROCESSABLE_ENTITY) {
context.commit(
'error/setErrors',
{
messages: err.response.data.errors,
status: err.response.status,
},
{ root: true }
)
}
})
},
async login(context, data: LoginRequest): Promise<void> {
await window.axios
- .post('/api/login', data)
- .then((response) => {
+ .post('/api/admin/login', data)
+ .then((response) => {
- context.commit('setUser', response.data)
+ context.commit('setAdmin', response.data)
context.commit(
'error/setErrors',
{
messages: null,
status: null,
},
{ root: true }
)
- router.push('/mypage')
+ router.push('/admin/mypage')
})
.catch((err) => {
if (err.response.status === UNPROCESSABLE_ENTITY) {
context.commit(
'error/setErrors',
{
messages: err.response.data.errors,
status: err.response.status,
},
{ root: true }
)
} else {
context.commit('error/setErrors',
{
messages: err.response,
status: err.response.status,
},
{ root: true }
)
}
})
},
async logout(context): Promise<void> {
await window.axios
- .post('/api/logout')
+ .post('/api/admin/logout')
.then((response) => {
- context.commit('setUser', null)
- router.push('/login')
+ context.commit('setAdmin', null)
+ router.push('/admin/login')
})
.catch((err) => {
context.commit(
'error/setErrors',
{
messages: err.response,
status: err.response.status,
},
{ root: true }
)
})
},
- async currentUser(context): Promise<void> {
+ async currentAdmin(context): Promise<void> {
await window.axios
- .get('/api/current_user')
- .then((response) => {
- const user = response.data || null
- context.commit('setUser', user)
+ .get('/api/admin/current_admin')
+ .then((response) => {
+ const admin = response.data || null
+ context.commit('setAdmin', admin)
})
.catch((err) => {
context.commit(
'error/setErrors',
{
messages: err.response.data.errors,
status: err.response.status,
},
{ root: true }
)
})
},
}
export default {
namespaced: true,
state,
getters,
mutations,
actions,
}
resources/ts/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
+ import admin from './admin'
import error from './error'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
auth,
+ admin,
error
},
})
export default store
resources/ts/app.ts
import Vue from 'vue'
// ルーティングの定義をインポートする
import router from './router'
// ルートコンポーネントをインポートする
import App from './App.vue'
import store from './store'
import bootstrap from './bootstrap'
bootstrap()
const createApp = async () => {
await store.dispatch('auth/currentUser')
+ await store.dispatch('admin/currentAdmin')
new Vue({
el: '#app',
router,
store,
render: (h) => h(App),
})
}
createApp()
view
~/workspace/myapp/resources/ts
$ mkdir -p layouts/admin pages/admin/mypage
$ touch layouts/admin/{Default,MyPageLayout}.vue \
> pages/admin/mypage/Index.vue
$ cp pages/{Register,Login}.vue pages/admin/
resources/ts/layouts/admin/Default.vue
<template>
<div>
<header>
<nav>
<ul v-if="!isLogin">
<li><router-link tag="a" to="/admin/login">管理者ログイン</router-link></li>
<li><router-link tag="a" to="/admin/register">管理者登録</router-link></li>
</ul>
<ul v-else>
<li @click="logout">ログアウト</li>
</ul>
</nav>
</header>
<main>
<div class="container">
<router-view />
</div>
</main>
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { mapGetters } from 'vuex'
@Component({
computed: {
...mapGetters({
isLogin: 'admin/isLogin'
})
}
})
export default class AdminLayout extends Vue {
async logout(): Promise<void> {
await this.$store.dispatch('admin/logout')
}
}
</script>
resources/ts/layouts/admin/MyPageLayout.vue
<template>
<div>
<header>
<nav>
<ul v-if="!isLogin">
<li><router-link tag="a" to="/admin/login">管理者ログイン</router-link></li>
<li><router-link tag="a" to="/admin/register">管理者登録</router-link></li>
</ul>
<ul v-else>
<li @click="logout">ログアウト</li>
</ul>
</nav>
</header>
<main>
<div class="container">
<router-view :admin="admin" />
</div>
</main>
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
import { mapState, mapGetters } from 'vuex'
@Component({
computed: {
...mapState({
admin: state => state.admin.admin
}),
...mapGetters({
isLogin: 'admin/isLogin',
}),
}
})
export default class AdminMyPageLayout extends Vue {
async logout(): Promise<void> {
await this.$store.dispatch('admin/logout')
}
}
</script>
resources/ts/pages/admin/Login.vue
<template>
<div>
<h1>管理者ログイン</h1>
<form @submit.prevent="login">
<div v-for="(formItem, index) in submitData" :key="index">
<p v-if="formItem.errorMessages">{{ formItem.errorMessages[0] }}</p>
<input
v-if="formItem.type==='text'"
type="text"
v-model="formItem.value"
:placeholder="formItem.placeholder"
@keyup="validation(formItem)"
required
>
<input
v-else-if="formItem.type==='password'"
type="password"
v-model="formItem.value"
@keyup="validation(formItem)"
:placeholder="formItem.placeholder"
required
>
</div>
<div>
<button :disabled="disabled">ログイン</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import { Form } from '@/types/Form'
import FormValidator from '@/mixins/formValidator'
@Component
export default class Login extends Mixins(FormValidator) {
submitData: Form = {
login_id: {
name: 'login_id',
type: 'text',
value: '',
rules: [(val: string) => !!val || 'ログインIDは必須です'],
errorMessages: [],
placeholder: 'login_id',
},
password: {
name: 'password',
type: 'password',
value: '',
rules: [(val: string) => !!val || 'パスワードは必須です'],
errorMessages: [],
placeholder: 'at least 8 chars.',
},
}
async login(): Promise<void> {
if (this.disabled) {
Object.keys(this.submitData).forEach((key: string): void => {
this.validation(this.submitData[key])
})
} else {
const formData = new FormData()
formData.append('login_id', this.submitData.login_id.value)
formData.append('password', this.submitData.password.value)
await this.$store.dispatch('admin/login', formData)
}
}
}
</script>
resources/ts/pages/admin/Register.vue
<template>
<div class="container">
<h1>管理者登録</h1>
<form @submit.prevent="register">
<div v-for="(formItem, index) in submitData" :key="index">
<p v-if="formItem.errorMessages">{{ formItem.errorMessages[0] }}</p>
<input
v-if="formItem.type==='text'"
type="text"
v-model="formItem.value"
:placeholder="formItem.placeholder"
@keyup="validation(formItem)"
required
>
<input
v-else-if="formItem.type==='email'"
type="email"
v-model="formItem.value"
:placeholder="formItem.placeholder"
@keyup="validation(formItem)"
required
>
<input
v-else-if="formItem.type==='password'"
type="password"
v-model="formItem.value"
@keyup="validation(formItem)"
:placeholder="formItem.placeholder"
required
>
</div>
<div>
<button :disabled="disabled">管理者登録</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { Component, Mixins} from 'vue-property-decorator'
import { Form } from '@/types/Form'
import FormValidator from '@/mixins/formValidator'
@Component
export default class Register extends Mixins(FormValidator) {
submitData: Form = {
name: {
name: 'お名前',
type: 'text',
value: '',
rules: [
(val: string) => !!val || '必須項目です',
(val: string) => (val.length >= 2 && val.length <= 20) || '2文字以上20文字以下で設定してください',
],
errorMessages: [],
placeholder: 'your nickname',
},
email: {
name: 'メールアドレス',
type: 'email',
value: '',
rules: [
(val: string) => !!val || '必須項目です',
(val: string) => !!val.match(/^\w+[\w\-\._]*\w+@\w+[\w\-\._]*\.\w+[\w\-\._]*[A-Za-z]+$/i) || 'メールアドレスの形式が正しくありません'
],
errorMessages: [],
placeholder: 'sample@example.com',
},
login_id: {
name: 'ログインID',
type: 'text',
value: '',
rules: [
(val: string) => !!val || '必須項目です',
(val: string) => !!val.match(/^[\w\-\._]+$/g) || '半角英数字、「.」、「_」、「-」のみ使えます',
(val: string) => val.length >= 8 && val.length <= 20 || '8文字以上20文字以下で入力してください'
],
errorMessages: [],
placeholder: 'login ID',
},
password: {
name: 'パスワード',
type: 'password',
value: '',
rules: [
(val: string) => !!val || '必須項目です',
(val: string) => !!val.match(/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]+$/) || '半角小文字、半角大文字、数字をそれぞれ1種類以上含めてください',
(val: string) => (val.length >= 8 && val.length <= 20) || '8文字以上20文字以下で設定してください',
],
errorMessages: [],
placeholder: 'at leeast 8 chars.',
},
password_confirmation: {
name: 'パスワード(確認用)',
type: 'password',
value: '',
rules: [
(val: string) => !!val || '必須項目です',
(val: string) => this.submitData.password.value === val || 'パスワードが一致しません'
],
errorMessages: [],
placeholder: 'confirm',
},
}
async register(): Promise<void> {
if (this.disabled) {
Object.keys(this.submitData).forEach((key: string): void => {
this.validation(this.submitData[key])
})
} else {
const formData = new FormData()
formData.append('name', this.submitData.name.value)
formData.append('email', this.submitData.email.value)
formData.append('login_id', this.submitData.login_id.value)
formData.append('password', this.submitData.password.value)
formData.append('password_confirmation', this.submitData.password_confirmation.value)
await this.$store.dispatch('admin/register', formData)
}
}
}
</script>
resources/ts/pages/admin/mypage/Index.vue
<template>
<div>
<p v-if="!!admin">{{ admin.name }}さん、管理者画面へようこそ</p>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'
import { Admin } from '@/types/Models/Admin'
@Component
export default class AdminMyPageIndex extends Vue {
@Prop({ type: Object, required: true, default: null })
admin!: Admin
}
</script>
resources/ts/router.ts
import Vue from 'vue'
import VueRouter from 'vue-router'
// ページコンポーネントをインポートする
import Index from './pages/Index.vue'
// レイアウト
import DefaultLayout from './layouts/Default.vue'
import MyPageLayout from './layouts/MyPageLayout.vue'
+ import AdminDefaultLayout from './layouts/admin/Default.vue'
+ import AdminMyPageLayout from './layouts/admin/MyPageLayout.vue'
// ユーザー画面
import Login from './pages/Login.vue'
import Register from './pages/Register.vue'
import MyPageIndex from '@/pages/mypage/Index.vue'
// 管理者画面
+ import AdminLogin from '@/pages/admin/Login.vue'
+ import AdminRegister from '@/pages/admin/Register.vue'
+ import AdminMyPageIndex from '@/pages/admin/mypage/Index.vue'
import store from './store'
// VueRouterプラグインを使用する
Vue.use(VueRouter)
// パスとコンポーネントのマッピング
const routes = [
{
path: '/',
component: DefaultLayout,
children: [
{
path: '/',
component: Index,
},
{
path: '/register',
component: Register,
beforeEnter (to, from, next) {
if (store.getters['auth/isLogin']) {
next('/')
} else {
next()
}
}
},
{
path: '/login',
component: Login,
beforeEnter (to, from, next) {
if (store.getters['auth/isLogin']) {
next('/')
} else {
next()
}
}
},
],
},
{
path: '/mypage',
component: MyPageLayout,
beforeEnter (to, from, next) {
if (!store.getters['auth/isLogin']) {
next('/login')
} else {
next()
}
},
children: [
{
path: '/',
component: MyPageIndex,
}
],
},
// ↓追加
{
path: '/admin',
component: AdminDefaultLayout,
children: [
{
path: '/',
component: AdminRegister,
beforeEnter (to, from, next) {
if (store.getters['admin/isLogin']) {
next('/admin/mypage')
} else {
next('/admin/login')
}
}
},
{
path: 'register',
component: AdminRegister,
beforeEnter (to, from, next) {
if (store.getters['admin/isLogin']) {
next('/admin/mypage')
} else {
next()
}
}
},
{
path: 'login',
component: AdminLogin,
beforeEnter (to, from, next) {
if (store.getters['admin/isLogin']) {
next('/admin/mypage')
} else {
next()
}
}
},
],
},
{
path: '/admin/mypage',
component: AdminMyPageLayout,
beforeEnter (to, from, next) {
if (!store.getters['admin/isLogin']) {
next('/admin/login')
} else {
next()
}
},
children: [
{
path: '/',
component: AdminMyPageIndex,
}
],
},
]
const router = new VueRouter({
mode: 'history', //URL中の#(ハッシュ)を消す
routes
})
export default router