Support email verification
This commit is contained in:
parent
706ca1938b
commit
b5468cc143
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
15
app/Http/Middleware/CheckUserVerified.php
Normal file
15
app/Http/Middleware/CheckUserVerified.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
class CheckUserVerified
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
if (option('require_verification') && !auth()->user()->verified) {
|
||||
abort(403, trans('auth.check.verified'));
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
39
app/Mail/EmailVerification.php
Normal file
39
app/Mail/EmailVerification.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class EmailVerification extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $url;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($url)
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ class User extends Authenticatable
|
|||
'score' => 'integer',
|
||||
'avatar' => 'integer',
|
||||
'permission' => 'integer',
|
||||
'verified' => 'bool',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'driver' => menv('MAIL_DRIVER', 'smtp'),
|
||||
'driver' => menv('MAIL_DRIVER'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddVerificationToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,10 @@
|
|||
<span v-else-if="props.column.field === 'permission'">
|
||||
{{ props.row | humanizePermission }}
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'verified'">
|
||||
<span v-if="props.row.verified" v-t="'admin.verified'"></span>
|
||||
<span v-else v-t="'admin.unverified'"></span>
|
||||
</span>
|
||||
<div v-else-if="props.column.field === 'operations'">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
|
|
@ -35,6 +39,7 @@
|
|||
>{{ $t('general.more') }} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu operations-menu">
|
||||
<li><a @click="changeEmail(props.row)" v-t="'admin.changeEmail'" href="#"></a></li>
|
||||
<li><a @click="toggleVerification(props.row)" v-t="'admin.toggleVerification'" href="#"></a></li>
|
||||
<li><a @click="changeNickName(props.row)" v-t="'admin.changeNickName'" href="#"></a></li>
|
||||
<li><a @click="changePassword(props.row)" v-t="'admin.changePassword'" href="#"></a></li>
|
||||
<li><a @click="changeScore(props.row)" v-t="'admin.changeScore'" href="#"></a></li>
|
||||
|
|
@ -110,6 +115,7 @@ export default {
|
|||
{ field: 'score', label: this.$t('general.user.score'), type: 'number', width: '102px' },
|
||||
{ field: 'players_count', label: this.$t('admin.playersCount'), type: 'number' },
|
||||
{ field: 'permission', label: this.$t('admin.status'), globalSearchDisabled: true },
|
||||
{ field: 'verified', label: this.$t('admin.verification'), type: 'boolean', globalSearchDisabled: true },
|
||||
{ field: 'register_at', label: this.$t('general.user.register-at') },
|
||||
{ field: 'operations', label: this.$t('admin.operationsTitle'), sortable: false, globalSearchDisabled: true }
|
||||
],
|
||||
|
|
@ -188,6 +194,14 @@ export default {
|
|||
toastr.warning(msg);
|
||||
}
|
||||
},
|
||||
async toggleVerification(user) {
|
||||
const { msg } = await this.$http.post(
|
||||
'/admin/users?action=verification',
|
||||
{ uid: user.uid }
|
||||
);
|
||||
user.verified = !user.verified;
|
||||
toastr.success(msg);
|
||||
},
|
||||
async changeNickName(user) {
|
||||
const { dismiss, value } = await swal({
|
||||
text: this.$t('admin.newUserNickname'),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<section class="content">
|
||||
<email-verification />
|
||||
<div class="row">
|
||||
<!-- Left col -->
|
||||
<div class="col-md-8">
|
||||
|
|
@ -192,6 +193,7 @@
|
|||
import toastr from 'toastr';
|
||||
import Paginate from 'vuejs-paginate';
|
||||
import ClosetItem from './ClosetItem';
|
||||
import EmailVerification from './EmailVerification';
|
||||
import { debounce } from '../../js/utils';
|
||||
import { swal } from '../../js/notify';
|
||||
|
||||
|
|
@ -201,6 +203,7 @@ export default {
|
|||
Paginate,
|
||||
ClosetItem,
|
||||
Previewer: () => import('../common/Previewer'),
|
||||
EmailVerification,
|
||||
},
|
||||
data: () => ({
|
||||
category: 'skin',
|
||||
|
|
|
|||
|
|
@ -1,63 +1,67 @@
|
|||
<template>
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" v-t="'user.used.title'"></h3>
|
||||
</div><!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="progress-group">
|
||||
<span class="progress-text" v-t="'user.used.players'"></span>
|
||||
<span class="progress-number"><b>{{ playersUsed }}</b> / {{ playersTotal }}</span>
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-aqua" :style="{ width: playersPercentage + '%' }"></div>
|
||||
</div>
|
||||
</div><!-- /.progress-group -->
|
||||
<div class="progress-group">
|
||||
<span class="progress-text" v-t="'user.used.storage'"></span>
|
||||
<span class="progress-number" id="user-storage">
|
||||
<template v-if="storageUsed > 1024">
|
||||
<b>{{ round(storageUsed / 1024) }}</b> / {{ round(storageTotal / 1024) }} MB
|
||||
</template>
|
||||
<template v-else>
|
||||
<b>{{ storageUsed }}</b> / {{ storageTotal }} KB
|
||||
</template>
|
||||
</span>
|
||||
<div>
|
||||
<email-verification />
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title" v-t="'user.used.title'"></h3>
|
||||
</div><!-- /.box-header -->
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="progress-group">
|
||||
<span class="progress-text" v-t="'user.used.players'"></span>
|
||||
<span class="progress-number"><b>{{ playersUsed }}</b> / {{ playersTotal }}</span>
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-aqua" :style="{ width: playersPercentage + '%' }"></div>
|
||||
</div>
|
||||
</div><!-- /.progress-group -->
|
||||
<div class="progress-group">
|
||||
<span class="progress-text" v-t="'user.used.storage'"></span>
|
||||
<span class="progress-number" id="user-storage">
|
||||
<template v-if="storageUsed > 1024">
|
||||
<b>{{ round(storageUsed / 1024) }}</b> / {{ round(storageTotal / 1024) }} MB
|
||||
</template>
|
||||
<template v-else>
|
||||
<b>{{ storageUsed }}</b> / {{ storageTotal }} KB
|
||||
</template>
|
||||
</span>
|
||||
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-yellow" id="user-storage-bar" :style="{ width: storagePercentage + '%' }"></div>
|
||||
</div>
|
||||
</div><!-- /.progress-group -->
|
||||
</div><!-- /.col -->
|
||||
<div class="col-md-4">
|
||||
<p class="text-center">
|
||||
<strong v-t="'user.cur-score'"></strong>
|
||||
</p>
|
||||
<p id="score" data-toggle="modal" data-target="#modal-score-instruction">
|
||||
{{ score }}
|
||||
</p>
|
||||
<p class="text-center" style="font-size: smaller; margin-top: 20px;" v-t="'user.score-notice'"></p>
|
||||
</div><!-- /.col -->
|
||||
</div><!-- /.row -->
|
||||
</div><!-- ./box-body -->
|
||||
<div class="box-footer">
|
||||
<button v-if="canSign" class="btn btn-primary pull-left" @click="sign">
|
||||
<i class="far fa-calendar-check" aria-hidden="true"></i> {{ $t('user.sign') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary pull-left"
|
||||
:title="$t('user.last-sign', { time: lastSignAt.toLocaleString() })"
|
||||
disabled
|
||||
>
|
||||
<i class="far fa-calendar-check" aria-hidden="true"></i>
|
||||
{{ remainingTimeText }}
|
||||
</button>
|
||||
</div><!-- /.box-footer -->
|
||||
<div class="progress sm">
|
||||
<div class="progress-bar progress-bar-yellow" id="user-storage-bar" :style="{ width: storagePercentage + '%' }"></div>
|
||||
</div>
|
||||
</div><!-- /.progress-group -->
|
||||
</div><!-- /.col -->
|
||||
<div class="col-md-4">
|
||||
<p class="text-center">
|
||||
<strong v-t="'user.cur-score'"></strong>
|
||||
</p>
|
||||
<p id="score" data-toggle="modal" data-target="#modal-score-instruction">
|
||||
{{ score }}
|
||||
</p>
|
||||
<p class="text-center" style="font-size: smaller; margin-top: 20px;" v-t="'user.score-notice'"></p>
|
||||
</div><!-- /.col -->
|
||||
</div><!-- /.row -->
|
||||
</div><!-- ./box-body -->
|
||||
<div class="box-footer">
|
||||
<button v-if="canSign" class="btn btn-primary pull-left" @click="sign">
|
||||
<i class="far fa-calendar-check" aria-hidden="true"></i> {{ $t('user.sign') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary pull-left"
|
||||
:title="$t('user.last-sign', { time: lastSignAt.toLocaleString() })"
|
||||
disabled
|
||||
>
|
||||
<i class="far fa-calendar-check" aria-hidden="true"></i>
|
||||
{{ remainingTimeText }}
|
||||
</button>
|
||||
</div><!-- /.box-footer -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmailVerification from './EmailVerification';
|
||||
import { swal } from '../../js/notify';
|
||||
import toastr from 'toastr';
|
||||
|
||||
|
|
@ -65,6 +69,9 @@ const ONE_DAY = 24 * 3600 * 1000;
|
|||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
EmailVerification,
|
||||
},
|
||||
data: () => ({
|
||||
score: 0,
|
||||
lastSignAt: new Date(),
|
||||
|
|
|
|||
36
resources/assets/src/components/user/EmailVerification.vue
Normal file
36
resources/assets/src/components/user/EmailVerification.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div v-if="!verified" class="callout callout-info">
|
||||
<h4><i class="fa fa-envelope"></i> {{ $t('user.verification.title') }}</h4>
|
||||
<p>{{ $t('user.verification.message') }}
|
||||
<span v-if="pending">
|
||||
<i class="fa fa-spin fa-spinner"></i>
|
||||
{{ $t('user.verification.sending') }}
|
||||
</span>
|
||||
<a v-else @click="resend" href="#">
|
||||
{{ $t('user.verification.resend') }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { swal } from '../../js/notify';
|
||||
|
||||
export default {
|
||||
name: 'EmailVerification',
|
||||
data() {
|
||||
return {
|
||||
verified: !__bs_data__.unverified,
|
||||
pending: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async resend() {
|
||||
this.pending = true;
|
||||
const { errno, msg } = await this.$http.post('/user/email-verification');
|
||||
swal({ type: errno === 0 ? 'success' : 'warning', text: msg });
|
||||
this.pending = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<section class="content">
|
||||
<email-verification />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div v-once class="box box-primary">
|
||||
|
|
@ -192,11 +193,15 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import EmailVerification from './EmailVerification';
|
||||
import toastr from 'toastr';
|
||||
import { swal } from '../../js/notify';
|
||||
|
||||
export default {
|
||||
name: 'Profile',
|
||||
components: {
|
||||
EmailVerification,
|
||||
},
|
||||
data: () => ({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
|
|
|
|||
|
|
@ -326,6 +326,26 @@ test('change email', async () => {
|
|||
expect(wrapper.text()).toContain('d@e.f');
|
||||
});
|
||||
|
||||
test('toggle verification', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [
|
||||
{ uid: 1, verified: false },
|
||||
] });
|
||||
Vue.prototype.$http.post.mockResolvedValue({ errno: 0, msg: '0' });
|
||||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(2) > a');
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=verification',
|
||||
{ uid: 1 }
|
||||
);
|
||||
await flushPromises();
|
||||
expect(wrapper.text()).toContain('admin.verified');
|
||||
});
|
||||
|
||||
test('change nickname', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [
|
||||
{ uid: 1, nickname: 'old' },
|
||||
|
|
@ -342,7 +362,7 @@ test('change nickname', async () => {
|
|||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(2) > a');
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(3) > a');
|
||||
|
||||
button.trigger('click');
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled();
|
||||
|
|
@ -374,7 +394,7 @@ test('change password', async () => {
|
|||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(3) > a');
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(4) > a');
|
||||
|
||||
button.trigger('click');
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled();
|
||||
|
|
@ -406,7 +426,7 @@ test('change score', async () => {
|
|||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(4) > a');
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(5) > a');
|
||||
|
||||
button.trigger('click');
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled();
|
||||
|
|
@ -434,7 +454,7 @@ test('toggle admin', async () => {
|
|||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(6) > a');
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(7) > a');
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
|
@ -463,7 +483,7 @@ test('toggle ban', async () => {
|
|||
|
||||
const wrapper = mount(Users);
|
||||
await wrapper.vm.$nextTick();
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(7) > a');
|
||||
const button = wrapper.find('.operations-menu > li:nth-child(8) > a');
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import { swal } from '@/js/notify';
|
|||
|
||||
jest.mock('@/js/notify');
|
||||
|
||||
window.__bs_data__ = { unverified: false };
|
||||
|
||||
test('fetch closet data before mount', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({});
|
||||
mount(Closet);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { swal } from '@/js/notify';
|
|||
|
||||
jest.mock('@/js/notify');
|
||||
|
||||
window.__bs_data__ = { unverified: false };
|
||||
|
||||
function scoreInfo(data = {}) {
|
||||
return {
|
||||
user: { score: 835, lastSignAt: '2018-08-07 16:06:49' },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import Vue from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import EmailVerification from '@/components/user/EmailVerification';
|
||||
import { swal } from '@/js/notify';
|
||||
|
||||
jest.mock('@/js/notify');
|
||||
|
||||
test('message box should not be render if verified', () => {
|
||||
window.__bs_data__ = { unverified: false };
|
||||
const wrapper = mount(EmailVerification);
|
||||
expect(wrapper.isEmpty()).toBeTrue();
|
||||
});
|
||||
|
||||
test('resend email', async () => {
|
||||
window.__bs_data__ = { unverified: true };
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ errno: 1, msg: '1' })
|
||||
.mockResolvedValueOnce({ errno: 0, msg: '0' });
|
||||
const wrapper = mount(EmailVerification);
|
||||
const button = wrapper.find('a');
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(swal).toBeCalledWith({ type: 'warning', text: '1' });
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(swal).toBeCalledWith({ type: 'success', text: '0' });
|
||||
});
|
||||
|
|
@ -7,6 +7,8 @@ import { swal } from '@/js/notify';
|
|||
|
||||
jest.mock('@/js/notify');
|
||||
|
||||
window.__bs_data__ = { unverified: false };
|
||||
|
||||
test('computed values', () => {
|
||||
window.__bs_data__ = { admin: true };
|
||||
const wrapper = mount(Profile);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ users:
|
|||
change: Edit Email
|
||||
existed: :email is existed.
|
||||
success: Email changed successfully.
|
||||
verification:
|
||||
success: Account verification status toggled successfully.
|
||||
nickname:
|
||||
change: Edit Nickname
|
||||
success: Nickname changed successfully.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ forgot:
|
|||
button: Send
|
||||
message: We will send you an E-mail to verify.
|
||||
login-link: I do remember it
|
||||
close: Password resetting is not available now.
|
||||
disabled: Password resetting is not available.
|
||||
frequent-mail: You click the send button too fast. Wait for 60 secs, guy.
|
||||
unregistered: The email address is not registered.
|
||||
success: Mail is sent. Will be expired in 1 hour, please check.
|
||||
|
|
@ -56,6 +57,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.
|
||||
|
|
|
|||
|
|
@ -208,6 +208,11 @@ user:
|
|||
preference:
|
||||
title: Preference
|
||||
description: Default stands for the Steve model with 4-pixel width arms, and Slim for the Alex model with 3-pixel width arms.
|
||||
verification:
|
||||
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...
|
||||
|
||||
admin:
|
||||
operationsTitle: Operations
|
||||
|
|
@ -221,6 +226,8 @@ admin:
|
|||
cannotDeleteAdmin: You can't delete admins.
|
||||
cannotDeleteSuperAdmin: You can't delete super admin in this way
|
||||
changeEmail: Edit Email
|
||||
verification: Email Verification
|
||||
toggleVerification: Toggle Verification Status
|
||||
changeNickName: Edit Nickname
|
||||
changePassword: Edit Password
|
||||
changeScore: Edit Score
|
||||
|
|
@ -235,6 +242,8 @@ admin:
|
|||
normal: Normal
|
||||
admin: Admin
|
||||
superAdmin: Super Admin
|
||||
unverified: Unverified
|
||||
verified: Verified
|
||||
pidNotice: >-
|
||||
Please enter the tid of texture. Inputing 0 can clear texture of this
|
||||
player.
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ general:
|
|||
user_can_register:
|
||||
title: Open Registration
|
||||
label: Everyone is allowed to register.
|
||||
require_verification:
|
||||
title: Account Verification
|
||||
label: Users must verify their email address first.
|
||||
regs_per_ip: Max accounts of one IP
|
||||
ip_get_method:
|
||||
title: Get IP via
|
||||
|
|
|
|||
|
|
@ -14,6 +14,18 @@ last-sign: Last signed at :time
|
|||
sign-remain-time: Available after :time :unit
|
||||
announcement: Announcement
|
||||
|
||||
verification:
|
||||
disabled: Email verification is not available.
|
||||
frequent-mail: You click the send button too fast. Wait for 60 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
|
||||
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: |
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ users:
|
|||
change: 修改邮箱
|
||||
existed: :email 已被占用
|
||||
success: 邮箱修改成功
|
||||
verification:
|
||||
success: 用户的邮箱验证状态已修改
|
||||
nickname:
|
||||
change: 修改昵称
|
||||
success: 昵称已成功设置为 :new
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ login:
|
|||
|
||||
check:
|
||||
anonymous: 非法访问,请先登录
|
||||
verified: 你必须验证邮箱后才能访问此页面
|
||||
admin: 看起来你并不是管理员哦
|
||||
banned: 你已经被本站封禁啦,请联系管理员解决
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ forgot:
|
|||
button: 发送
|
||||
message: 我们将会向您发送一封验证邮件
|
||||
login-link: 我又想起来了
|
||||
close: 本站已关闭重置密码功能
|
||||
disabled: 本站已关闭重置密码功能
|
||||
frequent-mail: 你邮件发送得太频繁啦,过 60 秒后再点发送吧
|
||||
unregistered: 该邮箱尚未注册
|
||||
success: 邮件已发送,一小时内有效,请注意查收。
|
||||
|
|
@ -56,6 +57,13 @@ bind:
|
|||
introduction: 邮箱地址仅用于重置密码,我们将不会向您发送任何垃圾邮件
|
||||
registered: 该邮箱已被占用
|
||||
|
||||
verify:
|
||||
title: 邮箱验证
|
||||
success: 邮箱验证成功
|
||||
message: 欢迎使用 :sitename!
|
||||
button: 返回首页
|
||||
invalid: 无效的链接
|
||||
expired: 链接已失效,请重新发送验证邮件
|
||||
|
||||
validation:
|
||||
identification: 邮箱或角色名格式错误
|
||||
|
|
|
|||
|
|
@ -206,6 +206,11 @@ user:
|
|||
preference:
|
||||
title: 优先模型
|
||||
description: Default 即为默认的 Steve 模型(手臂 4 个像素宽),Slim 即为手臂宽 3 个像素的 Alex 模型。
|
||||
verification:
|
||||
title: 验证你的邮箱地址
|
||||
message: 你必须验证你的邮箱才能正常使用本站的皮肤托管等功能。没有收到验证邮件?
|
||||
resend: 点击这里再次发送。
|
||||
sending: 正在发送……
|
||||
|
||||
admin:
|
||||
operationsTitle: 更多操作
|
||||
|
|
@ -219,6 +224,8 @@ admin:
|
|||
cannotDeleteAdmin: 你不能删除管理员账号哦
|
||||
cannotDeleteSuperAdmin: 超级管理员账号不能被这样删除的啦
|
||||
changeEmail: 修改邮箱
|
||||
verification: 邮箱验证
|
||||
toggleVerification: 修改邮箱验证状态
|
||||
changeNickName: 修改昵称
|
||||
changePassword: 更改密码
|
||||
changeScore: 更改积分
|
||||
|
|
@ -233,6 +240,8 @@ admin:
|
|||
normal: 普通用户
|
||||
admin: 管理员
|
||||
superAdmin: 超级管理员
|
||||
unverified: 未验证
|
||||
verified: 已验证
|
||||
pidNotice: 输入要更换的材质的 TID,输入 0 即可清除该角色的材质
|
||||
changePlayerTexture: '更换角色 :player 的材质'
|
||||
changeTexture: 更换材质
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ general:
|
|||
user_can_register:
|
||||
title: 开放注册
|
||||
label: 任何人都可以注册
|
||||
require_verification:
|
||||
title: 邮箱验证
|
||||
label: 用户必须验证邮箱后才能使用皮肤托管等功能
|
||||
regs_per_ip: 每个 IP 限制注册数
|
||||
ip_get_method:
|
||||
title: IP 获取方式
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ last-sign: 上次签到于 :time
|
|||
sign-remain-time: :time :unit 后可签到
|
||||
announcement: 公告
|
||||
|
||||
|
||||
verification:
|
||||
disabled: 本站已关闭邮箱验证功能
|
||||
frequent-mail: 你邮件发送得太频繁啦,过 60 秒后再点发送吧
|
||||
verified: 你已经验证过邮箱了
|
||||
success: 验证邮件已发送,请检查你的收件箱。
|
||||
failed: 邮件发送失败,详细信息::msg
|
||||
mail:
|
||||
title: 验证您在 :sitename 上的账户邮箱
|
||||
message: 您收到这封邮件,是因为有人在 :sitename 注册时使用了本邮箱地址。
|
||||
reset: 点击此链接验证您的邮箱::url
|
||||
ignore: 如果您并没有访问过我们的网站,或没有进行上述操作,请忽略这封邮件
|
||||
|
||||
score-intro:
|
||||
title: 积分是个啥?
|
||||
introduction: |
|
||||
|
|
|
|||
32
resources/views/auth/verify.blade.php
Normal file
32
resources/views/auth/verify.blade.php
Normal 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">@lang('auth.verify.message', ['sitename' => option_localized('site_name')])</p>
|
||||
|
||||
<div class="callout callout-success">
|
||||
<i class="icon fa fa-check"></i> @lang('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">
|
||||
@lang('auth.verify.button')
|
||||
</a>
|
||||
</div><!-- /.col -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- /.login-box-body -->
|
||||
</div>
|
||||
<!-- /.login-box -->
|
||||
|
||||
@endsection
|
||||
3
resources/views/mails/email-verification.blade.php
Normal file
3
resources/views/mails/email-verification.blade.php
Normal 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>
|
||||
|
|
@ -21,4 +21,12 @@
|
|||
<section class="content"></section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
|
||||
<script>
|
||||
Object.defineProperty(window, '__bs_data__', {
|
||||
value: Object.freeze({
|
||||
unverified: {{ option('require_verification') && !$user->verified ? 'true' : 'false' }}
|
||||
}),
|
||||
writable: false
|
||||
})
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -73,4 +73,12 @@
|
|||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<script>
|
||||
Object.defineProperty(window, '__bs_data__', {
|
||||
value: Object.freeze({
|
||||
unverified: {{ option('require_verification') && !$user->verified ? 'true' : 'false' }}
|
||||
}),
|
||||
writable: false
|
||||
})
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
<script>
|
||||
Object.defineProperty(window, '__bs_data__', {
|
||||
value: Object.freeze({
|
||||
admin: {{ (string) $user->isAdmin() ?: 'false' }}
|
||||
admin: {{ (string) $user->isAdmin() ?: 'false' }},
|
||||
unverified: {{ option('require_verification') && !$user->verified ? 'true' : 'false' }}
|
||||
}),
|
||||
writable: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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/{uid}', 'AuthController@handleReset')->middleware('signed');
|
||||
|
||||
Route::get ('/verify/{uid}', 'AuthController@verify')->name('auth.verify')->middleware('signed');
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -53,16 +54,21 @@ Route::group(['middleware' => ['web', '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::get ('/player/list', 'PlayerController@listAll');
|
||||
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(['prefix' => 'player', 'middleware' => 'verified'], function () {
|
||||
Route::any ('', 'PlayerController@index');
|
||||
Route::get ('/list', 'PlayerController@listAll');
|
||||
Route::post('/add', 'PlayerController@add');
|
||||
Route::any ('/show', 'PlayerController@show');
|
||||
Route::post('/preference', 'PlayerController@setPreference');
|
||||
Route::post('/set', 'PlayerController@setTexture');
|
||||
Route::post('/texture/clear', 'PlayerController@clearTexture');
|
||||
Route::post('/rename', 'PlayerController@rename');
|
||||
Route::post('/delete', 'PlayerController@delete');
|
||||
});
|
||||
|
||||
// Closet
|
||||
Route::get ('/closet', 'ClosetController@index');
|
||||
|
|
@ -82,7 +88,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');
|
||||
|
|
|
|||
|
|
@ -768,7 +768,9 @@ class AdminControllerTest extends BrowserKitTestCase
|
|||
'nickname' => $user->nickname,
|
||||
'score' => $user->score,
|
||||
'avatar' => $user->avatar,
|
||||
'permission' => $user->permission
|
||||
'permission' => $user->permission,
|
||||
'verified' => (bool) $user->verified,
|
||||
'verification_token' => (string) $user->verification_token
|
||||
]
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ class AuthControllerTest extends TestCase
|
|||
$this->get('/auth/forgot')->assertSee('Forgot Password');
|
||||
|
||||
config(['mail.driver' => '']);
|
||||
$this->get('/auth/forgot')->assertSee(trans('auth.forgot.close'));
|
||||
$this->get('/auth/forgot')->assertSee(trans('auth.forgot.disabled'));
|
||||
}
|
||||
|
||||
public function testHandleForgot()
|
||||
|
|
@ -384,7 +384,7 @@ class AuthControllerTest extends TestCase
|
|||
'captcha' => 'a'
|
||||
])->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('auth.forgot.close')
|
||||
'msg' => trans('auth.forgot.disabled')
|
||||
]);
|
||||
config(['mail.driver' => 'smtp']);
|
||||
|
||||
|
|
@ -501,6 +501,27 @@ class AuthControllerTest extends TestCase
|
|||
$this->assertTrue($user->verifyPassword('12345678'));
|
||||
}
|
||||
|
||||
public function testVerify()
|
||||
{
|
||||
$url = URL::signedRoute('auth.verify', ['uid' => 1]);
|
||||
|
||||
// Should be forbidden if account verification is disabled
|
||||
option(['require_verification' => false]);
|
||||
$this->get($url)->assertSee(trans('user.verification.disabled'));
|
||||
option(['require_verification' => true]);
|
||||
|
||||
$this->get($url)->assertSee(trans('auth.verify.invalid'));
|
||||
|
||||
$user = factory(User::class)->create();
|
||||
$url = URL::signedRoute('auth.verify', ['uid' => $user->uid]);
|
||||
$this->get($url)->assertSee(trans('auth.verify.invalid'));
|
||||
|
||||
$user = factory(User::class)->create(['verified' => false]);
|
||||
$url = URL::signedRoute('auth.verify', ['uid' => $user->uid]);
|
||||
$this->get($url)->assertViewIs('auth.verify');
|
||||
$this->assertEquals(1, User::find($user->uid)->verified);
|
||||
}
|
||||
|
||||
public function testCaptcha()
|
||||
{
|
||||
$this->get('/auth/captcha')
|
||||
|
|
|
|||
|
|
@ -48,6 +48,26 @@ class MiddlewareTest extends TestCase
|
|||
$this->assertEquals('a@b.c', User::find($noEmailUser->uid)->email);
|
||||
}
|
||||
|
||||
public function testCheckUserVerified()
|
||||
{
|
||||
$unverified = factory(User::class)->create(['verified' => false]);
|
||||
|
||||
option(['require_verification' => false]);
|
||||
$this->actingAs($unverified)
|
||||
->get('/skinlib/upload')
|
||||
->assertSuccessful();
|
||||
|
||||
option(['require_verification' => true]);
|
||||
$this->actingAs($unverified)
|
||||
->get('/skinlib/upload')
|
||||
->assertStatus(403)
|
||||
->assertSee(trans('auth.check.verified'));
|
||||
|
||||
$this->actAs('normal')
|
||||
->get('/skinlib/upload')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function testCheckAdministrator()
|
||||
{
|
||||
// Without logged in
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
use App\Events;
|
||||
use App\Models\User;
|
||||
use App\Mail\EmailVerification;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
|
@ -21,6 +23,11 @@ class UserControllerTest extends TestCase
|
|||
->assertViewHas('statistics')
|
||||
->assertSee((new Parsedown())->text(option_localized('announcement')))
|
||||
->assertSee((string) $user->score);
|
||||
|
||||
$unverified = factory(User::class)->create(['verified' => false]);
|
||||
$this->actAs($unverified)
|
||||
->get('/user')
|
||||
->assertDontSee(trans('user.verification.notice.title'));
|
||||
}
|
||||
|
||||
public function testScoreInfo()
|
||||
|
|
@ -88,7 +95,6 @@ class UserControllerTest extends TestCase
|
|||
// Can sign after 0 o'clock
|
||||
option(['sign_after_zero' => true]);
|
||||
$diff = \Carbon\Carbon::now()->diffInSeconds(\Carbon\Carbon::tomorrow());
|
||||
$unit = '';
|
||||
if ($diff / 3600 >= 1) {
|
||||
$diff = round($diff / 3600);
|
||||
$unit = 'hour';
|
||||
|
|
@ -117,6 +123,66 @@ class UserControllerTest extends TestCase
|
|||
]);
|
||||
}
|
||||
|
||||
public function testSendVerificationEmail()
|
||||
{
|
||||
Mail::fake();
|
||||
|
||||
$unverified = factory(User::class)->create(['verified' => false]);
|
||||
$verified = factory(User::class)->create();
|
||||
|
||||
// Should be forbidden if account verification is disabled
|
||||
option(['require_verification' => false]);
|
||||
$this->actingAs($unverified)
|
||||
->postJson('/user/email-verification')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('user.verification.disabled')
|
||||
]);
|
||||
option(['require_verification' => true]);
|
||||
|
||||
// Too frequent
|
||||
$this->actingAs($unverified)
|
||||
->withSession([
|
||||
'last_mail_time' => time() - 10
|
||||
])
|
||||
->postJson('/user/email-verification')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('user.verification.frequent-mail')
|
||||
]);
|
||||
$this->flushSession();
|
||||
|
||||
// Already verified
|
||||
$this->actingAs($verified)
|
||||
->postJson('/user/email-verification')
|
||||
->assertJson([
|
||||
'errno' => 1,
|
||||
'msg' => trans('user.verification.verified')
|
||||
]);
|
||||
|
||||
$this->actingAs($unverified)
|
||||
->postJson('/user/email-verification')
|
||||
->assertJson([
|
||||
'errno' => 0,
|
||||
'msg' => trans('user.verification.success')
|
||||
]);
|
||||
Mail::assertSent(EmailVerification::class, function ($mail) use ($unverified) {
|
||||
return $mail->hasTo($unverified->email);
|
||||
});
|
||||
|
||||
// Should handle exception when sending email
|
||||
Mail::shouldReceive('to')
|
||||
->once()
|
||||
->andThrow(new \Mockery\Exception('A fake exception.'));
|
||||
$this->flushSession();
|
||||
$this->actingAs($unverified)
|
||||
->postJson('/user/email-verification')
|
||||
->assertJson([
|
||||
'errno' => 2,
|
||||
'msg' => trans('user.verification.failed', ['msg' => 'A fake exception.'])
|
||||
]);
|
||||
}
|
||||
|
||||
public function testProfile()
|
||||
{
|
||||
$this->actAs('normal')
|
||||
|
|
@ -351,10 +417,13 @@ class UserControllerTest extends TestCase
|
|||
'msg' => trans('user.profile.email.success')
|
||||
]);
|
||||
$this->assertEquals('a@b.c', User::find($user->uid)->email);
|
||||
$this->assertEquals(0, User::find($user->uid)->verified);
|
||||
// After changed email, user should re-login.
|
||||
$this->assertGuest();
|
||||
|
||||
$user = User::find($user->uid);
|
||||
$user->verified = true;
|
||||
$user->save();
|
||||
// Delete account without `password` field
|
||||
$this->actAs($user)
|
||||
->postJson(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user