diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 1111c965..fae608cb 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -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; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index c4a745bd..54cc2357 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -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); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 9abf27c1..a28cdbfe 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; } diff --git a/app/Http/Middleware/CheckUserVerified.php b/app/Http/Middleware/CheckUserVerified.php new file mode 100644 index 00000000..7f05a053 --- /dev/null +++ b/app/Http/Middleware/CheckUserVerified.php @@ -0,0 +1,21 @@ +handle($request, $next, true); + + if ($result instanceof Response) { + return $result; + } + + if (! $result->verified) { + abort(403, trans('auth.check.verified')); + } + + return $next($request); + } +} diff --git a/resources/assets/src/js/user/verification.js b/resources/assets/src/js/user/verification.js new file mode 100644 index 00000000..ae8f183b --- /dev/null +++ b/resources/assets/src/js/user/verification.js @@ -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(); +}); diff --git a/resources/lang/en/auth.yml b/resources/lang/en/auth.yml index c70499ab..10de14dd 100644 --- a/resources/lang/en/auth.yml +++ b/resources/lang/en/auth.yml @@ -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. diff --git a/resources/lang/en/user.yml b/resources/lang/en/user.yml index f2528383..2b22d282 100644 --- a/resources/lang/en/user.yml +++ b/resources/lang/en/user.yml @@ -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: | diff --git a/resources/lang/zh_CN/auth.yml b/resources/lang/zh_CN/auth.yml index 75c33d69..ff92d49f 100644 --- a/resources/lang/zh_CN/auth.yml +++ b/resources/lang/zh_CN/auth.yml @@ -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: 邮箱或角色名格式错误 diff --git a/resources/lang/zh_CN/user.yml b/resources/lang/zh_CN/user.yml index 1f96b2e7..a5caf111 100644 --- a/resources/lang/zh_CN/user.yml +++ b/resources/lang/zh_CN/user.yml @@ -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: | diff --git a/resources/views/auth/verify.tpl b/resources/views/auth/verify.tpl new file mode 100644 index 00000000..f59b45ba --- /dev/null +++ b/resources/views/auth/verify.tpl @@ -0,0 +1,32 @@ +@extends('auth.master') + +@section('title', trans('auth.verify.title')) + +@section('content') + +
+ + +
+ + +
+ {{ trans('auth.verify.success') }} +
+ + + +
+ +
+ + +@endsection diff --git a/resources/views/common/email-verification.tpl b/resources/views/common/email-verification.tpl new file mode 100644 index 00000000..d37d13c8 --- /dev/null +++ b/resources/views/common/email-verification.tpl @@ -0,0 +1,12 @@ +
+

{{ trans('user.verification.notice.title') }}

+

{{ trans('user.verification.notice.message') }} + + {{ trans('user.verification.notice.resend') }} + + +

+
diff --git a/resources/views/mails/email-verification.tpl b/resources/views/mails/email-verification.tpl new file mode 100644 index 00000000..345cf1ef --- /dev/null +++ b/resources/views/mails/email-verification.tpl @@ -0,0 +1,3 @@ +

{!! trans('user.verification.mail.message', ['sitename' => option_localized('site_name')]) !!}

+

{!! trans('user.verification.mail.reset', ['url' => $url]) !!}

+

{!! trans('user.verification.mail.ignore') !!}

diff --git a/resources/views/user/closet.tpl b/resources/views/user/closet.tpl index a45b6630..393d11ab 100644 --- a/resources/views/user/closet.tpl +++ b/resources/views/user/closet.tpl @@ -19,6 +19,11 @@
+ + @if (! $user->verified) + @include('common.email-verification') + @endif +
diff --git a/resources/views/user/index.tpl b/resources/views/user/index.tpl index 260e1398..7d4eaee8 100644 --- a/resources/views/user/index.tpl +++ b/resources/views/user/index.tpl @@ -16,9 +16,9 @@
-
- -
+ @if (! $user->verified) + @include('common.email-verification') + @endif
diff --git a/resources/views/user/profile.tpl b/resources/views/user/profile.tpl index 94690611..eba4cdf0 100644 --- a/resources/views/user/profile.tpl +++ b/resources/views/user/profile.tpl @@ -15,6 +15,11 @@
+ + @if (! $user->verified) + @include('common.email-verification') + @endif +
diff --git a/routes/web.php b/routes/web.php index 7ef3eb5d..720b2669 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');