mirror of
https://github.com/zoe-may/TDoG-Skin.git
synced 2024-11-24 05:42:19 +08:00
首次提交
This commit is contained in:
parent
42f5f087b4
commit
9b2ceeb224
48
.env
Normal file
48
.env
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
APP_DEBUG=false
|
||||||
|
APP_ENV=production
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=skin_tdogmc_top
|
||||||
|
DB_USERNAME=skin_tdogmc_top
|
||||||
|
DB_PASSWORD=Lcm18876588595
|
||||||
|
DB_PREFIX=
|
||||||
|
|
||||||
|
# Hash Algorithm for Passwords
|
||||||
|
#
|
||||||
|
# Available values:
|
||||||
|
# - BCRYPT, ARGON2I, PHP_PASSWORD_HASH
|
||||||
|
# - MD5, SALTED2MD5
|
||||||
|
# - SHA256, SALTED2SHA256
|
||||||
|
# - SHA512, SALTED2SHA512
|
||||||
|
#
|
||||||
|
# New sites are *highly* recommended to use BCRYPT.
|
||||||
|
#
|
||||||
|
PWD_METHOD=BCRYPT
|
||||||
|
APP_KEY=base64:J3pS/AID0gUU1u6GHFFOSBtFMOEyx/EFIKc77A0ZGMc=
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=smtp.163.com
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=tdogmc@163.com
|
||||||
|
MAIL_PASSWORD=EMJCETKJALZNMPBY
|
||||||
|
MAIL_ENCRYPTION=ssl
|
||||||
|
MAIL_FROM_ADDRESS=tdogmc@163.com
|
||||||
|
MAIL_FROM_NAME=神之愿服务器
|
||||||
|
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
SESSION_DRIVER=file
|
||||||
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
PLUGINS_DIR=null
|
||||||
|
PLUGINS_URL=null
|
||||||
|
PLUGINS_REGISTRY=https://d2jw1l0ullrzt6.cloudfront.net/registry_{lang}.json
|
||||||
|
|
||||||
|
JWT_SECRET=Jfz3FrSndLFfOE0AaT7UBsvEaufHBHYUAcjeO3eCegaMykRR6MuS5pb0lyDnsyzs
|
45
.env.example
Normal file
45
.env.example
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
APP_DEBUG=false
|
||||||
|
APP_ENV=production
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=blessingskin
|
||||||
|
DB_USERNAME=username
|
||||||
|
DB_PASSWORD=secret
|
||||||
|
DB_PREFIX=
|
||||||
|
|
||||||
|
# Hash Algorithm for Passwords
|
||||||
|
#
|
||||||
|
# Available values:
|
||||||
|
# - BCRYPT, ARGON2I, PHP_PASSWORD_HASH
|
||||||
|
# - MD5, SALTED2MD5
|
||||||
|
# - SHA256, SALTED2SHA256
|
||||||
|
# - SHA512, SALTED2SHA512
|
||||||
|
#
|
||||||
|
# New sites are *highly* recommended to use BCRYPT.
|
||||||
|
#
|
||||||
|
PWD_METHOD=BCRYPT
|
||||||
|
APP_KEY=
|
||||||
|
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_ENCRYPTION=
|
||||||
|
MAIL_FROM_ADDRESS=
|
||||||
|
MAIL_FROM_NAME=
|
||||||
|
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
SESSION_DRIVER=file
|
||||||
|
QUEUE_CONNECTION=sync
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
PLUGINS_DIR=null
|
||||||
|
PLUGINS_URL=null
|
1
.htaccess
Executable file
1
.htaccess
Executable file
@ -0,0 +1 @@
|
|||||||
|
|
9
404.html
Executable file
9
404.html
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
<html>
|
||||||
|
<head><title>404 Not Found</title></head>
|
||||||
|
<body>
|
||||||
|
<center><h1>404 Not Found</h1></center>
|
||||||
|
<hr><center>nginx</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016-present The Blessing Skin Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
52
app/Console/Commands/BsInstallCommand.php
Executable file
52
app/Console/Commands/BsInstallCommand.php
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class BsInstallCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bs:install {email} {password} {nickname}';
|
||||||
|
|
||||||
|
protected $description = 'Execute installation and create a super administrator.';
|
||||||
|
|
||||||
|
public function handle(Filesystem $filesystem)
|
||||||
|
{
|
||||||
|
if ($filesystem->exists(storage_path('install.lock'))) {
|
||||||
|
$this->info('You have installed Blessing Skin Server. Nothing to do.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->call('migrate', ['--force' => true]);
|
||||||
|
if (!$this->getLaravel()->runningUnitTests()) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
$this->call('key:generate');
|
||||||
|
$this->call('passport:keys', ['--no-interaction' => true]);
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
option(['site_url' => url('/')]);
|
||||||
|
|
||||||
|
$admin = new User();
|
||||||
|
$admin->email = $this->argument('email');
|
||||||
|
$admin->nickname = $this->argument('nickname');
|
||||||
|
$admin->score = option('user_initial_score');
|
||||||
|
$admin->avatar = 0;
|
||||||
|
$admin->password = app('cipher')->hash($this->argument('password'), config('secure.salt'));
|
||||||
|
$admin->ip = '127.0.0.1';
|
||||||
|
$admin->permission = User::SUPER_ADMIN;
|
||||||
|
$admin->register_at = Carbon::now();
|
||||||
|
$admin->last_sign_at = Carbon::now()->subDay();
|
||||||
|
$admin->verified = true;
|
||||||
|
$admin->save();
|
||||||
|
|
||||||
|
$filesystem->put(storage_path('install.lock'), '');
|
||||||
|
|
||||||
|
$this->info('Installation completed!');
|
||||||
|
$this->info('We recommend to modify your "Site URL" option if incorrect.');
|
||||||
|
}
|
||||||
|
}
|
28
app/Console/Commands/OptionsCacheCommand.php
Executable file
28
app/Console/Commands/OptionsCacheCommand.php
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\Option;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
|
class OptionsCacheCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'options:cache';
|
||||||
|
|
||||||
|
protected $description = 'Cache Blessing Skin options';
|
||||||
|
|
||||||
|
public function handle(Filesystem $filesystem, Application $app)
|
||||||
|
{
|
||||||
|
$path = storage_path('options.php');
|
||||||
|
$filesystem->delete($path);
|
||||||
|
$app->forgetInstance(Option::class);
|
||||||
|
|
||||||
|
$content = var_export(resolve(Option::class)->all(), true);
|
||||||
|
$notice = '// This is auto-generated. DO NOT edit manually.'.PHP_EOL;
|
||||||
|
$content = '<?php'.PHP_EOL.$notice.'return '.$content.';';
|
||||||
|
$filesystem->put($path, $content);
|
||||||
|
$this->info('Options cached successfully.');
|
||||||
|
}
|
||||||
|
}
|
25
app/Console/Commands/PluginDisableCommand.php
Executable file
25
app/Console/Commands/PluginDisableCommand.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class PluginDisableCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'plugin:disable {name}';
|
||||||
|
|
||||||
|
protected $description = 'Disable a plugin';
|
||||||
|
|
||||||
|
public function handle(PluginManager $plugins)
|
||||||
|
{
|
||||||
|
$plugin = $plugins->get($this->argument('name'));
|
||||||
|
if ($plugin) {
|
||||||
|
$plugins->disable($this->argument('name'));
|
||||||
|
$title = trans($plugin->title);
|
||||||
|
$this->info(trans('admin.plugins.operations.disabled', ['plugin' => $title]));
|
||||||
|
} else {
|
||||||
|
$this->warn(trans('admin.plugins.operations.not-found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
app/Console/Commands/PluginEnableCommand.php
Executable file
26
app/Console/Commands/PluginEnableCommand.php
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class PluginEnableCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'plugin:enable {name}';
|
||||||
|
|
||||||
|
protected $description = 'Enable a plugin';
|
||||||
|
|
||||||
|
public function handle(PluginManager $plugins)
|
||||||
|
{
|
||||||
|
$name = $this->argument('name');
|
||||||
|
$result = $plugins->enable($name);
|
||||||
|
if ($result === true) {
|
||||||
|
$plugin = $plugins->get($name);
|
||||||
|
$title = trans($plugin->title);
|
||||||
|
$this->info(trans('admin.plugins.operations.enabled', ['plugin' => $title]));
|
||||||
|
} elseif ($result === false) {
|
||||||
|
$this->warn(trans('admin.plugins.operations.not-found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
app/Console/Commands/SaltRandomCommand.php
Executable file
44
app/Console/Commands/SaltRandomCommand.php
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SaltRandomCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'salt:random {--show : Display the salt instead of modifying files}';
|
||||||
|
|
||||||
|
protected $description = 'Set the application salt';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$salt = $this->generateRandomSalt();
|
||||||
|
|
||||||
|
if ($this->option('show')) {
|
||||||
|
return $this->line('<comment>'.$salt.'</comment>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we will replace the application salt in the environment file so it is
|
||||||
|
// automatically setup for this developer. This salt gets generated using a
|
||||||
|
// secure random byte generator and is later base64 encoded for storage.
|
||||||
|
$this->setKeyInEnvironmentFile($salt);
|
||||||
|
|
||||||
|
$this->laravel['config']['secure.salt'] = $salt;
|
||||||
|
|
||||||
|
$this->info("Application salt [$salt] set successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setKeyInEnvironmentFile(string $salt)
|
||||||
|
{
|
||||||
|
file_put_contents($this->laravel->environmentFilePath(), str_replace(
|
||||||
|
'SALT = '.$this->laravel['config']['secure.salt'],
|
||||||
|
'SALT = '.$salt,
|
||||||
|
file_get_contents($this->laravel->environmentFilePath())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateRandomSalt(): string
|
||||||
|
{
|
||||||
|
return bin2hex(resolve(\Illuminate\Contracts\Encryption\Encrypter::class)->generateKey('AES-128-CBC'));
|
||||||
|
}
|
||||||
|
}
|
49
app/Console/Commands/UpdateCommand.php
Executable file
49
app/Console/Commands/UpdateCommand.php
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Composer\Semver\Comparator;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Contracts\Console\Kernel as Artisan;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class UpdateCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'update';
|
||||||
|
|
||||||
|
protected $description = 'Execute update.';
|
||||||
|
|
||||||
|
public function handle(Artisan $artisan, Filesystem $filesystem)
|
||||||
|
{
|
||||||
|
$this->procedures()->each(function ($procedure, $version) {
|
||||||
|
if (Comparator::lessThan(option('version'), $version)) {
|
||||||
|
$procedure();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
option(['version' => config('app.version')]);
|
||||||
|
$artisan->call('migrate', ['--force' => true]);
|
||||||
|
$artisan->call('view:clear');
|
||||||
|
$filesystem->put(storage_path('install.lock'), '');
|
||||||
|
Cache::flush();
|
||||||
|
|
||||||
|
$this->info(trans('setup.updates.success.title'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
protected function procedures()
|
||||||
|
{
|
||||||
|
return collect([
|
||||||
|
// this is just for testing
|
||||||
|
'0.0.1' => fn () => event('__0.0.1'),
|
||||||
|
'5.0.0' => function () {
|
||||||
|
if (option('home_pic_url') === './app/bg.jpg') {
|
||||||
|
option(['home_pic_url' => './app/bg.webp']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
18
app/Console/Kernel.php
Executable file
18
app/Console/Kernel.php
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
protected $commands = [
|
||||||
|
\Laravel\Passport\Console\KeysCommand::class,
|
||||||
|
Commands\BsInstallCommand::class,
|
||||||
|
Commands\OptionsCacheCommand::class,
|
||||||
|
Commands\PluginDisableCommand::class,
|
||||||
|
Commands\PluginEnableCommand::class,
|
||||||
|
Commands\SaltRandomCommand::class,
|
||||||
|
Commands\UpdateCommand::class,
|
||||||
|
];
|
||||||
|
}
|
14
app/Events/ConfigureAdminMenu.php
Executable file
14
app/Events/ConfigureAdminMenu.php
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class ConfigureAdminMenu extends Event
|
||||||
|
{
|
||||||
|
public $menu;
|
||||||
|
|
||||||
|
public function __construct(array &$menu)
|
||||||
|
{
|
||||||
|
// Pass array by reference
|
||||||
|
$this->menu = &$menu;
|
||||||
|
}
|
||||||
|
}
|
14
app/Events/ConfigureExploreMenu.php
Executable file
14
app/Events/ConfigureExploreMenu.php
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class ConfigureExploreMenu extends Event
|
||||||
|
{
|
||||||
|
public $menu;
|
||||||
|
|
||||||
|
public function __construct(array &$menu)
|
||||||
|
{
|
||||||
|
// Pass array by reference
|
||||||
|
$this->menu = &$menu;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/ConfigureRoutes.php
Executable file
15
app/Events/ConfigureRoutes.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Routing\Router;
|
||||||
|
|
||||||
|
class ConfigureRoutes extends Event
|
||||||
|
{
|
||||||
|
public $router;
|
||||||
|
|
||||||
|
public function __construct(Router $router)
|
||||||
|
{
|
||||||
|
$this->router = $router;
|
||||||
|
}
|
||||||
|
}
|
13
app/Events/ConfigureUserMenu.php
Executable file
13
app/Events/ConfigureUserMenu.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class ConfigureUserMenu extends Event
|
||||||
|
{
|
||||||
|
public $menu;
|
||||||
|
|
||||||
|
public function __construct(array &$menu)
|
||||||
|
{
|
||||||
|
$this->menu = &$menu;
|
||||||
|
}
|
||||||
|
}
|
7
app/Events/Event.php
Executable file
7
app/Events/Event.php
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
abstract class Event
|
||||||
|
{
|
||||||
|
}
|
15
app/Events/PlayerProfileUpdated.php
Executable file
15
app/Events/PlayerProfileUpdated.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
|
||||||
|
class PlayerProfileUpdated extends Event
|
||||||
|
{
|
||||||
|
public $player;
|
||||||
|
|
||||||
|
public function __construct(Player $player)
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PlayerRetrieved.php
Executable file
15
app/Events/PlayerRetrieved.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
|
||||||
|
class PlayerRetrieved extends Event
|
||||||
|
{
|
||||||
|
public $player;
|
||||||
|
|
||||||
|
public function __construct(Player $player)
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PlayerWasAdded.php
Executable file
15
app/Events/PlayerWasAdded.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
|
||||||
|
class PlayerWasAdded extends Event
|
||||||
|
{
|
||||||
|
public $player;
|
||||||
|
|
||||||
|
public function __construct(Player $player)
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
}
|
||||||
|
}
|
13
app/Events/PlayerWasDeleted.php
Executable file
13
app/Events/PlayerWasDeleted.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class PlayerWasDeleted extends Event
|
||||||
|
{
|
||||||
|
public $playerName;
|
||||||
|
|
||||||
|
public function __construct($playerName)
|
||||||
|
{
|
||||||
|
$this->playerName = $playerName;
|
||||||
|
}
|
||||||
|
}
|
13
app/Events/PlayerWillBeAdded.php
Executable file
13
app/Events/PlayerWillBeAdded.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class PlayerWillBeAdded extends Event
|
||||||
|
{
|
||||||
|
public $playerName;
|
||||||
|
|
||||||
|
public function __construct($playerName)
|
||||||
|
{
|
||||||
|
$this->playerName = $playerName;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PlayerWillBeDeleted.php
Executable file
15
app/Events/PlayerWillBeDeleted.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
|
||||||
|
class PlayerWillBeDeleted extends Event
|
||||||
|
{
|
||||||
|
public $player;
|
||||||
|
|
||||||
|
public function __construct(Player $player)
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PluginBootFailed.php
Executable file
15
app/Events/PluginBootFailed.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
|
||||||
|
class PluginBootFailed extends Event
|
||||||
|
{
|
||||||
|
public Plugin $plugin;
|
||||||
|
|
||||||
|
public function __construct(Plugin $plugin)
|
||||||
|
{
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PluginWasDeleted.php
Executable file
15
app/Events/PluginWasDeleted.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
|
||||||
|
class PluginWasDeleted extends Event
|
||||||
|
{
|
||||||
|
public $plugin;
|
||||||
|
|
||||||
|
public function __construct(Plugin $plugin)
|
||||||
|
{
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PluginWasDisabled.php
Executable file
15
app/Events/PluginWasDisabled.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
|
||||||
|
class PluginWasDisabled extends Event
|
||||||
|
{
|
||||||
|
public $plugin;
|
||||||
|
|
||||||
|
public function __construct(Plugin $plugin)
|
||||||
|
{
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/PluginWasEnabled.php
Executable file
15
app/Events/PluginWasEnabled.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
|
||||||
|
class PluginWasEnabled extends Event
|
||||||
|
{
|
||||||
|
public $plugin;
|
||||||
|
|
||||||
|
public function __construct(Plugin $plugin)
|
||||||
|
{
|
||||||
|
$this->plugin = $plugin;
|
||||||
|
}
|
||||||
|
}
|
13
app/Events/RenderingBadges.php
Executable file
13
app/Events/RenderingBadges.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class RenderingBadges extends Event
|
||||||
|
{
|
||||||
|
public $badges;
|
||||||
|
|
||||||
|
public function __construct(array &$badges)
|
||||||
|
{
|
||||||
|
$this->badges = &$badges;
|
||||||
|
}
|
||||||
|
}
|
18
app/Events/RenderingFooter.php
Executable file
18
app/Events/RenderingFooter.php
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class RenderingFooter extends Event
|
||||||
|
{
|
||||||
|
public $contents;
|
||||||
|
|
||||||
|
public function __construct(array &$contents)
|
||||||
|
{
|
||||||
|
$this->contents = &$contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addContent(string $content)
|
||||||
|
{
|
||||||
|
$this->contents[] = $content;
|
||||||
|
}
|
||||||
|
}
|
18
app/Events/RenderingHeader.php
Executable file
18
app/Events/RenderingHeader.php
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class RenderingHeader extends Event
|
||||||
|
{
|
||||||
|
public $contents;
|
||||||
|
|
||||||
|
public function __construct(array &$contents)
|
||||||
|
{
|
||||||
|
$this->contents = &$contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addContent(string $content)
|
||||||
|
{
|
||||||
|
$this->contents[] = $content;
|
||||||
|
}
|
||||||
|
}
|
13
app/Events/TextureDeleting.php
Executable file
13
app/Events/TextureDeleting.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class TextureDeleting extends Event
|
||||||
|
{
|
||||||
|
public $texture;
|
||||||
|
|
||||||
|
public function __construct(\App\Models\Texture $texture)
|
||||||
|
{
|
||||||
|
$this->texture = $texture;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/UserAuthenticated.php
Executable file
15
app/Events/UserAuthenticated.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserAuthenticated extends Event
|
||||||
|
{
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
public function __construct(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/UserLoggedIn.php
Executable file
15
app/Events/UserLoggedIn.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserLoggedIn extends Event
|
||||||
|
{
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
public function __construct(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
17
app/Events/UserProfileUpdated.php
Executable file
17
app/Events/UserProfileUpdated.php
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserProfileUpdated extends Event
|
||||||
|
{
|
||||||
|
public $type;
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
public function __construct($type, User $user)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
15
app/Events/UserRegistered.php
Executable file
15
app/Events/UserRegistered.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserRegistered extends Event
|
||||||
|
{
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
public function __construct(User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
16
app/Events/UserTryToLogin.php
Executable file
16
app/Events/UserTryToLogin.php
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class UserTryToLogin extends Event
|
||||||
|
{
|
||||||
|
public $identification;
|
||||||
|
|
||||||
|
public $authType;
|
||||||
|
|
||||||
|
public function __construct($identification, $authType)
|
||||||
|
{
|
||||||
|
$this->identification = $identification;
|
||||||
|
$this->authType = $authType;
|
||||||
|
}
|
||||||
|
}
|
64
app/Exceptions/Handler.php
Executable file
64
app/Exceptions/Handler.php
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Laravel\Passport\Exceptions\MissingScopeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class Handler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A list of the exception types that should not be reported.
|
||||||
|
*/
|
||||||
|
protected $dontReport = [
|
||||||
|
\Illuminate\Auth\AuthenticationException::class,
|
||||||
|
\Illuminate\Auth\Access\AuthorizationException::class,
|
||||||
|
\Symfony\Component\HttpKernel\Exception\HttpException::class,
|
||||||
|
\Illuminate\Validation\ValidationException::class,
|
||||||
|
\Illuminate\Session\TokenMismatchException::class,
|
||||||
|
ModelNotFoundException::class,
|
||||||
|
PrettyPageException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function render($request, Throwable $exception)
|
||||||
|
{
|
||||||
|
if ($exception instanceof ModelNotFoundException) {
|
||||||
|
$model = $exception->getModel();
|
||||||
|
if (Str::endsWith($model, 'Texture')) {
|
||||||
|
$exception = new ModelNotFoundException(trans('skinlib.non-existent'));
|
||||||
|
}
|
||||||
|
} elseif ($exception instanceof MissingScopeException) {
|
||||||
|
return json($exception->getMessage(), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::render($request, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function convertExceptionToArray(Throwable $e)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'exception' => true,
|
||||||
|
'trace' => collect($e->getTrace())
|
||||||
|
->map(fn ($trace) => Arr::only($trace, ['file', 'line']))
|
||||||
|
->filter(fn ($trace) => Arr::has($trace, 'file'))
|
||||||
|
->map(function ($trace) {
|
||||||
|
$trace['file'] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $trace['file']);
|
||||||
|
|
||||||
|
return $trace;
|
||||||
|
})
|
||||||
|
->filter(function ($trace) {
|
||||||
|
// @codeCoverageIgnoreStart
|
||||||
|
$isFromPlugins = !app()->runningUnitTests() &&
|
||||||
|
Str::contains($trace['file'], resolve('plugins')->getPluginsDirs()->all());
|
||||||
|
// @codeCoverageIgnoreEnd
|
||||||
|
return Str::startsWith($trace['file'], 'app') || $isFromPlugins;
|
||||||
|
})
|
||||||
|
->values(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
11
app/Exceptions/PrettyPageException.php
Executable file
11
app/Exceptions/PrettyPageException.php
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class PrettyPageException extends \Exception
|
||||||
|
{
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return response()->view('errors.pretty', ['code' => $this->code, 'message' => $this->message]);
|
||||||
|
}
|
||||||
|
}
|
149
app/Http/Controllers/AdminController.php
Executable file
149
app/Http/Controllers/AdminController.php
Executable file
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class AdminController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Filter $filter)
|
||||||
|
{
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-6', 'md-6'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'admin.widgets.dashboard.usage',
|
||||||
|
'admin.widgets.dashboard.notification',
|
||||||
|
],
|
||||||
|
['admin.widgets.dashboard.chart'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:admin.index', $grid);
|
||||||
|
|
||||||
|
return view('admin.index', [
|
||||||
|
'grid' => $grid,
|
||||||
|
'sum' => [
|
||||||
|
'users' => User::count(),
|
||||||
|
'players' => Player::count(),
|
||||||
|
'textures' => Texture::count(),
|
||||||
|
'storage' => Texture::select('size')->sum('size'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chartData()
|
||||||
|
{
|
||||||
|
$xAxis = Collection::times(31, fn ($i) => Carbon::today()->subDays(31 - $i)->isoFormat('l'));
|
||||||
|
|
||||||
|
$oneMonthAgo = Carbon::today()->subMonth();
|
||||||
|
|
||||||
|
$grouping = fn ($field) => fn ($item) => Carbon::parse($item->$field)->isoFormat('l');
|
||||||
|
$mapping = fn ($item) => count($item);
|
||||||
|
$aligning = fn ($data) => fn ($day) => ($data->get($day) ?? 0);
|
||||||
|
|
||||||
|
/** @var Collection */
|
||||||
|
$userRegistration = User::where('register_at', '>=', $oneMonthAgo)
|
||||||
|
->select('register_at')
|
||||||
|
->get()
|
||||||
|
->groupBy($grouping('register_at'))
|
||||||
|
->map($mapping);
|
||||||
|
|
||||||
|
/** @var Collection */
|
||||||
|
$textureUploads = Texture::where('upload_at', '>=', $oneMonthAgo)
|
||||||
|
->select('upload_at')
|
||||||
|
->get()
|
||||||
|
->groupBy($grouping('upload_at'))
|
||||||
|
->map($mapping);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'labels' => [
|
||||||
|
trans('admin.index.user-registration'),
|
||||||
|
trans('admin.index.texture-uploads'),
|
||||||
|
],
|
||||||
|
'xAxis' => $xAxis,
|
||||||
|
'data' => [
|
||||||
|
$xAxis->map($aligning($userRegistration)),
|
||||||
|
$xAxis->map($aligning($textureUploads)),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(
|
||||||
|
Request $request,
|
||||||
|
PluginManager $plugins,
|
||||||
|
Filesystem $filesystem,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$db = config('database.connections.'.config('database.default'));
|
||||||
|
$dbType = Arr::get([
|
||||||
|
'mysql' => 'MySQL/MariaDB',
|
||||||
|
'sqlite' => 'SQLite',
|
||||||
|
'pgsql' => 'PostgreSQL',
|
||||||
|
], config('database.default'), '');
|
||||||
|
|
||||||
|
$enabledPlugins = $plugins->getEnabledPlugins()->map(fn ($plugin) => [
|
||||||
|
'title' => trans($plugin->title), 'version' => $plugin->version,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($filesystem->exists(base_path('.git'))) {
|
||||||
|
$process = new \Symfony\Component\Process\Process(
|
||||||
|
['git', 'log', '--pretty=%H', '-1']
|
||||||
|
);
|
||||||
|
$process->run();
|
||||||
|
$commit = $process->isSuccessful() ? trim($process->getOutput()) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-6', 'md-6'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
['admin.widgets.status.info'],
|
||||||
|
['admin.widgets.status.plugins'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:admin.status', $grid);
|
||||||
|
|
||||||
|
return view('admin.status')
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('detail', [
|
||||||
|
'bs' => [
|
||||||
|
'version' => config('app.version'),
|
||||||
|
'env' => config('app.env'),
|
||||||
|
'debug' => config('app.debug') ? trans('general.yes') : trans('general.no'),
|
||||||
|
'commit' => Str::limit($commit ?? '', 16, ''),
|
||||||
|
'laravel' => app()->version(),
|
||||||
|
],
|
||||||
|
'server' => [
|
||||||
|
'php' => PHP_VERSION,
|
||||||
|
'web' => $request->server('SERVER_SOFTWARE', trans('general.unknown')),
|
||||||
|
'os' => sprintf('%s %s %s', php_uname('s'), php_uname('r'), php_uname('m')),
|
||||||
|
],
|
||||||
|
'db' => [
|
||||||
|
'type' => $dbType,
|
||||||
|
'host' => Arr::get($db, 'host', ''),
|
||||||
|
'port' => Arr::get($db, 'port', ''),
|
||||||
|
'username' => Arr::get($db, 'username'),
|
||||||
|
'database' => Arr::get($db, 'database'),
|
||||||
|
'prefix' => Arr::get($db, 'prefix'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->with('plugins', $enabledPlugins);
|
||||||
|
}
|
||||||
|
}
|
373
app/Http/Controllers/AuthController.php
Executable file
373
app/Http/Controllers/AuthController.php
Executable file
@ -0,0 +1,373 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Events;
|
||||||
|
use App\Exceptions\PrettyPageException;
|
||||||
|
use App\Mail\ForgotPassword;
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Rules;
|
||||||
|
use Auth;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Cache;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Mail;
|
||||||
|
use Session;
|
||||||
|
use URL;
|
||||||
|
use Vectorface\Whip\Whip;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function login(Filter $filter)
|
||||||
|
{
|
||||||
|
$whip = new Whip();
|
||||||
|
$ip = $whip->getValidIpAddress();
|
||||||
|
$ip = $filter->apply('client_ip', $ip);
|
||||||
|
|
||||||
|
$rows = [
|
||||||
|
'auth.rows.login.notice',
|
||||||
|
'auth.rows.login.message',
|
||||||
|
'auth.rows.login.form',
|
||||||
|
'auth.rows.login.registration-link',
|
||||||
|
];
|
||||||
|
$rows = $filter->apply('auth_page_rows:login', $rows);
|
||||||
|
|
||||||
|
return view('auth.login', [
|
||||||
|
'rows' => $rows,
|
||||||
|
'extra' => [
|
||||||
|
'tooManyFails' => cache(sha1('login_fails_'.$ip)) > 3,
|
||||||
|
'recaptcha' => option('recaptcha_sitekey'),
|
||||||
|
'invisible' => (bool) option('recaptcha_invisible'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleLogin(
|
||||||
|
Request $request,
|
||||||
|
Rules\Captcha $captcha,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
|
'identification' => 'required',
|
||||||
|
'password' => 'required|min:6|max:32',
|
||||||
|
]);
|
||||||
|
$identification = $data['identification'];
|
||||||
|
$password = $data['password'];
|
||||||
|
|
||||||
|
$can = $filter->apply('can_login', null, [$identification, $password]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess type of identification
|
||||||
|
$authType = filter_var($identification, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.login.attempt', [$identification, $password, $authType]);
|
||||||
|
event(new Events\UserTryToLogin($identification, $authType));
|
||||||
|
|
||||||
|
if ($authType == 'email') {
|
||||||
|
$user = User::where('email', $identification)->first();
|
||||||
|
} else {
|
||||||
|
$player = Player::where('name', $identification)->first();
|
||||||
|
$user = optional($player)->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require CAPTCHA if user fails to login more than 3 times
|
||||||
|
$whip = new Whip();
|
||||||
|
$ip = $whip->getValidIpAddress();
|
||||||
|
$ip = $filter->apply('client_ip', $ip);
|
||||||
|
$loginFailsCacheKey = sha1('login_fails_'.$ip);
|
||||||
|
$loginFails = (int) Cache::get($loginFailsCacheKey, 0);
|
||||||
|
|
||||||
|
if ($loginFails > 3) {
|
||||||
|
$request->validate(['captcha' => ['required', $captcha]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return json(trans('auth.validation.user'), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.login.ready', [$user]);
|
||||||
|
|
||||||
|
if ($user->verifyPassword($request->input('password'))) {
|
||||||
|
Session::forget('login_fails');
|
||||||
|
Cache::forget($loginFailsCacheKey);
|
||||||
|
|
||||||
|
Auth::login($user, $request->input('keep'));
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.login.succeeded', [$user]);
|
||||||
|
event(new Events\UserLoggedIn($user));
|
||||||
|
|
||||||
|
return json(trans('auth.login.success'), 0, [
|
||||||
|
'redirectTo' => $request->session()->pull('last_requested_path', url('/user')),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$loginFails++;
|
||||||
|
Cache::put($loginFailsCacheKey, $loginFails, 3600);
|
||||||
|
$dispatcher->dispatch('auth.login.failed', [$user, $loginFails]);
|
||||||
|
|
||||||
|
return json(trans('auth.validation.password'), 1, [
|
||||||
|
'login_fails' => $loginFails,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Dispatcher $dispatcher)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.logout.before', [$user]);
|
||||||
|
Auth::logout();
|
||||||
|
$dispatcher->dispatch('auth.logout.after', [$user]);
|
||||||
|
|
||||||
|
return json(trans('auth.logout.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(Filter $filter)
|
||||||
|
{
|
||||||
|
$rows = [
|
||||||
|
'auth.rows.register.notice',
|
||||||
|
'auth.rows.register.form',
|
||||||
|
];
|
||||||
|
$rows = $filter->apply('auth_page_rows:register', $rows);
|
||||||
|
|
||||||
|
return view('auth.register', [
|
||||||
|
'site_name' => option_localized('site_name'),
|
||||||
|
'rows' => $rows,
|
||||||
|
'extra' => [
|
||||||
|
'player' => (bool) option('register_with_player_name'),
|
||||||
|
'recaptcha' => option('recaptcha_sitekey'),
|
||||||
|
'invisible' => (bool) option('recaptcha_invisible'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRegister(
|
||||||
|
Request $request,
|
||||||
|
Rules\Captcha $captcha,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$can = $filter->apply('can_register', null);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rule = option('register_with_player_name') ?
|
||||||
|
['player_name' => [
|
||||||
|
'required',
|
||||||
|
new Rules\PlayerName(),
|
||||||
|
'min:'.option('player_name_length_min'),
|
||||||
|
'max:'.option('player_name_length_max'),
|
||||||
|
]] :
|
||||||
|
['nickname' => 'required|max:255'];
|
||||||
|
$data = $request->validate(array_merge([
|
||||||
|
'email' => 'required|email|unique:users',
|
||||||
|
'password' => 'required|min:8|max:32',
|
||||||
|
'captcha' => ['required', $captcha],
|
||||||
|
], $rule));
|
||||||
|
$playerName = $request->input('player_name');
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.registration.attempt', [$data]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
option('register_with_player_name') &&
|
||||||
|
Player::where('name', $playerName)->count() > 0
|
||||||
|
) {
|
||||||
|
return json(trans('user.player.add.repeated'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If amount of registered accounts of IP is more than allowed amount,
|
||||||
|
// reject this registration.
|
||||||
|
$whip = new Whip();
|
||||||
|
$ip = $whip->getValidIpAddress();
|
||||||
|
$ip = $filter->apply('client_ip', $ip);
|
||||||
|
if (User::where('ip', $ip)->count() >= option('regs_per_ip')) {
|
||||||
|
return json(trans('auth.register.max', ['regs' => option('regs_per_ip')]), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.registration.ready', [$data]);
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->email = $data['email'];
|
||||||
|
$user->nickname = $data[option('register_with_player_name') ? 'player_name' : 'nickname'];
|
||||||
|
$user->score = option('user_initial_score');
|
||||||
|
$user->avatar = 0;
|
||||||
|
$password = app('cipher')->hash($data['password'], config('secure.salt'));
|
||||||
|
$password = $filter->apply('user_password', $password);
|
||||||
|
$user->password = $password;
|
||||||
|
$user->ip = $ip;
|
||||||
|
$user->permission = User::NORMAL;
|
||||||
|
$user->register_at = Carbon::now();
|
||||||
|
$user->last_sign_at = Carbon::now()->subDay();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.registration.completed', [$user]);
|
||||||
|
event(new Events\UserRegistered($user));
|
||||||
|
|
||||||
|
if (option('register_with_player_name')) {
|
||||||
|
$dispatcher->dispatch('player.adding', [$playerName, $user]);
|
||||||
|
|
||||||
|
$player = new Player();
|
||||||
|
$player->uid = $user->uid;
|
||||||
|
$player->name = $playerName;
|
||||||
|
$player->tid_skin = 0;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.added', [$player, $user]);
|
||||||
|
event(new Events\PlayerWasAdded($player));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.login.ready', [$user]);
|
||||||
|
Auth::login($user);
|
||||||
|
$dispatcher->dispatch('auth.login.succeeded', [$user]);
|
||||||
|
|
||||||
|
return json(trans('auth.register.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forgot()
|
||||||
|
{
|
||||||
|
if (config('mail.default') != '') {
|
||||||
|
return view('auth.forgot', [
|
||||||
|
'extra' => [
|
||||||
|
'recaptcha' => option('recaptcha_sitekey'),
|
||||||
|
'invisible' => (bool) option('recaptcha_invisible'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
throw new PrettyPageException(trans('auth.forgot.disabled'), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleForgot(
|
||||||
|
Request $request,
|
||||||
|
Rules\Captcha $captcha,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'captcha' => ['required', $captcha],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!config('mail.default')) {
|
||||||
|
return json(trans('auth.forgot.disabled'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $data['email'];
|
||||||
|
$dispatcher->dispatch('auth.forgot.attempt', [$email]);
|
||||||
|
|
||||||
|
$rateLimit = 180;
|
||||||
|
$whip = new Whip();
|
||||||
|
$ip = $whip->getValidIpAddress();
|
||||||
|
$ip = $filter->apply('client_ip', $ip);
|
||||||
|
$lastMailCacheKey = sha1('last_mail_'.$ip);
|
||||||
|
$remain = $rateLimit + Cache::get($lastMailCacheKey, 0) - time();
|
||||||
|
if ($remain > 0) {
|
||||||
|
return json(trans('auth.forgot.frequent-mail'), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where('email', $email)->first();
|
||||||
|
if (!$user) {
|
||||||
|
return json(trans('auth.forgot.unregistered'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.forgot.ready', [$user]);
|
||||||
|
|
||||||
|
$url = URL::temporarySignedRoute(
|
||||||
|
'auth.reset',
|
||||||
|
Carbon::now()->addHour(),
|
||||||
|
['uid' => $user->uid],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
Mail::to($email)->send(new ForgotPassword(url($url)));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
report($e);
|
||||||
|
$dispatcher->dispatch('auth.forgot.failed', [$user, $url]);
|
||||||
|
|
||||||
|
return json(trans('auth.forgot.failed', ['msg' => $e->getMessage()]), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.forgot.sent', [$user, $url]);
|
||||||
|
Cache::put($lastMailCacheKey, time(), 3600);
|
||||||
|
|
||||||
|
return json(trans('auth.forgot.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(Request $request, $uid)
|
||||||
|
{
|
||||||
|
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||||
|
|
||||||
|
return view('auth.reset')->with('user', User::find($uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleReset(Dispatcher $dispatcher, Request $request, $uid)
|
||||||
|
{
|
||||||
|
abort_unless($request->hasValidSignature(false), 403, trans('auth.reset.invalid'));
|
||||||
|
|
||||||
|
['password' => $password] = $request->validate([
|
||||||
|
'password' => 'required|min:8|max:32',
|
||||||
|
]);
|
||||||
|
$user = User::find($uid);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('auth.reset.before', [$user, $password]);
|
||||||
|
$user->changePassword($password);
|
||||||
|
$dispatcher->dispatch('auth.reset.after', [$user, $password]);
|
||||||
|
|
||||||
|
return json(trans('auth.reset.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function captcha(\Gregwar\Captcha\CaptchaBuilder $builder)
|
||||||
|
{
|
||||||
|
$builder->build(100, 34);
|
||||||
|
session(['captcha' => $builder->getPhrase()]);
|
||||||
|
|
||||||
|
return response($builder->output(), 200, [
|
||||||
|
'Content-Type' => 'image/jpeg',
|
||||||
|
'Cache-Control' => 'no-store',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fillEmail(Request $request)
|
||||||
|
{
|
||||||
|
$email = $request->validate(['email' => 'required|email|unique:users'])['email'];
|
||||||
|
$user = $request->user();
|
||||||
|
$user->email = $email;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect('/user');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify(Request $request)
|
||||||
|
{
|
||||||
|
if (!option('require_verification')) {
|
||||||
|
throw new PrettyPageException(trans('user.verification.disabled'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
|
||||||
|
|
||||||
|
return view('auth.verify');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleVerify(Request $request, User $user)
|
||||||
|
{
|
||||||
|
abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid'));
|
||||||
|
|
||||||
|
['email' => $email] = $request->validate(['email' => 'required|email']);
|
||||||
|
|
||||||
|
if ($user->email !== $email) {
|
||||||
|
return back()->with('errorMessage', trans('auth.verify.not-matched'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->verified = true;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect()->route('user.home');
|
||||||
|
}
|
||||||
|
}
|
198
app/Http/Controllers/ClosetController.php
Executable file
198
app/Http/Controllers/ClosetController.php
Executable file
@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Auth;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ClosetController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Filter $filter)
|
||||||
|
{
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-8', 'md-4'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'user.widgets.email-verification',
|
||||||
|
'user.widgets.closet.list',
|
||||||
|
],
|
||||||
|
['shared.previewer'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:user.closet', $grid);
|
||||||
|
|
||||||
|
return view('user.closet')
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('extra', [
|
||||||
|
'unverified' => option('require_verification') && !auth()->user()->verified,
|
||||||
|
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),
|
||||||
|
'length' => trans(
|
||||||
|
'user.player.player-name-length',
|
||||||
|
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClosetData(Request $request)
|
||||||
|
{
|
||||||
|
$category = $request->input('category', 'skin');
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user
|
||||||
|
->closet()
|
||||||
|
->when(
|
||||||
|
$category === 'cape',
|
||||||
|
fn (Builder $query) => $query->where('type', 'cape'),
|
||||||
|
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
|
||||||
|
)
|
||||||
|
->when(
|
||||||
|
$request->input('q'),
|
||||||
|
fn (Builder $query, $search) => $query->like('item_name', $search)
|
||||||
|
)
|
||||||
|
->orderBy('texture_tid', 'DESC')
|
||||||
|
->paginate((int) $request->input('perPage', 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allIds()
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->closet()->pluck('texture_tid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
['tid' => $tid, 'name' => $name] = $request->validate([
|
||||||
|
'tid' => 'required|integer',
|
||||||
|
'name' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
$name = $filter->apply('add_closet_item_name', $name, [$tid]);
|
||||||
|
$dispatcher->dispatch('closet.adding', [$tid, $name, $user]);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_add_closet_item', true, [$tid, $name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->score < option('score_per_closet_item')) {
|
||||||
|
return json(trans('user.closet.add.lack-score'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tid = $request->tid;
|
||||||
|
$texture = Texture::find($tid);
|
||||||
|
if (!$texture) {
|
||||||
|
return json(trans('user.closet.add.not-found'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$texture->public && ($texture->uploader != $user->uid && !$user->isAdmin())) {
|
||||||
|
return json(trans('skinlib.show.private'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->closet()->where('tid', $request->tid)->count() > 0) {
|
||||||
|
return json(trans('user.closet.add.repeated'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->closet()->attach($tid, ['item_name' => $request->name]);
|
||||||
|
$user->score -= option('score_per_closet_item');
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$texture->likes++;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.added', [$texture, $name, $user]);
|
||||||
|
|
||||||
|
$uploader = User::find($texture->uploader);
|
||||||
|
if ($uploader && $uploader->uid != $user->uid) {
|
||||||
|
$uploader->score += option('score_award_per_like', 0);
|
||||||
|
$uploader->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(trans('user.closet.add.success', ['name' => $request->input('name')]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rename(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
$tid
|
||||||
|
) {
|
||||||
|
['name' => $name] = $request->validate(['name' => 'required']);
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$name = $filter->apply('rename_closet_item_name', $name, [$tid]);
|
||||||
|
$dispatcher->dispatch('closet.renaming', [$tid, $name, $user]);
|
||||||
|
|
||||||
|
$item = $user->closet()->find($tid);
|
||||||
|
if (empty($item)) {
|
||||||
|
return json(trans('user.closet.remove.non-existent'), 1);
|
||||||
|
}
|
||||||
|
$previousName = $item->pivot->item_name;
|
||||||
|
|
||||||
|
$can = $filter->apply('can_rename_closet_item', true, [$item, $name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->closet()->updateExistingPivot($tid, ['item_name' => $name]);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.renamed', [$item, $previousName, $user]);
|
||||||
|
|
||||||
|
return json(trans('user.closet.rename.success', ['name' => $name]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(Dispatcher $dispatcher, Filter $filter, $tid)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.removing', [$tid, $user]);
|
||||||
|
|
||||||
|
$item = $user->closet()->find($tid);
|
||||||
|
if (empty($item)) {
|
||||||
|
return json(trans('user.closet.remove.non-existent'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$can = $filter->apply('can_remove_closet_item', true, [$item]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->closet()->detach($tid);
|
||||||
|
|
||||||
|
if (option('return_score')) {
|
||||||
|
$user->score += option('score_per_closet_item');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$texture = Texture::find($tid);
|
||||||
|
$texture->likes--;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.removed', [$texture, $user]);
|
||||||
|
|
||||||
|
$uploader = User::find($texture->uploader);
|
||||||
|
$uploader->score -= option('score_award_per_like', 0);
|
||||||
|
$uploader->save();
|
||||||
|
|
||||||
|
return json(trans('user.closet.remove.success'), 0);
|
||||||
|
}
|
||||||
|
}
|
47
app/Http/Controllers/ClosetManagementController.php
Executable file
47
app/Http/Controllers/ClosetManagementController.php
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ClosetManagementController extends Controller
|
||||||
|
{
|
||||||
|
public function list(User $user)
|
||||||
|
{
|
||||||
|
return $user->closet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(Request $request, Dispatcher $dispatcher, User $user)
|
||||||
|
{
|
||||||
|
$tid = $request->input('tid');
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = Texture::findOrFail($tid);
|
||||||
|
$name = $texture->name;
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.adding', [$tid, $name, $user]);
|
||||||
|
|
||||||
|
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.added', [$texture, $name, $user]);
|
||||||
|
|
||||||
|
return json('', 0, compact('user', 'texture'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(Request $request, Dispatcher $dispatcher, User $user)
|
||||||
|
{
|
||||||
|
$tid = $request->input('tid');
|
||||||
|
$dispatcher->dispatch('closet.removing', [$tid, $user]);
|
||||||
|
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = Texture::findOrFail($tid);
|
||||||
|
|
||||||
|
$user->closet()->detach($texture->tid);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('closet.removed', [$texture, $user]);
|
||||||
|
|
||||||
|
return json('', 0, compact('user', 'texture'));
|
||||||
|
}
|
||||||
|
}
|
13
app/Http/Controllers/Controller.php
Executable file
13
app/Http/Controllers/Controller.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
|
class Controller extends BaseController
|
||||||
|
{
|
||||||
|
use DispatchesJobs;
|
||||||
|
use ValidatesRequests;
|
||||||
|
}
|
42
app/Http/Controllers/HomeController.php
Executable file
42
app/Http/Controllers/HomeController.php
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class HomeController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('home')
|
||||||
|
->with('user', auth()->user())
|
||||||
|
->with('site_description', option_localized('site_description'))
|
||||||
|
->with('transparent_navbar', (bool) option('transparent_navbar', false))
|
||||||
|
->with('fixed_bg', option('fixed_bg'))
|
||||||
|
->with('hide_intro', option('hide_intro'))
|
||||||
|
->with('home_pic_url', option('home_pic_url') ?: config('options.home_pic_url'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apiRoot()
|
||||||
|
{
|
||||||
|
$copyright = Arr::get(
|
||||||
|
[
|
||||||
|
'Powered with ❤ by Blessing Skin Server.',
|
||||||
|
'Powered by Blessing Skin Server.',
|
||||||
|
'Proudly powered by Blessing Skin Server.',
|
||||||
|
'由 Blessing Skin Server 强力驱动。',
|
||||||
|
'采用 Blessing Skin Server 搭建。',
|
||||||
|
'使用 Blessing Skin Server 稳定运行。',
|
||||||
|
'自豪地采用 Blessing Skin Server。',
|
||||||
|
],
|
||||||
|
option_localized('copyright_prefer', 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'blessing_skin' => config('app.version'),
|
||||||
|
'spec' => 0,
|
||||||
|
'copyright' => $copyright,
|
||||||
|
'site_name' => option('site_name'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
100
app/Http/Controllers/MarketController.php
Executable file
100
app/Http/Controllers/MarketController.php
Executable file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use App\Services\Unzip;
|
||||||
|
use Composer\CaBundle\CaBundle;
|
||||||
|
use Composer\Semver\Comparator;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class MarketController extends Controller
|
||||||
|
{
|
||||||
|
public function marketData(PluginManager $manager)
|
||||||
|
{
|
||||||
|
$plugins = $this->fetch()->map(function ($item) use ($manager) {
|
||||||
|
$plugin = $manager->get($item['name']);
|
||||||
|
|
||||||
|
if ($plugin) {
|
||||||
|
$item['installed'] = $plugin->version;
|
||||||
|
$item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']);
|
||||||
|
} else {
|
||||||
|
$item['installed'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requirements = Arr::get($item, 'require', []);
|
||||||
|
unset($item['require']);
|
||||||
|
$item['dependencies'] = [
|
||||||
|
'all' => $requirements,
|
||||||
|
'unsatisfied' => $manager->getUnsatisfied(new Plugin('', $item)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Request $request, PluginManager $manager, Unzip $unzip)
|
||||||
|
{
|
||||||
|
$name = $request->input('name');
|
||||||
|
$plugins = $this->fetch();
|
||||||
|
$metadata = $plugins->firstWhere('name', $name);
|
||||||
|
|
||||||
|
if (!$metadata) {
|
||||||
|
return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fakePlugin = new Plugin('', $metadata);
|
||||||
|
$unsatisfied = $manager->getUnsatisfied($fakePlugin);
|
||||||
|
$conflicts = $manager->getConflicts($fakePlugin);
|
||||||
|
if ($unsatisfied->isNotEmpty() || $conflicts->isNotEmpty()) {
|
||||||
|
$reason = $manager->formatUnresolved($unsatisfied, $conflicts);
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.market.unresolved'), 1, compact('reason'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = tempnam(sys_get_temp_dir(), $name);
|
||||||
|
$response = Http::withOptions([
|
||||||
|
'sink' => $path,
|
||||||
|
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||||
|
])->get($metadata['dist']['url']);
|
||||||
|
|
||||||
|
if ($response->ok()) {
|
||||||
|
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.market.install-success'), 0);
|
||||||
|
} else {
|
||||||
|
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetch(): Collection
|
||||||
|
{
|
||||||
|
$lang = in_array(app()->getLocale(), config('plugins.locales'))
|
||||||
|
? app()->getLocale()
|
||||||
|
: config('app.fallback_locale');
|
||||||
|
|
||||||
|
$plugins = collect(explode(',', config('plugins.registry')))
|
||||||
|
->map(function ($registry) use ($lang) {
|
||||||
|
$registry = str_replace('{lang}', $lang, $registry);
|
||||||
|
$response = Http::withOptions([
|
||||||
|
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||||
|
])->get(trim($registry));
|
||||||
|
|
||||||
|
if ($response->ok()) {
|
||||||
|
return $response->json()['packages'];
|
||||||
|
} else {
|
||||||
|
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $response->status()]));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->flatten(1);
|
||||||
|
|
||||||
|
return $plugins;
|
||||||
|
}
|
||||||
|
}
|
72
app/Http/Controllers/NotificationsController.php
Executable file
72
app/Http/Controllers/NotificationsController.php
Executable file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Notifications;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||||
|
|
||||||
|
class NotificationsController extends Controller
|
||||||
|
{
|
||||||
|
public function send(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'receiver' => 'required|in:all,normal,uid,email',
|
||||||
|
'uid' => 'required_if:receiver,uid|nullable|integer|exists:users',
|
||||||
|
'email' => 'required_if:receiver,email|nullable|email|exists:users',
|
||||||
|
'title' => 'required|max:20',
|
||||||
|
'content' => 'string|nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$notification = new Notifications\SiteMessage($data['title'], $data['content']);
|
||||||
|
|
||||||
|
switch ($data['receiver']) {
|
||||||
|
case 'all':
|
||||||
|
$users = User::all();
|
||||||
|
break;
|
||||||
|
case 'normal':
|
||||||
|
$users = User::where('permission', User::NORMAL)->get();
|
||||||
|
break;
|
||||||
|
case 'uid':
|
||||||
|
$users = User::where('uid', $data['uid'])->get();
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
$users = User::where('email', $data['email'])->get();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Notification::send($users, $notification);
|
||||||
|
|
||||||
|
session(['sentResult' => trans('admin.notifications.send.success')]);
|
||||||
|
|
||||||
|
return redirect('/admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all()
|
||||||
|
{
|
||||||
|
return auth()->user()
|
||||||
|
->unreadNotifications
|
||||||
|
->map(fn ($notification) => [
|
||||||
|
'id' => $notification->id,
|
||||||
|
'title' => $notification->data['title'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read($id)
|
||||||
|
{
|
||||||
|
$notification = auth()
|
||||||
|
->user()
|
||||||
|
->unreadNotifications
|
||||||
|
->first(fn ($notification) => $notification->id === $id);
|
||||||
|
$notification->markAsRead();
|
||||||
|
|
||||||
|
$converter = new GithubFlavoredMarkdownConverter();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $notification->data['title'],
|
||||||
|
'content' => $converter->convertToHtml($notification->data['content'] ?? '')->getContent(),
|
||||||
|
'time' => $notification->created_at->toDateTimeString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
266
app/Http/Controllers/OptionsController.php
Executable file
266
app/Http/Controllers/OptionsController.php
Executable file
@ -0,0 +1,266 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Facades\Option;
|
||||||
|
use App\Services\OptionForm;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class OptionsController extends Controller
|
||||||
|
{
|
||||||
|
public function customize(Request $request)
|
||||||
|
{
|
||||||
|
$homepage = Option::form('homepage', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->text('home_pic_url')->hint();
|
||||||
|
|
||||||
|
$form->text('favicon_url')->hint()->description();
|
||||||
|
|
||||||
|
$form->checkbox('transparent_navbar')->label();
|
||||||
|
|
||||||
|
$form->checkbox('hide_intro')->label();
|
||||||
|
|
||||||
|
$form->checkbox('fixed_bg')->label();
|
||||||
|
|
||||||
|
$form->select('copyright_prefer')
|
||||||
|
->option('0', 'Powered with ❤ by Blessing Skin Server.')
|
||||||
|
->option('1', 'Powered by Blessing Skin Server.')
|
||||||
|
->option('2', 'Proudly powered by Blessing Skin Server.')
|
||||||
|
->option('3', '由 Blessing Skin Server 强力驱动。')
|
||||||
|
->option('4', '采用 Blessing Skin Server 搭建。')
|
||||||
|
->option('5', '使用 Blessing Skin Server 稳定运行。')
|
||||||
|
->option('6', '自豪地采用 Blessing Skin Server。')
|
||||||
|
->description();
|
||||||
|
|
||||||
|
$form->textarea('copyright_text')->rows(6)->description();
|
||||||
|
})->handle(function () {
|
||||||
|
Option::set('copyright_prefer_'.config('app.locale'), request('copyright_prefer'));
|
||||||
|
Option::set('copyright_text_'.config('app.locale'), request('copyright_text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$customJsCss = Option::form('customJsCss', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->textarea('custom_css', 'CSS')->rows(6);
|
||||||
|
$form->textarea('custom_js', 'JavaScript')->rows(6);
|
||||||
|
})->addMessage()->handle();
|
||||||
|
|
||||||
|
if ($request->isMethod('post') && $request->input('action') === 'color') {
|
||||||
|
$navbar = $request->input('navbar');
|
||||||
|
if ($navbar) {
|
||||||
|
option(['navbar_color' => $navbar]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sidebar = $request->input('sidebar');
|
||||||
|
if ($sidebar) {
|
||||||
|
option(['sidebar_color' => $sidebar]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.customize', [
|
||||||
|
'colors' => [
|
||||||
|
'navbar' => [
|
||||||
|
'primary', 'secondary', 'success', 'danger', 'indigo',
|
||||||
|
'purple', 'pink', 'teal', 'cyan', 'dark', 'gray',
|
||||||
|
'fuchsia', 'maroon', 'olive', 'navy',
|
||||||
|
'lime', 'light', 'warning', 'white', 'orange',
|
||||||
|
],
|
||||||
|
'sidebar' => [
|
||||||
|
'primary', 'warning', 'info', 'danger', 'success', 'indigo',
|
||||||
|
'navy', 'purple', 'fuchsia', 'pink', 'maroon', 'orange',
|
||||||
|
'lime', 'teal', 'olive',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'forms' => [
|
||||||
|
'homepage' => $homepage,
|
||||||
|
'custom_js_css' => $customJsCss,
|
||||||
|
],
|
||||||
|
'extra' => [
|
||||||
|
'navbar' => option('navbar_color'),
|
||||||
|
'sidebar' => option('sidebar_color'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function score()
|
||||||
|
{
|
||||||
|
$rate = Option::form('rate', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->group('score_per_storage')->text('score_per_storage')->addon();
|
||||||
|
|
||||||
|
$form->group('private_score_per_storage')
|
||||||
|
->text('private_score_per_storage')->addon()->hint();
|
||||||
|
|
||||||
|
$form->group('score_per_closet_item')
|
||||||
|
->text('score_per_closet_item')->addon();
|
||||||
|
|
||||||
|
$form->checkbox('return_score')->label();
|
||||||
|
|
||||||
|
$form->group('score_per_player')->text('score_per_player')->addon();
|
||||||
|
|
||||||
|
$form->text('user_initial_score');
|
||||||
|
})->handle();
|
||||||
|
|
||||||
|
$report = Option::form('report', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->text('reporter_score_modification')->description();
|
||||||
|
|
||||||
|
$form->text('reporter_reward_score');
|
||||||
|
})->handle();
|
||||||
|
|
||||||
|
$sign = Option::form('sign', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->group('sign_score')
|
||||||
|
->text('sign_score_from')->addon(trans('options.sign.sign_score.addon1'))
|
||||||
|
->text('sign_score_to')->addon(trans('options.sign.sign_score.addon2'));
|
||||||
|
|
||||||
|
$form->group('sign_gap_time')->text('sign_gap_time')->addon();
|
||||||
|
|
||||||
|
$form->checkbox('sign_after_zero')->label()->hint();
|
||||||
|
})->after(function () {
|
||||||
|
$sign_score = request('sign_score_from').','.request('sign_score_to');
|
||||||
|
Option::set('sign_score', $sign_score);
|
||||||
|
})->with([
|
||||||
|
'sign_score_from' => @explode(',', option('sign_score'))[0],
|
||||||
|
'sign_score_to' => @explode(',', option('sign_score'))[1],
|
||||||
|
])->handle();
|
||||||
|
|
||||||
|
$sharing = Option::form('sharing', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->group('score_award_per_texture')
|
||||||
|
->text('score_award_per_texture')
|
||||||
|
->addon(trans('general.user.score'));
|
||||||
|
$form->checkbox('take_back_scores_after_deletion')->label();
|
||||||
|
$form->group('score_award_per_like')
|
||||||
|
->text('score_award_per_like')
|
||||||
|
->addon(trans('general.user.score'));
|
||||||
|
})->handle();
|
||||||
|
|
||||||
|
return view('admin.score', ['forms' => compact('rate', 'report', 'sign', 'sharing')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options()
|
||||||
|
{
|
||||||
|
$general = Option::form('general', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->text('site_name');
|
||||||
|
$form->text('site_description')->description();
|
||||||
|
|
||||||
|
$form->text('site_url')
|
||||||
|
->hint()
|
||||||
|
->format(function ($url) {
|
||||||
|
if (Str::endsWith($url, '/')) {
|
||||||
|
$url = substr($url, 0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::endsWith($url, '/index.php')) {
|
||||||
|
$url = substr($url, 0, -10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
});
|
||||||
|
|
||||||
|
$form->checkbox('register_with_player_name')->label();
|
||||||
|
$form->checkbox('require_verification')->label();
|
||||||
|
|
||||||
|
$form->text('regs_per_ip');
|
||||||
|
|
||||||
|
$form->group('max_upload_file_size')
|
||||||
|
->text('max_upload_file_size')->addon('KB')
|
||||||
|
->hint(trans('options.general.max_upload_file_size.hint', ['size' => ini_get('upload_max_filesize')]));
|
||||||
|
|
||||||
|
$form->select('player_name_rule')
|
||||||
|
->option('official', trans('options.general.player_name_rule.official'))
|
||||||
|
->option('cjk', trans('options.general.player_name_rule.cjk'))
|
||||||
|
->option('utf8', trans('options.general.player_name_rule.utf8'))
|
||||||
|
->option('custom', trans('options.general.player_name_rule.custom'));
|
||||||
|
|
||||||
|
$form->text('custom_player_name_regexp')->hint()->placeholder();
|
||||||
|
|
||||||
|
$form->group('player_name_length')
|
||||||
|
->text('player_name_length_min')
|
||||||
|
->addon('~')
|
||||||
|
->text('player_name_length_max')
|
||||||
|
->addon(trans('options.general.player_name_length.suffix'));
|
||||||
|
|
||||||
|
$form->checkbox('auto_del_invalid_texture')->label()->hint();
|
||||||
|
|
||||||
|
$form->checkbox('allow_downloading_texture')->label();
|
||||||
|
|
||||||
|
$form->select('status_code_for_private')
|
||||||
|
->option('403', '403 Forbidden')
|
||||||
|
->option('404', '404 Not Found');
|
||||||
|
|
||||||
|
$form->text('texture_name_regexp')->hint()->placeholder();
|
||||||
|
|
||||||
|
$form->textarea('content_policy')->rows(3)->description();
|
||||||
|
})->handle(function () {
|
||||||
|
Option::set('site_name_'.config('app.locale'), request('site_name'));
|
||||||
|
Option::set('site_description_'.config('app.locale'), request('site_description'));
|
||||||
|
Option::set('content_policy_'.config('app.locale'), request('content_policy'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$announ = Option::form('announ', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->textarea('announcement')->rows(10)->description();
|
||||||
|
})->renderWithoutTable()->handle(function () {
|
||||||
|
Option::set('announcement_'.config('app.locale'), request('announcement'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$meta = Option::form('meta', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->text('meta_keywords')->hint();
|
||||||
|
$form->text('meta_description')->hint();
|
||||||
|
$form->textarea('meta_extras')->rows(6);
|
||||||
|
})->handle();
|
||||||
|
|
||||||
|
$recaptcha = Option::form('recaptcha', 'reCAPTCHA', function ($form) {
|
||||||
|
$form->text('recaptcha_sitekey', 'sitekey');
|
||||||
|
$form->text('recaptcha_secretkey', 'secretkey');
|
||||||
|
$form->checkbox('recaptcha_invisible')->label();
|
||||||
|
})->handle();
|
||||||
|
|
||||||
|
return view('admin.options')
|
||||||
|
->with('forms', compact('general', 'announ', 'meta', 'recaptcha'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resource(Request $request)
|
||||||
|
{
|
||||||
|
$resources = Option::form('resources', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->checkbox('force_ssl')->label()->hint();
|
||||||
|
$form->checkbox('auto_detect_asset_url')->label()->description();
|
||||||
|
|
||||||
|
$form->text('cache_expire_time')->hint(OptionForm::AUTO_DETECT);
|
||||||
|
$form->text('cdn_address')
|
||||||
|
->hint(OptionForm::AUTO_DETECT)
|
||||||
|
->description(OptionForm::AUTO_DETECT);
|
||||||
|
})
|
||||||
|
->type('primary')
|
||||||
|
->hint(OptionForm::AUTO_DETECT)
|
||||||
|
->after(function () {
|
||||||
|
$cdnAddress = request('cdn_address');
|
||||||
|
if ($cdnAddress == null) {
|
||||||
|
$cdnAddress = '';
|
||||||
|
}
|
||||||
|
if (Str::endsWith($cdnAddress, '/')) {
|
||||||
|
$cdnAddress = substr($cdnAddress, 0, -1);
|
||||||
|
}
|
||||||
|
Option::set('cdn_address', $cdnAddress);
|
||||||
|
})
|
||||||
|
->handle();
|
||||||
|
|
||||||
|
$cache = Option::form('cache', OptionForm::AUTO_DETECT, function ($form) {
|
||||||
|
$form->checkbox('enable_avatar_cache')->label();
|
||||||
|
$form->checkbox('enable_preview_cache')->label();
|
||||||
|
})
|
||||||
|
->type('warning')
|
||||||
|
->addButton([
|
||||||
|
'text' => trans('options.cache.clear'),
|
||||||
|
'type' => 'a',
|
||||||
|
'class' => 'float-right',
|
||||||
|
'style' => 'warning',
|
||||||
|
'href' => '?clear-cache',
|
||||||
|
])
|
||||||
|
->addMessage(trans('options.cache.driver', ['driver' => config('cache.default')]), 'info');
|
||||||
|
|
||||||
|
if ($request->has('clear-cache')) {
|
||||||
|
Cache::flush();
|
||||||
|
$cache->addMessage(trans('options.cache.cleared'), 'success');
|
||||||
|
}
|
||||||
|
$cache->handle();
|
||||||
|
|
||||||
|
return view('admin.resource')->with('forms', compact('resources', 'cache'));
|
||||||
|
}
|
||||||
|
}
|
260
app/Http/Controllers/PlayerController.php
Executable file
260
app/Http/Controllers/PlayerController.php
Executable file
@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Events\PlayerWasAdded;
|
||||||
|
use App\Events\PlayerWasDeleted;
|
||||||
|
use App\Events\PlayerWillBeAdded;
|
||||||
|
use App\Events\PlayerWillBeDeleted;
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Rules;
|
||||||
|
use Auth;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class PlayerController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware(function (Request $request, $next) {
|
||||||
|
/** @var Player */
|
||||||
|
$player = $request->route('player');
|
||||||
|
if ($player->user->isNot($request->user())) {
|
||||||
|
return json(trans('admin.players.no-permission'), 1)
|
||||||
|
->setStatusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}, [
|
||||||
|
'only' => ['delete', 'rename', 'setTexture', 'clearTexture'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Filter $filter)
|
||||||
|
{
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-6', 'md-6'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'user.widgets.players.list',
|
||||||
|
'user.widgets.players.notice',
|
||||||
|
],
|
||||||
|
['shared.previewer'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:user.player', $grid);
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return view('user.player')
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('extra', [
|
||||||
|
'count' => $user->players()->count(),
|
||||||
|
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),
|
||||||
|
'length' => trans(
|
||||||
|
'user.player.player-name-length',
|
||||||
|
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
||||||
|
),
|
||||||
|
'score' => auth()->user()->score,
|
||||||
|
'cost' => (int) option('score_per_player'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list()
|
||||||
|
{
|
||||||
|
return Auth::user()->players;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(Request $request, Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$name = $request->validate([
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
new Rules\PlayerName(),
|
||||||
|
'min:'.option('player_name_length_min'),
|
||||||
|
'max:'.option('player_name_length_max'),
|
||||||
|
'unique:players',
|
||||||
|
],
|
||||||
|
])['name'];
|
||||||
|
$name = $filter->apply('new_player_name', $name);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.add.attempt', [$name, $user]);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_add_player', true, [$name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->score < (int) option('score_per_player')) {
|
||||||
|
return json(trans('user.player.add.lack-score'), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.adding', [$name, $user]);
|
||||||
|
event(new PlayerWillBeAdded($name));
|
||||||
|
|
||||||
|
$player = new Player();
|
||||||
|
$player->uid = $user->uid;
|
||||||
|
$player->name = $name;
|
||||||
|
$player->tid_skin = 0;
|
||||||
|
$player->tid_cape = 0;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$user->score -= (int) option('score_per_player');
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.added', [$player, $user]);
|
||||||
|
event(new PlayerWasAdded($player));
|
||||||
|
|
||||||
|
return json(trans('user.player.add.success', ['name' => $name]), 0, $player->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Player $player
|
||||||
|
) {
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
$playerName = $player->name;
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.delete.attempt', [$player, $user]);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_delete_player', true, [$player]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.deleting', [$player, $user]);
|
||||||
|
event(new PlayerWillBeDeleted($player));
|
||||||
|
|
||||||
|
$player->delete();
|
||||||
|
|
||||||
|
if (option('return_score')) {
|
||||||
|
$user->score += (int) option('score_per_player');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.deleted', [$player, $user]);
|
||||||
|
event(new PlayerWasDeleted($playerName));
|
||||||
|
|
||||||
|
return json(trans('user.player.delete.success', ['name' => $playerName]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rename(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Player $player
|
||||||
|
) {
|
||||||
|
$name = $request->validate([
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
new Rules\PlayerName(),
|
||||||
|
'min:'.option('player_name_length_min'),
|
||||||
|
'max:'.option('player_name_length_max'),
|
||||||
|
Rule::unique('players')->ignoreModel($player),
|
||||||
|
],
|
||||||
|
])['name'];
|
||||||
|
$name = $filter->apply('new_player_name', $name);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.renaming', [$player, $name]);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_rename_player', true, [$player, $name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$old = $player->replicate();
|
||||||
|
$player->name = $name;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.renamed', [$player, $old]);
|
||||||
|
|
||||||
|
return json(
|
||||||
|
trans('user.player.rename.success', ['old' => $old->name, 'new' => $name]),
|
||||||
|
0,
|
||||||
|
$player->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTexture(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Player $player
|
||||||
|
) {
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
foreach (['skin', 'cape'] as $type) {
|
||||||
|
$tid = $request->input($type);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_set_texture', true, [$player, $type, $tid]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tid) {
|
||||||
|
$texture = Texture::find($tid);
|
||||||
|
if (empty($texture)) {
|
||||||
|
return json(trans('skinlib.non-existent'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->closet()->where('texture_tid', $tid)->doesntExist()) {
|
||||||
|
return json(trans('user.closet.remove.non-existent'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.texture.updating', [$player, $texture]);
|
||||||
|
|
||||||
|
$field = "tid_$type";
|
||||||
|
$player->$field = $tid;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.texture.updated', [$player, $texture]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(trans('user.player.set.success', ['name' => $player->name]), 0, $player->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearTexture(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Player $player
|
||||||
|
) {
|
||||||
|
$types = $request->input('type', []);
|
||||||
|
|
||||||
|
foreach (['skin', 'cape'] as $type) {
|
||||||
|
$can = $filter->apply('can_clear_texture', true, [$player, $type]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has($type) || in_array($type, $types)) {
|
||||||
|
$dispatcher->dispatch('player.texture.resetting', [$player, $type]);
|
||||||
|
|
||||||
|
$field = "tid_$type";
|
||||||
|
$player->$field = 0;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.texture.reset', [$player, $type]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(trans('user.player.clear.success', ['name' => $player->name]), 0, $player->toArray());
|
||||||
|
}
|
||||||
|
}
|
136
app/Http/Controllers/PlayersManagementController.php
Executable file
136
app/Http/Controllers/PlayersManagementController.php
Executable file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Rules;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class PlayersManagementController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware(function (Request $request, $next) {
|
||||||
|
/** @var Player */
|
||||||
|
$player = $request->route('player');
|
||||||
|
$owner = $player->user;
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$currentUser = $request->user();
|
||||||
|
|
||||||
|
if (
|
||||||
|
$owner->uid !== $currentUser->uid &&
|
||||||
|
$owner->permission >= $currentUser->permission
|
||||||
|
) {
|
||||||
|
return json(trans('admin.players.no-permission'), 1)
|
||||||
|
->setStatusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
})->except(['list']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(Request $request)
|
||||||
|
{
|
||||||
|
$query = $request->query('q');
|
||||||
|
|
||||||
|
return Player::usingSearchString($query)->paginate(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(
|
||||||
|
Player $player,
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$name = $request->validate([
|
||||||
|
'player_name' => [
|
||||||
|
'required',
|
||||||
|
new Rules\PlayerName(),
|
||||||
|
'min:'.option('player_name_length_min'),
|
||||||
|
'max:'.option('player_name_length_max'),
|
||||||
|
'unique:players,name',
|
||||||
|
],
|
||||||
|
])['player_name'];
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.renaming', [$player, $name]);
|
||||||
|
|
||||||
|
$oldName = $player->name;
|
||||||
|
$player->name = $name;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.renamed', [$player, $oldName]);
|
||||||
|
|
||||||
|
return json(trans('admin.players.name.success', ['player' => $player->name]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function owner(
|
||||||
|
Player $player,
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$uid = $request->validate(['uid' => 'required|integer'])['uid'];
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.owner.updating', [$player, $uid]);
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = User::find($request->uid);
|
||||||
|
if (empty($user)) {
|
||||||
|
return json(trans('admin.users.operations.non-existent'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$player->uid = $uid;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.owner.updated', [$player, $user]);
|
||||||
|
|
||||||
|
return json(trans('admin.players.owner.success', [
|
||||||
|
'player' => $player->name,
|
||||||
|
'user' => $user->nickname,
|
||||||
|
]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function texture(
|
||||||
|
Player $player,
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
|
'tid' => 'required|integer',
|
||||||
|
'type' => ['required', Rule::in(['skin', 'cape'])],
|
||||||
|
]);
|
||||||
|
$tid = (int) $data['tid'];
|
||||||
|
$type = $data['type'];
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.texture.updating', [$player, $type, $tid]);
|
||||||
|
|
||||||
|
if (Texture::where('tid', $tid)->doesntExist() && $tid !== 0) {
|
||||||
|
return json(trans('admin.players.textures.non-existent', ['tid' => $tid]), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$field = 'tid_'.$type;
|
||||||
|
$previousTid = $player->$field;
|
||||||
|
$player->$field = $tid;
|
||||||
|
$player->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.texture.updated', [$player, $type, $previousTid]);
|
||||||
|
|
||||||
|
return json(trans('admin.players.textures.success', ['player' => $player->name]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(
|
||||||
|
Player $player,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$dispatcher->dispatch('player.deleting', [$player]);
|
||||||
|
|
||||||
|
$player->delete();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('player.deleted', [$player]);
|
||||||
|
|
||||||
|
return json(trans('admin.players.delete.success'), 0);
|
||||||
|
}
|
||||||
|
}
|
140
app/Http/Controllers/PluginController.php
Executable file
140
app/Http/Controllers/PluginController.php
Executable file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use App\Services\Unzip;
|
||||||
|
use Composer\CaBundle\CaBundle;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||||
|
|
||||||
|
class PluginController extends Controller
|
||||||
|
{
|
||||||
|
public function config(PluginManager $plugins, $name)
|
||||||
|
{
|
||||||
|
$plugin = $plugins->get($name);
|
||||||
|
if ($plugin && $plugin->isEnabled()) {
|
||||||
|
if ($plugin->hasConfigClass()) {
|
||||||
|
return app()->call($plugin->getConfigClass().'@render');
|
||||||
|
} elseif ($plugin->hasConfigView()) {
|
||||||
|
return $plugin->getConfigView();
|
||||||
|
} else {
|
||||||
|
return abort(404, trans('admin.plugins.operations.no-config-notice'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return abort(404, trans('admin.plugins.operations.no-config-notice'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readme(PluginManager $plugins, $name)
|
||||||
|
{
|
||||||
|
$plugin = $plugins->get($name);
|
||||||
|
if (empty($plugin)) {
|
||||||
|
return abort(404, trans('admin.plugins.operations.no-readme-notice'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$readmePath = $plugin->getReadme();
|
||||||
|
if (empty($readmePath)) {
|
||||||
|
return abort(404, trans('admin.plugins.operations.no-readme-notice'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = trans($plugin->title);
|
||||||
|
$path = $plugin->getPath().'/'.$readmePath;
|
||||||
|
$converter = new GithubFlavoredMarkdownConverter();
|
||||||
|
$content = $converter->convertToHtml(file_get_contents($path));
|
||||||
|
|
||||||
|
return view('admin.plugin.readme', compact('content', 'title'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manage(Request $request, PluginManager $plugins)
|
||||||
|
{
|
||||||
|
$name = $request->input('name');
|
||||||
|
$plugin = $plugins->get($name);
|
||||||
|
|
||||||
|
if ($plugin) {
|
||||||
|
// Pass the plugin title through the translator.
|
||||||
|
$plugin->title = trans($plugin->title);
|
||||||
|
|
||||||
|
switch ($request->get('action')) {
|
||||||
|
case 'enable':
|
||||||
|
$result = $plugins->enable($name);
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
return json(trans('admin.plugins.operations.enabled', ['plugin' => $plugin->title]), 0);
|
||||||
|
} else {
|
||||||
|
$reason = $plugins->formatUnresolved($result['unsatisfied'], $result['conflicts']);
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.operations.unsatisfied.notice'), 1, compact('reason'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// no break
|
||||||
|
case 'disable':
|
||||||
|
$plugins->disable($name);
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.operations.disabled', ['plugin' => $plugin->title]), 0);
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$plugins->delete($name);
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.operations.deleted'), 0);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return json(trans('admin.invalid-action'), 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return json(trans('admin.plugins.operations.not-found'), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPluginData(PluginManager $plugins)
|
||||||
|
{
|
||||||
|
return $plugins->all()
|
||||||
|
->map(function (Plugin $plugin) {
|
||||||
|
return [
|
||||||
|
'name' => $plugin->name,
|
||||||
|
'title' => trans($plugin->title),
|
||||||
|
'description' => trans($plugin->description ?? ''),
|
||||||
|
'version' => $plugin->version,
|
||||||
|
'enabled' => $plugin->isEnabled(),
|
||||||
|
'readme' => (bool) $plugin->getReadme(),
|
||||||
|
'config' => $plugin->hasConfig(),
|
||||||
|
'icon' => array_merge(
|
||||||
|
['fa' => 'plug', 'faType' => 'fas', 'bg' => 'navy'],
|
||||||
|
$plugin->getManifestAttr('enchants.icon', [])
|
||||||
|
),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(Request $request, PluginManager $manager, Unzip $unzip)
|
||||||
|
{
|
||||||
|
$request->validate(['file' => 'required|file|mimetypes:application/zip']);
|
||||||
|
|
||||||
|
$path = $request->file('file')->getPathname();
|
||||||
|
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.market.install-success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function wget(Request $request, PluginManager $manager, Unzip $unzip)
|
||||||
|
{
|
||||||
|
$data = $request->validate(['url' => 'required|url']);
|
||||||
|
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'wget-plugin');
|
||||||
|
$response = Http::withOptions([
|
||||||
|
'sink' => $path,
|
||||||
|
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||||
|
])->get($data['url']);
|
||||||
|
|
||||||
|
if ($response->ok()) {
|
||||||
|
$unzip->extract($path, $manager->getPluginsDirs()->first());
|
||||||
|
|
||||||
|
return json(trans('admin.plugins.market.install-success'), 0);
|
||||||
|
} else {
|
||||||
|
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
170
app/Http/Controllers/ReportController.php
Executable file
170
app/Http/Controllers/ReportController.php
Executable file
@ -0,0 +1,170 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Report;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
public function submit(Request $request, Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'tid' => 'required|exists:textures',
|
||||||
|
'reason' => 'required',
|
||||||
|
]);
|
||||||
|
/** @var User */
|
||||||
|
$reporter = auth()->user();
|
||||||
|
$tid = $data['tid'];
|
||||||
|
$reason = $data['reason'];
|
||||||
|
|
||||||
|
$can = $filter->apply('user_can_report', true, [$tid, $reason, $reporter]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.submitting', [$tid, $reason, $reporter]);
|
||||||
|
|
||||||
|
if (Report::where('reporter', $reporter->uid)->where('tid', $tid)->count() > 0) {
|
||||||
|
return json(trans('skinlib.report.duplicate'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$score = option('reporter_score_modification', 0);
|
||||||
|
if ($score < 0 && $reporter->score < -$score) {
|
||||||
|
return json(trans('skinlib.upload.lack-score'), 1);
|
||||||
|
}
|
||||||
|
$reporter->score += $score;
|
||||||
|
$reporter->save();
|
||||||
|
|
||||||
|
$report = new Report();
|
||||||
|
$report->tid = $tid;
|
||||||
|
$report->uploader = Texture::find($tid)->uploader;
|
||||||
|
$report->reporter = $reporter->uid;
|
||||||
|
$report->reason = $reason;
|
||||||
|
$report->status = Report::PENDING;
|
||||||
|
$report->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.submitted', [$report]);
|
||||||
|
|
||||||
|
return json(trans('skinlib.report.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function track()
|
||||||
|
{
|
||||||
|
$reports = Report::where('reporter', auth()->id())
|
||||||
|
->orderBy('report_at', 'desc')
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return view('user.report', ['reports' => $reports]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manage(Request $request)
|
||||||
|
{
|
||||||
|
$q = $request->input('q');
|
||||||
|
|
||||||
|
return Report::usingSearchString($q)
|
||||||
|
->with(['texture', 'textureUploader', 'informer'])
|
||||||
|
->paginate(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function review(
|
||||||
|
Report $report,
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
|
'action' => ['required', Rule::in(['delete', 'ban', 'reject'])],
|
||||||
|
]);
|
||||||
|
$action = $data['action'];
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.reviewing', [$report, $action]);
|
||||||
|
|
||||||
|
if ($action == 'reject') {
|
||||||
|
if (
|
||||||
|
$report->informer &&
|
||||||
|
($score = option('reporter_score_modification', 0)) > 0 &&
|
||||||
|
$report->status == Report::PENDING
|
||||||
|
) {
|
||||||
|
$report->informer->score -= $score;
|
||||||
|
$report->informer->save();
|
||||||
|
}
|
||||||
|
$report->status = Report::REJECTED;
|
||||||
|
$report->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.rejected', [$report]);
|
||||||
|
|
||||||
|
return json(trans('general.op-success'), 0, ['status' => Report::REJECTED]);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'delete':
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = $report->texture;
|
||||||
|
if ($texture) {
|
||||||
|
$dispatcher->dispatch('texture.deleting', [$texture]);
|
||||||
|
Storage::disk('textures')->delete($texture->hash);
|
||||||
|
$texture->delete();
|
||||||
|
$dispatcher->dispatch('texture.deleted', [$texture]);
|
||||||
|
} else {
|
||||||
|
// The texture has been deleted by its uploader
|
||||||
|
// We will return the score, but will not give the informer any reward
|
||||||
|
self::returnScore($report);
|
||||||
|
$report->status = Report::RESOLVED;
|
||||||
|
$report->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.resolved', [$report, $action]);
|
||||||
|
|
||||||
|
return json(trans('general.texture-deleted'), 0, ['status' => Report::RESOLVED]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ban':
|
||||||
|
$uploader = User::find($report->uploader);
|
||||||
|
if (!$uploader) {
|
||||||
|
return json(trans('admin.users.operations.non-existent'), 1);
|
||||||
|
}
|
||||||
|
if (auth()->user()->permission <= $uploader->permission) {
|
||||||
|
return json(trans('admin.users.operations.no-permission'), 1);
|
||||||
|
}
|
||||||
|
$uploader->permission = User::BANNED;
|
||||||
|
$uploader->save();
|
||||||
|
$dispatcher->dispatch('user.banned', [$uploader]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::returnScore($report);
|
||||||
|
self::giveAward($report);
|
||||||
|
$report->status = Report::RESOLVED;
|
||||||
|
$report->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('report.resolved', [$report, $action]);
|
||||||
|
|
||||||
|
return json(trans('general.op-success'), 0, ['status' => Report::RESOLVED]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function returnScore($report)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
$report->status == Report::PENDING &&
|
||||||
|
($score = option('reporter_score_modification', 0)) < 0 &&
|
||||||
|
$report->informer
|
||||||
|
) {
|
||||||
|
$report->informer->score -= $score;
|
||||||
|
$report->informer->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function giveAward($report)
|
||||||
|
{
|
||||||
|
if ($report->status == Report::PENDING && $report->informer) {
|
||||||
|
$report->informer->score += option('reporter_reward_score', 0);
|
||||||
|
$report->informer->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
157
app/Http/Controllers/SetupController.php
Executable file
157
app/Http/Controllers/SetupController.php
Executable file
@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Exceptions\PrettyPageException;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Console\Kernel as Artisan;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
|
use Illuminate\Database\DatabaseManager;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Vectorface\Whip\Whip;
|
||||||
|
|
||||||
|
class SetupController extends Controller
|
||||||
|
{
|
||||||
|
public function database(
|
||||||
|
Request $request,
|
||||||
|
Filesystem $filesystem,
|
||||||
|
Connection $connection,
|
||||||
|
DatabaseManager $manager
|
||||||
|
) {
|
||||||
|
if ($request->isMethod('get')) {
|
||||||
|
try {
|
||||||
|
$connection->getPdo();
|
||||||
|
|
||||||
|
return redirect('setup/info');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return view('setup.wizard.database', [
|
||||||
|
'host' => env('DB_HOST'),
|
||||||
|
'port' => env('DB_PORT'),
|
||||||
|
'username' => env('DB_USERNAME'),
|
||||||
|
'password' => env('DB_PASSWORD'),
|
||||||
|
'database' => env('DB_DATABASE'),
|
||||||
|
'prefix' => env('DB_PREFIX'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config([
|
||||||
|
'database.connections.temp.driver' => $request->input('type'),
|
||||||
|
'database.connections.temp.host' => $request->input('host'),
|
||||||
|
'database.connections.temp.port' => $request->input('port'),
|
||||||
|
'database.connections.temp.username' => $request->input('username'),
|
||||||
|
'database.connections.temp.password' => $request->input('password'),
|
||||||
|
'database.connections.temp.database' => $request->input('db'),
|
||||||
|
'database.connections.temp.prefix' => $request->input('prefix'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$manager->connection('temp')->getPdo();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
$type = Arr::get([
|
||||||
|
'mysql' => 'MySQL/MariaDB',
|
||||||
|
'sqlite' => 'SQLite',
|
||||||
|
'pgsql' => 'PostgreSQL',
|
||||||
|
], $request->input('type'), '');
|
||||||
|
|
||||||
|
throw new PrettyPageException(trans('setup.database.connection-error', compact('msg', 'type')), $e->getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $filesystem->get(base_path('.env'));
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_CONNECTION.+/',
|
||||||
|
'DB_CONNECTION='.$request->input('type', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_HOST.+/',
|
||||||
|
'DB_HOST='.$request->input('host', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_PORT.+/',
|
||||||
|
'DB_PORT='.$request->input('port', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_DATABASE.+/',
|
||||||
|
'DB_DATABASE='.$request->input('db', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_USERNAME.+/',
|
||||||
|
'DB_USERNAME='.$request->input('username', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_PASSWORD.+/',
|
||||||
|
'DB_PASSWORD='.$request->input('password', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/DB_PREFIX.+/',
|
||||||
|
'DB_PREFIX='.$request->input('prefix', ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$filesystem->put(base_path('.env'), $content);
|
||||||
|
|
||||||
|
return redirect('setup/info');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function finish(Request $request, Filesystem $filesystem, Artisan $artisan)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'nickname' => 'required',
|
||||||
|
'password' => 'required|min:8|max:32|confirmed',
|
||||||
|
'site_name' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$artisan->call('passport:keys', ['--no-interaction' => true]);
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
$artisan->call('migrate', [
|
||||||
|
'--force' => true,
|
||||||
|
'--path' => [
|
||||||
|
'database/migrations',
|
||||||
|
'vendor/laravel/passport/database/migrations',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$siteUrl = url('/');
|
||||||
|
if (Str::endsWith($siteUrl, '/index.php')) {
|
||||||
|
$siteUrl = substr($siteUrl, 0, -10); // @codeCoverageIgnore
|
||||||
|
}
|
||||||
|
option([
|
||||||
|
'site_name' => $request->input('site_name'),
|
||||||
|
'site_url' => $siteUrl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$whip = new Whip();
|
||||||
|
$ip = $whip->getValidIpAddress();
|
||||||
|
|
||||||
|
// Register super admin
|
||||||
|
$user = new User();
|
||||||
|
$user->email = $data['email'];
|
||||||
|
$user->nickname = $data['nickname'];
|
||||||
|
$user->score = option('user_initial_score');
|
||||||
|
$user->avatar = 0;
|
||||||
|
$user->password = app('cipher')->hash($data['password'], config('secure.salt'));
|
||||||
|
$user->ip = $ip;
|
||||||
|
$user->permission = User::SUPER_ADMIN;
|
||||||
|
$user->register_at = Carbon::now();
|
||||||
|
$user->last_sign_at = Carbon::now()->subDay();
|
||||||
|
$user->verified = true;
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$filesystem->put(storage_path('install.lock'), '');
|
||||||
|
|
||||||
|
return view('setup.wizard.finish');
|
||||||
|
}
|
||||||
|
}
|
441
app/Http/Controllers/SkinlibController.php
Executable file
441
app/Http/Controllers/SkinlibController.php
Executable file
@ -0,0 +1,441 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Auth;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Filesystem\FilesystemAdapter;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||||
|
use Storage;
|
||||||
|
|
||||||
|
class SkinlibController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware(function (Request $request, $next) {
|
||||||
|
/** @var User */
|
||||||
|
$user = $request->user();
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = $request->route('texture');
|
||||||
|
|
||||||
|
if ($texture->uploader != $user->uid && !$user->isAdmin()) {
|
||||||
|
return json(trans('skinlib.no-permission'), 1)
|
||||||
|
->setStatusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
})->only(['rename', 'privacy', 'type', 'delete']);
|
||||||
|
|
||||||
|
$this->middleware(function (Request $request, $next) {
|
||||||
|
/** @var User */
|
||||||
|
$user = $request->user();
|
||||||
|
/** @var Texture */
|
||||||
|
$texture = $request->route('texture');
|
||||||
|
|
||||||
|
if (!$texture->public) {
|
||||||
|
if (!Auth::check() || ($user->uid != $texture->uploader && !$user->isAdmin())) {
|
||||||
|
$statusCode = (int) option('status_code_for_private');
|
||||||
|
if ($statusCode === 404) {
|
||||||
|
abort($statusCode, trans('skinlib.show.deleted'));
|
||||||
|
} else {
|
||||||
|
abort(403, trans('skinlib.show.private'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
})->only(['show', 'info']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function library(Request $request)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// Available filters: skin, steve, alex, cape
|
||||||
|
$type = $request->input('filter', 'skin');
|
||||||
|
$uploader = $request->input('uploader');
|
||||||
|
$keyword = $request->input('keyword');
|
||||||
|
$sort = $request->input('sort', 'time');
|
||||||
|
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
|
||||||
|
|
||||||
|
return Texture::orderBy($sortBy, 'desc')
|
||||||
|
->when(
|
||||||
|
$type === 'skin',
|
||||||
|
fn (Builder $query) => $query->whereIn('type', ['steve', 'alex']),
|
||||||
|
fn (Builder $query) => $query->where('type', $type),
|
||||||
|
)
|
||||||
|
->when($keyword, fn (Builder $query, $keyword) => $query->like('name', $keyword))
|
||||||
|
->when($uploader, fn (Builder $query, $uploader) => $query->where('uploader', $uploader))
|
||||||
|
->when($user, function (Builder $query, User $user) {
|
||||||
|
if (!$user->isAdmin()) {
|
||||||
|
// use closure-style `where` clause to lift up SQL priority
|
||||||
|
return $query->where(function (Builder $query) use ($user) {
|
||||||
|
$query
|
||||||
|
->where('public', true)
|
||||||
|
->orWhere('uploader', $user->uid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function (Builder $query) {
|
||||||
|
// show public textures only to anonymous visitors
|
||||||
|
return $query->where('public', true);
|
||||||
|
})
|
||||||
|
->join('users', 'uid', 'uploader')
|
||||||
|
->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
|
||||||
|
->paginate(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Filter $filter, Texture $texture)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
/** @var FilesystemAdapter */
|
||||||
|
$disk = Storage::disk('textures');
|
||||||
|
|
||||||
|
if ($disk->missing($texture->hash)) {
|
||||||
|
if (option('auto_del_invalid_texture')) {
|
||||||
|
$texture->delete();
|
||||||
|
}
|
||||||
|
abort(404, trans('skinlib.show.deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$badges = [];
|
||||||
|
$uploader = $texture->owner;
|
||||||
|
if ($uploader) {
|
||||||
|
if ($uploader->isAdmin()) {
|
||||||
|
$badges[] = ['text' => 'STAFF', 'color' => 'primary'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$badges = $filter->apply('user_badges', $badges, [$uploader]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-8', 'md-4'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
['shared.previewer'],
|
||||||
|
['skinlib.widgets.show.side'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:skinlib.show', $grid);
|
||||||
|
|
||||||
|
return view('skinlib.show')
|
||||||
|
->with('texture', $texture)
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('extra', [
|
||||||
|
'download' => option('allow_downloading_texture'),
|
||||||
|
'currentUid' => $user ? $user->uid : 0,
|
||||||
|
'admin' => $user && $user->isAdmin(),
|
||||||
|
'inCloset' => $user && $user->closet()->where('tid', $texture->tid)->count() > 0,
|
||||||
|
'uploaderExists' => (bool) $uploader,
|
||||||
|
'nickname' => optional($uploader)->nickname ?? trans('general.unexistent-user'),
|
||||||
|
'report' => intval(option('reporter_score_modification', 0)),
|
||||||
|
'badges' => $badges,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function info(Texture $texture)
|
||||||
|
{
|
||||||
|
return $texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload(Filter $filter)
|
||||||
|
{
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-6', 'md-6'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
['skinlib.widgets.upload.input'],
|
||||||
|
['shared.previewer'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:skinlib.upload', $grid);
|
||||||
|
|
||||||
|
$converter = new GithubFlavoredMarkdownConverter();
|
||||||
|
|
||||||
|
return view('skinlib.upload')
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('extra', [
|
||||||
|
'rule' => ($regexp = option('texture_name_regexp'))
|
||||||
|
? trans('skinlib.upload.name-rule-regexp', compact('regexp'))
|
||||||
|
: trans('skinlib.upload.name-rule'),
|
||||||
|
'privacyNotice' => trans(
|
||||||
|
'skinlib.upload.private-score-notice',
|
||||||
|
['score' => option('private_score_per_storage')]
|
||||||
|
),
|
||||||
|
'score' => (int) auth()->user()->score,
|
||||||
|
'scorePublic' => (int) option('score_per_storage'),
|
||||||
|
'scorePrivate' => (int) option('private_score_per_storage'),
|
||||||
|
'closetItemCost' => (int) option('score_per_closet_item'),
|
||||||
|
'award' => (int) option('score_award_per_texture'),
|
||||||
|
'contentPolicy' => $converter->convertToHtml(option_localized('content_policy'))->getContent(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleUpload(
|
||||||
|
Request $request,
|
||||||
|
Filter $filter,
|
||||||
|
Dispatcher $dispatcher
|
||||||
|
) {
|
||||||
|
$file = $request->file('file');
|
||||||
|
if ($file && !$file->isValid()) {
|
||||||
|
Log::error($file->getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $request->validate([
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
option('texture_name_regexp') ? 'regex:'.option('texture_name_regexp') : 'string',
|
||||||
|
],
|
||||||
|
'file' => 'required|mimes:png|max:'.option('max_upload_file_size'),
|
||||||
|
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
|
||||||
|
'public' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @var UploadedFile */
|
||||||
|
$file = $filter->apply('uploaded_texture_file', $file);
|
||||||
|
|
||||||
|
$name = $data['name'];
|
||||||
|
$name = $filter->apply('uploaded_texture_name', $name, [$file]);
|
||||||
|
|
||||||
|
$can = $filter->apply('can_upload_texture', true, [$file, $name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $data['type'];
|
||||||
|
$size = getimagesize($file);
|
||||||
|
|
||||||
|
if ($size[0] % 64 != 0 || $size[1] % 32 != 0) {
|
||||||
|
$message = trans('skinlib.upload.invalid-size', [
|
||||||
|
'type' => $type === 'cape' ? trans('general.cape') : trans('general.skin'),
|
||||||
|
'width' => $size[0],
|
||||||
|
'height' => $size[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json($message, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ratio = $size[0] / $size[1];
|
||||||
|
if ($type == 'steve' || $type == 'alex') {
|
||||||
|
if ($ratio != 2 && $ratio != 1 || $type === 'alex' && $ratio === 2) {
|
||||||
|
$message = trans('skinlib.upload.invalid-size', [
|
||||||
|
'type' => trans('general.skin'),
|
||||||
|
'width' => $size[0],
|
||||||
|
'height' => $size[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json($message, 1);
|
||||||
|
}
|
||||||
|
} elseif ($type == 'cape') {
|
||||||
|
if ($ratio != 2) {
|
||||||
|
$message = trans('skinlib.upload.invalid-size', [
|
||||||
|
'type' => trans('general.cape'),
|
||||||
|
'width' => $size[0],
|
||||||
|
'height' => $size[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json($message, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = hash_file('sha256', $file);
|
||||||
|
$hash = $filter->apply('uploaded_texture_hash', $hash, [$file]);
|
||||||
|
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$duplicated = Texture::where('hash', $hash)
|
||||||
|
->where(
|
||||||
|
fn (Builder $query) => $query->where('public', true)->orWhere('uploader', $user->uid)
|
||||||
|
)
|
||||||
|
->first();
|
||||||
|
if ($duplicated) {
|
||||||
|
// if the texture already uploaded was set to private,
|
||||||
|
// then allow to re-upload it.
|
||||||
|
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = ceil($file->getSize() / 1024);
|
||||||
|
$isPublic = is_string($data['public'])
|
||||||
|
? $data['public'] === '1'
|
||||||
|
: $data['public'];
|
||||||
|
$cost = $size * (
|
||||||
|
$isPublic
|
||||||
|
? option('score_per_storage')
|
||||||
|
: option('private_score_per_storage')
|
||||||
|
);
|
||||||
|
$cost += option('score_per_closet_item');
|
||||||
|
$cost -= option('score_award_per_texture', 0);
|
||||||
|
if ($user->score < $cost) {
|
||||||
|
return json(trans('skinlib.upload.lack-score'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.uploading', [$file, $name, $hash]);
|
||||||
|
|
||||||
|
$texture = new Texture();
|
||||||
|
$texture->name = $name;
|
||||||
|
$texture->type = $type;
|
||||||
|
$texture->hash = $hash;
|
||||||
|
$texture->size = $size;
|
||||||
|
$texture->public = $isPublic;
|
||||||
|
$texture->uploader = $user->uid;
|
||||||
|
$texture->likes = 1;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
/** @var FilesystemAdapter */
|
||||||
|
$disk = Storage::disk('textures');
|
||||||
|
if ($disk->missing($hash)) {
|
||||||
|
$file->storePubliclyAs('', $hash, ['disk' => 'textures']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->score -= $cost;
|
||||||
|
$user->closet()->attach($texture->tid, ['item_name' => $name]);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.uploaded', [$texture, $file]);
|
||||||
|
|
||||||
|
return json(trans('skinlib.upload.success', ['name' => $name]), 0, [
|
||||||
|
'tid' => $texture->tid,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Texture $texture, Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
$can = $filter->apply('can_delete_texture', true, [$texture]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.deleting', [$texture]);
|
||||||
|
|
||||||
|
// check if file occupied
|
||||||
|
if (Texture::where('hash', $texture->hash)->count() === 1) {
|
||||||
|
Storage::disk('textures')->delete($texture->hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
$texture->delete();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.deleted', [$texture]);
|
||||||
|
|
||||||
|
return json(trans('skinlib.delete.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privacy(Texture $texture, Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
$can = $filter->apply('can_update_texture_privacy', true, [$texture]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploader = $texture->owner;
|
||||||
|
$score_diff = $texture->size
|
||||||
|
* (option('private_score_per_storage') - option('score_per_storage'))
|
||||||
|
* ($texture->public ? -1 : 1);
|
||||||
|
if ($texture->public && option('take_back_scores_after_deletion', true)) {
|
||||||
|
$score_diff -= option('score_award_per_texture', 0);
|
||||||
|
}
|
||||||
|
if ($uploader->score + $score_diff < 0) {
|
||||||
|
return json(trans('skinlib.upload.lack-score'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$texture->public) {
|
||||||
|
$duplicated = Texture::where('hash', $texture->hash)
|
||||||
|
->where('public', true)
|
||||||
|
->first();
|
||||||
|
if ($duplicated) {
|
||||||
|
return json(trans('skinlib.upload.repeated'), 2, ['tid' => $duplicated->tid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.privacy.updating', [$texture]);
|
||||||
|
|
||||||
|
$uploader->score += $score_diff;
|
||||||
|
$uploader->save();
|
||||||
|
|
||||||
|
$texture->public = !$texture->public;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.privacy.updated', [$texture]);
|
||||||
|
|
||||||
|
$message = trans('skinlib.privacy.success', [
|
||||||
|
'privacy' => (
|
||||||
|
$texture->public
|
||||||
|
? trans('general.public')
|
||||||
|
: trans('general.private')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json($message, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rename(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Texture $texture
|
||||||
|
) {
|
||||||
|
$data = $request->validate(['name' => [
|
||||||
|
'required',
|
||||||
|
option('texture_name_regexp')
|
||||||
|
? 'regex:'.option('texture_name_regexp')
|
||||||
|
: 'string',
|
||||||
|
]]);
|
||||||
|
$name = $data['name'];
|
||||||
|
|
||||||
|
$can = $filter->apply('can_update_texture_name', true, [$texture, $name]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.name.updating', [$texture, $name]);
|
||||||
|
|
||||||
|
$old = $texture->replicate();
|
||||||
|
$texture->name = $name;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.name.updated', [$texture, $old]);
|
||||||
|
|
||||||
|
return json(trans('skinlib.rename.success', ['name' => $name]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function type(
|
||||||
|
Request $request,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter,
|
||||||
|
Texture $texture
|
||||||
|
) {
|
||||||
|
$data = $request->validate([
|
||||||
|
'type' => ['required', Rule::in(['steve', 'alex', 'cape'])],
|
||||||
|
]);
|
||||||
|
$type = $data['type'];
|
||||||
|
|
||||||
|
$can = $filter->apply('can_update_texture_type', true, [$texture, $type]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.type.updating', [$texture, $type]);
|
||||||
|
|
||||||
|
$old = $texture->replicate();
|
||||||
|
$texture->type = $type;
|
||||||
|
$texture->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('texture.type.updated', [$texture, $old]);
|
||||||
|
|
||||||
|
return json(trans('skinlib.model.success', ['model' => $type]), 0);
|
||||||
|
}
|
||||||
|
}
|
177
app/Http/Controllers/TextureController.php
Executable file
177
app/Http/Controllers/TextureController.php
Executable file
@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Blessing\Minecraft;
|
||||||
|
use Cache;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Image;
|
||||||
|
use Storage;
|
||||||
|
|
||||||
|
class TextureController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('cache.headers:public;max_age='.option('cache_expire_time'))
|
||||||
|
->only(['json']);
|
||||||
|
|
||||||
|
$this->middleware('cache.headers:etag;public;max_age='.option('cache_expire_time'))
|
||||||
|
->only([
|
||||||
|
'preview',
|
||||||
|
'raw',
|
||||||
|
'texture',
|
||||||
|
'avatarByPlayer',
|
||||||
|
'avatarByUser',
|
||||||
|
'avatarByTexture',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function json($player)
|
||||||
|
{
|
||||||
|
$player = Player::where('name', $player)->firstOrFail();
|
||||||
|
$isBanned = $player->user->permission === User::BANNED;
|
||||||
|
abort_if($isBanned, 403, trans('general.player-banned'));
|
||||||
|
|
||||||
|
return response()->json($player)->setLastModified($player->last_modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function previewByHash(Minecraft $minecraft, Request $request, $hash)
|
||||||
|
{
|
||||||
|
$texture = Texture::where('hash', $hash)->firstOrFail();
|
||||||
|
|
||||||
|
return $this->preview($minecraft, $request, $texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preview(Minecraft $minecraft, Request $request, Texture $texture)
|
||||||
|
{
|
||||||
|
$tid = $texture->tid;
|
||||||
|
$hash = $texture->hash;
|
||||||
|
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
|
||||||
|
$format = $usePNG ? 'png' : 'webp';
|
||||||
|
|
||||||
|
$disk = Storage::disk('textures');
|
||||||
|
abort_if($disk->missing($hash), 404);
|
||||||
|
|
||||||
|
$height = (int) $request->query('height', 200);
|
||||||
|
$now = Carbon::now();
|
||||||
|
$response = Cache::remember(
|
||||||
|
'preview-t'.$tid."-$format",
|
||||||
|
option('enable_preview_cache') ? $now->addYear() : $now->addMinute(),
|
||||||
|
function () use ($minecraft, $disk, $texture, $hash, $height, $usePNG) {
|
||||||
|
$file = $disk->get($hash);
|
||||||
|
if ($texture->type === 'cape') {
|
||||||
|
$image = $minecraft->renderCape($file, $height);
|
||||||
|
} else {
|
||||||
|
$image = $minecraft->renderSkin($file, 12, $texture->type === 'alex');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastModified = $disk->lastModified($hash);
|
||||||
|
|
||||||
|
return Image::make($image)
|
||||||
|
->response($usePNG ? 'png' : 'webp', 100)
|
||||||
|
->setLastModified(Carbon::createFromTimestamp($lastModified));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function raw($tid)
|
||||||
|
{
|
||||||
|
abort_unless(option('allow_downloading_texture'), 403);
|
||||||
|
|
||||||
|
$texture = Texture::findOrFail($tid);
|
||||||
|
|
||||||
|
return $this->texture($texture->hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function texture(string $hash)
|
||||||
|
{
|
||||||
|
$disk = Storage::disk('textures');
|
||||||
|
abort_if($disk->missing($hash), 404);
|
||||||
|
|
||||||
|
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
|
||||||
|
|
||||||
|
return response($disk->get($hash))
|
||||||
|
->withHeaders([
|
||||||
|
'Content-Type' => 'image/png',
|
||||||
|
'Content-Length' => $disk->size($hash),
|
||||||
|
])
|
||||||
|
->setLastModified($lastModified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function avatarByPlayer(Minecraft $minecraft, Request $request, $name)
|
||||||
|
{
|
||||||
|
$player = Player::where('name', $name)->firstOrFail();
|
||||||
|
|
||||||
|
return $this->avatar($minecraft, $request, $player->skin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function avatarByUser(Minecraft $minecraft, Request $request, $uid)
|
||||||
|
{
|
||||||
|
$texture = Texture::find(optional(User::find($uid))->avatar);
|
||||||
|
|
||||||
|
return $this->avatar($minecraft, $request, $texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function avatarByHash(Minecraft $minecraft, Request $request, $hash)
|
||||||
|
{
|
||||||
|
$texture = Texture::where('hash', $hash)->first();
|
||||||
|
|
||||||
|
return $this->avatar($minecraft, $request, $texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function avatarByTexture(Minecraft $minecraft, Request $request, $tid)
|
||||||
|
{
|
||||||
|
$texture = Texture::find($tid);
|
||||||
|
|
||||||
|
return $this->avatar($minecraft, $request, $texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function avatar(Minecraft $minecraft, Request $request, ?Texture $texture)
|
||||||
|
{
|
||||||
|
if (!empty($texture) && $texture->type !== 'steve' && $texture->type !== 'alex') {
|
||||||
|
return abort(422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = (int) $request->query('size', 100);
|
||||||
|
$mode = $request->has('3d') ? '3d' : '2d';
|
||||||
|
$usePNG = $request->has('png') || !(imagetypes() & IMG_WEBP);
|
||||||
|
$format = $usePNG ? 'png' : 'webp';
|
||||||
|
|
||||||
|
$disk = Storage::disk('textures');
|
||||||
|
if (is_null($texture) || $disk->missing($texture->hash)) {
|
||||||
|
return Image::make(resource_path("misc/textures/avatar$mode.png"))
|
||||||
|
->resize($size, $size)
|
||||||
|
->response($usePNG ? 'png' : 'webp', 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = $texture->hash;
|
||||||
|
$now = Carbon::now();
|
||||||
|
$response = Cache::remember(
|
||||||
|
'avatar-'.$mode.'-t'.$texture->tid.'-s'.$size."-$format",
|
||||||
|
option('enable_avatar_cache') ? $now->addYear() : $now->addMinute(),
|
||||||
|
function () use ($minecraft, $disk, $hash, $size, $mode, $usePNG) {
|
||||||
|
$file = $disk->get($hash);
|
||||||
|
if ($mode === '3d') {
|
||||||
|
$image = $minecraft->render3dAvatar($file, 25);
|
||||||
|
} else {
|
||||||
|
$image = $minecraft->render2dAvatar($file, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastModified = Carbon::createFromTimestamp($disk->lastModified($hash));
|
||||||
|
|
||||||
|
return Image::make($image)
|
||||||
|
->resize($size, $size)
|
||||||
|
->response($usePNG ? 'png' : 'webp', 100)
|
||||||
|
->setLastModified($lastModified);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
70
app/Http/Controllers/TranslationsController.php
Executable file
70
app/Http/Controllers/TranslationsController.php
Executable file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Translations\JavaScript;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Spatie\TranslationLoader\LanguageLine;
|
||||||
|
|
||||||
|
class TranslationsController extends Controller
|
||||||
|
{
|
||||||
|
public function list()
|
||||||
|
{
|
||||||
|
return LanguageLine::paginate(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request, Application $app, JavaScript $js)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'group' => 'required|string',
|
||||||
|
'key' => 'required|string',
|
||||||
|
'text' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$line = new LanguageLine();
|
||||||
|
$line->group = $data['group'];
|
||||||
|
$line->key = $data['key'];
|
||||||
|
$line->setTranslation($app->getLocale(), $data['text']);
|
||||||
|
$line->save();
|
||||||
|
|
||||||
|
if ($data['group'] === 'front-end') {
|
||||||
|
$js->resetTime($app->getLocale());
|
||||||
|
}
|
||||||
|
$request->session()->put('success', true);
|
||||||
|
|
||||||
|
return redirect('/admin/i18n');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
Request $request,
|
||||||
|
Application $app,
|
||||||
|
JavaScript $js,
|
||||||
|
LanguageLine $line
|
||||||
|
) {
|
||||||
|
$data = $request->validate(['text' => 'required|string']);
|
||||||
|
|
||||||
|
$line->setTranslation($app->getLocale(), $data['text']);
|
||||||
|
$line->save();
|
||||||
|
|
||||||
|
if ($line->group === 'front-end') {
|
||||||
|
$js->resetTime($app->getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(trans('admin.i18n.updated'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(
|
||||||
|
Application $app,
|
||||||
|
JavaScript $js,
|
||||||
|
LanguageLine $line
|
||||||
|
) {
|
||||||
|
$line->delete();
|
||||||
|
|
||||||
|
if ($line->group === 'front-end') {
|
||||||
|
$js->resetTime($app->getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(trans('admin.i18n.deleted'), 0);
|
||||||
|
}
|
||||||
|
}
|
93
app/Http/Controllers/UpdateController.php
Executable file
93
app/Http/Controllers/UpdateController.php
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Unzip;
|
||||||
|
use Cache;
|
||||||
|
use Composer\CaBundle\CaBundle;
|
||||||
|
use Composer\Semver\Comparator;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class UpdateController extends Controller
|
||||||
|
{
|
||||||
|
public const SPEC = 2;
|
||||||
|
|
||||||
|
public function showUpdatePage()
|
||||||
|
{
|
||||||
|
$info = $this->getUpdateInfo();
|
||||||
|
$canUpdate = $this->canUpdate(Arr::get($info, 'info'));
|
||||||
|
|
||||||
|
return view('admin.update', [
|
||||||
|
'info' => [
|
||||||
|
'latest' => Arr::get($info, 'info.latest'),
|
||||||
|
'current' => config('app.version'),
|
||||||
|
],
|
||||||
|
'error' => Arr::get($info, 'error', $canUpdate['reason']),
|
||||||
|
'can_update' => $canUpdate['can'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(Unzip $unzip, Filesystem $filesystem)
|
||||||
|
{
|
||||||
|
$info = $this->getUpdateInfo();
|
||||||
|
if (!$info['ok'] || !$this->canUpdate($info['info'])['can']) {
|
||||||
|
return json(trans('admin.update.info.up-to-date'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $info['info'];
|
||||||
|
$path = tempnam(sys_get_temp_dir(), 'bs');
|
||||||
|
|
||||||
|
$response = Http::withOptions([
|
||||||
|
'sink' => $path,
|
||||||
|
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||||
|
])->get($info['url']);
|
||||||
|
|
||||||
|
if ($response->ok()) {
|
||||||
|
$unzip->extract($path, base_path());
|
||||||
|
|
||||||
|
// Delete options cache. This allows us to update the version.
|
||||||
|
$filesystem->delete(storage_path('options.php'));
|
||||||
|
|
||||||
|
return json(trans('admin.update.complete'), 0);
|
||||||
|
} else {
|
||||||
|
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getUpdateInfo()
|
||||||
|
{
|
||||||
|
$response = Http::withOptions([
|
||||||
|
'verify' => CaBundle::getSystemCaRootBundlePath(),
|
||||||
|
])->get(config('app.update_source'));
|
||||||
|
|
||||||
|
if ($response->ok()) {
|
||||||
|
$info = $response->json();
|
||||||
|
if (Arr::get($info, 'spec') === self::SPEC) {
|
||||||
|
return ['ok' => true, 'info' => $info];
|
||||||
|
} else {
|
||||||
|
return ['ok' => false, 'error' => trans('admin.update.errors.spec')];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ['ok' => false, 'error' => 'HTTP status code: '.$response->status()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function canUpdate($info = [])
|
||||||
|
{
|
||||||
|
$php = Arr::get($info, 'php');
|
||||||
|
preg_match('/(\d+\.\d+\.\d+)/', PHP_VERSION, $matches);
|
||||||
|
$version = $matches[1];
|
||||||
|
if (Comparator::lessThan($version, $php)) {
|
||||||
|
return [
|
||||||
|
'can' => false,
|
||||||
|
'reason' => trans('admin.update.errors.php', ['version' => $php]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$can = Comparator::greaterThan(Arr::get($info, 'latest'), config('app.version'));
|
||||||
|
|
||||||
|
return ['can' => $can, 'reason' => ''];
|
||||||
|
}
|
||||||
|
}
|
360
app/Http/Controllers/UserController.php
Executable file
360
app/Http/Controllers/UserController.php
Executable file
@ -0,0 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Events\UserProfileUpdated;
|
||||||
|
use App\Mail\EmailVerification;
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
use Auth;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Blessing\Rejection;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||||
|
use Mail;
|
||||||
|
use Session;
|
||||||
|
use URL;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user
|
||||||
|
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Filter $filter)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
[$min, $max] = explode(',', option('sign_score'));
|
||||||
|
$scoreIntro = trans('user.score-intro.introduction', [
|
||||||
|
'initial_score' => option('user_initial_score'),
|
||||||
|
'score-from' => $min,
|
||||||
|
'score-to' => $max,
|
||||||
|
'return-score' => option('return_score')
|
||||||
|
? trans('user.score-intro.will-return-score')
|
||||||
|
: trans('user.score-intro.no-return-score'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-7', 'md-5'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'user.widgets.email-verification',
|
||||||
|
'user.widgets.dashboard.usage',
|
||||||
|
],
|
||||||
|
['user.widgets.dashboard.announcement'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:user.index', $grid);
|
||||||
|
|
||||||
|
$converter = new GithubFlavoredMarkdownConverter();
|
||||||
|
|
||||||
|
return view('user.index')->with([
|
||||||
|
'score_intro' => $scoreIntro,
|
||||||
|
'rates' => [
|
||||||
|
'storage' => option('score_per_storage'),
|
||||||
|
'player' => option('score_per_player'),
|
||||||
|
'closet' => option('score_per_closet_item'),
|
||||||
|
],
|
||||||
|
'announcement' => $converter->convertToHtml(option_localized('announcement')),
|
||||||
|
'grid' => $grid,
|
||||||
|
'extra' => ['unverified' => option('require_verification') && !$user->verified],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scoreInfo()
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'user' => [
|
||||||
|
'score' => $user->score,
|
||||||
|
'lastSignAt' => $user->last_sign_at,
|
||||||
|
],
|
||||||
|
'rate' => [
|
||||||
|
'storage' => (int) option('score_per_storage'),
|
||||||
|
'players' => (int) option('score_per_player'),
|
||||||
|
],
|
||||||
|
'usage' => [
|
||||||
|
'players' => $user->players()->count(),
|
||||||
|
'storage' => (int) Texture::where('uploader', $user->uid)->sum('size'),
|
||||||
|
],
|
||||||
|
'signAfterZero' => (bool) option('sign_after_zero'),
|
||||||
|
'signGapTime' => (int) option('sign_gap_time'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sign(Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$can = $filter->apply('can_sign', true);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastSignTime = Carbon::parse($user->last_sign_at);
|
||||||
|
$remainingTime = option('sign_after_zero')
|
||||||
|
? Carbon::now()->diffInSeconds(
|
||||||
|
$lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
: Carbon::now()->diffInSeconds(
|
||||||
|
$lastSignTime->addHours((int) option('sign_gap_time')),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($remainingTime <= 0) {
|
||||||
|
[$min, $max] = explode(',', option('sign_score'));
|
||||||
|
$acquiredScore = rand((int) $min, (int) $max);
|
||||||
|
$acquiredScore = $filter->apply('sign_score', $acquiredScore);
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.sign.before', [$acquiredScore]);
|
||||||
|
|
||||||
|
$user->score += $acquiredScore;
|
||||||
|
$user->last_sign_at = Carbon::now();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.sign.after', [$acquiredScore]);
|
||||||
|
|
||||||
|
return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [
|
||||||
|
'score' => $user->score,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return json('', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendVerificationEmail()
|
||||||
|
{
|
||||||
|
if (!option('require_verification')) {
|
||||||
|
return json(trans('user.verification.disabled'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit of 60s
|
||||||
|
$remain = 60 + session('last_mail_time', 0) - time();
|
||||||
|
|
||||||
|
if ($remain > 0) {
|
||||||
|
return json(trans('user.verification.frequent-mail'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if ($user->verified) {
|
||||||
|
return json(trans('user.verification.verified'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = URL::signedRoute('auth.verify', ['user' => $user], null, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($user->email)->send(new EmailVerification(url($url)));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::put('last_mail_time', time());
|
||||||
|
|
||||||
|
return json(trans('user.verification.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile(Filter $filter)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
$grid = [
|
||||||
|
'layout' => [
|
||||||
|
['md-6', 'md-6'],
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'user.widgets.profile.avatar',
|
||||||
|
'user.widgets.profile.password',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'user.widgets.profile.nickname',
|
||||||
|
'user.widgets.profile.email',
|
||||||
|
'user.widgets.profile.delete-account',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$grid = $filter->apply('grid:user.profile', $grid);
|
||||||
|
|
||||||
|
return view('user.profile')
|
||||||
|
->with('user', $user)
|
||||||
|
->with('grid', $grid)
|
||||||
|
->with('site_name', option_localized('site_name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
|
||||||
|
{
|
||||||
|
$action = $request->input('action', '');
|
||||||
|
/** @var User */
|
||||||
|
$user = Auth::user();
|
||||||
|
$addition = $request->except('action');
|
||||||
|
|
||||||
|
$can = $filter->apply('user_can_edit_profile', true, [$action, $addition]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.profile.updating', [$user, $action, $addition]);
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'nickname':
|
||||||
|
$request->validate(['new_nickname' => 'required']);
|
||||||
|
|
||||||
|
$nickname = $request->input('new_nickname');
|
||||||
|
$user->nickname = $nickname;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
||||||
|
event(new UserProfileUpdated($action, $user));
|
||||||
|
|
||||||
|
return json(trans('user.profile.nickname.success', ['nickname' => $nickname]), 0);
|
||||||
|
|
||||||
|
case 'password':
|
||||||
|
$request->validate([
|
||||||
|
'current_password' => 'required|min:6|max:32',
|
||||||
|
'new_password' => 'required|min:8|max:32',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$user->verifyPassword($request->input('current_password'))) {
|
||||||
|
return json(trans('user.profile.password.wrong-password'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->changePassword($request->input('new_password'));
|
||||||
|
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
||||||
|
event(new UserProfileUpdated($action, $user));
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
return json(trans('user.profile.password.success'), 0);
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
$data = $request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required|min:6|max:32',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (User::where('email', $data['email'])->count() > 0) {
|
||||||
|
return json(trans('user.profile.email.existed'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->verifyPassword($data['password'])) {
|
||||||
|
return json(trans('user.profile.email.wrong-password'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->email = $data['email'];
|
||||||
|
$user->verified = false;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.profile.updated', [$user, $action, $addition]);
|
||||||
|
event(new UserProfileUpdated($action, $user));
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
return json(trans('user.profile.email.success'), 0);
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$request->validate([
|
||||||
|
'password' => 'required|min:6|max:32',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
return json(trans('user.profile.delete.admin'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->verifyPassword($request->input('password'))) {
|
||||||
|
return json(trans('user.profile.delete.wrong-password'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.deleting', [$user]);
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
$dispatcher->dispatch('user.deleted', [$user]);
|
||||||
|
session()->flush();
|
||||||
|
|
||||||
|
return json(trans('user.profile.delete.success'), 0);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return json(trans('general.illegal-parameters'), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAvatar(Request $request, Filter $filter, Dispatcher $dispatcher)
|
||||||
|
{
|
||||||
|
$request->validate(['tid' => 'required|integer']);
|
||||||
|
$tid = $request->input('tid');
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);
|
||||||
|
if ($can instanceof Rejection) {
|
||||||
|
return json($can->getReason(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.avatar.updating', [$user, $tid]);
|
||||||
|
|
||||||
|
if ($tid == 0) {
|
||||||
|
$user->avatar = 0;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
|
||||||
|
|
||||||
|
return json(trans('user.profile.avatar.success'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$texture = Texture::find($tid);
|
||||||
|
if ($texture) {
|
||||||
|
if ($texture->type == 'cape') {
|
||||||
|
return json(trans('user.profile.avatar.wrong-type'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$texture->public &&
|
||||||
|
$user->uid !== $texture->uploader &&
|
||||||
|
!$user->isAdmin()
|
||||||
|
) {
|
||||||
|
return json(trans('skinlib.show.private'), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->avatar = $tid;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$dispatcher->dispatch('user.avatar.updated', [$user, $tid]);
|
||||||
|
|
||||||
|
return json(trans('user.profile.avatar.success'), 0);
|
||||||
|
} else {
|
||||||
|
return json(trans('skinlib.non-existent'), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleDarkMode()
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
$user->is_dark_mode = !$user->is_dark_mode;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
174
app/Http/Controllers/UsersManagementController.php
Executable file
174
app/Http/Controllers/UsersManagementController.php
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
<?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();
|
||||||
|
|
||||||
|
if ($permission === User::BANNED) {
|
||||||
|
$dispatcher->dispatch('user.banned', [$user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
71
app/Http/Kernel.php
Executable file
71
app/Http/Kernel.php
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||||
|
|
||||||
|
class Kernel extends HttpKernel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The application's global HTTP middleware stack.
|
||||||
|
*
|
||||||
|
* These middleware are run during every request to your application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $middleware = [
|
||||||
|
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||||
|
\Illuminate\Foundation\Http\Middleware\TrimStrings::class,
|
||||||
|
\App\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||||
|
\App\Http\Middleware\DetectLanguagePrefer::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application's route middleware groups.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $middlewareGroups = [
|
||||||
|
'web' => [
|
||||||
|
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
|
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
|
||||||
|
\App\Http\Middleware\EnforceEverGreen::class,
|
||||||
|
\App\Http\Middleware\RedirectToSetup::class,
|
||||||
|
'bindings',
|
||||||
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'bindings',
|
||||||
|
],
|
||||||
|
|
||||||
|
'authorize' => [
|
||||||
|
'auth:web',
|
||||||
|
\App\Http\Middleware\RejectBannedUser::class,
|
||||||
|
\App\Http\Middleware\EnsureEmailFilled::class,
|
||||||
|
\App\Http\Middleware\FireUserAuthenticated::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application's route middleware.
|
||||||
|
*
|
||||||
|
* These middleware may be assigned to groups or used individually.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $routeMiddleware = [
|
||||||
|
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||||
|
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||||
|
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
|
'role' => \App\Http\Middleware\CheckRole::class,
|
||||||
|
'setup' => \App\Http\Middleware\CheckInstallation::class,
|
||||||
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
|
'verified' => \App\Http\Middleware\CheckUserVerified::class,
|
||||||
|
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class,
|
||||||
|
'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
|
||||||
|
];
|
||||||
|
}
|
20
app/Http/Middleware/Authenticate.php
Executable file
20
app/Http/Middleware/Authenticate.php
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||||
|
|
||||||
|
class Authenticate extends Middleware
|
||||||
|
{
|
||||||
|
protected function redirectTo($request)
|
||||||
|
{
|
||||||
|
if (!$request->expectsJson()) {
|
||||||
|
session([
|
||||||
|
'last_requested_path' => $request->fullUrl(),
|
||||||
|
'msg' => trans('auth.check.anonymous'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return '/auth/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
app/Http/Middleware/CheckInstallation.php
Executable file
18
app/Http/Middleware/CheckInstallation.php
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class CheckInstallation
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
$hasLock = resolve(Filesystem::class)->exists(storage_path('install.lock'));
|
||||||
|
if ($hasLock) {
|
||||||
|
return response()->view('setup.locked');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
24
app/Http/Middleware/CheckRole.php
Executable file
24
app/Http/Middleware/CheckRole.php
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CheckRole
|
||||||
|
{
|
||||||
|
protected $roles = [
|
||||||
|
'banned' => -1,
|
||||||
|
'normal' => 0,
|
||||||
|
'admin' => 1,
|
||||||
|
'super-admin' => 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(Request $request, Closure $next, $role)
|
||||||
|
{
|
||||||
|
$permission = $request->user()->permission;
|
||||||
|
abort_if($permission < $this->roles[$role], 403);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
13
app/Http/Middleware/CheckUserVerified.php
Executable file
13
app/Http/Middleware/CheckUserVerified.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
class CheckUserVerified
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
abort_if(option('require_verification') && !auth()->user()->verified, 403, trans('auth.check.verified'));
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
25
app/Http/Middleware/ConvertEmptyStringsToNull.php
Executable file
25
app/Http/Middleware/ConvertEmptyStringsToNull.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull as Converter;
|
||||||
|
|
||||||
|
class ConvertEmptyStringsToNull extends Converter
|
||||||
|
{
|
||||||
|
protected $excepts = [
|
||||||
|
'admin/options',
|
||||||
|
'admin/score',
|
||||||
|
'admin/resource',
|
||||||
|
'admin/customize',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if (in_array($request->path(), $this->excepts)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
}
|
35
app/Http/Middleware/DetectLanguagePrefer.php
Executable file
35
app/Http/Middleware/DetectLanguagePrefer.php
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class DetectLanguagePrefer
|
||||||
|
{
|
||||||
|
public function handle(Request $request, \Closure $next)
|
||||||
|
{
|
||||||
|
$locale = $request->input('lang')
|
||||||
|
?? $request->cookie('locale')
|
||||||
|
?? $request->getPreferredLanguage();
|
||||||
|
if (
|
||||||
|
($info = Arr::get(config('locales'), $locale)) &&
|
||||||
|
($alias = Arr::get($info, 'alias'))
|
||||||
|
) {
|
||||||
|
$locale = $alias;
|
||||||
|
}
|
||||||
|
$locale ?? app()->getLocale();
|
||||||
|
if (!Arr::has(config('locales'), $locale)) {
|
||||||
|
$locale = config('app.fallback_locale');
|
||||||
|
}
|
||||||
|
|
||||||
|
app()->setLocale($locale);
|
||||||
|
|
||||||
|
/** @var Response */
|
||||||
|
$response = $next($request);
|
||||||
|
$response->cookie('locale', $locale, 120);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
25
app/Http/Middleware/EnforceEverGreen.php
Executable file
25
app/Http/Middleware/EnforceEverGreen.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Exceptions\PrettyPageException;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class EnforceEverGreen
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
$userAgent = $request->userAgent();
|
||||||
|
|
||||||
|
preg_match('/Chrome\/(\d+)/', $userAgent, $matches);
|
||||||
|
$isOldChrome = Arr::has($matches, 1) && $matches[1] < 55;
|
||||||
|
|
||||||
|
if ($isOldChrome || Str::contains($userAgent, ['Trident', 'MSIE'])) {
|
||||||
|
throw new PrettyPageException(trans('errors.http.ie'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
19
app/Http/Middleware/EnsureEmailFilled.php
Executable file
19
app/Http/Middleware/EnsureEmailFilled.php
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class EnsureEmailFilled
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if ($request->user()->email != '' && $request->is('auth/bind')) {
|
||||||
|
return redirect('/user');
|
||||||
|
} elseif ($request->user()->email == '' && !$request->is('auth/bind')) {
|
||||||
|
return redirect('/auth/bind');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
17
app/Http/Middleware/FireUserAuthenticated.php
Executable file
17
app/Http/Middleware/FireUserAuthenticated.php
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class FireUserAuthenticated
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if (auth()->check()) {
|
||||||
|
event(new \App\Events\UserAuthenticated($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
13
app/Http/Middleware/RedirectIfAuthenticated.php
Executable file
13
app/Http/Middleware/RedirectIfAuthenticated.php
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
|
|
||||||
|
class RedirectIfAuthenticated
|
||||||
|
{
|
||||||
|
public function handle($request, \Closure $next)
|
||||||
|
{
|
||||||
|
return Auth::check() ? redirect('user') : $next($request);
|
||||||
|
}
|
||||||
|
}
|
33
app/Http/Middleware/RedirectToSetup.php
Executable file
33
app/Http/Middleware/RedirectToSetup.php
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Composer\Semver\Comparator;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
class RedirectToSetup
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
$version = config('app.version');
|
||||||
|
$hasLock = resolve(Filesystem::class)->exists(storage_path('install.lock'));
|
||||||
|
|
||||||
|
// If lock isn't existed, it means that BS isn't installed.
|
||||||
|
// Database is unavailable at this time, so we should disable the loader.
|
||||||
|
if (!$hasLock) {
|
||||||
|
config(['translation-loader.translation_loaders' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasLock && !$request->is('setup*') && Comparator::greaterThan($version, option('version', $version))) {
|
||||||
|
Artisan::call('update');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasLock || $request->is('setup*')) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect('/setup');
|
||||||
|
}
|
||||||
|
}
|
25
app/Http/Middleware/RejectBannedUser.php
Executable file
25
app/Http/Middleware/RejectBannedUser.php
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class RejectBannedUser
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if ($request->user()->permission == User::BANNED) {
|
||||||
|
if ($request->expectsJson()) {
|
||||||
|
$response = json(trans('auth.check.banned'), -1);
|
||||||
|
$response->setStatusCode(403);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
} else {
|
||||||
|
abort(403, trans('auth.check.banned'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
57
app/Http/View/Composers/FootComposer.php
Executable file
57
app/Http/View/Composers/FootComposer.php
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use App\Services\Translations\JavaScript;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class FootComposer
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected JavaScript $javascript;
|
||||||
|
|
||||||
|
protected Dispatcher $dispatcher;
|
||||||
|
|
||||||
|
protected Filter $filter;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Request $request,
|
||||||
|
JavaScript $javascript,
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$this->request = $request;
|
||||||
|
$this->javascript = $javascript;
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
$this->injectJavaScript($view);
|
||||||
|
$this->addExtra($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function injectJavaScript(View $view)
|
||||||
|
{
|
||||||
|
$scripts = [];
|
||||||
|
$scripts = $this->filter->apply('scripts', $scripts);
|
||||||
|
|
||||||
|
$view->with([
|
||||||
|
'i18n' => $this->javascript->generate(app()->getLocale()),
|
||||||
|
'scripts' => $scripts,
|
||||||
|
'inline_js' => option('custom_js'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addExtra(View $view)
|
||||||
|
{
|
||||||
|
$content = [];
|
||||||
|
$this->dispatcher->dispatch(new \App\Events\RenderingFooter($content));
|
||||||
|
$view->with('extra_foot', $content);
|
||||||
|
}
|
||||||
|
}
|
107
app/Http/View/Composers/HeadComposer.php
Executable file
107
app/Http/View/Composers/HeadComposer.php
Executable file
@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class HeadComposer
|
||||||
|
{
|
||||||
|
protected Dispatcher $dispatcher;
|
||||||
|
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected Filter $filter;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
Dispatcher $dispatcher,
|
||||||
|
Request $request,
|
||||||
|
Filter $filter
|
||||||
|
) {
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->request = $request;
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
$this->addFavicon($view);
|
||||||
|
$this->applyThemeColor($view);
|
||||||
|
$this->seo($view);
|
||||||
|
$this->injectStyles($view);
|
||||||
|
$this->addExtra($view);
|
||||||
|
$this->serializeGlobals($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFavicon(View $view)
|
||||||
|
{
|
||||||
|
$url = option('favicon_url', config('options.favicon_url'));
|
||||||
|
$url = Str::startsWith($url, 'http') ? $url : url($url);
|
||||||
|
$view->with('favicon', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyThemeColor(View $view)
|
||||||
|
{
|
||||||
|
$colors = [
|
||||||
|
'primary' => '#007bff',
|
||||||
|
'secondary' => '#6c757d',
|
||||||
|
'success' => '#28a745',
|
||||||
|
'warning' => '#ffc107',
|
||||||
|
'danger' => '#dc3545',
|
||||||
|
'navy' => '#001f3f',
|
||||||
|
'olive' => '#3d9970',
|
||||||
|
'lime' => '#01ff70',
|
||||||
|
'fuchsia' => '#f012be',
|
||||||
|
'maroon' => '#d81b60',
|
||||||
|
'indigo' => '#6610f2',
|
||||||
|
'purple' => '#6f42c1',
|
||||||
|
'pink' => '#e83e8c',
|
||||||
|
'orange' => '#fd7e14',
|
||||||
|
'teal' => '#20c997',
|
||||||
|
'cyan' => '#17a2b8',
|
||||||
|
'gray' => '#6c757d',
|
||||||
|
];
|
||||||
|
$view->with('theme_color', Arr::get($colors, option('navbar_color')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function seo(View $view)
|
||||||
|
{
|
||||||
|
$view->with('seo', [
|
||||||
|
'keywords' => option('meta_keywords'),
|
||||||
|
'description' => option('meta_description'),
|
||||||
|
'extra' => option('meta_extras'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function injectStyles(View $view)
|
||||||
|
{
|
||||||
|
$links = [];
|
||||||
|
$links = $this->filter->apply('head_links', $links);
|
||||||
|
$view->with('links', $links);
|
||||||
|
$view->with('inline_css', option('custom_css'));
|
||||||
|
$view->with('custom_cdn_host', option('cdn_address'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addExtra(View $view)
|
||||||
|
{
|
||||||
|
$content = [];
|
||||||
|
$this->dispatcher->dispatch(new \App\Events\RenderingHeader($content));
|
||||||
|
$view->with('extra_head', $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serializeGlobals(View $view)
|
||||||
|
{
|
||||||
|
$blessing = [
|
||||||
|
'version' => config('app.version'),
|
||||||
|
'locale' => config('app.locale'),
|
||||||
|
'base_url' => url('/'),
|
||||||
|
'site_name' => option_localized('site_name'),
|
||||||
|
'route' => request()->path(),
|
||||||
|
];
|
||||||
|
$view->with('blessing', $blessing);
|
||||||
|
}
|
||||||
|
}
|
38
app/Http/View/Composers/LanguagesMenuComposer.php
Executable file
38
app/Http/View/Composers/LanguagesMenuComposer.php
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class LanguagesMenuComposer
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
$query = $this->request->query();
|
||||||
|
$path = $this->request->path();
|
||||||
|
|
||||||
|
$langs = collect(config('locales'))
|
||||||
|
->reject(fn ($locale) => Arr::has($locale, 'alias'))
|
||||||
|
->map(function ($locale, $id) use ($query, $path) {
|
||||||
|
$query = array_merge($query, ['lang' => $id]);
|
||||||
|
$locale['url'] = url($path.'?'.http_build_query($query));
|
||||||
|
|
||||||
|
return $locale;
|
||||||
|
});
|
||||||
|
|
||||||
|
$current = 'locales.'.app()->getLocale();
|
||||||
|
$view->with([
|
||||||
|
'current' => config($current.'.name'),
|
||||||
|
'langs' => $langs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
105
app/Http/View/Composers/SideMenuComposer.php
Executable file
105
app/Http/View/Composers/SideMenuComposer.php
Executable file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use App\Events;
|
||||||
|
use App\Services\Plugin;
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class SideMenuComposer
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected Filter $filter;
|
||||||
|
|
||||||
|
public function __construct(Request $request, Filter $filter)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
$type = $view->gatherData()['type'];
|
||||||
|
|
||||||
|
$menu = config('menu');
|
||||||
|
switch ($type) {
|
||||||
|
case 'user':
|
||||||
|
event(new Events\ConfigureUserMenu($menu));
|
||||||
|
break;
|
||||||
|
case 'explore':
|
||||||
|
event(new Events\ConfigureExploreMenu($menu));
|
||||||
|
break;
|
||||||
|
case 'admin':
|
||||||
|
event(new Events\ConfigureAdminMenu($menu));
|
||||||
|
$menu['admin'] = $this->collectPluginConfigs($menu['admin']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$menu = $menu[$type];
|
||||||
|
$menu = $this->filter->apply('side_menu', $menu, [$type]);
|
||||||
|
|
||||||
|
$view->with('items', array_map(fn ($item) => $this->transform($item), $menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform(array $item): array
|
||||||
|
{
|
||||||
|
$isActive = $this->request->is(Arr::get($item, 'link'));
|
||||||
|
foreach (Arr::get($item, 'children', []) as $k => $v) {
|
||||||
|
if ($this->request->is(Arr::get($v, 'link'))) {
|
||||||
|
$isActive = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = [];
|
||||||
|
if ($isActive) {
|
||||||
|
$item['active'] = true;
|
||||||
|
$classes[] = 'active menu-open';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Arr::has($item, 'children')) {
|
||||||
|
$item['children'] = array_map(
|
||||||
|
fn ($item) => $this->transform($item),
|
||||||
|
$item['children'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item['classes'] = $classes;
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collectPluginConfigs(array &$menu)
|
||||||
|
{
|
||||||
|
$menu = array_map(function ($item) {
|
||||||
|
if (Arr::get($item, 'id') === 'plugin-configs') {
|
||||||
|
$pluginConfigs = resolve(PluginManager::class)
|
||||||
|
->getEnabledPlugins()
|
||||||
|
->filter(fn (Plugin $plugin) => $plugin->hasConfig())
|
||||||
|
->map(function ($plugin) {
|
||||||
|
return [
|
||||||
|
'title' => trans($plugin->title),
|
||||||
|
'link' => 'admin/plugins/config/'.$plugin->name,
|
||||||
|
'icon' => 'fa-circle',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't display this menu item when no plugin config is available
|
||||||
|
if ($pluginConfigs->isNotEmpty()) {
|
||||||
|
array_push($item['children'], ...$pluginConfigs->values()->all());
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
}, $menu);
|
||||||
|
|
||||||
|
return array_filter($menu); // Remove empty items
|
||||||
|
}
|
||||||
|
}
|
41
app/Http/View/Composers/UserMenuComposer.php
Executable file
41
app/Http/View/Composers/UserMenuComposer.php
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class UserMenuComposer
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected Filter $filter;
|
||||||
|
|
||||||
|
public function __construct(Request $request, Filter $filter)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$avatarUrl = route('avatar.texture', ['tid' => $user->avatar, 'size' => 36], false);
|
||||||
|
$avatar = $this->filter->apply('user_avatar', $avatarUrl, [$user]);
|
||||||
|
$avatarPNG = route(
|
||||||
|
'avatar.texture',
|
||||||
|
['tid' => $user->avatar, 'size' => 36, 'png' => true],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$avatarPNG = $this->filter->apply('user_avatar', $avatarPNG, [$user]);
|
||||||
|
$cli = $this->request->is('admin', 'admin/*');
|
||||||
|
|
||||||
|
$view->with([
|
||||||
|
'user' => $user,
|
||||||
|
'avatar' => $avatar,
|
||||||
|
'avatar_png' => $avatarPNG,
|
||||||
|
'cli' => $cli,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
49
app/Http/View/Composers/UserPanelComposer.php
Executable file
49
app/Http/View/Composers/UserPanelComposer.php
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\View\Composers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class UserPanelComposer
|
||||||
|
{
|
||||||
|
protected Dispatcher $dispatcher;
|
||||||
|
|
||||||
|
protected Filter $filter;
|
||||||
|
|
||||||
|
public function __construct(Dispatcher $dispatcher, Filter $filter)
|
||||||
|
{
|
||||||
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function compose(View $view)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
$avatarUrl = route('avatar.texture', ['tid' => $user->avatar, 'size' => 45], false);
|
||||||
|
$avatar = $this->filter->apply('user_avatar', $avatarUrl, [$user]);
|
||||||
|
$avatarPNG = route(
|
||||||
|
'avatar.texture',
|
||||||
|
['tid' => $user->avatar, 'size' => 45, 'png' => true],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$avatarPNG = $this->filter->apply('user_avatar', $avatarPNG, [$user]);
|
||||||
|
|
||||||
|
$badges = [];
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$badges[] = ['text' => 'STAFF', 'color' => 'primary'];
|
||||||
|
}
|
||||||
|
$this->dispatcher->dispatch(new \App\Events\RenderingBadges($badges));
|
||||||
|
$badges = $this->filter->apply('user_badges', $badges, [$user]);
|
||||||
|
|
||||||
|
$view->with([
|
||||||
|
'user' => $user,
|
||||||
|
'avatar' => $avatar,
|
||||||
|
'avatar_png' => $avatarPNG,
|
||||||
|
'badges' => $badges,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
34
app/Listeners/CleanUpCloset.php
Executable file
34
app/Listeners/CleanUpCloset.php
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class CleanUpCloset
|
||||||
|
{
|
||||||
|
public function handle(Texture $texture)
|
||||||
|
{
|
||||||
|
// no need to update users' closet
|
||||||
|
// if texture was switched from "private" to "public"
|
||||||
|
if ($texture->exists && $texture->public) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$likers = $texture
|
||||||
|
->likers()
|
||||||
|
->where('user_uid', '!=', $texture->uploader)
|
||||||
|
->get();
|
||||||
|
$likers->each(function (User $user) use ($texture) {
|
||||||
|
$user->closet()->detach($texture->tid);
|
||||||
|
if (option('return_score')) {
|
||||||
|
$user->score += (int) option('score_per_closet_item');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($texture->exists) {
|
||||||
|
$texture->decrement('likes', $likers->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
app/Listeners/CleanUpFrontEndLocaleFiles.php
Executable file
26
app/Listeners/CleanUpFrontEndLocaleFiles.php
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
use Symfony\Component\Finder\SplFileInfo;
|
||||||
|
|
||||||
|
class CleanUpFrontEndLocaleFiles
|
||||||
|
{
|
||||||
|
protected Filesystem $filesystem;
|
||||||
|
|
||||||
|
public function __construct(Filesystem $filesystem)
|
||||||
|
{
|
||||||
|
$this->filesystem = $filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$files = $this->filesystem->allFiles(public_path('lang'));
|
||||||
|
array_walk($files, function (SplFileInfo $file) {
|
||||||
|
if ($file->getExtension() === 'js') {
|
||||||
|
$this->filesystem->delete($file->getPathname());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
app/Listeners/CopyPluginAssets.php
Executable file
28
app/Listeners/CopyPluginAssets.php
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Services\Plugin;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class CopyPluginAssets
|
||||||
|
{
|
||||||
|
protected Filesystem $filesystem;
|
||||||
|
|
||||||
|
public function __construct(Filesystem $filesystem)
|
||||||
|
{
|
||||||
|
$this->filesystem = $filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle($event)
|
||||||
|
{
|
||||||
|
$plugin = $event instanceof Plugin ? $event : $event->plugin;
|
||||||
|
$dir = public_path('plugins/'.$plugin->name);
|
||||||
|
$this->filesystem->deleteDirectory($dir);
|
||||||
|
|
||||||
|
$this->filesystem->copyDirectory(
|
||||||
|
$plugin->getPath().DIRECTORY_SEPARATOR.'assets',
|
||||||
|
$dir.'/assets'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
22
app/Listeners/NotifyFailedPlugin.php
Executable file
22
app/Listeners/NotifyFailedPlugin.php
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Event;
|
||||||
|
|
||||||
|
class NotifyFailedPlugin
|
||||||
|
{
|
||||||
|
public function handle($event)
|
||||||
|
{
|
||||||
|
$plugin = $event->plugin;
|
||||||
|
Event::listen(\App\Events\RenderingFooter::class, function ($event) use ($plugin) {
|
||||||
|
/** @var User */
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user && $user->isAdmin()) {
|
||||||
|
$message = trans('errors.plugins.boot', ['plugin' => trans($plugin->title)]);
|
||||||
|
$event->addContent("<script>blessing.notify.toast.error('$message')</script>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
app/Listeners/ResetPlayerForRemovedClosetItem.php
Executable file
16
app/Listeners/ResetPlayerForRemovedClosetItem.php
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Models\Texture;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ResetPlayerForRemovedClosetItem
|
||||||
|
{
|
||||||
|
public function handle(Texture $texture, User $user)
|
||||||
|
{
|
||||||
|
$type = $texture->type === 'cape' ? 'tid_cape' : 'tid_skin';
|
||||||
|
|
||||||
|
$user->players()->where($type, $texture->tid)->update([$type => 0]);
|
||||||
|
}
|
||||||
|
}
|
28
app/Listeners/ResetPlayers.php
Executable file
28
app/Listeners/ResetPlayers.php
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\Texture;
|
||||||
|
|
||||||
|
class ResetPlayers
|
||||||
|
{
|
||||||
|
public function handle(Texture $texture)
|
||||||
|
{
|
||||||
|
// no need to update players
|
||||||
|
// if texture was switched from "private" to "public"
|
||||||
|
if ($texture->exists && $texture->public) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $texture->type == 'cape' ? 'tid_cape' : 'tid_skin';
|
||||||
|
$query = Player::where($type, $texture->tid);
|
||||||
|
|
||||||
|
// texture was switched from "private" to "public"
|
||||||
|
if ($texture->exists) {
|
||||||
|
$query = $query->where('uid', '<>', $texture->uploader);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->update([$type => 0]);
|
||||||
|
}
|
||||||
|
}
|
24
app/Listeners/SendEmailVerification.php
Executable file
24
app/Listeners/SendEmailVerification.php
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Mail\EmailVerification;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
|
||||||
|
class SendEmailVerification
|
||||||
|
{
|
||||||
|
public function handle(User $user)
|
||||||
|
{
|
||||||
|
if (option('require_verification')) {
|
||||||
|
$url = URL::signedRoute('auth.verify', ['user' => $user->uid], null, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($user->email)->send(new EmailVerification(url($url)));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
report($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
app/Listeners/SetAppLocale.php
Executable file
34
app/Listeners/SetAppLocale.php
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SetAppLocale
|
||||||
|
{
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle($event)
|
||||||
|
{
|
||||||
|
/** @var User */
|
||||||
|
$user = $event->user;
|
||||||
|
|
||||||
|
if ($this->request->has('lang')) {
|
||||||
|
$user->locale = $this->request->input('lang');
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$locale = $user->locale;
|
||||||
|
if ($locale) {
|
||||||
|
app()->setLocale($locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
app/Listeners/UpdateScoreForDeletedTexture.php
Executable file
28
app/Listeners/UpdateScoreForDeletedTexture.php
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
class UpdateScoreForDeletedTexture
|
||||||
|
{
|
||||||
|
public function handle($texture)
|
||||||
|
{
|
||||||
|
$uploader = $texture->owner;
|
||||||
|
if ($uploader) {
|
||||||
|
$ret = 0;
|
||||||
|
if (option('return_score')) {
|
||||||
|
$ret += $texture->size * (
|
||||||
|
$texture->public
|
||||||
|
? (int) option('score_per_storage')
|
||||||
|
: (int) option('private_score_per_storage')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($texture->public && option('take_back_scores_after_deletion', true)) {
|
||||||
|
$ret -= (int) option('score_award_per_texture', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploader->score += $ret;
|
||||||
|
$uploader->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
app/Mail/EmailVerification.php
Executable file
29
app/Mail/EmailVerification.php
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class EmailVerification extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public $url;
|
||||||
|
|
||||||
|
public function __construct($url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
$site_name = option_localized('site_name');
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject(trans('user.verification.mail.title', ['sitename' => $site_name]))
|
||||||
|
->view('mails.email-verification');
|
||||||
|
}
|
||||||
|
}
|
29
app/Mail/ForgotPassword.php
Executable file
29
app/Mail/ForgotPassword.php
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ForgotPassword extends Mailable
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public $url = '';
|
||||||
|
|
||||||
|
public function __construct(string $url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function build()
|
||||||
|
{
|
||||||
|
$site_name = option_localized('site_name');
|
||||||
|
|
||||||
|
return $this
|
||||||
|
->subject(trans('auth.forgot.mail.title', ['sitename' => $site_name]))
|
||||||
|
->view('mails.password-reset');
|
||||||
|
}
|
||||||
|
}
|
33
app/Models/Concerns/HasPassword.php
Executable file
33
app/Models/Concerns/HasPassword.php
Executable file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Concerns;
|
||||||
|
|
||||||
|
use App\Services\Cipher\BaseCipher;
|
||||||
|
use Blessing\Filter;
|
||||||
|
|
||||||
|
trait HasPassword
|
||||||
|
{
|
||||||
|
public function verifyPassword(string $raw)
|
||||||
|
{
|
||||||
|
/** @var BaseCipher */
|
||||||
|
$cipher = resolve('cipher');
|
||||||
|
/** @var Filter */
|
||||||
|
$filter = resolve(Filter::class);
|
||||||
|
$password = $this->password;
|
||||||
|
$user = $this;
|
||||||
|
|
||||||
|
$passed = $cipher->verify($raw, $password, config('secure.salt'));
|
||||||
|
$passed = $filter->apply('verify_password', $passed, [$raw, $user]);
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changePassword(string $password): bool
|
||||||
|
{
|
||||||
|
$password = resolve('cipher')->hash($password, config('secure.salt'));
|
||||||
|
$password = resolve(Filter::class)->apply('user_password', $password);
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
}
|
99
app/Models/Player.php
Executable file
99
app/Models/Player.php
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Events\PlayerProfileUpdated;
|
||||||
|
use App\Models;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $pid
|
||||||
|
* @property int $uid
|
||||||
|
* @property string $name
|
||||||
|
* @property int $tid_skin
|
||||||
|
* @property int $tid_cape
|
||||||
|
* @property Carbon $last_modified
|
||||||
|
* @property User $user
|
||||||
|
* @property Texture $skin
|
||||||
|
* @property Texture $cape
|
||||||
|
* @property string $model
|
||||||
|
*/
|
||||||
|
class Player extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use SearchString;
|
||||||
|
|
||||||
|
public const CREATED_AT = null;
|
||||||
|
public const UPDATED_AT = 'last_modified';
|
||||||
|
|
||||||
|
public $primaryKey = 'pid';
|
||||||
|
protected $fillable = [
|
||||||
|
'uid', 'name', 'tid_skin', 'tid_cape', 'last_modified',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'pid' => 'integer',
|
||||||
|
'uid' => 'integer',
|
||||||
|
'tid_skin' => 'integer',
|
||||||
|
'tid_cape' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $dispatchesEvents = [
|
||||||
|
'retrieved' => \App\Events\PlayerRetrieved::class,
|
||||||
|
'updated' => PlayerProfileUpdated::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $searchStringColumns = [
|
||||||
|
'pid', 'uid',
|
||||||
|
'tid_skin' => '/^(?:tid_)?skin$/',
|
||||||
|
'tid_cape' => '/^(?:tid_)?cape$/',
|
||||||
|
'name' => ['searchable' => true],
|
||||||
|
'last_modified' => ['date' => true],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Models\User::class, 'uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function skin()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Models\Texture::class, 'tid_skin');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cape()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Models\Texture::class, 'tid_cape');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModelAttribute()
|
||||||
|
{
|
||||||
|
return optional($this->skin)->model ?? 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CustomSkinAPI R1.
|
||||||
|
*/
|
||||||
|
public function toJson($options = 0)
|
||||||
|
{
|
||||||
|
$model = $this->model;
|
||||||
|
$profile = [
|
||||||
|
'username' => $this->name,
|
||||||
|
'skins' => [
|
||||||
|
$model => optional($this->skin)->hash,
|
||||||
|
],
|
||||||
|
'cape' => optional($this->cape)->hash,
|
||||||
|
];
|
||||||
|
|
||||||
|
return json_encode($profile, $options | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function serializeDate(DateTimeInterface $date)
|
||||||
|
{
|
||||||
|
return $date->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
}
|
68
app/Models/Report.php
Executable file
68
app/Models/Report.php
Executable file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $tid
|
||||||
|
* @property int $uploader
|
||||||
|
* @property int $reporter
|
||||||
|
* @property string $reason
|
||||||
|
* @property int $status
|
||||||
|
* @property Carbon $report_at
|
||||||
|
* @property Texture $texture
|
||||||
|
* @property User $informer The reporter.
|
||||||
|
*/
|
||||||
|
class Report extends Model
|
||||||
|
{
|
||||||
|
use SearchString;
|
||||||
|
|
||||||
|
public const CREATED_AT = 'report_at';
|
||||||
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
|
public const PENDING = 0;
|
||||||
|
public const RESOLVED = 1;
|
||||||
|
public const REJECTED = 2;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'uploader', 'reporter', 'reason', 'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'tid' => 'integer',
|
||||||
|
'uploader' => 'integer',
|
||||||
|
'reporter' => 'integer',
|
||||||
|
'status' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $searchStringColumns = [
|
||||||
|
'id', 'tid', 'uploader', 'reporter',
|
||||||
|
'reason', 'status',
|
||||||
|
'report_at' => ['date' => true],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function texture()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Texture::class, 'tid', 'tid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function textureUploader()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploader', 'uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function informer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'reporter', 'uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function serializeDate(DateTimeInterface $date)
|
||||||
|
{
|
||||||
|
return $date->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
}
|
19
app/Models/Scope.php
Executable file
19
app/Models/Scope.php
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $description
|
||||||
|
*/
|
||||||
|
class Scope extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name', 'description',
|
||||||
|
];
|
||||||
|
}
|
74
app/Models/Texture.php
Executable file
74
app/Models/Texture.php
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $tid
|
||||||
|
* @property string $name
|
||||||
|
* @property string $type
|
||||||
|
* @property string $hash
|
||||||
|
* @property int $size
|
||||||
|
* @property int $uploader
|
||||||
|
* @property bool $public
|
||||||
|
* @property Carbon $upload_at
|
||||||
|
* @property int $likes
|
||||||
|
* @property string $model
|
||||||
|
* @property User $owner
|
||||||
|
* @property Collection $likers
|
||||||
|
*/
|
||||||
|
class Texture extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $primaryKey = 'tid';
|
||||||
|
public const CREATED_AT = 'upload_at';
|
||||||
|
public const UPDATED_AT = null;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name', 'type', 'uploader', 'public', 'likes',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'tid' => 'integer',
|
||||||
|
'size' => 'integer',
|
||||||
|
'uploader' => 'integer',
|
||||||
|
'public' => 'boolean',
|
||||||
|
'likes' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $dispatchesEvents = [
|
||||||
|
'deleting' => \App\Events\TextureDeleting::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getModelAttribute()
|
||||||
|
{
|
||||||
|
// Don't worry about cape...
|
||||||
|
return $this->type === 'alex' ? 'slim' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeLike($query, $field, $value)
|
||||||
|
{
|
||||||
|
return $query->where($field, 'LIKE', "%$value%");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function owner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploader');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function likers()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(User::class, 'user_closet')->withPivot('item_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function serializeDate(DateTimeInterface $date)
|
||||||
|
{
|
||||||
|
return $date->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
}
|
115
app/Models/User.php
Executable file
115
app/Models/User.php
Executable file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\HasPassword;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Passport\HasApiTokens;
|
||||||
|
use Lorisleiva\LaravelSearchString\Concerns\SearchString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $uid
|
||||||
|
* @property string $email
|
||||||
|
* @property string $password
|
||||||
|
* @property string $nickname
|
||||||
|
* @property string|null $locale
|
||||||
|
* @property int $avatar
|
||||||
|
* @property int $score
|
||||||
|
* @property int $permission
|
||||||
|
* @property string $ip
|
||||||
|
* @property bool $is_dark_mode
|
||||||
|
* @property string $last_sign_at
|
||||||
|
* @property string $register_at
|
||||||
|
* @property bool $verified
|
||||||
|
* @property string $player_name
|
||||||
|
* @property Collection $players
|
||||||
|
* @property Collection $closet
|
||||||
|
*/
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
use Notifiable;
|
||||||
|
use HasFactory;
|
||||||
|
use HasPassword;
|
||||||
|
use HasApiTokens;
|
||||||
|
use SearchString;
|
||||||
|
|
||||||
|
public const BANNED = -1;
|
||||||
|
public const NORMAL = 0;
|
||||||
|
public const ADMIN = 1;
|
||||||
|
public const SUPER_ADMIN = 2;
|
||||||
|
|
||||||
|
protected $primaryKey = 'uid';
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'email', 'nickname', 'avatar', 'score', 'permission', 'last_sign_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'uid' => 'integer',
|
||||||
|
'score' => 'integer',
|
||||||
|
'avatar' => 'integer',
|
||||||
|
'permission' => 'integer',
|
||||||
|
'verified' => 'bool',
|
||||||
|
'is_dark_mode' => 'bool',
|
||||||
|
];
|
||||||
|
|
||||||
|
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],
|
||||||
|
'is_dark_mode' => ['boolean' => true],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function isAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->permission >= static::ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closet()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Texture::class, 'user_closet')->withPivot('item_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayerNameAttribute()
|
||||||
|
{
|
||||||
|
$player = $this->players->first();
|
||||||
|
|
||||||
|
return $player ? $player->name : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayerNameAttribute($value)
|
||||||
|
{
|
||||||
|
$player = $this->players->first();
|
||||||
|
if ($player) {
|
||||||
|
$player->name = $value;
|
||||||
|
$player->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete()
|
||||||
|
{
|
||||||
|
Player::where('uid', $this->uid)->delete();
|
||||||
|
|
||||||
|
return parent::delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function players()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Player::class, 'uid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthIdentifier()
|
||||||
|
{
|
||||||
|
return $this->uid;
|
||||||
|
}
|
||||||
|
}
|
51
app/Notifications/SiteMessage.php
Executable file
51
app/Notifications/SiteMessage.php
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class SiteMessage extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $title;
|
||||||
|
|
||||||
|
public $content;
|
||||||
|
|
||||||
|
public function __construct(string $title, $content = '')
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
$this->content = $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function via($notifiable)
|
||||||
|
{
|
||||||
|
return ['database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $this->title,
|
||||||
|
'content' => $this->content,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
37
app/Observers/ScopeObserver.php
Executable file
37
app/Observers/ScopeObserver.php
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Scope;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class ScopeObserver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the Scope "saved" event.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function saved()
|
||||||
|
{
|
||||||
|
$this->refreshCachedScopes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Scope "deleted" event.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function deleted()
|
||||||
|
{
|
||||||
|
$this->refreshCachedScopes();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function refreshCachedScopes()
|
||||||
|
{
|
||||||
|
Cache::forget('scopes');
|
||||||
|
Cache::rememberForever('scopes', function () {
|
||||||
|
return Scope::pluck('description', 'name')->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
app/Providers/AppServiceProvider.php
Executable file
59
app/Providers/AppServiceProvider.php
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$this->app->singleton('cipher', 'App\Services\Cipher\\'.config('secure.cipher'));
|
||||||
|
$this->app->singleton(Services\Option::class);
|
||||||
|
$this->app->alias(Services\Option::class, 'options');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Request $request)
|
||||||
|
{
|
||||||
|
Paginator::useBootstrap();
|
||||||
|
|
||||||
|
$this->configureUrlGenerator($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control the URL generated by url() function.
|
||||||
|
*
|
||||||
|
* @codeCoverageIgnore
|
||||||
|
*/
|
||||||
|
protected function configureUrlGenerator(Request $request): void
|
||||||
|
{
|
||||||
|
if (!option('auto_detect_asset_url')) {
|
||||||
|
$rootUrl = option('site_url');
|
||||||
|
|
||||||
|
// Replace HTTP_HOST with site_url set in options,
|
||||||
|
// to prevent CDN source problems.
|
||||||
|
if (URL::isValidUrl($rootUrl)) {
|
||||||
|
URL::forceRootUrl($rootUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the request is secure or not.
|
||||||
|
* True is always returned when "X-Forwarded-Proto" header is set.
|
||||||
|
*
|
||||||
|
* We define this function because Symfony's "Request::isSecure()" method
|
||||||
|
* needs "setTrustedProxies()" which sucks when load balancer is enabled.
|
||||||
|
*/
|
||||||
|
$isRequestSecure = $request->server('HTTPS') === 'on'
|
||||||
|
|| $request->server('HTTP_X_FORWARDED_PROTO') === 'https'
|
||||||
|
|| $request->server('HTTP_X_FORWARDED_SSL') === 'on';
|
||||||
|
|
||||||
|
if (option('force_ssl') || $isRequestSecure) {
|
||||||
|
URL::forceScheme('https');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
app/Providers/AuthServiceProvider.php
Executable file
54
app/Providers/AuthServiceProvider.php
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Laravel\Passport\Passport;
|
||||||
|
|
||||||
|
class AuthServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The policy mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $policies = [
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any authentication / authorization services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
$this->registerPolicies();
|
||||||
|
|
||||||
|
Passport::routes();
|
||||||
|
|
||||||
|
$defaultScopes = [
|
||||||
|
'User.Read' => 'auth.oauth.scope.user.read',
|
||||||
|
'Notification.Read' => 'auth.oauth.scope.notification.read',
|
||||||
|
'Notification.ReadWrite' => 'auth.oauth.scope.notification.readwrite',
|
||||||
|
'Player.Read' => 'auth.oauth.scope.player.read',
|
||||||
|
'Player.ReadWrite' => 'auth.oauth.scope.player.readwrite',
|
||||||
|
'Closet.Read' => 'auth.oauth.scope.closet.read',
|
||||||
|
'Closet.ReadWrtie' => 'auth.oauth.scope.closet.readwrite',
|
||||||
|
'UsersManagement.Read' => 'auth.oauth.scope.users-management.read',
|
||||||
|
'UsersManagement.ReadWrite' => 'auth.oauth.scope.users-management.readwrite',
|
||||||
|
'PlayersManagement.Read' => 'auth.oauth.scope.players-management.read',
|
||||||
|
'PlayersManagement.ReadWrite' => 'auth.oauth.scope.players-management.readwrite',
|
||||||
|
'ClosetManagement.Read' => 'auth.oauth.scope.closet-management.read',
|
||||||
|
'ClosetManagement.ReadWrite' => 'auth.oauth.scope.closet-management.readwrite',
|
||||||
|
'ReportsManagement.Read' => 'auth.oauth.scope.reports-management.read',
|
||||||
|
'ReportsManagement.ReadWrite' => 'auth.oauth.scope.reports-management.readwrite',
|
||||||
|
];
|
||||||
|
|
||||||
|
$scopes = Cache::get('scopes', []);
|
||||||
|
|
||||||
|
Passport::tokensCan(array_merge($defaultScopes, $scopes));
|
||||||
|
|
||||||
|
Passport::setDefaultScope(['User.Read']);
|
||||||
|
}
|
||||||
|
}
|
54
app/Providers/EventServiceProvider.php
Executable file
54
app/Providers/EventServiceProvider.php
Executable file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Listeners;
|
||||||
|
use App\Models\Scope;
|
||||||
|
use App\Observers\ScopeObserver;
|
||||||
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
||||||
|
class EventServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
// The event listener mappings for the application.
|
||||||
|
protected $listen = [
|
||||||
|
'App\Events\PluginWasEnabled' => [
|
||||||
|
Listeners\CopyPluginAssets::class,
|
||||||
|
Listeners\CleanUpFrontEndLocaleFiles::class,
|
||||||
|
],
|
||||||
|
'plugin.versionChanged' => [
|
||||||
|
Listeners\CopyPluginAssets::class,
|
||||||
|
Listeners\CleanUpFrontEndLocaleFiles::class,
|
||||||
|
],
|
||||||
|
'App\Events\PluginBootFailed' => [
|
||||||
|
Listeners\NotifyFailedPlugin::class,
|
||||||
|
],
|
||||||
|
'auth.registration.completed' => [
|
||||||
|
Listeners\SendEmailVerification::class,
|
||||||
|
],
|
||||||
|
'texture.privacy.updated' => [
|
||||||
|
Listeners\ResetPlayers::class,
|
||||||
|
Listeners\CleanUpCloset::class,
|
||||||
|
],
|
||||||
|
'texture.deleted' => [
|
||||||
|
Listeners\UpdateScoreForDeletedTexture::class,
|
||||||
|
Listeners\ResetPlayers::class,
|
||||||
|
Listeners\CleanUpCloset::class,
|
||||||
|
],
|
||||||
|
'closet.removed' => [
|
||||||
|
Listeners\ResetPlayerForRemovedClosetItem::class,
|
||||||
|
],
|
||||||
|
'Illuminate\Auth\Events\Authenticated' => [
|
||||||
|
Listeners\SetAppLocale::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any events for your application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
Scope::observe(ScopeObserver::class);
|
||||||
|
}
|
||||||
|
}
|
20
app/Providers/PluginServiceProvider.php
Executable file
20
app/Providers/PluginServiceProvider.php
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services\PluginManager;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class PluginServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
$this->app->singleton(PluginManager::class);
|
||||||
|
$this->app->alias(PluginManager::class, 'plugins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(PluginManager $plugins)
|
||||||
|
{
|
||||||
|
$plugins->boot();
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user