From b5468cc143f5868d5ae44958ba61985cc9df8131 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Fri, 17 Aug 2018 12:32:44 +0800 Subject: [PATCH] Support email verification --- app/Http/Controllers/AdminController.php | 10 +- app/Http/Controllers/AuthController.php | 24 +++- app/Http/Controllers/UserController.php | 53 ++++++++ app/Http/Kernel.php | 13 +- app/Http/Middleware/CheckUserVerified.php | 15 +++ app/Mail/EmailVerification.php | 39 ++++++ app/Models/User.php | 1 + config/mail.php | 2 +- config/options.php | 1 + database/factories/UserModelFactory.php | 4 + ...130617_add_verification_to_users_table.php | 40 ++++++ .../assets/src/components/admin/Users.vue | 14 +++ .../assets/src/components/user/Closet.vue | 3 + .../assets/src/components/user/Dashboard.vue | 115 ++++++++++-------- .../src/components/user/EmailVerification.vue | 36 ++++++ .../assets/src/components/user/Profile.vue | 5 + .../tests/components/admin/Users.test.js | 30 ++++- .../tests/components/user/Closet.test.js | 2 + .../tests/components/user/Dashboard.test.js | 2 + .../components/user/EmailVerification.test.js | 29 +++++ .../tests/components/user/Profile.test.js | 2 + resources/lang/en/admin.yml | 2 + resources/lang/en/auth.yml | 11 +- resources/lang/en/front-end.yml | 9 ++ resources/lang/en/options.yml | 3 + resources/lang/en/user.yml | 12 ++ resources/lang/zh_CN/admin.yml | 2 + resources/lang/zh_CN/auth.yml | 10 +- resources/lang/zh_CN/front-end.yml | 9 ++ resources/lang/zh_CN/options.yml | 3 + resources/lang/zh_CN/user.yml | 13 ++ resources/views/auth/verify.blade.php | 32 +++++ .../views/mails/email-verification.blade.php | 3 + resources/views/user/closet.tpl | 8 ++ resources/views/user/index.tpl | 8 ++ resources/views/user/profile.tpl | 3 +- routes/web.php | 28 +++-- tests/AdminControllerTest.php | 4 +- tests/AuthControllerTest.php | 25 +++- tests/MiddlewareTest.php | 20 +++ tests/UserControllerTest.php | 71 ++++++++++- 41 files changed, 625 insertions(+), 91 deletions(-) create mode 100644 app/Http/Middleware/CheckUserVerified.php create mode 100644 app/Mail/EmailVerification.php create mode 100644 database/migrations/2018_07_26_130617_add_verification_to_users_table.php create mode 100644 resources/assets/src/components/user/EmailVerification.vue create mode 100644 resources/assets/tests/components/user/EmailVerification.test.js create mode 100644 resources/views/auth/verify.blade.php create mode 100644 resources/views/mails/email-verification.blade.php diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 6ecda9a0..346332b5 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -172,6 +172,7 @@ class AdminController extends Controller }); $form->checkbox('user_can_register')->label(); + $form->checkbox('require_verification')->label(); $form->text('regs_per_ip'); @@ -241,7 +242,7 @@ class AdminController extends Controller $isSingleUser = $request->has('uid'); if ($isSingleUser) { - $users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at']) + $users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified']) ->where('uid', intval($request->input('uid'))) ->get(); } else { @@ -251,7 +252,7 @@ class AdminController extends Controller $page = $request->input('page', 1); $perPage = $request->input('perPage', 10); - $users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at']) + $users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified']) ->where('uid', 'like', '%' . $search . '%') ->orWhere('email', 'like', '%' . $search . '%') ->orWhere('nickname', 'like', '%' . $search . '%') @@ -341,6 +342,11 @@ class AdminController extends Controller return json(trans('admin.users.operations.email.success'), 0); + } elseif ($action == "verification") { + $user->verified = !$user->verified; + $user->save(); + + return json(trans('admin.users.operations.verification.success'), 0); } elseif ($action == "nickname") { $this->validate($request, [ 'nickname' => 'required|no_special_chars' diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 021c3b94..ab8fb019 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -138,7 +138,7 @@ class AuthController extends Controller if (config('mail.driver') != "") { return view('auth.forgot'); } else { - throw new PrettyPageException(trans('auth.forgot.close'), 8); + throw new PrettyPageException(trans('auth.forgot.disabled'), 8); } } @@ -148,8 +148,9 @@ class AuthController extends Controller 'captcha' => 'required'.(app()->environment('testing') ? '' : '|captcha') ]); - if (config('mail.driver') == "") - return json(trans('auth.forgot.close'), 1); + if (! config('mail.driver')) { + return json(trans('auth.forgot.disabled'), 1); + } if (Session::has('last_mail_time') && (time() - session('last_mail_time')) < 60) return json(trans('auth.forgot.frequent-mail'), 1); @@ -191,9 +192,22 @@ class AuthController extends Controller return json(trans('auth.reset.success'), 0); } - protected function checkCaptcha($request) + public function verify(UserRepository $users, $uid) { - return (strtolower($request->input('captcha')) == strtolower(session('phrase'))); + if (! option('require_verification')) { + throw new PrettyPageException(trans('user.verification.disabled'), 1); + } + + $user = $users->get($uid); + + if (! $user || $user->verified) { + throw new PrettyPageException(trans('auth.verify.invalid'), 1); + } + + $user->verified = true; + $user->save(); + + return view('auth.verify'); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 13963e6b..abbb4cb8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,12 +3,16 @@ namespace App\Http\Controllers; use App; +use URL; +use Mail; use View; use Utils; +use Session; use Parsedown; use App\Models\User; use App\Models\Texture; use Illuminate\Http\Request; +use App\Mail\EmailVerification; use App\Events\UserProfileUpdated; use Illuminate\Support\Facades\Auth; use App\Exceptions\PrettyPageException; @@ -16,6 +20,17 @@ use App\Services\Repositories\UserRepository; class UserController extends Controller { + public function __construct() + { + $this->middleware(function ($request, $next) { + if (! Auth::user()->verified) { + $this->sendVerificationEmail(); + } + + return $next($request); + })->only(['index', 'profile']); + } + public function index() { $user = Auth::user(); @@ -105,6 +120,40 @@ class UserController extends Controller return $hours > 1 ? round($hours) : $hours; } + public function sendVerificationEmail() + { + if (! option('require_verification')) { + return json(trans('user.verification.disabled'), 1); + } + + // Rate limit of 60s + $remain = 60 + session('last_mail_time', 0) - time(); + + if ($remain > 0) { + return json(trans('user.verification.frequent-mail')); + } + + $user = Auth::user(); + + if ($user->verified) { + return json(trans('user.verification.verified'), 1); + } + + $url = URL::signedRoute('auth.verify', ['uid' => $user->uid]); + + try { + Mail::to($user->email)->send(new EmailVerification($url)); + } catch (\Exception $e) { + // Write the exception to log + report($e); + return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2); + } + + Session::put('last_mail_time', time()); + + return json(trans('user.verification.success'), 0); + } + public function profile() { return view('user.profile')->with('user', Auth::user()); @@ -170,6 +219,10 @@ class UserController extends Controller return json(trans('user.profile.email.wrong-password'), 1); if ($user->setEmail($request->input('new_email'))) { + // Set account status to unverified + $user->verified = false; + $user->save(); + event(new UserProfileUpdated($action, $user)); Auth::logout(); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f57ab1ae..1e072c92 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -46,11 +46,12 @@ class Kernel extends HttpKernel * @var array */ protected $routeMiddleware = [ - 'auth' => \App\Http\Middleware\CheckAuthenticated::class, - 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, - 'admin' => \App\Http\Middleware\CheckAdministrator::class, - 'player' => \App\Http\Middleware\CheckPlayerExist::class, - 'setup' => \App\Http\Middleware\CheckInstallation::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'auth' => \App\Http\Middleware\CheckAuthenticated::class, + 'verified' => \App\Http\Middleware\CheckUserVerified::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'admin' => \App\Http\Middleware\CheckAdministrator::class, + 'player' => \App\Http\Middleware\CheckPlayerExist::class, + 'setup' => \App\Http\Middleware\CheckInstallation::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, ]; } diff --git a/app/Http/Middleware/CheckUserVerified.php b/app/Http/Middleware/CheckUserVerified.php new file mode 100644 index 00000000..986438b4 --- /dev/null +++ b/app/Http/Middleware/CheckUserVerified.php @@ -0,0 +1,15 @@ +user()->verified) { + abort(403, trans('auth.check.verified')); + } + + return $next($request); + } +} diff --git a/app/Mail/EmailVerification.php b/app/Mail/EmailVerification.php new file mode 100644 index 00000000..196f8743 --- /dev/null +++ b/app/Mail/EmailVerification.php @@ -0,0 +1,39 @@ +url = $url; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $site_name = option_localized('site_name'); + + return $this->from(config('mail.username'), $site_name) + ->subject(trans('user.verification.mail.title', ['sitename' => $site_name])) + ->view('mails.email-verification'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 2b684434..f8dffda0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -41,6 +41,7 @@ class User extends Authenticatable 'score' => 'integer', 'avatar' => 'integer', 'permission' => 'integer', + 'verified' => 'bool', ]; /** diff --git a/config/mail.php b/config/mail.php index da2ebb73..e6f2f414 100644 --- a/config/mail.php +++ b/config/mail.php @@ -16,7 +16,7 @@ return [ | */ - 'driver' => menv('MAIL_DRIVER', 'smtp'), + 'driver' => menv('MAIL_DRIVER'), /* |-------------------------------------------------------------------------- diff --git a/config/options.php b/config/options.php index 3684b09c..dccfc3b6 100644 --- a/config/options.php +++ b/config/options.php @@ -5,6 +5,7 @@ return [ 'site_name' => 'Blessing Skin', 'site_description' => 'Open-source PHP Minecraft Skin Hosting Service', 'user_can_register' => 'true', + 'require_verification' => 'false', 'regs_per_ip' => '3', 'ip_get_method' => '0', 'api_type' => 'false', diff --git a/database/factories/UserModelFactory.php b/database/factories/UserModelFactory.php index 7550e542..47bfc759 100644 --- a/database/factories/UserModelFactory.php +++ b/database/factories/UserModelFactory.php @@ -11,6 +11,7 @@ $factory->define(User::class, function (Faker\Generator $faker) { 'password' => app('cipher')->hash(str_random(10), config('secure.salt')), 'ip' => '127.0.0.1', 'permission' => 0, + 'verified' => true, 'last_sign_at' => $faker->dateTime->format('d-M-Y H:i:s'), 'register_at' => $faker->dateTime->format('d-M-Y H:i:s') ]; @@ -25,6 +26,7 @@ $factory->defineAs(User::class, 'admin', function (Faker\Generator $faker) { 'password' => app('cipher')->hash(str_random(10), config('secure.salt')), 'ip' => '127.0.0.1', 'permission' => 1, + 'verified' => true, 'last_sign_at' => $faker->dateTime->format('d-M-Y H:i:s'), 'register_at' => $faker->dateTime->format('d-M-Y H:i:s') ]; @@ -39,6 +41,7 @@ $factory->defineAs(User::class, 'superAdmin', function (Faker\Generator $faker) 'password' => app('cipher')->hash(str_random(10), config('secure.salt')), 'ip' => '127.0.0.1', 'permission' => 2, + 'verified' => true, 'last_sign_at' => $faker->dateTime->format('d-M-Y H:i:s'), 'register_at' => $faker->dateTime->format('d-M-Y H:i:s') ]; @@ -53,6 +56,7 @@ $factory->defineAs(User::class, 'banned', function (Faker\Generator $faker) { 'password' => app('cipher')->hash(str_random(10), config('secure.salt')), 'ip' => '127.0.0.1', 'permission' => -1, + 'verified' => true, 'last_sign_at' => $faker->dateTime->format('d-M-Y H:i:s'), 'register_at' => $faker->dateTime->format('d-M-Y H:i:s') ]; diff --git a/database/migrations/2018_07_26_130617_add_verification_to_users_table.php b/database/migrations/2018_07_26_130617_add_verification_to_users_table.php new file mode 100644 index 00000000..2e79e318 --- /dev/null +++ b/database/migrations/2018_07_26_130617_add_verification_to_users_table.php @@ -0,0 +1,40 @@ +boolean('verified')->default(false); + $table->string('verification_token')->default(''); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if (config('database.default') == 'sqlite') { + // Dropping columns from a SQLite database requires `doctrine/dbal` dependency. + // However, we won't install it because it's too hard to specify the version of + // all the new dependencies exactly to make them support PHP ^5.5.9. Damn it. + return; + } + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('verified'); + $table->dropColumn('verification_token'); + }); + } +} diff --git a/resources/assets/src/components/admin/Users.vue b/resources/assets/src/components/admin/Users.vue index 17cff12d..5113c7e9 100644 --- a/resources/assets/src/components/admin/Users.vue +++ b/resources/assets/src/components/admin/Users.vue @@ -25,6 +25,10 @@ {{ props.row | humanizePermission }} + + + +