rewrite users management page with React
This commit is contained in:
parent
f795194dc3
commit
3e1a10a461
|
|
@ -7,7 +7,6 @@ use App\Models\Texture;
|
|||
use App\Models\User;
|
||||
use App\Services\OptionForm;
|
||||
use App\Services\PluginManager;
|
||||
use Auth;
|
||||
use Blessing\Filter;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
|
@ -416,120 +415,4 @@ class AdminController extends Controller
|
|||
])
|
||||
->with('plugins', $enabledPlugins);
|
||||
}
|
||||
|
||||
public function getUserData(Request $request)
|
||||
{
|
||||
$isSingleUser = $request->has('uid');
|
||||
|
||||
if ($isSingleUser) {
|
||||
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified'])
|
||||
->where('uid', intval($request->input('uid')))
|
||||
->get();
|
||||
} else {
|
||||
$search = $request->input('search', '');
|
||||
$sortField = $request->input('sortField', 'uid');
|
||||
$sortType = $request->input('sortType', 'asc');
|
||||
$page = $request->input('page', 1);
|
||||
$perPage = $request->input('perPage', 10);
|
||||
|
||||
$users = User::select(['uid', 'email', 'nickname', 'score', 'permission', 'register_at', 'verified'])
|
||||
->where('uid', 'like', '%'.$search.'%')
|
||||
->orWhere('email', 'like', '%'.$search.'%')
|
||||
->orWhere('nickname', 'like', '%'.$search.'%')
|
||||
->orWhere('score', 'like', '%'.$search.'%')
|
||||
->orderBy($sortField, $sortType)
|
||||
->offset(($page - 1) * $perPage)
|
||||
->limit($perPage)
|
||||
->get();
|
||||
}
|
||||
|
||||
$users->transform(function ($user) {
|
||||
$user->operations = auth()->user()->permission;
|
||||
$user->players_count = $user->players->count();
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
return [
|
||||
'totalRecords' => $isSingleUser ? 1 : User::count(),
|
||||
'data' => $users,
|
||||
];
|
||||
}
|
||||
|
||||
public function userAjaxHandler(Request $request)
|
||||
{
|
||||
$action = $request->input('action');
|
||||
$user = User::find($request->uid);
|
||||
$currentUser = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
return json(trans('admin.users.operations.non-existent'), 1);
|
||||
}
|
||||
|
||||
if ($user->uid !== $currentUser->uid && $user->permission >= $currentUser->permission) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1);
|
||||
}
|
||||
|
||||
if ($action == 'email') {
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email',
|
||||
]);
|
||||
|
||||
if (User::where('email', $request->email)->count() != 0) {
|
||||
return json(trans('admin.users.operations.email.existed', ['email' => $request->input('email')]), 1);
|
||||
}
|
||||
|
||||
$user->email = $request->input('email');
|
||||
$user->save();
|
||||
|
||||
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']);
|
||||
|
||||
$user->nickname = $request->input('nickname');
|
||||
$user->save();
|
||||
|
||||
return json(trans('admin.users.operations.nickname.success', [
|
||||
'new' => $request->input('nickname'),
|
||||
]), 0);
|
||||
} elseif ($action == 'password') {
|
||||
$this->validate($request, [
|
||||
'password' => 'required|min:8|max:16',
|
||||
]);
|
||||
|
||||
$user->changePassword($request->input('password'));
|
||||
|
||||
return json(trans('admin.users.operations.password.success'), 0);
|
||||
} elseif ($action == 'score') {
|
||||
$this->validate($request, [
|
||||
'score' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user->score = $request->input('score');
|
||||
$user->save();
|
||||
|
||||
return json(trans('admin.users.operations.score.success'), 0);
|
||||
} elseif ($action == 'permission') {
|
||||
$user->permission = $this->validate($request, [
|
||||
'permission' => 'required|in:-1,0,1',
|
||||
])['permission'];
|
||||
$user->save();
|
||||
|
||||
return json([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.permission'),
|
||||
]);
|
||||
} elseif ($action == 'delete') {
|
||||
$user->delete();
|
||||
|
||||
return json(trans('admin.users.operations.delete.success'), 0);
|
||||
} else {
|
||||
return json(trans('admin.users.operations.invalid'), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
170
app/Http/Controllers/UsersManagementController.php
Normal file
170
app/Http/Controllers/UsersManagementController.php
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UsersManagementController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(function (Request $request, $next) {
|
||||
/** @var User */
|
||||
$targetUser = $request->route('user');
|
||||
/** @var User */
|
||||
$authUser = $request->user();
|
||||
|
||||
if (
|
||||
$targetUser->isNot($authUser) &&
|
||||
$targetUser->permission >= $authUser->permission
|
||||
) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1)
|
||||
->setStatusCode(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
})->except(['list']);
|
||||
}
|
||||
|
||||
public function list(Request $request)
|
||||
{
|
||||
$q = $request->input('q');
|
||||
|
||||
return User::usingSearchString($q)->paginate(10);
|
||||
}
|
||||
|
||||
public function email(User $user, Request $request, Dispatcher $dispatcher)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'email' => [
|
||||
'required', 'email', Rule::unique('users')->ignore($user),
|
||||
],
|
||||
]);
|
||||
$email = $data['email'];
|
||||
|
||||
$dispatcher->dispatch('user.email.updating', [$user, $email]);
|
||||
|
||||
$old = $user->replicate();
|
||||
$user->email = $email;
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.email.updated', [$user, $old]);
|
||||
|
||||
return json(trans('admin.users.operations.email.success'), 0);
|
||||
}
|
||||
|
||||
public function verification(User $user, Dispatcher $dispatcher)
|
||||
{
|
||||
$dispatcher->dispatch('user.verification.updating', [$user]);
|
||||
|
||||
$user->verified = !$user->verified;
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.verification.updated', [$user]);
|
||||
|
||||
return json(trans('admin.users.operations.verification.success'), 0);
|
||||
}
|
||||
|
||||
public function nickname(User $user, Request $request, Dispatcher $dispatcher)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'nickname' => 'required|string',
|
||||
]);
|
||||
$nickname = $data['nickname'];
|
||||
|
||||
$dispatcher->dispatch('user.nickname.updating', [$user, $nickname]);
|
||||
|
||||
$old = $user->replicate();
|
||||
$user->nickname = $nickname;
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.nickname.updated', [$user, $old]);
|
||||
|
||||
return json(trans('admin.users.operations.nickname.success', [
|
||||
'new' => $request->input('nickname'),
|
||||
]), 0);
|
||||
}
|
||||
|
||||
public function password(User $user, Request $request, Dispatcher $dispatcher)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'password' => 'required|string|min:8|max:16',
|
||||
]);
|
||||
$password = $data['password'];
|
||||
|
||||
$dispatcher->dispatch('user.password.updating', [$user, $password]);
|
||||
|
||||
$user->changePassword($password);
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.password.updated', [$user]);
|
||||
|
||||
return json(trans('admin.users.operations.password.success'), 0);
|
||||
}
|
||||
|
||||
public function score(User $user, Request $request, Dispatcher $dispatcher)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'score' => 'required|integer',
|
||||
]);
|
||||
$score = (int) $data['score'];
|
||||
|
||||
$dispatcher->dispatch('user.score.updating', [$user, $score]);
|
||||
|
||||
$old = $user->replicate();
|
||||
$user->score = $score;
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.score.updated', [$user, $old]);
|
||||
|
||||
return json(trans('admin.users.operations.score.success'), 0);
|
||||
}
|
||||
|
||||
public function permission(User $user, Request $request, Dispatcher $dispatcher)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'permission' => [
|
||||
'required',
|
||||
Rule::in([User::BANNED, User::NORMAL, User::ADMIN]),
|
||||
],
|
||||
]);
|
||||
$permission = (int) $data['permission'];
|
||||
|
||||
if (
|
||||
$permission === User::ADMIN &&
|
||||
$request->user()->permission < User::SUPER_ADMIN
|
||||
) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1)
|
||||
->setStatusCode(403);
|
||||
}
|
||||
|
||||
if ($user->is($request->user())) {
|
||||
return json(trans('admin.users.operations.no-permission'), 1)
|
||||
->setStatusCode(403);
|
||||
}
|
||||
|
||||
$dispatcher->dispatch('user.permission.updating', [$user, $permission]);
|
||||
|
||||
$old = $user->replicate();
|
||||
$user->permission = $permission;
|
||||
$user->save();
|
||||
|
||||
$dispatcher->dispatch('user.permission.updated', [$user, $old]);
|
||||
|
||||
return json(trans('admin.users.operations.permission'), 0);
|
||||
}
|
||||
|
||||
public function delete(User $user, Dispatcher $dispatcher)
|
||||
{
|
||||
$dispatcher->dispatch('user.deleting', [$user]);
|
||||
|
||||
$user->delete();
|
||||
|
||||
$dispatcher->dispatch('user.deleted', [$user]);
|
||||
|
||||
return json(trans('admin.users.operations.delete.success'), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Collection;
|
|||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
/**
|
||||
|
|
@ -31,6 +32,7 @@ class User extends Authenticatable implements JWTSubject
|
|||
use Notifiable;
|
||||
use HasPassword;
|
||||
use HasApiTokens;
|
||||
use SearchString;
|
||||
|
||||
const BANNED = -1;
|
||||
const NORMAL = 0;
|
||||
|
|
@ -51,6 +53,16 @@ class User extends Authenticatable implements JWTSubject
|
|||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
protected $searchStringColumns = [
|
||||
'uid',
|
||||
'email' => ['searchable' => true],
|
||||
'nickname' => ['searchable' => true],
|
||||
'avatar', 'score', 'permission', 'ip',
|
||||
'last_sign_at' => ['date' => true],
|
||||
'register_at' => ['date' => true],
|
||||
'verified' => ['boolean' => true],
|
||||
];
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->permission >= static::ADMIN;
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
:id="id"
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog" :class="{ 'modal-dialog-centered': center }" role="document">
|
||||
<div class="modal-content" :class="[`bg-${type}`]">
|
||||
<div v-if="showHeader" class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="dismiss"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot>
|
||||
<template v-if="text">
|
||||
<p v-for="(line, i) in lines" :key="i">{{ line }}</p>
|
||||
</template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-else-if="dangerousHTML" v-html="dangerousHTML" />
|
||||
<template v-if="mode === 'prompt'">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="value"
|
||||
:type="inputType"
|
||||
class="form-control"
|
||||
:placeholder="placeholder"
|
||||
@input="valid = true"
|
||||
>
|
||||
</div>
|
||||
<div v-if="!valid" class="alert alert-danger">
|
||||
<i class="icon far fa-times-circle" />
|
||||
{{ validatorMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="modal-footer" :class="footerClasses">
|
||||
<slot name="footer">
|
||||
<button
|
||||
v-if="mode !== 'alert'"
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="[`btn-${cancelButtonType}`]"
|
||||
data-dismiss="modal"
|
||||
@click="dismiss"
|
||||
>
|
||||
{{ cancelButtonText }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="[`btn-${okButtonType}`]"
|
||||
@click="confirm"
|
||||
>
|
||||
{{ okButtonText }}
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import { trans } from '../scripts/i18n'
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'confirm',
|
||||
validator: value => ['alert', 'confirm', 'prompt'].includes(value),
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: trans('general.tip'),
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dangerousHTML: {
|
||||
type: String,
|
||||
},
|
||||
input: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
validator: {
|
||||
type: Function,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
showHeader: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
okButtonText: {
|
||||
type: String,
|
||||
default: trans('general.confirm'),
|
||||
},
|
||||
okButtonType: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
cancelButtonText: {
|
||||
type: String,
|
||||
default: trans('general.cancel'),
|
||||
},
|
||||
cancelButtonType: {
|
||||
type: String,
|
||||
default: 'secondary',
|
||||
},
|
||||
flexFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hidden: false,
|
||||
value: this.input,
|
||||
valid: true,
|
||||
validatorMessage: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lines() {
|
||||
return this.text.split(/\r?\n/)
|
||||
},
|
||||
footerClasses() {
|
||||
return {
|
||||
'd-flex': this.flexFooter,
|
||||
'justify-content-between': this.flexFooter,
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$el)
|
||||
.on('hide.bs.modal', () => {
|
||||
if (!this.hidden) {
|
||||
this.dismiss()
|
||||
}
|
||||
})
|
||||
.on('hidden.bs.modal', () => {
|
||||
this.hidden = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
if (typeof this.validator === 'function') {
|
||||
const result = this.validator(this.value)
|
||||
if (typeof result === 'string') {
|
||||
this.validatorMessage = result
|
||||
this.valid = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.hidden = true
|
||||
this.$emit('confirm', { value: this.value })
|
||||
$(this.$el).modal('hide')
|
||||
},
|
||||
dismiss() {
|
||||
this.hidden = true
|
||||
this.$emit('dismiss')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -53,7 +53,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: () => import('../views/admin/Users.vue'),
|
||||
react: () => import('../views/admin/UsersManagement'),
|
||||
el: '.content > .container-fluid',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ export type User = {
|
|||
nickname: string
|
||||
score: number
|
||||
avatar: number
|
||||
permission: number
|
||||
permission: UserPermission
|
||||
ip: string
|
||||
last_sign_at: string
|
||||
register_at: string
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export const enum UserPermission {
|
||||
Banned = -1,
|
||||
Normal = 0,
|
||||
Admin = 1,
|
||||
SuperAdmin = 2,
|
||||
}
|
||||
|
||||
export type Player = {
|
||||
pid: number
|
||||
name: string
|
||||
|
|
|
|||
|
|
@ -1,362 +0,0 @@
|
|||
<template>
|
||||
<div class="container-fluid">
|
||||
<vue-good-table
|
||||
mode="remote"
|
||||
:rows="users"
|
||||
:total-rows="totalRecords || users.length"
|
||||
:columns="columns"
|
||||
:search-options="tableOptions.search"
|
||||
:pagination-options="tableOptions.pagination"
|
||||
style-class="vgt-table striped"
|
||||
@on-page-change="onPageChange"
|
||||
@on-sort-change="onSortChange"
|
||||
@on-search="onSearch"
|
||||
@on-per-page-change="onPerPageChange"
|
||||
>
|
||||
<template #table-row="props">
|
||||
<span v-if="props.column.field === 'email'">
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
<a :title="$t('admin.changeEmail')" data-test="email" @click="changeEmail(props.row)">
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'nickname'">
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
<a :title="$t('admin.changeNickName')" data-test="nickname" @click="changeNickName(props.row)">
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'score'">
|
||||
{{ props.formattedRow[props.column.field] }}
|
||||
<a :title="$t('admin.changeScore')" data-test="score" @click="changeScore(props.row)">
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'players_count'">
|
||||
<a
|
||||
:href="props.row | playersLink"
|
||||
:title="$t('admin.inspectHisPlayers')"
|
||||
data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
>{{ props.formattedRow[props.column.field] }}</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'permission'">
|
||||
<span>{{ props.row | humanizePermission }}</span>
|
||||
<a
|
||||
v-if="props.row.permission < 1 || (props.row.operations === 2 && props.row.permission < 2)"
|
||||
:title="$t('admin.changePermission')"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-permission"
|
||||
data-test="permission"
|
||||
@click="editingUser = props.row, permission = props.row.permission"
|
||||
>
|
||||
<i class="fas fa-edit btn-edit" />
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="props.column.field === 'verified'">
|
||||
<span v-if="props.row.verified" v-t="'admin.verified'" />
|
||||
<span v-else v-t="'admin.unverified'" />
|
||||
<a
|
||||
:title="$t('admin.toggleVerification')"
|
||||
data-test="verification"
|
||||
@click="toggleVerification(props.row)"
|
||||
>
|
||||
<i
|
||||
class="fas btn-edit"
|
||||
:class="{ 'fa-toggle-on': props.row.verified, 'fa-toggle-off': !props.row.verified }"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<div v-else-if="props.column.field === 'operations'">
|
||||
<button class="btn btn-default" @click="changePassword(props.row)">
|
||||
{{ $t('admin.changePassword') }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="props.row.permission >= 2 || (props.row.operations === 1 && props.row.permission >= 1)"
|
||||
class="btn btn-danger"
|
||||
data-test="deleteUser"
|
||||
@click="deleteUser(props.row)"
|
||||
>
|
||||
{{ $t('admin.deleteUser') }}
|
||||
</button>
|
||||
</div>
|
||||
<span v-else v-text="props.formattedRow[props.column.field]" />
|
||||
</template>
|
||||
</vue-good-table>
|
||||
<modal
|
||||
id="modal-permission"
|
||||
:title="$t(this.$t('admin.newPermission'))"
|
||||
center
|
||||
@confirm="changePermission"
|
||||
>
|
||||
<label class="mr-3">
|
||||
<input
|
||||
v-model="permission"
|
||||
type="radio"
|
||||
name="permission"
|
||||
:value="-1"
|
||||
>
|
||||
{{ $t('admin.banned') }}
|
||||
</label>
|
||||
<label class="mr-3">
|
||||
<input
|
||||
v-model="permission"
|
||||
type="radio"
|
||||
name="permission"
|
||||
:value="0"
|
||||
>
|
||||
{{ $t('admin.normal') }}
|
||||
</label>
|
||||
<label v-if="editingUser.operations === 2">
|
||||
<input
|
||||
v-model="permission"
|
||||
type="radio"
|
||||
name="permission"
|
||||
:value="1"
|
||||
>
|
||||
{{ $t('admin.admin') }}
|
||||
</label>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueGoodTable } from 'vue-good-table'
|
||||
import 'vue-good-table/dist/vue-good-table.min.css'
|
||||
import { trans } from '../../scripts/i18n'
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import tableOptions from '../../components/mixins/tableOptions'
|
||||
import serverTable from '../../components/mixins/serverTable'
|
||||
import emitMounted from '../../components/mixins/emitMounted'
|
||||
import { showModal, toast } from '../../scripts/notify'
|
||||
import { truthy } from '../../scripts/validators'
|
||||
|
||||
export default {
|
||||
name: 'UsersManagement',
|
||||
components: {
|
||||
Modal,
|
||||
VueGoodTable,
|
||||
},
|
||||
filters: {
|
||||
humanizePermission(user) {
|
||||
switch (user.permission) {
|
||||
case -1:
|
||||
return trans('admin.banned')
|
||||
case 0:
|
||||
return trans('admin.normal')
|
||||
case 1:
|
||||
return trans('admin.admin')
|
||||
case 2:
|
||||
return trans('admin.superAdmin')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
playersLink(user) {
|
||||
return `${blessing.base_url}/admin/players?uid=${user.uid}`
|
||||
},
|
||||
},
|
||||
mixins: [
|
||||
emitMounted,
|
||||
tableOptions,
|
||||
serverTable,
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
users: [],
|
||||
columns: [
|
||||
{
|
||||
field: 'uid', label: 'UID', type: 'number',
|
||||
},
|
||||
{ field: 'email', label: this.$t('general.user.email') },
|
||||
{
|
||||
field: 'nickname', label: this.$t('general.user.nickname'), width: '150px',
|
||||
},
|
||||
{
|
||||
field: 'score', label: this.$t('general.user.score'), type: 'number', width: '102px',
|
||||
},
|
||||
{
|
||||
field: 'players_count', label: this.$t('admin.playersCount'), type: 'number', sortable: false,
|
||||
},
|
||||
{
|
||||
field: 'permission', label: this.$t('admin.permission'), 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,
|
||||
},
|
||||
],
|
||||
editingUser: {},
|
||||
permission: 0,
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
async fetchData() {
|
||||
const { data, totalRecords } = await this.$http.get(
|
||||
`/admin/users/list${location.search}`,
|
||||
!location.search && this.serverParams,
|
||||
)
|
||||
this.totalRecords = totalRecords
|
||||
this.users = data
|
||||
},
|
||||
async changeEmail(user) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.newUserEmail'),
|
||||
input: user.email,
|
||||
validator: truthy(this.$t('auth.emptyEmail')),
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=email',
|
||||
{ uid: user.uid, email: value },
|
||||
)
|
||||
if (code === 0) {
|
||||
user.email = value
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async toggleVerification(user) {
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=verification',
|
||||
{ uid: user.uid },
|
||||
)
|
||||
if (code === 0) {
|
||||
user.verified = !user.verified
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changeNickName(user) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.newUserNickname'),
|
||||
input: user.nickname,
|
||||
validator: truthy(this.$t('auth.emptyNickname')),
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=nickname',
|
||||
{ uid: user.uid, nickname: value },
|
||||
)
|
||||
if (code === 0) {
|
||||
user.nickname = value
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changePassword(user) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.newUserPassword'),
|
||||
inputType: 'password',
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=password',
|
||||
{ uid: user.uid, password: value },
|
||||
)
|
||||
code === 0 ? toast.success(message) : toast.error(message)
|
||||
},
|
||||
async changeScore(user) {
|
||||
let value
|
||||
try {
|
||||
({ value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: this.$t('admin.newScore'),
|
||||
input: user.score,
|
||||
inputType: 'number',
|
||||
}))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const score = Number.parseInt(value)
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=score',
|
||||
{ uid: user.uid, score },
|
||||
)
|
||||
if (code === 0) {
|
||||
user.score = score
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async changePermission() {
|
||||
const permission = Number.parseInt(this.permission)
|
||||
const { code, message } = await this.$http.post('/admin/users?action=permission', {
|
||||
uid: this.editingUser.uid,
|
||||
permission,
|
||||
})
|
||||
if (code === 0) {
|
||||
this.editingUser.permission = permission
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
async deleteUser({ uid, originalIndex }) {
|
||||
try {
|
||||
await showModal({
|
||||
text: this.$t('admin.deleteUserNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await this.$http.post(
|
||||
'/admin/users?action=delete',
|
||||
{ uid },
|
||||
)
|
||||
if (code === 0) {
|
||||
this.$delete(this.users, originalIndex)
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.operations-menu
|
||||
margin-left -35px
|
||||
|
||||
.fa-edit
|
||||
cursor pointer
|
||||
|
||||
.fa-toggle-on, .fa-toggle-off
|
||||
font-size 18px
|
||||
cursor pointer
|
||||
|
||||
.row-at-bottom
|
||||
margin-top -100px
|
||||
</style>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
@use '../../../styles/breakpoints';
|
||||
|
||||
.box {
|
||||
width: 48%;
|
||||
margin: 7px;
|
||||
|
||||
@include breakpoints.less-than('lg') {
|
||||
width: 98%;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
$border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
.border {
|
||||
@include breakpoints.less-than('sm') {
|
||||
border-bottom: $border;
|
||||
}
|
||||
@include breakpoints.greater-than('sm') {
|
||||
border-right: $border;
|
||||
}
|
||||
}
|
||||
157
resources/assets/src/views/admin/UsersManagement/Card.tsx
Normal file
157
resources/assets/src/views/admin/UsersManagement/Card.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import React from 'react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import { User } from '@/scripts/types'
|
||||
import {
|
||||
humanizePermission,
|
||||
verificationStatusText,
|
||||
canModifyUser,
|
||||
canModifyPermission,
|
||||
} from './utils'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
user: User
|
||||
currentUser: User
|
||||
onEmailChange(): void
|
||||
onNicknameChange(): void
|
||||
onScoreChange(): void
|
||||
onPermissionChange(): void
|
||||
onVerificationToggle(): void
|
||||
onPasswordChange(): void
|
||||
onDelete(): void
|
||||
}
|
||||
|
||||
const Card: React.FC<Props> = (props) => {
|
||||
const { user, currentUser } = props
|
||||
|
||||
const canModify = canModifyUser(user, currentUser)
|
||||
|
||||
return (
|
||||
<div className={`info-box ${styles.box}`}>
|
||||
<div className={styles.icon}>
|
||||
<img
|
||||
className="bs-avatar"
|
||||
src={`${blessing.base_url}/avatar/user/${user.uid}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="info-box-content">
|
||||
<div className="row">
|
||||
<div className="col-10">
|
||||
<b>{user.nickname}</b>
|
||||
</div>
|
||||
<div className="col-2">
|
||||
{canModify && (
|
||||
<div className="float-right dropdown">
|
||||
<a
|
||||
className="text-gray"
|
||||
href="#"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="fas fa-cog"></i>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-right">
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onEmailChange}
|
||||
>
|
||||
<i className="fas fa-at mr-2"></i>
|
||||
{t('admin.changeEmail')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onNicknameChange}
|
||||
>
|
||||
<i className="fas fa-signature mr-2"></i>
|
||||
{t('admin.changeNickName')}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onPasswordChange}
|
||||
>
|
||||
<i className="fas fa-asterisk mr-2"></i>
|
||||
{t('admin.changePassword')}
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onScoreChange}
|
||||
>
|
||||
<i className="fas fa-coins mr-2"></i>
|
||||
{t('admin.changeScore')}
|
||||
</a>
|
||||
{canModifyPermission(user, currentUser) && (
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onPermissionChange}
|
||||
>
|
||||
<i className="fas fa-user-secret mr-2"></i>
|
||||
{t('admin.changePermission')}
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item"
|
||||
onClick={props.onVerificationToggle}
|
||||
>
|
||||
<i className="fas fa-user-check mr-2"></i>
|
||||
{t('admin.toggleVerification')}
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
<a
|
||||
href="#"
|
||||
className="dropdown-item dropdown-item-danger"
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
<i className="fas fa-trash mr-2"></i>
|
||||
{t('admin.deleteUser')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>UID: {user.uid}</div>
|
||||
<div>
|
||||
{t('general.user.email')}
|
||||
{': '}
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
<div className="row m-2 border-top border-bottom">
|
||||
<div className={`col-sm-4 py-1 text-center ${styles.border}`}>
|
||||
<b className="d-block">{t('general.user.score')}</b>
|
||||
<span className="d-block py-1">{user.score}</span>
|
||||
</div>
|
||||
<div className={`col-sm-4 py-1 text-center ${styles.border}`}>
|
||||
<b className="d-block">{t('admin.permission')}</b>
|
||||
<span className="d-block py-1">
|
||||
{humanizePermission(user.permission)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-sm-4 py-1 text-center">
|
||||
<b className="d-block">{t('admin.verification')}</b>
|
||||
<span className="d-block py-1">
|
||||
{verificationStatusText(user.verified)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-gray">
|
||||
{t('general.user.register-at')}
|
||||
{': '}
|
||||
{user.register_at}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
114
resources/assets/src/views/admin/UsersManagement/Row.tsx
Normal file
114
resources/assets/src/views/admin/UsersManagement/Row.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React from 'react'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import { User } from '@/scripts/types'
|
||||
import ButtonEdit from '@/components/ButtonEdit'
|
||||
import {
|
||||
humanizePermission,
|
||||
verificationStatusText,
|
||||
canModifyUser,
|
||||
canModifyPermission,
|
||||
} from './utils'
|
||||
|
||||
interface Props {
|
||||
user: User
|
||||
currentUser: User
|
||||
onEmailChange(): void
|
||||
onNicknameChange(): void
|
||||
onScoreChange(): void
|
||||
onPermissionChange(): void
|
||||
onVerificationToggle(): void
|
||||
onPasswordChange(): void
|
||||
onDelete(): void
|
||||
}
|
||||
|
||||
const Row: React.FC<Props> = (props) => {
|
||||
const { user, currentUser } = props
|
||||
|
||||
const canModify = canModifyUser(user, currentUser)
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{user.uid}</td>
|
||||
<td>
|
||||
{user.email}
|
||||
{canModify && (
|
||||
<span className="ml-1">
|
||||
<ButtonEdit
|
||||
title={t('admin.changeEmail')}
|
||||
onClick={props.onEmailChange}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.nickname}
|
||||
{canModify && (
|
||||
<span className="ml-1">
|
||||
<ButtonEdit
|
||||
title={t('admin.changeNickName')}
|
||||
onClick={props.onNicknameChange}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{user.score}
|
||||
{canModify && (
|
||||
<span className="ml-1">
|
||||
<ButtonEdit
|
||||
title={t('admin.changeScore')}
|
||||
onClick={props.onScoreChange}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{humanizePermission(user.permission)}
|
||||
{canModifyPermission(user, currentUser) && (
|
||||
<span className="ml-1">
|
||||
<ButtonEdit
|
||||
title={t('admin.changePermission')}
|
||||
onClick={props.onPermissionChange}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{verificationStatusText(user.verified)}
|
||||
{canModify && (
|
||||
<a
|
||||
className="ml-1"
|
||||
href="#"
|
||||
title={t('admin.toggleVerification')}
|
||||
onClick={props.onVerificationToggle}
|
||||
>
|
||||
{user.verified ? (
|
||||
<i className="fas fa-toggle-on"></i>
|
||||
) : (
|
||||
<i className="fas fa-toggle-off"></i>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td>{user.register_at}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-default mr-2"
|
||||
disabled={!canModify}
|
||||
onClick={props.onPasswordChange}
|
||||
>
|
||||
{t('admin.changePassword')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={!canModify}
|
||||
onClick={props.onDelete}
|
||||
>
|
||||
{t('admin.deleteUser')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default Row
|
||||
364
resources/assets/src/views/admin/UsersManagement/index.tsx
Normal file
364
resources/assets/src/views/admin/UsersManagement/index.tsx
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import { useImmer } from 'use-immer'
|
||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'
|
||||
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { User, UserPermission, Paginator } from '@/scripts/types'
|
||||
import { toast, showModal } from '@/scripts/notify'
|
||||
import modeSwitchStyles from '@/styles/table-mode-switch.module.scss'
|
||||
import type { Props as ModalInputProps } from '@/components/ModalInput'
|
||||
import Loading from '@/components/Loading'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import Card from './Card'
|
||||
import Row from './Row'
|
||||
|
||||
const UsersManagement: React.FC = () => {
|
||||
const [users, setUsers] = useImmer<User[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const isLargeScreen = useIsLargeScreen()
|
||||
const [isTableMode, setIsTableMode] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const currentUser = useBlessingExtra<User>('currentUser', {
|
||||
uid: 0,
|
||||
permission: UserPermission.Admin,
|
||||
} as User)
|
||||
|
||||
useEffect(() => {
|
||||
if (isLargeScreen) {
|
||||
setIsTableMode(true)
|
||||
}
|
||||
}, [isLargeScreen])
|
||||
|
||||
const getUsers = async () => {
|
||||
setIsLoading(true)
|
||||
const { data, last_page }: Paginator<User> = await fetch.get(
|
||||
'/admin/users/list',
|
||||
{
|
||||
q: query,
|
||||
page,
|
||||
},
|
||||
)
|
||||
setUsers(() => data)
|
||||
setTotalPages(last_page)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers()
|
||||
}, [page])
|
||||
|
||||
const handleModeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsTableMode(event.target.value === 'table')
|
||||
}
|
||||
|
||||
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(event.target.value)
|
||||
}
|
||||
|
||||
const handleSubmitQuery = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
getUsers()
|
||||
}
|
||||
|
||||
const handleEmailChange = async (user: User, index: number) => {
|
||||
let email: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.newUserEmail'),
|
||||
input: user.email,
|
||||
validator: (value: string) => {
|
||||
if (!value) {
|
||||
return t('auth.emptyEmail')
|
||||
}
|
||||
},
|
||||
})
|
||||
email = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/email`,
|
||||
{ email },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => {
|
||||
users[index].email = email
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNicknameChange = async (user: User, index: number) => {
|
||||
let nickname: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.newUserNickname'),
|
||||
input: user.nickname,
|
||||
validator: (value: string) => {
|
||||
if (!value) {
|
||||
return t('auth.emptyNickname')
|
||||
}
|
||||
},
|
||||
})
|
||||
nickname = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/nickname`,
|
||||
{ nickname },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => {
|
||||
users[index].nickname = nickname
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScoreChange = async (user: User, index: number) => {
|
||||
let score: number
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.newScore'),
|
||||
input: user.score.toString(),
|
||||
inputType: 'number',
|
||||
})
|
||||
score = Number.parseInt(value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/score`,
|
||||
{ score },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => {
|
||||
users[index].score = score
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePermissionChange = async (user: User, index: number) => {
|
||||
const permissions: ModalInputProps['choices'] = [
|
||||
{ text: t('admin.banned'), value: '-1' },
|
||||
{ text: t('admin.normal'), value: '0' },
|
||||
]
|
||||
if (currentUser.permission > UserPermission.Admin) {
|
||||
permissions.push({ text: t('admin.admin'), value: '1' })
|
||||
}
|
||||
|
||||
let permission: UserPermission
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.newPermission'),
|
||||
input: user.permission.toString(),
|
||||
inputType: 'radios',
|
||||
choices: permissions,
|
||||
})
|
||||
permission = Number.parseInt(value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/permission`,
|
||||
{ permission },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => {
|
||||
users[index].permission = permission
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleVerificationToggle = async (user: User, index: number) => {
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/verification`,
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => {
|
||||
users[index].verified = !users[index].verified
|
||||
})
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordChange = async (user: User) => {
|
||||
let password: string
|
||||
try {
|
||||
const { value } = await showModal({
|
||||
mode: 'prompt',
|
||||
text: t('admin.newUserPassword'),
|
||||
inputType: 'password',
|
||||
placeholder: t('adminchangePassword'),
|
||||
})
|
||||
password = value
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.put<fetch.ResponseBody>(
|
||||
`/admin/users/${user.uid}/password`,
|
||||
{ password },
|
||||
)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (user: User) => {
|
||||
try {
|
||||
await showModal({
|
||||
text: t('admin.deleteUserNotice'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const { code, message } = await fetch.del(`/admin/users/${user.uid}`)
|
||||
if (code === 0) {
|
||||
toast.success(message)
|
||||
setUsers((users) => users.filter(({ uid }) => uid !== user.uid))
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className={`card-header ${modeSwitchStyles.header}`}>
|
||||
<form className="input-group" onSubmit={handleSubmitQuery}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
title={t('vendor.datatable.search')}
|
||||
value={query}
|
||||
onChange={handleQueryChange}
|
||||
/>
|
||||
<div className="input-group-append">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
{t('vendor.datatable.search')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="btn-group btn-group-toggle">
|
||||
<label
|
||||
className={`btn btn-secondary ${isTableMode ? 'active' : ''}`}
|
||||
title="Table Mode"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value="table"
|
||||
checked={isTableMode}
|
||||
onChange={handleModeChange}
|
||||
/>
|
||||
<i className="fas fa-list"></i>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-secondary ${isTableMode ? '' : 'active'}`}
|
||||
title="Card Mode"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
value="card"
|
||||
checked={!isTableMode}
|
||||
onChange={handleModeChange}
|
||||
/>
|
||||
<i className="fas fa-grip-vertical"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="card-body">
|
||||
<Loading />
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<div className="card-body text-center">{t('general.noResult')}</div>
|
||||
) : isTableMode ? (
|
||||
<div className="card-body table-responsive p-0">
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>UID</th>
|
||||
<th>{t('general.user.email')}</th>
|
||||
<th>{t('general.user.nickname')}</th>
|
||||
<th>{t('general.user.score')}</th>
|
||||
<th>{t('admin.permission')}</th>
|
||||
<th>{t('admin.verification')}</th>
|
||||
<th>{t('general.user.register-at')}</th>
|
||||
<th>{t('admin.operationsTitle')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, i) => (
|
||||
<Row
|
||||
key={user.uid}
|
||||
user={user}
|
||||
currentUser={currentUser}
|
||||
onEmailChange={() => handleEmailChange(user, i)}
|
||||
onNicknameChange={() => handleNicknameChange(user, i)}
|
||||
onScoreChange={() => handleScoreChange(user, i)}
|
||||
onPermissionChange={() => handlePermissionChange(user, i)}
|
||||
onVerificationToggle={() => handleVerificationToggle(user, i)}
|
||||
onPasswordChange={() => handlePasswordChange(user)}
|
||||
onDelete={() => handleDelete(user)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-body d-flex flex-wrap">
|
||||
{users.map((user, i) => (
|
||||
<Card
|
||||
key={user.uid}
|
||||
user={user}
|
||||
currentUser={currentUser}
|
||||
onEmailChange={() => handleEmailChange(user, i)}
|
||||
onNicknameChange={() => handleNicknameChange(user, i)}
|
||||
onScoreChange={() => handleScoreChange(user, i)}
|
||||
onPermissionChange={() => handlePermissionChange(user, i)}
|
||||
onVerificationToggle={() => handleVerificationToggle(user, i)}
|
||||
onPasswordChange={() => handlePasswordChange(user)}
|
||||
onDelete={() => handleDelete(user)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="card-footer">
|
||||
<div className="float-right">
|
||||
<Pagination page={page} totalPages={totalPages} onChange={setPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default hot(UsersManagement)
|
||||
27
resources/assets/src/views/admin/UsersManagement/utils.ts
Normal file
27
resources/assets/src/views/admin/UsersManagement/utils.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { t } from '@/scripts/i18n'
|
||||
import { User, UserPermission } from '@/scripts/types'
|
||||
|
||||
export function humanizePermission(permission: UserPermission): string {
|
||||
switch (permission) {
|
||||
case UserPermission.Banned:
|
||||
return t('admin.banned')
|
||||
case UserPermission.Normal:
|
||||
return t('admin.normal')
|
||||
case UserPermission.Admin:
|
||||
return t('admin.admin')
|
||||
case UserPermission.SuperAdmin:
|
||||
return t('admin.superAdmin')
|
||||
}
|
||||
}
|
||||
|
||||
export function verificationStatusText(isVerified: boolean): string {
|
||||
return isVerified ? t('admin.verified') : t('admin.unverified')
|
||||
}
|
||||
|
||||
export function canModifyUser(target: User, current: User): boolean {
|
||||
return target.uid === current.uid || current.permission > target.permission
|
||||
}
|
||||
|
||||
export function canModifyPermission(target: User, current: User): boolean {
|
||||
return current.permission > target.permission
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
import $ from 'jquery'
|
||||
import 'bootstrap'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
|
||||
test('id', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
id: 'id',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('#id').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('title', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
title: 'kumiko',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-title').text()).toBe('kumiko')
|
||||
})
|
||||
|
||||
test('close button at header', () => {
|
||||
const wrapper = mount(Modal)
|
||||
wrapper.find('.modal-header > button').trigger('click')
|
||||
expect(wrapper.emitted().dismiss).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('render lines', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
text: 'kumiko\nreina',
|
||||
},
|
||||
})
|
||||
const paragraphs = wrapper.findAll('p')
|
||||
expect(paragraphs).toHaveLength(2)
|
||||
expect(paragraphs.at(0).text()).toBe('kumiko')
|
||||
expect(paragraphs.at(1).text()).toBe('reina')
|
||||
})
|
||||
|
||||
test('dynamic html', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
dangerousHTML: '<div class="eupho">kumiko</div>',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.eupho').text()).toBe('kumiko')
|
||||
})
|
||||
|
||||
test('cancel by button at footer', () => {
|
||||
const wrapper = mount(Modal)
|
||||
wrapper.find('.btn-secondary').trigger('click')
|
||||
expect(wrapper.emitted().dismiss).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('alert mode', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
mode: 'alert',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.btn-secondary').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('prompt mode', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
mode: 'prompt',
|
||||
input: 'default-value',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.btn-secondary').exists()).toBeTrue()
|
||||
|
||||
wrapper.find('input').setValue('hazuki')
|
||||
wrapper.find('.btn-primary').trigger('click')
|
||||
expect(wrapper.emitted().confirm[0][0]).toStrictEqual({ value: 'hazuki' })
|
||||
})
|
||||
|
||||
test('input placeholder', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
mode: 'prompt',
|
||||
placeholder: 'hibike',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('input').attributes('placeholder')).toBe('hibike')
|
||||
})
|
||||
|
||||
test('validate input', () => {
|
||||
const stub = jest.fn()
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce('invalid')
|
||||
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
mode: 'prompt',
|
||||
validator: stub,
|
||||
},
|
||||
})
|
||||
const button = wrapper.find('.btn-primary')
|
||||
|
||||
button.trigger('click')
|
||||
expect(wrapper.find('.alert').exists()).toBeFalse()
|
||||
|
||||
button.trigger('click')
|
||||
expect(wrapper.find('.alert').text()).toContain('invalid')
|
||||
})
|
||||
|
||||
test('input type', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
mode: 'prompt',
|
||||
inputType: 'password',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('[type=password]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('modal type', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
type: 'danger',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-content').classes('bg-danger')).toBeTrue()
|
||||
})
|
||||
|
||||
test('hide header', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
showHeader: false,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-header').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('centered modal', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
center: true,
|
||||
},
|
||||
})
|
||||
expect(
|
||||
wrapper.find('.modal-dialog').classes('modal-dialog-centered'),
|
||||
).toBeTrue()
|
||||
})
|
||||
|
||||
test('customize ok button', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
okButtonText: 'OK',
|
||||
okButtonType: 'danger',
|
||||
},
|
||||
})
|
||||
const button = wrapper.find('.btn:nth-child(2)')
|
||||
expect(button.text().trim()).toBe('OK')
|
||||
expect(button.classes('btn-danger')).toBeTrue()
|
||||
})
|
||||
|
||||
test('customize cancel button', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
cancelButtonText: 'CANCEL',
|
||||
cancelButtonType: 'danger',
|
||||
},
|
||||
})
|
||||
const button = wrapper.find('.btn:nth-child(1)')
|
||||
expect(button.text().trim()).toBe('CANCEL')
|
||||
expect(button.classes('btn-danger')).toBeTrue()
|
||||
})
|
||||
|
||||
test('flex footer', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
propsData: {
|
||||
flexFooter: true,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-footer').classes())
|
||||
.toContainValues(['d-flex', 'justify-content-between'])
|
||||
})
|
||||
|
||||
test('default slot', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
slots: {
|
||||
default: '<div class="trumpet">reina</div>',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-body > .trumpet').text()).toBe('reina')
|
||||
})
|
||||
|
||||
test('footer slot', () => {
|
||||
const wrapper = mount(Modal, {
|
||||
slots: {
|
||||
footer: '<div class="contrabass">sapphire</div>',
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.modal-footer > .contrabass').text()).toBe('sapphire')
|
||||
})
|
||||
|
||||
test('prevent duplicated dismission', () => {
|
||||
const wrapper = mount(Modal)
|
||||
wrapper.find('.btn-secondary').trigger('click')
|
||||
$(wrapper.element).trigger('hide.bs.modal')
|
||||
$(wrapper.element).trigger('hidden.bs.modal')
|
||||
expect(wrapper.emitted().dismiss).toHaveLength(1)
|
||||
|
||||
wrapper.find('.btn-primary').trigger('click')
|
||||
$(wrapper.element).trigger('hide.bs.modal')
|
||||
$(wrapper.element).trigger('hidden.bs.modal')
|
||||
expect(wrapper.emitted().dismiss).toHaveLength(1)
|
||||
|
||||
$(wrapper.element).trigger('hide.bs.modal')
|
||||
$(wrapper.element).trigger('hidden.bs.modal')
|
||||
expect(wrapper.emitted().dismiss).toHaveLength(2)
|
||||
})
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { flushPromises } from '../../utils'
|
||||
import '@/scripts/i18n'
|
||||
import Modal from '@/components/Modal.vue'
|
||||
import { showModal, toast } from '@/scripts/notify'
|
||||
import Users from '@/views/admin/Users.vue'
|
||||
|
||||
jest.mock('@/scripts/i18n', () => ({
|
||||
trans: (key: string) => key,
|
||||
}))
|
||||
jest.mock('@/scripts/notify')
|
||||
|
||||
test('fetch data after initializing', () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({ data: [] })
|
||||
mount(Users)
|
||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
||||
'/admin/users/list',
|
||||
{
|
||||
page: 1, perPage: 10, search: '', sortField: 'uid', sortType: 'asc',
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('humanize permission', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, permission: -1 },
|
||||
{ uid: 2, permission: 0 },
|
||||
{ uid: 3, permission: 1 },
|
||||
{ uid: 4, permission: 2 },
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const text = wrapper.find('.vgt-table').text()
|
||||
expect(text).toContain('admin.banned')
|
||||
expect(text).toContain('admin.normal')
|
||||
expect(text).toContain('admin.admin')
|
||||
expect(text).toContain('admin.superAdmin')
|
||||
})
|
||||
|
||||
test('generate players page link', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, permission: 0 },
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-toggle="tooltip"]').attributes('href')).toBe('/admin/players?uid=1')
|
||||
})
|
||||
|
||||
test('permission option should not be displayed for super admins', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, permission: 2 },
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('permission option should be displayed for admin as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 1, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('permission option should be displayed for normal users as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 0, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('permission option should be displayed for banned users as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: -1, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('permission option should not be displayed for other admins as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 1, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeFalse()
|
||||
})
|
||||
|
||||
test('permission option should be displayed for normal users as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 0, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('permission option should be displayed for banned users as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: -1, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test=permission]').exists()).toBeTrue()
|
||||
})
|
||||
|
||||
test('deletion button should not be displayed for super admins', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, permission: 2 },
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
test('deletion button should be displayed for admins as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 1, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBeNil()
|
||||
})
|
||||
|
||||
test('deletion button should be displayed for normal users as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 0, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBeNil()
|
||||
})
|
||||
|
||||
test('deletion button should be displayed for banned users as super admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: -1, operations: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBeNil()
|
||||
})
|
||||
|
||||
test('deletion button should not be displayed for other admins as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 1, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
test('deletion button should be displayed for normal users as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 0, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBeNil()
|
||||
})
|
||||
|
||||
test('deletion button should be displayed for banned users as admin', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: -1, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="deleteUser"]').attributes('disabled')).toBeNil()
|
||||
})
|
||||
|
||||
test('change email', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, email: 'a@b.c' },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'd@e.f' })
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="email"]')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=email',
|
||||
{ uid: 1, email: 'd@e.f' },
|
||||
)
|
||||
expect(wrapper.text()).toContain('a@b.c')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
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
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="verification"')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=verification',
|
||||
{ uid: 1 },
|
||||
)
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('admin.verified')
|
||||
})
|
||||
|
||||
test('change nickname', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, nickname: 'old' },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'new' })
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="nickname"]')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=nickname',
|
||||
{ uid: 1, nickname: 'new' },
|
||||
)
|
||||
expect(wrapper.text()).toContain('old')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('new')
|
||||
})
|
||||
|
||||
test('change password', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1 },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: 'password' })
|
||||
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('.btn-default')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=password',
|
||||
{ uid: 1, password: 'password' },
|
||||
)
|
||||
await flushPromises()
|
||||
expect(toast.success).toBeCalledWith('0')
|
||||
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(toast.error).toBeCalledWith('1')
|
||||
})
|
||||
|
||||
test('change score', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, score: 23 },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValueOnce({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '45' })
|
||||
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('[data-test="score"]')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=score',
|
||||
{ uid: 1, score: 45 },
|
||||
)
|
||||
expect(wrapper.text()).toContain('23')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('45')
|
||||
})
|
||||
|
||||
test('change permission', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
uid: 1, permission: 0, operations: 2,
|
||||
},
|
||||
{
|
||||
uid: 2, permission: 0, operations: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
|
||||
wrapper
|
||||
.findAll('[data-test=permission]')
|
||||
.at(0)
|
||||
.trigger('click')
|
||||
expect(wrapper.findAll('[type=radio]')).toHaveLength(3)
|
||||
|
||||
wrapper
|
||||
.findAll('[data-test=permission]')
|
||||
.at(1)
|
||||
.trigger('click')
|
||||
expect(wrapper.findAll('[type=radio]')).toHaveLength(2)
|
||||
|
||||
const button = wrapper.findAll('[data-test=permission]').at(1)
|
||||
button.trigger('click')
|
||||
wrapper.find('[type=radio]:nth-child(1)').setChecked()
|
||||
wrapper.find(Modal).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=permission',
|
||||
{ uid: 2, permission: -1 },
|
||||
)
|
||||
|
||||
button.trigger('click')
|
||||
wrapper.find('[type=radio]:nth-child(1)').setChecked()
|
||||
wrapper.find(Modal).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('admin.banned')
|
||||
})
|
||||
|
||||
test('delete user', async () => {
|
||||
Vue.prototype.$http.get.mockResolvedValue({
|
||||
data: [
|
||||
{ uid: 1, nickname: 'to-be-deleted' },
|
||||
],
|
||||
})
|
||||
Vue.prototype.$http.post
|
||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
||||
.mockResolvedValue({ code: 0, message: '0' })
|
||||
showModal
|
||||
.mockRejectedValueOnce(null)
|
||||
.mockResolvedValue({ value: '' })
|
||||
|
||||
const wrapper = mount(Users)
|
||||
await flushPromises()
|
||||
const button = wrapper.find('.btn-danger')
|
||||
|
||||
button.trigger('click')
|
||||
expect(Vue.prototype.$http.post).not.toBeCalled()
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
||||
'/admin/users?action=delete',
|
||||
{ uid: 1 },
|
||||
)
|
||||
expect(wrapper.text()).toContain('to-be-deleted')
|
||||
|
||||
button.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('No data')
|
||||
})
|
||||
798
resources/assets/tests/views/admin/UsersManagement.test.tsx
Normal file
798
resources/assets/tests/views/admin/UsersManagement.test.tsx
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
import React from 'react'
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react'
|
||||
import { createPaginator } from '../../utils'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { User, UserPermission } from '@/scripts/types'
|
||||
import UsersManagement from '@/views/admin/UsersManagement'
|
||||
|
||||
jest.mock('@/scripts/net')
|
||||
|
||||
const fixture: Readonly<User> = Object.freeze<User>({
|
||||
uid: 1,
|
||||
email: 'a@b.c',
|
||||
nickname: 'abc',
|
||||
score: 1000,
|
||||
avatar: 0,
|
||||
permission: UserPermission.Normal,
|
||||
ip: '::1',
|
||||
last_sign_at: new Date().toString(),
|
||||
register_at: new Date().toString(),
|
||||
verified: true,
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
Object.assign(window, { innerWidth: 500 })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
Object.assign(window, { innerWidth: 1024 })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
window.blessing.extra = {
|
||||
currentUser: { ...fixture, uid: 2, permission: UserPermission.Admin },
|
||||
}
|
||||
})
|
||||
|
||||
test('search users', async () => {
|
||||
fetch.get.mockResolvedValue(createPaginator([]))
|
||||
|
||||
const { getByTitle, getByText } = render(<UsersManagement />)
|
||||
await waitFor(() =>
|
||||
expect(fetch.get).toBeCalledWith('/admin/users/list', { q: '', page: 1 }),
|
||||
)
|
||||
|
||||
fireEvent.input(getByTitle(t('vendor.datatable.search')), {
|
||||
target: { value: 's' },
|
||||
})
|
||||
fireEvent.click(getByText(t('vendor.datatable.search')))
|
||||
await waitFor(() =>
|
||||
expect(fetch.get).toBeCalledWith('/admin/users/list', {
|
||||
q: 's',
|
||||
page: 1,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('access control', () => {
|
||||
describe('current user is super administrator', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra = {
|
||||
currentUser: {
|
||||
...fixture,
|
||||
uid: 2,
|
||||
permission: UserPermission.SuperAdmin,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('target user is super administrator', async () => {
|
||||
fetch.get.mockResolvedValue(
|
||||
createPaginator([
|
||||
{ ...fixture, permission: UserPermission.SuperAdmin },
|
||||
]),
|
||||
)
|
||||
|
||||
const { getByTitle, getByText, queryByText, queryByTitle } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePassword'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.toggleVerification'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.deleteUser'))).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
expect(queryByTitle(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(
|
||||
queryByTitle(t('admin.toggleVerification')),
|
||||
).not.toBeInTheDocument()
|
||||
expect(getByText(t('admin.changePassword'))).toBeDisabled()
|
||||
expect(getByText(t('admin.deleteUser'))).toBeDisabled()
|
||||
})
|
||||
|
||||
it('target user is normal administrator', async () => {
|
||||
fetch.get.mockResolvedValue(
|
||||
createPaginator([{ ...fixture, permission: UserPermission.Admin }]),
|
||||
)
|
||||
|
||||
const { getByTitle, getByText, queryByText, queryByTitle } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.changeEmail'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeNickName'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePassword'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeScore'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePermission'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.toggleVerification'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.deleteUser'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
expect(queryByTitle(t('admin.changeEmail'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeNickName'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeScore'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changePermission'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.toggleVerification'))).toBeInTheDocument()
|
||||
expect(getByText(t('admin.changePassword'))).toBeEnabled()
|
||||
expect(getByText(t('admin.deleteUser'))).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('current user is normal administrator', () => {
|
||||
beforeEach(() => {
|
||||
window.blessing.extra = {
|
||||
currentUser: {
|
||||
...fixture,
|
||||
uid: 2,
|
||||
permission: UserPermission.Admin,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('target user is super administrator', async () => {
|
||||
fetch.get.mockResolvedValue(
|
||||
createPaginator([
|
||||
{ ...fixture, permission: UserPermission.SuperAdmin },
|
||||
]),
|
||||
)
|
||||
|
||||
const { getByTitle, getByText, queryByText, queryByTitle } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePassword'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.toggleVerification'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.deleteUser'))).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
expect(queryByTitle(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(
|
||||
queryByTitle(t('admin.toggleVerification')),
|
||||
).not.toBeInTheDocument()
|
||||
expect(getByText(t('admin.changePassword'))).toBeDisabled()
|
||||
expect(getByText(t('admin.deleteUser'))).toBeDisabled()
|
||||
})
|
||||
|
||||
it('target user is normal administrator', async () => {
|
||||
fetch.get.mockResolvedValue(
|
||||
createPaginator([{ ...fixture, permission: UserPermission.Admin }]),
|
||||
)
|
||||
|
||||
const { getByTitle, getByText, queryByText, queryByTitle } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePassword'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.toggleVerification'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.deleteUser'))).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
expect(queryByTitle(t('admin.changeEmail'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeNickName'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeScore'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(
|
||||
queryByTitle(t('admin.toggleVerification')),
|
||||
).not.toBeInTheDocument()
|
||||
expect(getByText(t('admin.changePassword'))).toBeDisabled()
|
||||
expect(getByText(t('admin.deleteUser'))).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('current user and target user are self', async () => {
|
||||
const user = {
|
||||
...fixture,
|
||||
permission: UserPermission.Admin,
|
||||
}
|
||||
window.blessing.extra = { currentUser: user }
|
||||
fetch.get.mockResolvedValue(createPaginator([user]))
|
||||
|
||||
const { getByTitle, getByText, queryByText, queryByTitle } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.changeEmail'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeNickName'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePassword'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changeScore'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('admin.toggleVerification'))).toBeInTheDocument()
|
||||
expect(queryByText(t('admin.deleteUser'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
expect(queryByTitle(t('admin.changeEmail'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeNickName'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changeScore'))).toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.changePermission'))).not.toBeInTheDocument()
|
||||
expect(queryByTitle(t('admin.toggleVerification'))).toBeInTheDocument()
|
||||
expect(getByText(t('admin.changePassword'))).toBeEnabled()
|
||||
expect(getByText(t('admin.deleteUser'))).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update email', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('empty value', async () => {
|
||||
const { getByText, getByDisplayValue, queryByText } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeEmail')))
|
||||
fireEvent.input(getByDisplayValue(fixture.email), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
expect(queryByText(t('auth.emptyEmail'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeEmail')))
|
||||
fireEvent.input(getByDisplayValue(fixture.email), {
|
||||
target: { value: 'd@e.f' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/email`, {
|
||||
email: 'd@e.f',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('d@e.f')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeEmail')))
|
||||
fireEvent.input(getByDisplayValue(fixture.email), {
|
||||
target: { value: 'd@e.f' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/email`, {
|
||||
email: 'd@e.f',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update nickname', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('empty value', async () => {
|
||||
const { getByText, getByDisplayValue, queryByText } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeNickName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.nickname), {
|
||||
target: { value: '' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
expect(queryByText(t('auth.emptyNickname'))).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.nickname)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeNickName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.nickname), {
|
||||
target: { value: 'kumiko' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/nickname`, {
|
||||
nickname: 'kumiko',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('kumiko')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeNickName')))
|
||||
fireEvent.input(getByDisplayValue(fixture.nickname), {
|
||||
target: { value: 'kumiko' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/nickname`, {
|
||||
nickname: 'kumiko',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.nickname)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update score', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeScore')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.score.toString())).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeScore')))
|
||||
fireEvent.input(getByDisplayValue(fixture.score.toString()), {
|
||||
target: { value: '999' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/score`, {
|
||||
score: 999,
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText('999')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByDisplayValue, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changeScore')))
|
||||
fireEvent.input(getByDisplayValue(fixture.score.toString()), {
|
||||
target: { value: '999' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/score`, {
|
||||
score: 999,
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.score.toString())).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update permission', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePermission')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(t('admin.normal'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePermission')))
|
||||
fireEvent.click(getByLabelText(t('admin.banned')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/permission`,
|
||||
{
|
||||
permission: UserPermission.Banned,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(t('admin.banned'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePermission')))
|
||||
fireEvent.click(getByLabelText(t('admin.banned')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/permission`,
|
||||
{
|
||||
permission: UserPermission.Banned,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(t('admin.normal'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('set as administrator', async () => {
|
||||
window.blessing.extra = {
|
||||
currentUser: {
|
||||
...fixture,
|
||||
uid: 2,
|
||||
permission: UserPermission.SuperAdmin,
|
||||
},
|
||||
}
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, getByLabelText, queryByText, queryByRole } = render(
|
||||
<UsersManagement />,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePermission')))
|
||||
fireEvent.click(getByLabelText(t('admin.admin')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/permission`,
|
||||
{
|
||||
permission: UserPermission.Admin,
|
||||
},
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(t('admin.admin'))).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggle verification', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.toggleVerification')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/verification`,
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(t('admin.unverified'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.toggleVerification')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/verification`,
|
||||
),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(t('admin.verified'))).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('update password', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePassword')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.score.toString())).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getByPlaceholderText,
|
||||
queryByText,
|
||||
queryByRole,
|
||||
} = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePassword')))
|
||||
fireEvent.input(getByPlaceholderText(t('adminchangePassword')), {
|
||||
target: { value: '123' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/password`, {
|
||||
password: '123',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getByPlaceholderText,
|
||||
queryByText,
|
||||
queryByRole,
|
||||
} = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.changePassword')))
|
||||
fireEvent.input(getByPlaceholderText(t('adminchangePassword')), {
|
||||
target: { value: '123' },
|
||||
})
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(`/admin/users/${fixture.uid}/password`, {
|
||||
password: '123',
|
||||
}),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('cancelled', async () => {
|
||||
const { getByText, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deleteUser')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('succeeded', async () => {
|
||||
fetch.del.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deleteUser')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.del).toBeCalledWith(`/admin/users/${fixture.uid}`),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(fixture.email)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
fetch.del.mockResolvedValue({ code: 1, message: 'failed' })
|
||||
|
||||
const { getByText, queryByText, queryByRole } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByText(t('admin.deleteUser')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.del).toBeCalledWith(`/admin/users/${fixture.uid}`),
|
||||
)
|
||||
expect(queryByText('failed')).toBeInTheDocument()
|
||||
expect(queryByRole('alert')).toHaveClass('alert-danger')
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('table mode', () => {
|
||||
beforeEach(() => {
|
||||
fetch.get.mockResolvedValue(createPaginator([fixture]))
|
||||
})
|
||||
|
||||
it('large screen', async () => {
|
||||
Object.assign(window, { innerWidth: 1024 })
|
||||
|
||||
const { queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
expect(queryByText(t('admin.operationsTitle'))).toBeInTheDocument()
|
||||
|
||||
Object.assign(window, { innerWidth: 500 })
|
||||
})
|
||||
|
||||
it('update email', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByTitle(t('admin.changeEmail')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('update nickname', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByTitle(t('admin.changeNickName')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.nickname)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('update score', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByTitle(t('admin.changeScore')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.score.toString())).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('update permission', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByTitle(t('admin.changePermission')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(t('admin.normal'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggle verification', async () => {
|
||||
fetch.put.mockResolvedValue({ code: 0, message: 'ok' })
|
||||
|
||||
const { getByTitle, queryByText, queryByRole } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByTitle(t('admin.toggleVerification')))
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetch.put).toBeCalledWith(
|
||||
`/admin/users/${fixture.uid}/verification`,
|
||||
),
|
||||
)
|
||||
expect(queryByText('ok')).toBeInTheDocument()
|
||||
expect(queryByRole('status')).toHaveClass('alert-success')
|
||||
expect(queryByText(t('admin.unverified'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('update password', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByText(t('admin.changePassword')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.put).not.toBeCalled()
|
||||
expect(queryByText(fixture.score.toString())).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('delete user', async () => {
|
||||
const { getByText, getByTitle, queryByText } = render(<UsersManagement />)
|
||||
|
||||
await waitFor(() => expect(fetch.get).toBeCalled())
|
||||
fireEvent.click(getByTitle('Table Mode'))
|
||||
fireEvent.click(getByText(t('admin.deleteUser')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
expect(fetch.del).not.toBeCalled()
|
||||
expect(queryByText(fixture.email)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -72,6 +72,7 @@
|
|||
- Fixed that button "See My Upload" existed when user isn't authenticated.
|
||||
- Fixed potential "Invalid Signature" issue.
|
||||
- Fixed that duplicated player name is not detected when updating player name in administration panel.
|
||||
- Fixed that normal administrator can set other user as administrator.
|
||||
|
||||
## Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
- 修复未登录的用户在浏览皮肤库时出现「我的上传」按钮问题
|
||||
- 修复可能的「Invalid Signature」问题
|
||||
- 修复在管理面板中修改角色名时不检测角色名是否重复的问题
|
||||
- 修复普通管理员可设置其他用户为管理员的问题
|
||||
|
||||
## 移除
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
{% extends 'admin.base' %}
|
||||
|
||||
{% block title %}{{ trans('general.user-manage') }}{% endblock %}
|
||||
|
||||
{% block before_foot %}
|
||||
{% set extra = {'currentUser': auth_user()} %}
|
||||
<script>
|
||||
blessing.extra = {{ extra|json_encode|raw }}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,19 @@ Route::prefix('closet')->middleware('auth:jwt,oauth')->group(function () {
|
|||
Route::prefix('admin')
|
||||
->middleware(['auth:jwt,oauth', 'role:admin'])
|
||||
->group(function () {
|
||||
Route::prefix('users')->group(function () {
|
||||
Route::get('', 'UsersManagementController@list')->name('list');
|
||||
Route::prefix('{user}')->group(function () {
|
||||
Route::put('email', 'UsersManagementController@email')->name('email');
|
||||
Route::put('verification', 'UsersManagementController@verification')->name('verification');
|
||||
Route::put('nickname', 'UsersManagementController@nickname')->name('nickname');
|
||||
Route::put('password', 'UsersManagementController@password')->name('password');
|
||||
Route::put('score', 'UsersManagementController@score')->name('score');
|
||||
Route::put('permission', 'UsersManagementController@permission')->name('permission');
|
||||
Route::delete('', 'UsersManagementController@delete')->name('delete');
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('players')->group(function () {
|
||||
Route::get('', 'PlayersManagementController@list');
|
||||
Route::put('{player}/name', 'PlayersManagementController@name');
|
||||
|
|
|
|||
|
|
@ -127,22 +127,30 @@ Route::prefix('admin')
|
|||
|
||||
Route::get('status', 'AdminController@status');
|
||||
|
||||
Route::prefix('users')->group(function () {
|
||||
Route::prefix('users')->name('users.')->group(function () {
|
||||
Route::view('', 'admin.users');
|
||||
Route::post('', 'AdminController@userAjaxHandler');
|
||||
Route::get('list', 'AdminController@getUserData');
|
||||
Route::get('list', 'UsersManagementController@list')->name('list');
|
||||
Route::prefix('{user}')->group(function () {
|
||||
Route::put('email', 'UsersManagementController@email')->name('email');
|
||||
Route::put('verification', 'UsersManagementController@verification')->name('verification');
|
||||
Route::put('nickname', 'UsersManagementController@nickname')->name('nickname');
|
||||
Route::put('password', 'UsersManagementController@password')->name('password');
|
||||
Route::put('score', 'UsersManagementController@score')->name('score');
|
||||
Route::put('permission', 'UsersManagementController@permission')->name('permission');
|
||||
Route::delete('', 'UsersManagementController@delete')->name('delete');
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('players')->name('players.')->group(function () {
|
||||
Route::view('', 'admin.players');
|
||||
Route::get('list', 'PlayersManagementController@list')->name('list');
|
||||
Route::view('', 'admin.players');
|
||||
Route::get('list', 'PlayersManagementController@list')->name('list');
|
||||
Route::prefix('{player}')->group(function () {
|
||||
Route::put('name', 'PlayersManagementController@name')->name('name');
|
||||
Route::put('owner', 'PlayersManagementController@owner')->name('owner');
|
||||
Route::put('textures', 'PlayersManagementController@texture')->name('texture');
|
||||
Route::delete('', 'PlayersManagementController@delete')->name('delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('closet')->group(function () {
|
||||
Route::post('{uid}', 'ClosetManagementController@add');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Models\Texture;
|
|||
use App\Models\User;
|
||||
use App\Services\Plugin;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminControllerTest extends TestCase
|
||||
{
|
||||
|
|
@ -57,215 +56,4 @@ class AdminControllerTest extends TestCase
|
|||
->assertSee('0.0.0');
|
||||
$filter->assertApplied('grid:admin.status');
|
||||
}
|
||||
|
||||
public function testUsers()
|
||||
{
|
||||
$this->get('/admin/users')->assertSee(trans('general.user-manage'));
|
||||
}
|
||||
|
||||
public function testGetUserData()
|
||||
{
|
||||
$this->getJson('/admin/users/list')
|
||||
->assertJsonStructure([
|
||||
'data' => [[
|
||||
'uid',
|
||||
'email',
|
||||
'nickname',
|
||||
'score',
|
||||
'permission',
|
||||
'register_at',
|
||||
'operations',
|
||||
'players_count',
|
||||
]],
|
||||
]);
|
||||
|
||||
$user = factory(User::class)->create();
|
||||
$this->getJson('/admin/users/list?uid='.$user->uid)
|
||||
->assertJson([
|
||||
'data' => [[
|
||||
'uid' => $user->uid,
|
||||
'email' => $user->email,
|
||||
'nickname' => $user->nickname,
|
||||
'score' => $user->score,
|
||||
'permission' => $user->permission,
|
||||
'players_count' => 0,
|
||||
]],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUserAjaxHandler()
|
||||
{
|
||||
// Operate on an not-existed user
|
||||
$this->postJson('/admin/users')
|
||||
->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.non-existent'),
|
||||
]);
|
||||
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// Operate without `action` field
|
||||
$this->postJson('/admin/users', ['uid' => $user->uid])
|
||||
->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.invalid'),
|
||||
]);
|
||||
|
||||
// An admin operating on a super admin should be forbidden
|
||||
$superAdmin = factory(User::class)->states('superAdmin')->create();
|
||||
$this->postJson('/admin/users', ['uid' => $superAdmin->uid])
|
||||
->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.no-permission'),
|
||||
]);
|
||||
|
||||
// Action is `email` but without `email` field
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'email']
|
||||
)->assertJsonValidationErrors(['email']);
|
||||
|
||||
// Action is `email` but with an invalid email address
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'email', 'email' => 'invalid']
|
||||
)->assertJsonValidationErrors(['email']);
|
||||
|
||||
// Using an existed email address
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'email', 'email' => $superAdmin->email]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.email.existed', ['email' => $superAdmin->email]),
|
||||
]);
|
||||
|
||||
// Set email successfully
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'email', 'email' => 'a@b.c']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.email.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'email' => 'a@b.c',
|
||||
]);
|
||||
|
||||
// Toggle verification
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'verification']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.verification.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'verified' => 0,
|
||||
]);
|
||||
|
||||
// Action is `nickname` but without `nickname` field
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'nickname']
|
||||
)->assertJsonValidationErrors(['nickname']);
|
||||
|
||||
// Set nickname successfully
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'nickname', 'nickname' => 'nickname']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.nickname.success', ['new' => 'nickname']),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'nickname' => 'nickname',
|
||||
]);
|
||||
|
||||
// Action is `password` but without `password` field
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'password']
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// Set a too short password
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'password', 'password' => '1']
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// Set a too long password
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'password', 'password' => Str::random(17)]
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// Set password successfully
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'password', 'password' => '12345678']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.password.success'),
|
||||
]);
|
||||
$user = User::find($user->uid);
|
||||
$this->assertTrue($user->verifyPassword('12345678'));
|
||||
|
||||
// Action is `score` but without `score` field
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'score']
|
||||
)->assertJsonValidationErrors(['score']);
|
||||
|
||||
// Action is `score` but with an not-an-integer value
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'score', 'score' => 'string']
|
||||
)->assertJsonValidationErrors(['score']);
|
||||
|
||||
// Set score successfully
|
||||
$this->postJson(
|
||||
'/admin/users',
|
||||
['uid' => $user->uid, 'action' => 'score', 'score' => 123]
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.score.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'score' => 123,
|
||||
]);
|
||||
|
||||
// Invalid permission value
|
||||
$this->postJson('/admin/users', [
|
||||
'uid' => $user->uid,
|
||||
'action' => 'permission',
|
||||
'permission' => -2,
|
||||
])->assertJsonValidationErrors(['permission']);
|
||||
$user = User::find($user->uid);
|
||||
$this->assertEquals(User::NORMAL, $user->permission);
|
||||
|
||||
// Update permission successfully
|
||||
$this->postJson('/admin/users', [
|
||||
'uid' => $user->uid,
|
||||
'action' => 'permission',
|
||||
'permission' => -1,
|
||||
])->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.permission'),
|
||||
]);
|
||||
$user = User::find($user->uid);
|
||||
$this->assertEquals(User::BANNED, $user->permission);
|
||||
|
||||
// Delete a user
|
||||
$this->postJson('/admin/users', ['uid' => $user->uid, 'action' => 'delete'])
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.delete.success'),
|
||||
]);
|
||||
$this->assertNull(User::find($user->uid));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
317
tests/HttpTest/ControllersTest/UsersManagementControllerTest.php
Normal file
317
tests/HttpTest/ControllersTest/UsersManagementControllerTest.php
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UsersManagementControllerTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->actingAs(factory(User::class)->states('admin')->create());
|
||||
}
|
||||
|
||||
public function testList()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
$this->getJson(route('admin.users.list'))
|
||||
->assertJson(['data' => [[/* admin is here */], $user->toArray()]]);
|
||||
}
|
||||
|
||||
public function testAccessControl()
|
||||
{
|
||||
// an administrator operating on other administrator should be forbidden
|
||||
$otherAdmin = factory(User::class)->states('admin')->create();
|
||||
|
||||
$this->putJson(route('admin.users.email', ['user' => $otherAdmin->uid]))
|
||||
->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.no-permission'),
|
||||
])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function testEmail()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// without `email` field
|
||||
$this->putJson(route('admin.users.email', ['user' => $user]))
|
||||
->assertJsonValidationErrors(['email']);
|
||||
|
||||
// with an invalid email address
|
||||
$this->putJson(
|
||||
route('admin.users.email', ['user' => $user]),
|
||||
['email' => 'invalid']
|
||||
)->assertJsonValidationErrors(['email']);
|
||||
|
||||
// use an existed email address
|
||||
$other = factory(User::class)->create();
|
||||
$this->putJson(
|
||||
route('admin.users.email', ['user' => $user]),
|
||||
['email' => $other->email]
|
||||
)->assertJsonValidationErrors(['email']);
|
||||
|
||||
// update successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.users.email', ['user' => $user]),
|
||||
['email' => 'a@b.c']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.email.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'email' => 'a@b.c',
|
||||
]);
|
||||
Event::assertDispatched('user.email.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
$this->assertEquals('a@b.c', $payload[1]);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.email.updated', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->fresh()->is($payload[0]));
|
||||
$this->assertEquals($user->email, $payload[1]->email);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testVerification()
|
||||
{
|
||||
Event::fake();
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
$this->putJson(
|
||||
route('admin.users.verification', ['user' => $user])
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.verification.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'verified' => false,
|
||||
]);
|
||||
Event::assertDispatched('user.verification.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertInstanceOf(User::class, $payload[0]);
|
||||
$this->assertEquals($user->uid, $payload[0]->uid);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.verification.updated', function ($eventName, $payload) {
|
||||
$this->assertFalse($payload[0]->verified);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testNickname()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// without `nickname` field
|
||||
$this->putJson(
|
||||
route('admin.users.nickname', ['user' => $user])
|
||||
)->assertJsonValidationErrors(['nickname']);
|
||||
|
||||
// update successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.users.nickname', ['user' => $user]),
|
||||
['nickname' => 'nickname']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.nickname.success', ['new' => 'nickname']),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'nickname' => 'nickname',
|
||||
]);
|
||||
Event::assertDispatched('user.nickname.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
$this->assertEquals('nickname', $payload[1]);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.nickname.updated', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->fresh()->is($payload[0]));
|
||||
$this->assertEquals($user->nickname, $payload[1]->nickname);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testPassword()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// without `password` field
|
||||
$this->putJson(
|
||||
route('admin.users.password', ['user' => $user])
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// too short password
|
||||
$this->putJson(
|
||||
route('admin.users.password', ['user' => $user]),
|
||||
['password' => '1']
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// too long password
|
||||
$this->putJson(
|
||||
route('admin.users.password', ['user' => $user]),
|
||||
['password' => Str::random(17)]
|
||||
)->assertJsonValidationErrors(['password']);
|
||||
|
||||
// update successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.users.password', ['user' => $user]),
|
||||
['password' => '12345678']
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.password.success'),
|
||||
]);
|
||||
$this->assertTrue($user->fresh()->verifyPassword('12345678'));
|
||||
Event::assertDispatched('user.password.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
$this->assertEquals('12345678', $payload[1]);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.password.updated', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->fresh()->is($payload[0]));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testScore()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// without `score` field
|
||||
$this->putJson(
|
||||
route('admin.users.score', ['user' => $user])
|
||||
)->assertJsonValidationErrors(['score']);
|
||||
|
||||
// with an non-integer value
|
||||
$this->putJson(
|
||||
route('admin.users.score', ['user' => $user]),
|
||||
['score' => 'string']
|
||||
)->assertJsonValidationErrors(['score']);
|
||||
|
||||
// update successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.users.score', ['user' => $user]),
|
||||
['score' => 123]
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.score.success'),
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'uid' => $user->uid,
|
||||
'score' => 123,
|
||||
]);
|
||||
Event::assertDispatched('user.score.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
$this->assertEquals(123, $payload[1]);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.score.updated', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->fresh()->is($payload[0]));
|
||||
$this->assertEquals($user->score, $payload[1]->score);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testPermission()
|
||||
{
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
// without `permission` field
|
||||
$this->putJson(route('admin.users.permission', ['user' => $user]))
|
||||
->assertJsonValidationErrors(['permission']);
|
||||
|
||||
// invalid permission value
|
||||
$this->putJson(
|
||||
route('admin.users.permission', ['user' => $user]),
|
||||
['permission' => -2]
|
||||
)->assertJsonValidationErrors(['permission']);
|
||||
|
||||
// non-super administrator can't set normal user as administrator
|
||||
$this->putJson(
|
||||
route('admin.users.permission', ['user' => $user]),
|
||||
['permission' => User::ADMIN]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.no-permission'),
|
||||
])->assertForbidden();
|
||||
|
||||
// administrator can't modify his/her permission
|
||||
$this->putJson(
|
||||
route('admin.users.permission', ['user' => auth()->user()]),
|
||||
['permission' => User::NORMAL]
|
||||
)->assertJson([
|
||||
'code' => 1,
|
||||
'message' => trans('admin.users.operations.no-permission'),
|
||||
])->assertForbidden();
|
||||
|
||||
// update successfully
|
||||
Event::fake();
|
||||
$this->putJson(
|
||||
route('admin.users.permission', ['user' => $user]),
|
||||
['permission' => User::BANNED]
|
||||
)->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.permission'),
|
||||
]);
|
||||
$this->assertEquals(User::BANNED, $user->fresh()->permission);
|
||||
Event::assertDispatched('user.permission.updating', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
$this->assertEquals(User::BANNED, $payload[1]);
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.permission.updated', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->fresh()->is($payload[0]));
|
||||
$this->assertEquals($user->permission, $payload[1]->permission);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function testDelete()
|
||||
{
|
||||
Event::fake();
|
||||
$user = factory(User::class)->create();
|
||||
|
||||
$this->deleteJson(route('admin.users.delete', ['user' => $user]))
|
||||
->assertJson([
|
||||
'code' => 0,
|
||||
'message' => trans('admin.users.operations.delete.success'),
|
||||
]);
|
||||
$this->assertNull(User::find($user->uid));
|
||||
Event::assertDispatched('user.deleting', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
|
||||
return true;
|
||||
});
|
||||
Event::assertDispatched('user.deleted', function ($eventName, $payload) use ($user) {
|
||||
$this->assertTrue($user->is($payload[0]));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user