diff --git a/web/app/Helpers/GridHelper.php b/web/app/Helpers/GridHelper.php index 808311f..5dedef3 100644 --- a/web/app/Helpers/GridHelper.php +++ b/web/app/Helpers/GridHelper.php @@ -166,12 +166,12 @@ class GridHelper public static function gameArbiter() { - return sprintf('http://%s:9999', self::getArbiter('Game')); + return sprintf('http://%s:64989', self::getArbiter('Game')); } public static function thumbnailArbiter() { - return sprintf('http://%s:9999', self::getArbiter('Thumbnail')); + return sprintf('http://%s:64989', self::getArbiter('Thumbnail')); } public static function gameArbiterMonitor() diff --git a/web/app/Http/Controllers/Web/AdminController.php b/web/app/Http/Controllers/Web/AdminController.php index c568e2a..7ebff96 100644 --- a/web/app/Http/Controllers/Web/AdminController.php +++ b/web/app/Http/Controllers/Web/AdminController.php @@ -3,9 +3,14 @@ namespace App\Http\Controllers\Web; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; use App\Http\Controllers\Controller; use App\Models\DynamicWebConfiguration; +use App\Models\PunishmentType; +use App\Models\Username; use App\Models\User; use App\Models\UserIp; @@ -32,12 +37,126 @@ class AdminController extends Controller // GET admin.useradmin function userAdmin(Request $request) { - $request->validate([ - 'ID' => ['required', 'int', 'exists:users,id'] + $user = User::where('id', $request->get('ID')); + if(!$user->exists()) + abort(400); + + return view('web.admin.useradmin')->with('user', $user->first()); + } + + // GET admin.manualmoderateuser + function manualModerateUser(Request $request) + { + $user = User::where('id', $request->get('ID')); + if(!$user->exists()) + abort(400); + + return view('web.admin.manualmoderateuser')->with('user', $user->first()); + } + + // POST admin.manualmoderateusersubmit + function manualModerateUserSubmit(Request $request) + { + $validator = Validator::make($request->all(), [ + 'ID' => [ + 'required', + Rule::exists('App\Models\User', 'id') + ], + 'moderate-action' => [ + 'required', + Rule::exists('App\Models\PunishmentType', 'id') + ], + 'internal-note' => 'required' + ], [ + 'moderate-action.required' => 'Please provide an account state.', + 'internal-note.required' => 'An internal note must be provided on why this user\'s state was changed.' ]); + if($validator->fails()) + return $this->manualModerateUserError($validator); + $user = User::where('id', $request->get('ID'))->first(); - return view('web.admin.useradmin')->with('user', $user); + + if(Auth::user()->id == $user->id) + { + $validator->errors()->add('ID', 'Cannot apply account state to current user.'); + return $this->manualModerateUserError($validator); + } + + if( + ($user->hasRoleset('ProtectedUser') && !Auth::user()->hasRoleset('Owner')) + + // XlXi: Prevent lower-ranks from banning higher ranks. + || ( + ($user->hasRoleset('Owner') && !Auth::user()->hasRoleset('Owner')) + && ($user->hasRoleset('Administrator') && !Auth::user()->hasRoleset('Administrator')) + ) + ) + { + $validator->errors()->add('ID', 'User is protected. Contact an owner.'); + return $this->manualModerateUserError($validator); + } + + // XlXi: Moderation action type 1 is None. + if($request->get('moderate-action') == 1 && !$user->hasActivePunishment()) + return $this->manualModerateUserSuccess(sprintf('%s already has an account state of None. No changes applied.', $user->username)); + + if($request->get('moderate-action') != 1 && $user->hasActivePunishment()) + { + $validator->errors()->add('ID', 'User already has an active punishment.'); + return $this->manualModerateUserError($validator); + } + + if(Auth::user()->hasRoleset('Administrator')) + { + if($request->has('scrub-username')) + { + $newUsername = sprintf('[ Content Deleted %d ]', $user->id); + + Username::where('user_id', $user->id) + ->update([ + 'scrubbed' => true, + 'scrubbed_by' => Auth::user()->id + ]); + + Username::create([ + 'username' => $newUsername, + 'user_id' => $user->id + ]); + + $user->username = $newUsername; + $user->save(); + } + } + + PunishmentType::where('id', $request->get('moderate-action')) + ->first() + ->applyToUser([ + 'user_id' => $user->id, + 'user_note' => $request->get('user-note') ?: '', + 'internal_note' => $request->get('internal-note') ?: '', + 'moderator_id' => Auth::user()->id + ]); + + return $this->manualModerateUserSuccess(sprintf('Successfully applied account state to %s.', $user->username)); + } + + function manualModerateUserError($validator) + { + $user = User::where('id', request()->get('ID'))->first(); + + return view('web.admin.manualmoderateuser') + ->with('user', $user) + ->withErrors($validator); + } + + function manualModerateUserSuccess($message) + { + $user = User::where('id', request()->get('ID'))->first(); + + return view('web.admin.manualmoderateuser') + ->with('user', $user) + ->with('success', $message); } // GET admin.usersearch @@ -46,6 +165,7 @@ class AdminController extends Controller $types = [ 'userid' => 'UserId', 'username' => 'UserName', + 'emailaddress' => 'EmailAddress', 'ipaddress' => 'IpAddress' ]; @@ -61,10 +181,6 @@ class AdminController extends Controller // POST admin.usersearchquery function userSearchQueryUserId(Request $request) { - $request->validate([ - 'userid' => ['required', 'int'] - ]); - $users = User::where('id', $request->get('userid')) ->paginate(25) ->appends($request->all()); @@ -74,10 +190,6 @@ class AdminController extends Controller function userSearchQueryUserName(Request $request) { - $request->validate([ - 'username' => ['required', 'string'] - ]); - $users = User::where('username', 'like', '%' . $request->get('username') . '%') ->paginate(25) ->appends($request->all()); @@ -85,11 +197,22 @@ class AdminController extends Controller return view('web.admin.usersearch')->with('users', $users); } + function userSearchQueryEmailAddress(Request $request) + { + if(!Auth::user()->hasRoleset('Owner')) + abort(403); + + $users = User::where('email', $request->get('emailaddress')) + ->paginate(25) + ->appends($request->all()); + + return view('web.admin.usersearch')->with('users', $users); + } + function userSearchQueryIpAddress(Request $request) { - $request->validate([ - 'ipaddress' => ['required', 'ip'] - ]); + if(!Auth::user()->hasRoleset('Owner')) + abort(403); $users = UserIp::where('ipAddress', $request->get('ipaddress')) ->join('users', 'users.id', '=', 'user_ips.userId') @@ -109,10 +232,6 @@ class AdminController extends Controller // POST admin.userlookupquery function userLookupQuery(Request $request) { - $request->validate([ - 'lookup' => ['required', 'string'] - ]); - $users = []; foreach(preg_split('/\r\n|\r|\n/', $request->get('lookup')) as $username) diff --git a/web/app/Http/Controllers/Web/Auth/RegisteredUserController.php b/web/app/Http/Controllers/Web/Auth/RegisteredUserController.php index b746f7a..cc24abe 100644 --- a/web/app/Http/Controllers/Web/Auth/RegisteredUserController.php +++ b/web/app/Http/Controllers/Web/Auth/RegisteredUserController.php @@ -13,6 +13,7 @@ use App\Http\Controllers\Controller; use App\Models\AvatarAsset; use App\Models\DefaultUserAsset; use App\Models\UserAsset; +use App\Models\Username; use App\Models\User; use App\Providers\RouteServiceProvider; @@ -39,7 +40,7 @@ class RegisteredUserController extends Controller public function store(Request $request) { $validator = Validator::make($request->all(), [ - 'username' => ['required', 'string', 'min:3', 'max:20', 'regex:/^[a-zA-Z0-9]+[ _.-]?[a-zA-Z0-9]+$/i', 'unique:users'], + 'username' => ['required', 'string', 'min:3', 'max:20', 'regex:/^[a-zA-Z0-9]+[ _.-]?[a-zA-Z0-9]+$/i', 'unique:usernames'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'confirmed', Rules\Password::defaults()], ], [ @@ -57,6 +58,10 @@ class RegisteredUserController extends Controller 'email' => $request->email, 'password' => Hash::make($request->password), ]); + Username::create([ + 'username' => $user->username, + 'user_id' => $user->id + ]); foreach(DefaultUserAsset::all() as $defaultAsset) { diff --git a/web/app/Http/Requests/Auth/LoginRequest.php b/web/app/Http/Requests/Auth/LoginRequest.php index 85cc228..9f34cf8 100644 --- a/web/app/Http/Requests/Auth/LoginRequest.php +++ b/web/app/Http/Requests/Auth/LoginRequest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; +use App\Models\Username; use App\Models\User; class LoginRequest extends FormRequest @@ -65,13 +66,22 @@ class LoginRequest extends FormRequest $this->merge([ $login_type => $this->input('username') ]); + $loginModel = ($login_type == 'username' ? 'App\\Models\\Username' : 'App\\Models\\User'); - if(!User::where($login_type, $this->only($login_type))->exists()) { + $previousUsername = $loginModel::where($login_type, $this->only($login_type))->first(); + if(!$previousUsername) { throw ValidationException::withMessages([ 'username' => $this->messages()['username.exists'], ]); } + if($login_type == 'username') + { + $this->merge([ + 'username' => $previousUsername->user->username + ]); + } + if(!Auth::attempt($this->only($login_type, 'password'), $this->boolean('remember'))) { RateLimiter::hit($this->throttleKey()); diff --git a/web/app/Models/Punishment.php b/web/app/Models/Punishment.php index 2e30c6a..b2a274a 100644 --- a/web/app/Models/Punishment.php +++ b/web/app/Models/Punishment.php @@ -10,6 +10,21 @@ class Punishment extends Model { use HasFactory; + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'punishment_type_id', + 'active', + 'user_note', + 'internal_note', + 'user_id', + 'moderator_id', + 'expiration' + ]; + /** * The attributes that should be cast. * @@ -81,4 +96,9 @@ class Punishment extends Model ->where('active', true) ->orderByDesc('id'); } + + public function pardoned() + { + return $this->pardoner_id !== null; + } } diff --git a/web/app/Models/PunishmentAutofill.php b/web/app/Models/PunishmentAutofill.php new file mode 100644 index 0000000..18e4ac1 --- /dev/null +++ b/web/app/Models/PunishmentAutofill.php @@ -0,0 +1,11 @@ +first(); + $userPunishment = $user->getPunishment(); + if(!$userPunishment) return false; + + $userPunishment->active = false; + $userPunishment->pardoner_id = Auth::user()->id; + $userPunishment->pardoner_note = $punishment['internal_note']; + $userPunishment->save(); + return $userPunishment; + } + + public function applyToUser($punishment) + { + if($this->name == 'None') + return $this->applyNoneState($punishment); + + return Punishment::create(array_merge($punishment, [ + 'punishment_type_id' => $this->id, + 'active' => true, + 'expiration' => $this->time !== null ? Carbon::now()->addDays($this->time) : null + ])); + } } diff --git a/web/app/Models/User.php b/web/app/Models/User.php index 5c605c1..7264c5b 100644 --- a/web/app/Models/User.php +++ b/web/app/Models/User.php @@ -142,7 +142,7 @@ class User extends Authenticatable public function punishments() { - return $this->hasMany(Punishment::class, 'user_id'); + return $this->hasMany(Punishment::class, 'user_id')->orderByDesc('id'); } public function hasAsset($assetId) @@ -166,24 +166,30 @@ class User extends Authenticatable public function getImageUrl() { - $renderId = $this->id; + if(!$this->thumbnail2DHash) + { + $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $this->id, 'position' => 'full', 'type' => '2d'])); + if($thumbnail->json('status') == 'loading') + return '/images/busy/user.png'; + + return $thumbnail->json('data'); + } - $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $renderId, 'position' => 'full', 'type' => '2d'])); - if($thumbnail->json('status') == 'loading') - return '/images/busy/user.png'; - - return $thumbnail->json('data'); + return route('content', $this->thumbnail2DHash); } public function getHeadshotImageUrl() { - $renderId = $this->id; + if(!$this->thumbnailBustHash) + { + $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $this->id, 'position' => 'bust', 'type' => '2d'])); + if($thumbnail->json('status') == 'loading') + return '/images/busy/user.png'; + + return $thumbnail->json('data'); + } - $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $renderId, 'position' => 'bust', 'type' => '2d'])); - if($thumbnail->json('status') == 'loading') - return '/images/busy/user.png'; - - return $thumbnail->json('data'); + return route('content', $this->thumbnailBustHash); } public function set2DHash($hash) @@ -250,6 +256,19 @@ class User extends Authenticatable return AvatarColor::newForUser($this->id); } + public function changeName($newName) + { + if($newName == $this->username) return false; + + Username::create([ + 'username' => $newName, + 'user_id' => $this->id + ]); + + $this->username = $newName; + $this->save(); + } + public function userToJson() { return [ diff --git a/web/app/Models/Username.php b/web/app/Models/Username.php new file mode 100644 index 0000000..dc3007b --- /dev/null +++ b/web/app/Models/Username.php @@ -0,0 +1,27 @@ + + */ + protected $fillable = [ + 'username', + 'user_id', + 'scrubbed_by' + ]; + + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } +} diff --git a/web/app/Models/asset.php b/web/app/Models/asset.php index 7d6c899..8f0a7cc 100644 --- a/web/app/Models/asset.php +++ b/web/app/Models/asset.php @@ -139,13 +139,25 @@ class Asset extends Model public function getThumbnail() { - $renderId = $this->id; + if($this->moderated) + return '/thumbs/DeletedThumbnail.png'; - $thumbnail = Http::get(route('thumbnails.v1.asset', ['id' => $renderId, 'type' => '2d'])); - if($thumbnail->json('status') == 'loading') - return ($this->assetTypeId == 9 ? '/images/busy/game.png' : '/images/busy/asset.png'); + if(!$this->approved) + return '/thumbs/PendingThumbnail.png'; - return $thumbnail->json('data'); + if(!$this->assetType->renderable) + return '/thumbs/UnavailableThumbnail.png'; + + if(!$this->thumbnail2DHash) + { + $thumbnail = Http::get(route('thumbnails.v1.asset', ['id' => $this->id, 'type' => '2d'])); + if($thumbnail->json('status') == 'loading') + return ($this->assetTypeId == 9 ? '/images/busy/game.png' : '/images/busy/asset.png'); + + return $thumbnail->json('data'); + } + + return route('content', $this->thumbnail2DHash); } public function set2DHash($hash) diff --git a/web/app/View/Components/Admin/UserPunishments.php b/web/app/View/Components/Admin/UserPunishments.php new file mode 100644 index 0000000..52d04f8 --- /dev/null +++ b/web/app/View/Components/Admin/UserPunishments.php @@ -0,0 +1,28 @@ +unsignedTinyInteger('punishment_type_id'); $table->boolean('active'); $table->string('user_note'); + $table->string('internal_note'); $table->unsignedBigInteger('user_id'); $table->unsignedBigInteger('moderator_id'); $table->unsignedBigInteger('pardoner_id')->nullable(); + $table->string('pardoner_note')->nullable(); $table->timestamp('expiration')->nullable(); $table->timestamps(); diff --git a/web/database/migrations/2023_01_07_014022_create_punishment_types_table.php b/web/database/migrations/2023_01_07_014022_create_punishment_types_table.php index fe8a09d..5d9693f 100644 --- a/web/database/migrations/2023_01_07_014022_create_punishment_types_table.php +++ b/web/database/migrations/2023_01_07_014022_create_punishment_types_table.php @@ -16,6 +16,7 @@ return new class extends Migration Schema::create('punishment_types', function (Blueprint $table) { $table->id(); + $table->string('name'); $table->string('label'); $table->integer('time')->nullable(); diff --git a/web/database/migrations/2023_01_28_204627_create_usernames_table.php b/web/database/migrations/2023_01_28_204627_create_usernames_table.php new file mode 100644 index 0000000..49b2cd8 --- /dev/null +++ b/web/database/migrations/2023_01_28_204627_create_usernames_table.php @@ -0,0 +1,37 @@ +id(); + + $table->string('username'); + $table->unsignedBigInteger('user_id'); + $table->boolean('scrubbed')->default(false); + $table->unsignedBigInteger('scrubbed_by')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('usernames'); + } +}; diff --git a/web/database/seeders/PunishmentTypeSeeder.php b/web/database/seeders/PunishmentTypeSeeder.php index 66d1d86..1690b5f 100644 --- a/web/database/seeders/PunishmentTypeSeeder.php +++ b/web/database/seeders/PunishmentTypeSeeder.php @@ -16,12 +16,13 @@ class PunishmentTypeSeeder extends Seeder */ public function run() { - PunishmentType::create(['label' => 'Reminder', 'time' => 0]); - PunishmentType::create(['label' => 'Warning', 'time' => 0]); - PunishmentType::create(['label' => 'Banned for 1 Day', 'time' => 1]); - PunishmentType::create(['label' => 'Banned for 3 Days', 'time' => 3]); - PunishmentType::create(['label' => 'Banned for 7 Days', 'time' => 7]); - PunishmentType::create(['label' => 'Banned for 14 Days', 'time' => 14]); - PunishmentType::create(['label' => 'Account Deleted']); + PunishmentType::create(['name' => 'None', 'label' => 'Unban', 'time' => 0]); + PunishmentType::create(['name' => 'Remind', 'label' => 'Reminder', 'time' => 0]); + PunishmentType::create(['name' => 'Warn', 'label' => 'Warning', 'time' => 0]); + PunishmentType::create(['name' => 'Ban 1 Day', 'label' => 'Banned for 1 Day', 'time' => 1]); + PunishmentType::create(['name' => 'Ban 3 Days', 'label' => 'Banned for 3 Days', 'time' => 3]); + PunishmentType::create(['name' => 'Ban 7 Days', 'label' => 'Banned for 7 Days', 'time' => 7]); + PunishmentType::create(['name' => 'Ban 14 Days', 'label' => 'Banned for 14 Days', 'time' => 14]); + PunishmentType::create(['name' => 'Delete', 'label' => 'Account Deleted']); } } diff --git a/web/resources/js/components/ModerationAutofills.js b/web/resources/js/components/ModerationAutofills.js new file mode 100644 index 0000000..45f229f --- /dev/null +++ b/web/resources/js/components/ModerationAutofills.js @@ -0,0 +1,44 @@ +/* + Copyright © XlXi 2023 +*/ + +import { Component } from 'react'; + +const autofills = [ + { Label: 'Account Theft', Autofill: 'This account has been closed as compromised.' }, + { Label: 'Requested Deletion', Autofill: 'Your account has been deleted as per your request. Thank you for being a part of the VirtuBrick community.' }, + { Label: 'Spam', Autofill: 'Do not repeatedly post or spam chat or content in VirtuBrick.' }, + { Label: 'Swear', Autofill: 'Do not swear, use profanity or otherwise say inappropriate things in VirtuBrick.' }, + { Label: 'Personal Info', Autofill: 'Do not ask for or give out personal, real-life, or private information on VirtuBrick.' }, + { Label: 'Spam Alt', Autofill: 'Do not create accounts just for the purpose of breaking the rules.' }, + { Label: 'Dating', Autofill: 'Dating, Sexting, or other inappropriate behavior is not acceptable on VirtuBrick.' }, + { Label: 'Inappropriate Talk', Autofill: 'This content is not appropriate for VirtuBrick. Do not chat, post, or otherwise discuss inappropriate topics on VirtuBrick.' }, + { Label: 'Link', Autofill: 'The only links you are allowed to post on VirtuBrick are virtubrick.net links, youtube.com links, twitter.com links, and twitch.tv links. No other links are allowed. Posting any other links will result in further moderation actions.' }, + { Label: 'Harassment', Autofill: 'Do not harass other users. Do not say inappropriate or mean things about others on VirtuBrick.' }, + { Label: 'Scam', Autofill: 'Scamming is a violation of the Terms of Service. Do not continue to scam on VirtuBrick.' }, + { Label: 'Bad Image', Autofill: 'This image is not appropriate for VirtuBrick. Please review our rules and upload only appropriate content.' } +]; + +class ModerationAutofills extends Component { + constructor(props) { + super(props); + } + + autofill(autofillText) { + document.getElementById('user-note').value = autofillText; + } + + render() { + return ( + <> + { + autofills.map(({Label, Autofill}) => + + ) + } + + ); + } +} + +export default ModerationAutofills; \ No newline at end of file diff --git a/web/resources/js/pages/ManualUserModeration.js b/web/resources/js/pages/ManualUserModeration.js new file mode 100644 index 0000000..842a631 --- /dev/null +++ b/web/resources/js/pages/ManualUserModeration.js @@ -0,0 +1,18 @@ +/* + Copyright © XlXi 2023 +*/ + +import $ from 'jquery'; + +import React from 'react'; +import { render } from 'react-dom'; + +import ModerationAutofills from '../components/ModerationAutofills'; + +const autofillId = 'vb-mod-autofill'; + +$(document).ready(function() { + if (document.getElementById(autofillId)) { + render(, document.getElementById(autofillId)); + } +}); \ No newline at end of file diff --git a/web/resources/sass/VirtuBrick.scss b/web/resources/sass/VirtuBrick.scss index 2f3c848..7ff1185 100644 --- a/web/resources/sass/VirtuBrick.scss +++ b/web/resources/sass/VirtuBrick.scss @@ -1692,4 +1692,9 @@ table > tbody > tr > th { html.vbrick-light & { background-color: #0000000f; } +} + +// Select +.vb-small-select { + max-width: 150px; } \ No newline at end of file diff --git a/web/resources/views/components/admin/navigation/user-admin.blade.php b/web/resources/views/components/admin/navigation/user-admin.blade.php index dd8029c..72143a7 100644 --- a/web/resources/views/components/admin/navigation/user-admin.blade.php +++ b/web/resources/views/components/admin/navigation/user-admin.blade.php @@ -4,7 +4,7 @@ - + diff --git a/web/resources/views/components/admin/user-punishments.blade.php b/web/resources/views/components/admin/user-punishments.blade.php new file mode 100644 index 0000000..a467730 --- /dev/null +++ b/web/resources/views/components/admin/user-punishments.blade.php @@ -0,0 +1,66 @@ +@props([ + 'user' +]) + +
+ + + + + + + + + + + + + + @foreach($user->punishments as $punishment) + + + + + + + + + + + + + @endforeach + +
IDActionModeratorCreatedExpirationAcknowledged
+ +  {{ $punishment->id }}{{ $punishment->punishment_type->label }} + + + + {{ $punishment->reviewed() }}{{ $punishment->expirationStr() }}{{ $punishment->active ? 'No' : 'Yes' }}
+
+

Note to User: {{ $punishment->user_note }}

+

Internal Note: {{ $punishment->internal_note }}

+ @if($punishment->pardoned()) +

Pardoned By: {{ $punishment->pardoner->username }}

+

Pardoner Note: {{ $punishment->pardoner_note }}

+ @endif + @if($punishment->context->count() > 0) +

Abuses:

+ @endif + @foreach($punishment->context as $context) +
+

Reason: {{ $context->user_note }} (TODO: audit)

+ @if($context->description) +

Offensive Item: {{ $context->description }}

+ @endif + @if($context->content_hash) + + @endif +
+ @endforeach +
+
+
\ No newline at end of file diff --git a/web/resources/views/web/admin/manualmoderateuser.blade.php b/web/resources/views/web/admin/manualmoderateuser.blade.php new file mode 100644 index 0000000..c513171 --- /dev/null +++ b/web/resources/views/web/admin/manualmoderateuser.blade.php @@ -0,0 +1,73 @@ +@extends('layouts.admin') + +@section('title', 'Moderate User (' . $user->username . ')') + +@section('page-specific') + + +@endsection + +@push('content') +
+ +

Moderate User

+

{{ $user->username }} home page

+ @if($errors->any()) + @foreach($errors->all() as $error) +
{{ $error }}
+ @endforeach + @endif + @if(isset($success)) +
{{ $success }}
+ @endif +
+
+ @csrf +
+
+ + @foreach(\App\Models\PunishmentType::all() as $punishmentType) +
+ + +
+ @endforeach +
+
+ +
+
+
+ + + + + + +
+ + +
+ @admin +
+ + +
+
+ + +
+ @else +

Please contact an administrator or higher if you believe this user should be poison or IP banned.
If you believe this user to be suicidal, contact an owner and options will be explored to get this user help.

+ @endadmin + + +
+
+
+
+ +

Past Punishments

+ +
+@endpush \ No newline at end of file diff --git a/web/resources/views/web/admin/useradmin.blade.php b/web/resources/views/web/admin/useradmin.blade.php index c852907..016a4a3 100644 --- a/web/resources/views/web/admin/useradmin.blade.php +++ b/web/resources/views/web/admin/useradmin.blade.php @@ -60,7 +60,17 @@ @endif {{ $user->username }} - TODO + @php + $prevUsernames = \App\Models\Username::where('user_id', $user->id) + ->where('username', '!=', $user->username); + @endphp + @if($prevUsernames->count() > 0) + + @foreach($prevUsernames->get() as &$userName) +

{{ $userName->username }} ({{ $userName->created_at->isoFormat('lll') }})

+ @endforeach +
+ @endif @@ -81,63 +91,7 @@

Punishments

-
- - - - - - - - - - - - - - @foreach($user->punishments as $punishment) - - - - - - - - - - - - - @endforeach - -
IDActionModeratorCreatedExpirationAcknowledged
- -  {{ $punishment->id }}{{ $punishment->punishment_type->label }} - - - - {{ $punishment->reviewed() }}{{ $punishment->expirationStr() }}{{ $punishment->active ? 'No' : 'Yes' }}
-
-

Note to User: {{ $punishment->user_note }}

- @if($punishment->context->count() > 0) -

Abuses:

- @endif - @foreach($punishment->context as $context) -
-

Reason: {{ $context->user_note }} (TODO: audit)

- @if($context->description) -

Offensive Item: {{ $context->description }}

- @endif - @if($context->content_hash) - - @endif -
- @endforeach -
-
-
+

Seller Ban

diff --git a/web/resources/views/web/admin/usersearch.blade.php b/web/resources/views/web/admin/usersearch.blade.php index f2b9c47..d43fe3c 100644 --- a/web/resources/views/web/admin/usersearch.blade.php +++ b/web/resources/views/web/admin/usersearch.blade.php @@ -19,6 +19,7 @@ @owner + @endowner
diff --git a/web/routes/web.php b/web/routes/web.php index 97c3548..44cda9e 100644 --- a/web/routes/web.php +++ b/web/routes/web.php @@ -56,6 +56,8 @@ Route::group(['as' => 'admin.', 'prefix' => 'admin'], function() { Route::group(['prefix' => 'users'], function() { Route::get('/useradmin', 'AdminController@userAdmin')->name('useradmin'); + Route::get('/manualmoderateuser', 'AdminController@manualModerateUser')->name('manualmoderateuser'); + Route::post('/manualmoderateuser', 'AdminController@manualModerateUserSubmit')->name('manualmoderateusersubmit'); Route::get('/find', 'AdminController@userSearch')->name('usersearch'); Route::get('/userlookuptool', 'AdminController@userLookup')->name('userlookup'); Route::post('/userlookuptool', 'AdminController@userLookupQuery')->name('userlookupquery'); diff --git a/web/webpack.mix.js b/web/webpack.mix.js index 9476e51..90027e7 100644 --- a/web/webpack.mix.js +++ b/web/webpack.mix.js @@ -15,6 +15,7 @@ mix.js('resources/js/app.js', 'public/js') .js('resources/js/pages/AvatarEditor.js', 'public/js') .js('resources/js/pages/Item.js', 'public/js') .js('resources/js/pages/Place.js', 'public/js') + .js('resources/js/pages/ManualUserModeration.js', 'public/js/adm') .js('resources/js/pages/AppDeployer.js', 'public/js/adm') .js('resources/js/pages/SiteConfiguration.js', 'public/js/adm') .react()