Thumbnail and user punishment changes.

- User manual moderation page.
- Sped up thumbnail loading times majorly. This improves the shop and character editor.
- Named punishment types.
- Added email address search on the find user page.
- Added internal note and pardon note to user punishments.
- User admin page shows previous usernames if the user has any.
- Username changing support.
This commit is contained in:
Graphictoria 2023-01-25 18:53:42 -05:00
parent 131ff27343
commit f94d6e3555
25 changed files with 590 additions and 106 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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)
{

View File

@ -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());

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PunishmentAutofill extends Model
{
use HasFactory;
}

View File

@ -2,8 +2,10 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class PunishmentType extends Model
{
@ -18,4 +20,30 @@ class PunishmentType extends Model
'label',
'time'
];
public function applyNoneState($punishment)
{
$user = User::where('id', $punishment['user_id'])->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
]));
}
}

View File

@ -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 [

View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Username extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'username',
'user_id',
'scrubbed_by'
];
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@ -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)

View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Components\Admin;
use Illuminate\View\Component;
class UserPunishments extends Component
{
/**
* Create a new component instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.admin.user-punishments');
}
}

View File

@ -19,9 +19,11 @@ return new class extends Migration
$table->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();

View File

@ -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();

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('usernames', function (Blueprint $table) {
$table->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');
}
};

View File

@ -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']);
}
}

View File

@ -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}) =>
<button type="button" className="btn btn-sm btn-secondary d-block mb-1" onClick={ () => this.autofill(Autofill) }>{ Label }</button>
)
}
</>
);
}
}
export default ModerationAutofills;

View File

@ -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(<ModerationAutofills />, document.getElementById(autofillId));
}
});

View File

@ -1692,4 +1692,9 @@ table > tbody > tr > th {
html.vbrick-light & {
background-color: #0000000f;
}
}
// Select
.vb-small-select {
max-width: 150px;
}

View File

@ -4,7 +4,7 @@
<x-admin.navigation-tabs>
<x-admin.navigation-tab-link label="Account" :route="route('admin.useradmin', ['ID' => $uid])" />
<x-admin.navigation-tab-link label="Moderate" :route="route('admin.dashboard')" />
<x-admin.navigation-tab-link label="Moderate" :route="route('admin.manualmoderateuser', ['ID' => $uid])" />
<x-admin.navigation-tab-link label="Transactions" :route="route('admin.dashboard')" />
<x-admin.navigation-tab-link label="Trades" :route="route('admin.dashboard')" />
<x-admin.navigation-tab-link label="Send Personal Msg" :route="route('admin.dashboard')" />

View File

@ -0,0 +1,66 @@
@props([
'user'
])
<div class="card">
<table class="table virtubrick-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Action</th>
<th scope="col">Moderator</th>
<th scope="col">Created</th>
<th scope="col">Expiration</th>
<th scope="col">Acknowledged</th>
</tr>
</thead>
<tbody>
@foreach($user->punishments as $punishment)
<tr>
<th scope="col">
<button class="btn btn-sm p-0 px-1 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#punishment-collapse-{{ $punishment->id }}" aria-expanded="false" aria-controls="punishment-collapse-{{ $punishment->id }}">
<i class="fa-solid fa-bars"></i>
</button>
</th>
<th scope="col">&nbsp;{{ $punishment->id }}</th>
<th scope="col">{{ $punishment->punishment_type->label }}</th>
<th scope="col">
<a href="{{ route('admin.useradmin', ['ID' => $punishment->moderator->id]) }}" class="text-decoration-none">
<x-user-circle :user="$punishment->moderator" :size=24 />
</a>
</th>
<th scope="col">{{ $punishment->reviewed() }}</th>
<th scope="col">{{ $punishment->expirationStr() }}</th>
<th scope="col">{{ $punishment->active ? 'No' : 'Yes' }}</th>
</tr>
<tr class="collapse" id="punishment-collapse-{{ $punishment->id }}">
<td colspan="7" class="bg-secondary">
<div class="mx-2">
<p><b>Note to User:</b> {{ $punishment->user_note }}</p>
<p><b>Internal Note:</b> {{ $punishment->internal_note }}</p>
@if($punishment->pardoned())
<p><b>Pardoned By:</b> <a class="text-decoration-none" href="{{ route('admin.useradmin', ['ID' => $punishment->pardoner->id]) }}">{{ $punishment->pardoner->username }}</a></p>
<p><b>Pardoner Note:</b> {{ $punishment->pardoner_note }}</p>
@endif
@if($punishment->context->count() > 0)
<p><b>Abuses:</b></p>
@endif
@foreach($punishment->context as $context)
<div class="card bg-secondary p-2 mb-2 border-1">
<p><b>Reason:</b> {{ $context->user_note }} (<a href="#" class="text-decoration-none">TODO: audit</a>)</p>
@if($context->description)
<p><b>Offensive Item:</b> {{ $context->description }}</p>
@endif
@if($context->content_hash)
<img src="{{ route('content', $context->content_hash) }}" class="img-fluid" width="210" height="210"/>
@endif
</div>
@endforeach
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>

View File

@ -0,0 +1,73 @@
@extends('layouts.admin')
@section('title', 'Moderate User (' . $user->username . ')')
@section('page-specific')
<!-- Secure Page JS -->
<script src="{{ mix('js/adm/ManualUserModeration.js') }}"></script>
@endsection
@push('content')
<div class="container-md">
<x-admin.navigation.user-admin :uid="$user->id" />
<h4 class="mb-0">Moderate User</h4>
<p class="mb-2">{{ $user->username }} <a href="{{ $user->getProfileUrl() }}" class="text-decoration-none">home page</a></p>
@if($errors->any())
@foreach($errors->all() as $error)
<div class="alert alert-danger virtubrick-alert virtubrick-error-popup">{{ $error }}</div>
@endforeach
@endif
@if(isset($success))
<div class="alert alert-success virtubrick-alert virtubrick-error-popup">{{ $success }}</div>
@endif
<div class="card me-2 p-3">
<form method="POST" action="{{ route('admin.manualmoderateuser', ['ID' => $user->id]) }}" enctype="multipart/form-data">
@csrf
<div class="row">
<div class="col-3">
<label class="form-label">Account State override:</label>
@foreach(\App\Models\PunishmentType::all() as $punishmentType)
<div class="form-check">
<input class="form-check-input" type="radio" name="moderate-action" id="moderate-action-{{ Str::slug($punishmentType->name, '-') }}" value="{{ $punishmentType->id }}">
<label class="form-check-label" for="moderate-action-{{ Str::slug($punishmentType->name, '-') }}">{{ $punishmentType->name }}</label>
</div>
@endforeach
</div>
<div class="col-3">
<label class="form-label">Auto-fill fields:</label>
<div id="vb-mod-autofill"></div>
</div>
<div class="col-6">
<label for="user-note" class="form-label">Note to {{ $user->username }}:</label>
<textarea type="text" class="form-control mb-3" name="user-note" id="user-note"></textarea>
<label for="internal-note" class="form-label">Internal Moderation Note:</label>
<textarea type="text" class="form-control mb-3" name="internal-note" id="internal-note"></textarea>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" value="1" name="scrub-username" id="scrub-username">
<label class="form-check-label" for="scrub-username"><b>Scrub Username:</b> This will set the user's name to [Content Deleted] and hide the user's name history.</label>
</div>
@admin
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" value="1" name="poison-ban" id="poison-ban">
<label class="form-check-label" for="poison-ban"><b>Posion:</b> Disables new account creation on the user's IP address.</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" value="1" name="ip-ban" id="ip-ban">
<label class="form-check-label" for="ip-ban"><b>IP Ban:</b> Restricts the user from accessing the website. If selected, you will be prompted with options for this punishment.</label>
</div>
@else
<p class="text-warning">Please contact an administrator or higher if you believe this user should be poison or IP banned.<br/>If you believe this user to be suicidal, contact an owner and options will be explored to get this user help.</p>
@endadmin
<button type="submit" class="btn btn-danger w-100 mt-1">Ban User</button>
</div>
</div>
</form>
</div>
<h4 class="mt-3">Past Punishments</h4>
<x-admin.user-punishments :user="$user" />
</div>
@endpush

View File

@ -60,7 +60,17 @@
</div>
@endif
<x-admin.user-admin-label label="Username">{{ $user->username }}</x-admin.user-admin-label>
<x-admin.user-admin-label label="Previous User Names"><b>TODO</b></x-admin.user-admin-label>
@php
$prevUsernames = \App\Models\Username::where('user_id', $user->id)
->where('username', '!=', $user->username);
@endphp
@if($prevUsernames->count() > 0)
<x-admin.user-admin-label label="Previous User Names">
@foreach($prevUsernames->get() as &$userName)
<p>{{ $userName->username }} ({{ $userName->created_at->isoFormat('lll') }})</p>
@endforeach
</x-admin.user-admin-label>
@endif
<x-admin.user-admin-label label="Moderation Status"><x-admin.moderation-status :user="$user" /></x-admin.user-admin-label>
<x-admin.user-admin-label label="User Id">
<x-admin.user-search-input id="userid" definition="User ID" :value="$user->id" :nolabel=true />
@ -81,63 +91,7 @@
</div>
<h4 class="mt-3">Punishments</h4>
<div class="card">
<table class="table virtubrick-table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Action</th>
<th scope="col">Moderator</th>
<th scope="col">Created</th>
<th scope="col">Expiration</th>
<th scope="col">Acknowledged</th>
</tr>
</thead>
<tbody>
@foreach($user->punishments as $punishment)
<tr>
<th scope="col">
<button class="btn btn-sm p-0 px-1 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#punishment-collapse-{{ $punishment->id }}" aria-expanded="false" aria-controls="punishment-collapse-{{ $punishment->id }}">
<i class="fa-solid fa-bars"></i>
</button>
</th>
<th scope="col">&nbsp;{{ $punishment->id }}</th>
<th scope="col">{{ $punishment->punishment_type->label }}</th>
<th scope="col">
<a href="{{ route('admin.useradmin', ['ID' => $punishment->moderator->id]) }}" class="text-decoration-none">
<x-user-circle :user="$punishment->moderator" :size=24 />
</a>
</th>
<th scope="col">{{ $punishment->reviewed() }}</th>
<th scope="col">{{ $punishment->expirationStr() }}</th>
<th scope="col">{{ $punishment->active ? 'No' : 'Yes' }}</th>
</tr>
<tr class="collapse" id="punishment-collapse-{{ $punishment->id }}">
<td colspan="7" class="bg-secondary">
<div class="mx-2">
<p><b>Note to User:</b> {{ $punishment->user_note }}</p>
@if($punishment->context->count() > 0)
<p><b>Abuses:</b></p>
@endif
@foreach($punishment->context as $context)
<div class="card bg-secondary p-2 mb-2 border-1">
<p><b>Reason:</b> {{ $context->user_note }} (<a href="#" class="text-decoration-none">TODO: audit</a>)</p>
@if($context->description)
<p><b>Offensive Item:</b> {{ $context->description }}</p>
@endif
@if($context->content_hash)
<img src="{{ route('content', $context->content_hash) }}" class="img-fluid" width="210" height="210"/>
@endif
</div>
@endforeach
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<x-admin.user-punishments :user="$user" />
<h4 class="mt-3">Seller Ban</h4>
<div class="card me-2 p-3 pt-0 vb-admin-divider">

View File

@ -19,6 +19,7 @@
<x-admin.user-search-input id="userid" definition="User ID" />
<x-admin.user-search-input id="username" definition="Username" />
@owner
<x-admin.user-search-input id="emailaddress" definition="Email Address" />
<x-admin.user-search-input id="ipaddress" definition="IP Address" />
@endowner
</div>

View File

@ -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');

View File

@ -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()