Multiple Authentication trong Laravel


Phần lớn các hệ thống website thông thường sẽ gồm phần quản lý admin và phần user thông thường sử dụng. Chính vì vậy các web site sẽ rất cần sử dụng multiple authentication. Cùng mình xây dựng chức năng này nhá!

Chuẩn bị

Các vấn đề gặp phải khi không sử dụng “multiple authentication”
Lúc trước khi chân ướt chân ráo, mình sử dụng bảng users xác thực cho cả admin và người dùng. Mình sử dụng 1 field is_admin(boolean) để kiểm tra nó là admin hay là user. Và mình dùng middleware để chặn người dùng truy cập vào trang admin. Sau đó nó nảy sinh khác nhiều vấn đề như sau:

  • Khi admin đã xác thực thì có thể truy cập vào các quyền của user. Cực kỳ rối.
  • 1 người vừa là admin vừa là user nhưng chỉ có 1 email thì không tạo được.
  • Code không cẩn thận rất dễ thay đổi quyền user thành admin khi cập nhật thông tin.
  • Dễ xảy ra lỗi dữ liệu (không chính xác) cho thống kê.
  • Và một số vấn đề khác mà khi làm việc với nó bạn sẽ thấy.

Ta bắt tay vào xây dựng thôi nào.

Lưu ý: Trước khi xem bài này các bạn phải xem qua bài Authentication trong Laravel và thực hiện theo để có các file cần thiết cho phần user và hiểu được những gì bên dưới này nhá.

Cấu trúc bảng

Ở đây mình sẽ xây dùng 2 luồng cơ bản: Là adminuser.
Phần user đã có sẵn rồi nên ta chỉ làm cho thằng admin thôi, hehe.

Admin Tạo bảng cần thiết cho admin

php artisan make:migration create_admins_table --create=admins

php artisan make:migration create_admin_password_resets_table --create=admin_password_resets

Thêm nội dung vào file vừa tạo

class CreateAdminsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admins');
    }
}

class CreateAdminPasswordResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('admin_password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('admin_password_resets');
    }
}

Sau khi thay đổi nhớ chạy migrate để tạo các bảng:

php artisan migrate

Models

Khai báo model Admin tương tự như thằng User

Sử dụng command:

php artisan make:model Admin

Mở model Admin vừa tạo và cập nhật code bên trong đó:

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable
{
    use Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];
}

Config

Những cái mình làm ở đây tương tự như thằng User, tính ra là còn ít hơn nữa là. Chẳng qua thằng User được Laravel setup sẵn nên mình thấy nhanh, còn thằng Admin phải tạo bằng tay thoi.

Mở file config/auth.php và cấu hình guardsprovider (chèn vào chứ không phải thêm mới nhang);

'guards' => [
    'admin' => [
        'driver'   => 'session',
        'provider' => 'admins',
    ],
],

'providers' => [
    'admins' => [
        'driver' => 'eloquent',
        'model'  => App\Models\Admin::class,
    ],
],

'passwords' => [
    'admins' => [
        'provider' => 'admins',
        'table'    => 'admin_password_resets',
        'expire'   => 60,
        'throttle' => 60,
    ],
],

Nhớ rằng phải cấu hình file .env để có thể gửi mail được nha. Xem thêm tại đây

Multiple authenticate

Vậy là xong phần chuẩn bị rồi. Bây giờ chúng ta bắt tay vào thực hiện làm nhiều luồng xác thực cho trang web.
Ở phần trước ((Xem tại đây)[https://nqdat.com/authentication-trong-laravel/]), mình đã giới thiệu các chức năng của các controller, nên phần này mình sẽ nói nhanh.

Route

Khi cài đặt package laravel/ui đã khởi tạo route cần thiết cho chúng ta. nhiều khi các bạn không biết vì sau nó chạy nữa là. Mở file vendor/laravel/ui/src/AuthRouteMethod.php và theo dõi các route bên dưới

<?php

namespace Laravel\Ui;

class AuthRouteMethods
{
    /**
     * Register the typical authentication routes for an application.
     *
     * @param  array  $options
     * @return callable
     */
    public function auth()
    {
        return function ($options = []) {
            // Login Routes...
            if ($options['login'] ?? true) {
                $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
                $this->post('login', 'Auth\LoginController@login');
            }
            
            // Logout Routes...
            if ($options['logout'] ?? true) {
                $this->post('logout', 'Auth\LoginController@logout')->name('logout');
            }

            // Registration Routes...
            if ($options['register'] ?? true) {
                $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
                $this->post('register', 'Auth\RegisterController@register');
            }

            // Password Reset Routes...
            if ($options['reset'] ?? true) {
                $this->resetPassword();
            }

            // Password Confirmation Routes...
            if ($options['confirm'] ??
                class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) {
                $this->confirmPassword();
            }

            // Email Verification Routes...
            if ($options['verify'] ?? false) {
                $this->emailVerification();
            }
        };
    }

    /**
     * Register the typical reset password routes for an application.
     *
     * @return void
     */
    public function resetPassword()
    {
        return function () {
            $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
            $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
            $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
            $this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
        };
    }

    /**
     * Register the typical confirm password routes for an application.
     *
     * @return void
     */
    public function confirmPassword()
    {
        return function () {
            $this->get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
            $this->post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
        };
    }

    /**
     * Register the typical email verification routes for an application.
     *
     * @return void
     */
    public function emailVerification()
    {
        return function () {
            $this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
            $this->get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
            $this->post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
        };
    }
}

Tùy vào $option mà mình nên truyền gì vào trong Auth::routes(['verify' => true]); như bài trước.

Mình sẽ tạo file route mới dành cho admin routes/admin.php. Trong đó khai báo những route cần thiết:

<?php

use App\Http\Controllers\Admin\Auth\LoginController;
use App\Http\Controllers\Admin\Auth\ForgotPasswordController;
use App\Http\Controllers\Admin\Auth\ResetPasswordController;
use App\Http\Controllers\Admin\DashboardController;

Route::get('login', [LoginController::class, 'showLoginForm'])->name('admin.login');
Route::post('login', [LoginController::class, 'login']);
Route::post('logout', [LoginController::class, 'logout'])->name('admin.logout');
Route::get('logout', [LoginController::class, 'logout']); // @Todo Remove logout GET method
Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('admin.password.request');
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('admin.password.email');
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->name('admin.password.update');

Route::middleware('admin.auth')->group(function () {
    Route::get('dashboard', [DashboardController::class, 'dashboard'])->name('admin.dashboard');
});

Tạo file như này vẫn chưa sử dụng được đâu nha. Phải khai báo nó vào app/Providers/RouteServiceProvider.php


public function boot()
{
    $this->routes(function () {
        Route::prefix('api')
            ->middleware('api')
            ->namespace($this->namespace)
            ->group(base_path('routes/api.php'));

        Route::middleware('web')
            ->namespace($this->namespace)
            ->group(base_path('routes/web.php'));

        Route::middleware('web')
            ->prefix('admin')
            ->namespace($this->namespace)
            ->group(base_path('routes/admin.php'));
    });
}

Trên đây mình khai báo một nhóm route có prefix là admin nằm trong file routes/admin.php.

Controller

Nhìn vào phần khai báo các route, có sử dụng các controller. Tất nhiên mình cũng tạo các controller theo folder đó.

Lần lượt dùng lệnh command hoặc tạo file và copy code vào:

ForgotPasswordController

<?php

namespace App\Http\Controllers\Admin\Auth;

use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;

class ForgotPasswordController extends Controller
{
    use SendsPasswordResetEmails;

    protected function guard()
    {
        return Auth::guard('admin');
    }

    protected function broker()
    {
        return Password::broker('admins');
    }

    public function showLinkRequestForm()
    {
        return view('admin.auth.passwords.email');
    }
}

LoginController

<?php

namespace App\Http\Controllers\Admin\Auth;

use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    public function __construct()
    {
        $this->middleware('admin.guest')->except('logout');
    }

    protected function guard()
    {
        return Auth::guard('admin');
    }

    public function showLoginForm()
    {
        return view('admin.auth.login');
    }

    protected function redirectTo()
    {
        return route('admin.dashboard');
    }

    protected function loggedOut(Request $request)
    {
        return $request->wantsJson()
            ? new Response('', 204)
            : redirect()->route('admin.login');
    }
}

ResetPasswordController

<?php

namespace App\Http\Controllers\Admin\Auth;

use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;

class ResetPasswordController extends Controller
{
    use ResetsPasswords;

    public function showResetForm(Request $request, $token = null)
    {
        return view('admin.auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }

    public function broker()
    {
        return Password::broker('admins');
    }

    protected function guard()
    {
        return Auth::guard('admin');
    }

    protected function redirectTo()
    {
        return route('admin.dashboard');
    }
}

DashboardController

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class DashboardController extends Controller
{
    public function __construct()
    {
        // đã khai báo middleware ở route group
    }


    public function dashboard()
    {
        return view('admin.dashboard');
    }
}

Trong các function trên, nếu không hiểu chỗ nào cứ mạnh dạng để lại comment ở phía bình luận hoặc trao đổi trực tiếp với mình thông qua các kênh thông tin ở phần bên dưới nhá.

Middleware

Xem xơ xơ qua các route và controller phía trên thì mình có sử dụng 2 middleware đó là admin.authadmin.guest

Tiến hành tạo 2 file middlware nhá:

php artisan make:middleware AdminAuth
php artisan make:middleware RedirectIfAdminAuth

2 file sau khi được tạo nằm trong app/Http/Middleware. Nếu chưa thấy thì nhớ reload from disk.
Chèn code vào nhang:
AdminAuth

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;

class AdminAuth
{
    protected $auth;

    protected $guard = 'admin';

    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }
    
    public function handle($request, Closure $next)
    {
        $this->authenticate($request);

        return $next($request);
    }
    
    protected function authenticate($request)
    {
        if ($this->auth->guard($this->guard)->check()) {
            $this->auth->shouldUse($this->guard);
            return;
        }

        $this->unauthenticated($request);
    }
    
    protected function unauthenticated($request)
    {
        throw new AuthenticationException(
            'Unauthenticated.', [$this->guard], $this->redirectTo($request)
        );
    }

    protected function redirectTo($request)
    {
        if (!$request->expectsJson()) {
            return route('admin.login');
        }
    }
}

RedirectIfAdminAuth

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAdminAuth
{
    public function handle($request, Closure $next, $guard = 'admin')
    {
        if (Auth::guard($guard)->check()) {
            return redirect()->route('admin.dashboard');
        }

        return $next($request);
    }
}

Cuối cùng khai báo 2 middleware này vào app/Http/Kernel.php

protected $routeMiddleware = [
    ...
    'admin.auth' => \App\Http\Middleware\AdminAuth::class,
    'admin.guest' => \App\Http\Middleware\RedirectIfAdminAuth::class,
];

View

Tạo folder resources/views/admin, sau đó copy folder auth có sắn vào và chỉ giữ lại file email.blade.php, reset.blade.php, login.blade.php.
Copy folder layouts bỏ vào thằng admin lun. Đồng thời tạo file dashboard.blade.php trong thằng admin này.

Thật ra những file này là clone từ thằng user ra, thay đổi lại các đường dẫn trong đó. Ở đây thây đổi các cái route để handle phần admin và đường dẫn của file app.blade.php

email.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Reset Password') }}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    <form method="POST" action="{{ route('admin.password.email') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Send Password Reset Link') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

reset.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Reset Password') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.password.update') }}">
                        @csrf

                        <input type="hidden" name="token" value="{{ $token }}">

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus>

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Reset Password') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

login.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('admin.login') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <div class="col-md-6 offset-md-4">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                    <label class="form-check-label" for="remember">
                                        {{ __('Remember Me') }}
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Login') }}
                                </button>

                                @if (Route::has('admin.password.request'))
                                    <a class="btn btn-link" href="{{ route('admin.password.request') }}">
                                        {{ __('Forgot Your Password?') }}
                                    </a>
                                @endif
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

app.blade.php

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
                    <span class="navbar-toggler-icon"></span>
                </button>

                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <!-- Left Side Of Navbar -->
                    <ul class="navbar-nav mr-auto">

                    </ul>

                    <!-- Right Side Of Navbar -->
                    <ul class="navbar-nav ml-auto">
                        <!-- Authentication Links -->
                        @if(Auth::guard('admin')->check())
                            <li class="nav-item dropdown">
                                <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                                    {{ Auth::guard('admin')->user()->name }}
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('admin.logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        @else
                            @if (Route::has('admin.login'))
                                <li class="nav-item">
                                    <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
                                </li>
                            @endif
                        @endif
                    </ul>
                </div>
            </div>
        </nav>

        <main class="py-4">
            @yield('content')
        </main>
    </div>
</body>
</html>

dashboard.blade.php

@extends('admin.layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Dashboard') }}</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    {{ __('You are logged in!') }}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Đến đây là hoàn tất quá trình. Cuối cùng bạn phải tạo 1 tài khoản admin bằng tay vì admin không có chức năng đăng ký tài khoản. Sau đó thực hiện chức năng quên mật khẩu và khôi phục mật khẩu.

Tips
Vì admin không có chức năng đăng ký tài khoản, nên khi vừa khởi chạy project ta phải tạo 1 tài khoản admin, sau đó phải quên mật khẩu để lấy lại mật khẩu mới. Cực kì mất công nó làm cho hệ thống thiếu chuyên nghiệp. Thay vì đó mình sẽ tạo 1 lệnh command và khi khởi chạy project của mình, mình chỉ cần chạy lệnh và nhập thông tin trực tiếp vào đó. Ví dụ như email, password.
Tham khảo Artisan Console trong Laravel

Kết luận

Bài viết trên mình đã hướng dẫn cho các bạn về multiple authentication rất chi tiết.

Nếu gặp lỗi gì trong quá trình cài đặt hoặc kiến thức mình cung cấp không chính xác thì có thể liên hệ trực tiếp với minh qua các kênh thông tin phía cuối trang.

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC