Add user email verification

This commit is contained in:
printempw 2018-07-26 17:33:21 +08:00
parent 84f29de969
commit ebcc693ab4
16 changed files with 244 additions and 18 deletions

View File

@ -274,6 +274,25 @@ class AuthController extends Controller
return json(trans('auth.reset.success'), 0);
}
public function verify(Request $request, UserRepository $users)
{
// Get user instance from repository
$user = $users->get($request->get('uid'));
if (! $user || $user->verified) {
throw new PrettyPageException(trans('auth.verify.invalid'), 1);
}
if ($user->verification_token != $request->get('token')) {
throw new PrettyPageException(trans('auth.verify.expired'), 1);
}
$user->verified = true;
$user->save();
return view('auth.verify');
}
public function captcha()
{
$builder = new \Gregwar\Captcha\CaptchaBuilder;

View File

@ -3,8 +3,10 @@
namespace App\Http\Controllers;
use App;
use Mail;
use View;
use Utils;
use Session;
use App\Models\User;
use App\Models\Texture;
use Illuminate\Http\Request;
@ -24,6 +26,9 @@ class UserController extends Controller
public function __construct(UserRepository $users)
{
$this->user = $users->get(session('uid'));
// Send email verification link to new users
$this->user->verification_token || $this->sendVerificationEmail();
}
public function index()
@ -94,6 +99,49 @@ class UserController extends Controller
return $hours > 1 ? round($hours) : $hours;
}
public function sendVerificationEmail()
{
// Rate limit of 60s
$remain = 60 + session('last_mail_time', 0) - time();
if ($remain > 0) {
return json(trans('user.verification.frequent-mail', compact('remain')), 1);
}
if ($this->user->verified) {
return json(trans('user.verification.verified'), 1);
}
$key = config('app.key');
$key = starts_with($key, 'base64:') ? base64_decode(substr($key, 7)) : $key;
$token = hash_hmac('sha256', str_random(40), $key);
$this->user->verification_token = $token;
$this->user->save();
$email = $this->user->email;
$url = option('site_url')."/auth/verify?uid={$this->user->uid}&token=$token";
try {
Mail::send('mails.email-verification', compact('url'), function ($m) use ($email) {
$site_name = option_localized('site_name');
$m->from(config('mail.username'), $site_name);
$m->to($email)->subject(trans('user.verification.mail.title', ['sitename' => $site_name]));
});
} 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', $this->user);

View File

@ -44,10 +44,11 @@ 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,
'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,
];
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
class CheckUserVerified
{
public function handle($request, \Closure $next)
{
$result = (new CheckAuthenticated)->handle($request, $next, true);
if ($result instanceof Response) {
return $result;
}
if (! $result->verified) {
abort(403, trans('auth.check.verified'));
}
return $next($request);
}
}

View File

@ -0,0 +1,24 @@
$('#send-verification-email').click(async () => {
try {
const { errno, msg } = await fetch({
type: 'POST',
url: url('user/email-verification'),
dataType: 'json',
beforeSend: () => {
$('#send-verification-email').hide();
$('#sending-indicator').show();
}
});
swal({
type: errno === 0 ? 'success' : 'warning',
html: msg
});
} catch (error) {
showAjaxError(error);
}
$('#send-verification-email').show();
$('#sending-indicator').hide();
});

View File

@ -7,6 +7,7 @@ login:
check:
anonymous: Illegal access. Please log in first.
verified: To access this page, you should verify your email address first.
admin: Only admins are permitted to access this page.
banned: You are banned on this site. Please contact the admin.
token: Invalid token. Please log in.
@ -53,6 +54,14 @@ bind:
introduction: Email addresses will be used for password resetting. We won't send you any spam.
registered: The email address is already registered.
verify:
title: Email Verification
success: Your account was now verified.
message: Welcome to :sitename!
button: Homepage
invalid: Invalid link.
expired: This link is expired, please resend a verification email.
validation:
identification: Invalid format of email or player name.
email: Email format is invalid.

View File

@ -14,6 +14,22 @@ last-sign: Last signed at :time
sign-remain-time: Available after :time :unit
announcement: Announcement
verification:
frequent-mail: You click the send button too fast. Wait for :remain secs, guy.
verified: Your account is already verified.
success: Verification link was sent, please check your inbox.
failed: We failed to send you the verification link. Detailed message :msg
notice:
title: Verify Your Account
message: You must verify your email address before using the skin hosting service. Haven't received the email?
resend: Click here to send again.
sending: Sending...
mail:
title: Verify Your Account on :sitename
message: You are receiving this email because someone registered an account with this email address on :sitename.
reset: "Click here to verify your account: :url"
ignore: If you did not register an account, no further action is required.
score-intro:
title: What is score?
introduction: |

View File

@ -7,6 +7,7 @@ login:
check:
anonymous: 非法访问,请先登录
verified: 你必须验证邮箱后才能访问此页面
admin: 看起来你并不是管理员哦
banned: 你已经被本站封禁啦,请联系管理员解决
token: 无效的 token请重新登录
@ -53,6 +54,13 @@ bind:
introduction: 邮箱地址仅用于重置密码,我们将不会向您发送任何垃圾邮件
registered: 该邮箱已被占用
verify:
title: 邮箱验证
success: 邮箱验证成功
message: 欢迎使用 :sitename
button: 返回首页
invalid: 无效的链接
expired: 链接已失效,请重新发送验证邮件
validation:
identification: 邮箱或角色名格式错误

View File

@ -14,6 +14,22 @@ last-sign: 上次签到于 :time
sign-remain-time: :time :unit 后可签到
announcement: 公告
verification:
frequent-mail: 你邮件发送得太频繁啦,过 :remain 秒后再点发送吧
verified: 你已经验证过邮箱了
success: 验证邮件已发送,请检查你的收件箱。
failed: 邮件发送失败,详细信息::msg
notice:
title: 验证你的邮箱地址
message: 你必须验证你的邮箱才能正常使用本站的皮肤托管等功能。没有收到验证邮件?
resend: 点击这里再次发送。
sending: 正在发送……
mail:
title: 验证您在 :sitename 上的账户邮箱
message: 您收到这封邮件,是因为有人在 :sitename 注册时使用了本邮箱地址。
reset: 点击此链接验证您的邮箱::url
ignore: 如果您并没有访问过我们的网站,或没有进行上述操作,请忽略这封邮件。
score-intro:
title: 积分是个啥?
introduction: |

View File

@ -0,0 +1,32 @@
@extends('auth.master')
@section('title', trans('auth.verify.title'))
@section('content')
<div class="login-box">
<div class="login-logo">
<a href="{{ url('/') }}">{{ option_localized('site_name') }}</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg">{{ trans('auth.verify.message', ['sitename' => option_localized('site_name')]) }}</p>
<div class="callout callout-success">
<i class="icon fa fa-check"></i> {{ trans('auth.verify.success') }}
</div>
<div class="row">
<div class="col-xs-6 pull-right">
<a href="{{ url('/') }}" class="btn btn-primary btn-block btn-flat">
{{ trans('auth.verify.button') }}
</a>
</div><!-- /.col -->
</div>
</div>
<!-- /.login-box-body -->
</div>
<!-- /.login-box -->
@endsection

View File

@ -0,0 +1,12 @@
<div class="callout callout-info">
<h4><i class="fa fa-envelope"></i> {{ trans('user.verification.notice.title') }}</h4>
<p>{{ trans('user.verification.notice.message') }}
<a id="send-verification-email" href="javascript:;">
{{ trans('user.verification.notice.resend') }}
</a>
<span id="sending-indicator" style="display:none;">
<i class="fa fa-spin fa-spinner"></i>
{{ trans('user.verification.notice.sending') }}
</span>
</p>
</div>

View File

@ -0,0 +1,3 @@
<p>{!! trans('user.verification.mail.message', ['sitename' => option_localized('site_name')]) !!}</p>
<p>{!! trans('user.verification.mail.reset', ['url' => $url]) !!}</p>
<p>{!! trans('user.verification.mail.ignore') !!}</p>

View File

@ -19,6 +19,11 @@
<!-- Main content -->
<section class="content">
@if (! $user->verified)
@include('common.email-verification')
@endif
<div class="row">
<!-- Left col -->
<div class="col-md-8">

View File

@ -16,9 +16,9 @@
<!-- Main content -->
<section class="content">
<div class="row">
</div><!-- /.row -->
@if (! $user->verified)
@include('common.email-verification')
@endif
<div class="row">
<div class="col-md-8">

View File

@ -15,6 +15,11 @@
<!-- Main content -->
<section class="content">
@if (! $user->verified)
@include('common.email-verification')
@endif
<div class="row">
<div class="col-md-6">
<div class="box box-primary">

View File

@ -35,8 +35,9 @@ Route::group(['prefix' => 'auth'], function ()
Route::post('/login', 'AuthController@handleLogin');
Route::post('/register', 'AuthController@handleRegister');
Route::post('/forgot', 'AuthController@handleForgot');
Route::post('/reset', 'AuthController@handleReset');
Route::get ('/verify', 'AuthController@verify');
});
/**
@ -52,15 +53,21 @@ Route::group(['middleware' => 'auth', 'prefix' => 'user'], function ()
Route::post('/profile', 'UserController@handleProfile');
Route::post('/profile/avatar', 'UserController@setAvatar');
// Email Verification
Route::post('/email-verification', 'UserController@sendVerificationEmail');
// Player
Route::any ('/player', 'PlayerController@index');
Route::post('/player/add', 'PlayerController@add');
Route::any ('/player/show', 'PlayerController@show');
Route::post('/player/preference', 'PlayerController@setPreference');
Route::post('/player/set', 'PlayerController@setTexture');
Route::post('/player/texture/clear', 'PlayerController@clearTexture');
Route::post('/player/rename', 'PlayerController@rename');
Route::post('/player/delete', 'PlayerController@delete');
Route::group(['middleware' => 'verified'], function ()
{
Route::any ('/player', 'PlayerController@index');
Route::post('/player/add', 'PlayerController@add');
Route::any ('/player/show', 'PlayerController@show');
Route::post('/player/preference', 'PlayerController@setPreference');
Route::post('/player/set', 'PlayerController@setTexture');
Route::post('/player/texture/clear', 'PlayerController@clearTexture');
Route::post('/player/rename', 'PlayerController@rename');
Route::post('/player/delete', 'PlayerController@delete');
});
// Closet
Route::get ('/closet', 'ClosetController@index');
@ -80,7 +87,7 @@ Route::group(['prefix' => 'skinlib'], function ()
Route::any('/show/{tid}', 'SkinlibController@show');
Route::any('/data', 'SkinlibController@getSkinlibFiltered');
Route::group(['middleware' => 'auth'], function ()
Route::group(['middleware' => ['auth', 'verified']], function ()
{
Route::get ('/upload', 'SkinlibController@upload');
Route::post('/upload', 'SkinlibController@handleUpload');