Support limiting single player

This commit is contained in:
Pig Fang 2019-03-22 21:40:12 +08:00
parent 6793ccea30
commit aec3fe4a87
21 changed files with 414 additions and 5 deletions

View File

@ -508,11 +508,17 @@ class AdminController extends Controller
return json(trans('admin.players.delete.success'), 0);
} elseif ($action == 'name') {
$this->validate($request, [
$name = $this->validate($request, [
'name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
]);
])['name'];
$player->rename($request->input('name'));
$player->rename($name);
if (option('single_player', false)) {
$owner = $player->user;
$owner->nickname = $name;
$owner->save();
}
return json(trans('admin.players.name.success', ['player' => $player->name]), 0, ['name' => $player->name]);
} else {

View File

@ -65,6 +65,10 @@ class PlayerController extends Controller
{
$user = Auth::user();
if (option('single_player', false)) {
return json(trans('user.player.add.single'), 1);
}
$this->validate($request, [
'player_name' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
]);
@ -99,6 +103,10 @@ class PlayerController extends Controller
{
$playerName = $this->player->name;
if (option('single_player', false)) {
return json(trans('user.player.delete.single'), 1);
}
event(new PlayerWillBeDeleted($this->player));
$this->player->delete();
@ -133,6 +141,12 @@ class PlayerController extends Controller
$this->player->rename($newName);
if (option('single_player', false)) {
$user = auth()->user();
$user->nickname = $newName;
$user->save();
}
return json(trans('user.player.rename.success', ['old' => $oldName, 'new' => $newName]), 0);
}
@ -169,4 +183,34 @@ class PlayerController extends Controller
return json(trans('user.player.clear.success', ['name' => $this->player->name]), 0);
}
public function bind(Request $request)
{
$name = $this->validate($request, [
'player' => 'required|player_name|min:'.option('player_name_length_min').'|max:'.option('player_name_length_max'),
])['player'];
$user = Auth::user();
event(new CheckPlayerExists($name));
$player = Player::where('name', $name)->first();
if (! $player) {
event(new PlayerWillBeAdded($name));
$player = new Player;
$player->uid = $user->uid;
$player->name = $name;
$player->tid_skin = 0;
$player->save();
event(new PlayerWasAdded($player));
} elseif ($player->uid != $user->uid) {
return json(trans('user.player.rename.repeated'), 1);
}
$user->players()->where('name', '<>', $name)->delete();
$user->nickname = $name;
$user->save();
return json(trans('user.player.bind.success'), 0);
}
}

View File

@ -175,6 +175,10 @@ class UserController extends Controller
switch ($action) {
case 'nickname':
if (option('single_player', false)) {
return json(trans('user.profile.nickname.single'), 1);
}
$this->validate($request, [
'new_nickname' => 'required|no_special_chars|max:255',
]);

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Middleware;
use Closure;
class RequireBindPlayer
{
public function handle($request, Closure $next)
{
if (! option('single_player', false)) {
if ($request->is('user/player/bind')) {
return redirect('/user');
} else {
return $next($request);
}
}
// This allows us to fetch players list.
if ($request->is('user/player/list')) {
return $next($request);
}
$count = auth()->user()->players()->count();
if ($request->is('user/player/bind')) {
if ($count == 1) {
return redirect('/user');
} else {
return $next($request);
}
}
if ($count == 1) {
return $next($request);
} else {
return redirect('user/player/bind');
}
}
}

View File

@ -60,6 +60,27 @@ class User extends Authenticatable
return $this->belongsToMany(Texture::class, 'user_closet')->withPivot('item_name');
}
/**
* Retrieve the player name of first player.
*/
public function getPlayerNameAttribute()
{
$player = $this->players->first();
return $player ? $player->name : '';
}
/**
* Update the player name of first player.
*/
public function setPlayerNameAttribute($value)
{
$player = $this->players->first();
if ($player) {
$player->name = $value;
$player->save();
}
}
/**
* Check if given password is correct.
*

View File

@ -18,6 +18,11 @@ export default [
component: () => import('./views/user/Players.vue'),
el: '.content',
},
{
path: 'user/player/bind',
component: () => import('./views/user/Bind.vue'),
el: 'form',
},
{
path: 'user/profile',
component: () => import('./views/user/Profile.vue'),

View File

@ -0,0 +1,84 @@
<template>
<form>
<div v-if="players.length">
<p v-t="'user.bindExistedPlayer'" />
<div class="form-group">
<select v-model="selected" class="player-select">
<option v-for="name in players" :key="name">{{ name }}</option>
</select>
</div>
</div>
<div v-else>
<p v-t="'user.bindNewPlayer'" />
<div class="form-group has-feedback">
<input
v-model="selected"
class="form-control"
:placeholder="$t('general.player.player-name')"
>
<span class="glyphicon glyphicon-user form-control-feedback" />
</div>
</div>
<div v-show="message" class="callout callout-warning" v-text="message" />
<button v-if="pending" class="btn btn-primary btn-block btn-flat" disabled>
<i class="fa fa-spinner fa-spin" /> {{ $t('general.wait') }}
</button>
<button
v-else
class="btn btn-primary btn-block btn-flat"
@click.prevent="submit"
>
{{ $t('general.submit') }}
</button>
</form>
</template>
<script>
import { swal } from '../../js/notify'
export default {
name: 'BindPlayer',
data() {
return {
players: [],
selected: '',
pending: false,
message: '',
}
},
mounted() {
this.fetchPlayers()
},
methods: {
async fetchPlayers() {
const players = await this.$http.get('/user/player/list')
this.players = players.map(player => player.name)
;[this.selected] = this.players
},
async submit() {
this.pending = true
const { errno, msg } = await this.$http.post(
'/user/player/bind',
{ player: this.selected }
)
this.pending = false
if (errno === 0) {
await swal({ text: msg, type: 'success' })
window.location.href = `${blessing.base_url}/user`
} else {
this.message = msg
}
},
},
}
</script>
<style lang="stylus">
@import "../../stylus/auth.styl"
.player-select
width 100%
</style>

View File

@ -0,0 +1,42 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import Bind from '@/views/user/Bind.vue'
import { swal } from '@/js/notify'
jest.mock('@/js/notify')
test('list existed players', async () => {
Vue.prototype.$http.get
.mockResolvedValue([{ name: 'a' }, { name: 'b' }])
const wrapper = mount(Bind)
await wrapper.vm.$nextTick()
const options = wrapper.findAll('option')
expect(options).toHaveLength(2)
})
test('show input box', async () => {
Vue.prototype.$http.get.mockResolvedValue([])
const wrapper = mount(Bind)
await wrapper.vm.$nextTick()
const input = wrapper.find('input')
expect(input.exists()).toBeTrue()
})
test('submit', async () => {
Vue.prototype.$http.get.mockResolvedValue([])
Vue.prototype.$http.post
.mockResolvedValueOnce({ errno: 1, msg: 'fail' })
.mockResolvedValueOnce({ errno: 0, msg: 'ok' })
swal.mockResolvedValue({})
const wrapper = mount(Bind)
wrapper.find('input').setValue('abc')
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.find('.callout').text()).toBe('fail')
wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
expect(swal).toBeCalledWith({ text: 'ok', type: 'success' })
})

View File

@ -139,6 +139,8 @@ user:
typeToSearch: Type to search
useAs: Apply...
resetSelected: Clear selected
bindNewPlayer: You're required to create a player to go ahead. This player will be bound with your account.
bindExistedPlayer: You're required to select a player to go ahead. This player will be bound with your account. Other players will be deleted.
closet:
use-as:
button: Apply...
@ -328,6 +330,7 @@ general:
reset: Reset
skinlib: Skin Library
loading: Loading
wait: Please wait...
user:
email: Email
nickname: Nick Name

View File

@ -93,9 +93,11 @@ player:
add:
repeated: The player name is already registered.
lack-score: You don't have enough score to add a player.
single: You must own exactly ONE player so you can't add more.
success: Player :name was added successfully.
delete:
single: You must own exactly ONE player so you can't delete it.
success: Player :name was deleted successfully.
rename:
@ -108,6 +110,10 @@ player:
clear:
success: The textures of player :name was resetted successfully.
bind:
title: Bind Players
success: Bound successfully!
profile:
avatar:
title: Change Avatar?
@ -128,6 +134,7 @@ profile:
title: Change Nickname
empty: No nickname is set now.
rule: Whatever you like expect special characters
single: You're not allowed to update nickname, because we've bound your player with your account.
success: Nickname is successfully updated to :nickname
email:

View File

@ -139,6 +139,8 @@ user:
typeToSearch: 输入即搜索
useAs: 使用...
resetSelected: 重置已选材质
bindNewPlayer: 您现在需要创建一个角色,才能继续使用我们的服务。这个角色将与您的账号绑定。
bindExistedPlayer: 您现在需要选择一个角色,才能继续使用我们的服务。这个角色将与您的账号绑定。其它角色将被删除。
closet:
use-as:
button: 使用...
@ -322,6 +324,7 @@ general:
reset: 重置
skinlib: 皮肤库
loading: 正在加载
wait: 请稍等...
user:
email: 邮箱
nickname: 昵称

View File

@ -102,9 +102,11 @@ player:
add:
repeated: 该角色名已被占用
lack-score: 添加角色失败,积分不足
single: 您必须拥有且只有一个角色,因此您不能添加更多角色。
success: 成功添加了角色 :name
delete:
single: 您必须拥有且只有一个角色,因此您不能删除。
success: 角色 :name 已被删除
rename:
@ -117,6 +119,10 @@ player:
clear:
success: 角色 :name 的材质已被成功重置
bind:
title: 绑定角色
success: 绑定成功
profile:
avatar:
title: 更改头像?
@ -137,6 +143,7 @@ profile:
title: 更改昵称
empty: 当前未设置昵称,
rule: 可使用除一些特殊符号外的任意字符
single: 您不能更改昵称,因为我们已将角色与您的账号进行了绑定。
success: 昵称已成功设置为 :nickname
email:

View File

@ -12,7 +12,7 @@
<div class="login-box-body">
<p class="login-box-msg">@lang('auth.bind.message')</p>
<form method="post" id="login-form">
<form method="post" id="login-form">
@csrf
<div class="form-group has-feedback">
<input name="email" type="email" class="form-control" placeholder="@lang('auth.email')">

View File

@ -0,0 +1,16 @@
@extends('auth.master')
@section('title', trans('user.player.bind.title'))
@section('content')
<div class="login-box">
<div class="login-logo">
<a href="{{ url('/') }}">{{ option_localized('site_name') }}</a>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg">@lang('user.player.bind.title')</p>
<form></form>
</div>
</div>
@endsection

View File

@ -41,7 +41,10 @@ Route::group(['prefix' => 'auth'], function () {
/*
* User Center
*/
Route::group(['middleware' => ['web', 'auth'], 'prefix' => 'user'], function () {
Route::group([
'middleware' => ['web', 'auth', \App\Http\Middleware\RequireBindPlayer::class],
'prefix' => 'user'
], function () {
Route::any('', 'UserController@index');
Route::get('/score-info', 'UserController@scoreInfo');
Route::post('/sign', 'UserController@sign');
@ -64,6 +67,8 @@ Route::group(['middleware' => ['web', 'auth'], 'prefix' => 'user'], function ()
Route::post('/texture/clear', 'PlayerController@clearTexture');
Route::post('/rename', 'PlayerController@rename');
Route::post('/delete', 'PlayerController@delete');
Route::view('/bind', 'user.bind');
Route::post('/bind', 'PlayerController@bind');
});
// Closet

View File

@ -711,6 +711,16 @@ class AdminControllerTest extends BrowserKitTestCase
'name' => 'new_name',
]);
// Single player
option(['single_player' => true]);
$this->postJson('/admin/players', [
'pid' => $player->pid,
'action' => 'name',
'name' => 'abc',
])->seeJson(['errno' => 0]);
$player->refresh();
$this->assertEquals('abc', $player->user->nickname);
// Delete a player
$this->postJson('/admin/players', [
'pid' => $player->pid,

View File

@ -295,6 +295,7 @@ class AuthControllerTest extends TestCase
'errno' => 2,
'msg' => trans('user.player.add.repeated'),
]);
$this->assertNull(User::where('email', 'a@b.c')->first());
option(['register_with_player_name' => false]);

View File

@ -4,6 +4,7 @@ namespace Tests;
use DB;
use App\Models\User;
use App\Models\Player;
use App\Services\Facades\Option;
use Illuminate\Support\Facades\Schema;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -196,4 +197,22 @@ class MiddlewareTest extends TestCase
->get('/auth/login')
->assertRedirect('/user');
}
public function testRequireBindPlayer()
{
$user = factory(User::class)->create();
$this->actAs($user)->get('/user')->assertViewIs('user.index');
$this->get('/user/player/bind')->assertRedirect('/user');
option(['single_player' => true]);
$this->getJson('/user/player/list')->assertHeader('content-type', 'application/json');
$this->get('/user/player/bind')->assertViewIs('user.bind');
$this->get('/user')->assertRedirect('/user/player/bind');
factory(Player::class)->create(['uid' => $user->uid]);
$this->get('/user')->assertViewIs('user.index');
$this->get('/user/player/bind')->assertRedirect('/user');
}
}

View File

@ -3,6 +3,7 @@
namespace Tests;
use App\Models\User;
use App\Models\Player;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class UserTest extends TestCase
@ -26,4 +27,20 @@ class UserTest extends TestCase
$user->getNickName()
);
}
public function testGetPlayerNameAttribute()
{
$user = factory(User::class)->create();
$player = factory(Player::class)->create(['uid' => $user->uid]);
$this->assertEquals($player->name, $user->player_name);
}
public function testSetPlayerNameAttribute()
{
$user = factory(User::class)->create();
$player = factory(Player::class)->create(['uid' => $user->uid]);
$user->player_name = 'a';
$player->refresh();
$this->assertEquals('a', $player->name);
}
}

View File

@ -2,6 +2,7 @@
namespace Tests;
use Event;
use App\Events;
use App\Models\User;
use App\Models\Player;
@ -108,6 +109,14 @@ class PlayerControllerTest extends TestCase
'errno' => 6,
'msg' => trans('user.player.add.repeated'),
]);
// Single player
option(['single_player' => true]);
$this->postJson('/user/player/add', ['player_name' => 'abc'])
->assertJson([
'errno' => 1,
'msg' => trans('user.player.add.single'),
]);
}
public function testDelete()
@ -143,6 +152,17 @@ class PlayerControllerTest extends TestCase
$user->score,
User::find($user->uid)->score
);
// Single player
option(['single_player' => true]);
$player = factory(Player::class)->create(['uid' => $user->uid]);
$this->actingAs($user)
->postJson('/user/player/delete', ['pid' => $player->pid])
->assertJson([
'errno' => 1,
'msg' => trans('user.player.delete.single'),
]);
$this->assertNotNull(Player::find($player->pid));
}
public function testShow()
@ -210,6 +230,14 @@ class PlayerControllerTest extends TestCase
['old' => $player->name, 'new' => 'new_name']
),
]);
// Single player
option(['single_player' => true]);
$this->postJson('/user/player/rename', [
'pid' => $player->pid,
'new_player_name' => 'abc',
])->assertJson(['errno' => 0]);
$this->assertEquals('abc', $player->user->nickname);
}
public function testSetTexture()
@ -284,4 +312,44 @@ class PlayerControllerTest extends TestCase
$this->assertEquals(0, Player::find($player->pid)->tid_skin);
$this->assertEquals(0, Player::find($player->pid)->tid_cape);
}
public function testBind()
{
Event::fake();
option(['single_player' => true]);
$user = factory(User::class)->create();
$this->actAs($user)->postJson('/user/player/bind')
->assertJson([
'errno' => 1,
'msg' => trans('validation.required', ['attribute' => 'player']),
]);
$this->postJson('/user/player/bind', ['player' => 'abc'])
->assertJson([
'errno' => 0,
'msg' => trans('user.player.bind.success')
]);
Event::assertDispatched(Events\CheckPlayerExists::class);
Event::assertDispatched(Events\PlayerWillBeAdded::class);
Event::assertDispatched(Events\PlayerWasAdded::class);
$player = Player::where('name', 'abc')->first();
$this->assertNotNull($player);
$this->assertEquals($user->uid, $player->uid);
$this->assertEquals('abc', $player->name);
$user->refresh();
$this->assertEquals('abc', $user->nickname);
$player2 = factory(Player::class)->create();
$player3 = factory(Player::class)->create(['uid' => $user->uid]);
$this->postJson('/user/player/bind', ['player' => $player2->name])
->assertJson([
'errno' => 1,
'msg' => trans('user.player.rename.repeated')
]);
$this->postJson('/user/player/bind', ['player' => $player->name])
->assertJson(['errno' => 0]);
$this->assertNull(Player::where('name', $player3->name)->first());
}
}

View File

@ -243,6 +243,13 @@ class UserControllerTest extends TestCase
'msg' => trans('validation.max.string', ['attribute' => 'new nickname', 'max' => 255]),
]);
// Single player
option(['single_player' => true]);
factory(\App\Models\Player::class)->create(['uid' => $user->uid]);
$this->postJson('/user/profile', ['action' => 'nickname'])
->assertJson(['errno' => 1, 'msg' => trans('user.profile.nickname.single')]);
option(['single_player' => false]);
// Change nickname successfully
$this->expectsEvents(Events\UserProfileUpdated::class);
$this->postJson('/user/profile', [