Catalog changes and punishments.

- Updated pagination to 35 assets per page in order to fit with newer card scaling.
- Implemented the ability to go to traverse pages in the shop.
- Allowed the CPU/Memory usage updater job to run when the site is under maintenance.
- User punishments.
- Revamped punishment notice page.
- Created middleware to redirect to punishment notice page when a punishment is active.
- Completed moderation status label on user admin/search pages.
- Added routes for punishments.
- Fixed user homepage button on user admin page.
- Added punishments section on user admin page.
- Removed legacy bans.
- Prevent banned user thumbnails from being rendered.
This commit is contained in:
Graphictoria 2023-01-06 20:57:15 -05:00
parent 241f2deb16
commit 0083a01d85
27 changed files with 545 additions and 120 deletions

View File

@ -17,7 +17,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
$schedule->job(new UpdateUsageCounters)->everyMinute(); $schedule->job(new UpdateUsageCounters)->everyMinute()->evenInMaintenanceMode();
} }
/** /**

View File

@ -46,7 +46,6 @@ class CommentsController extends Controller
]; ];
foreach($comments as $comment) { foreach($comments as $comment) {
// TODO: XlXi: user profile link
$poster = [ $poster = [
'name' => $comment->user->username, 'name' => $comment->user->username,
'thumbnail' => 'https://www.virtubrick.local/images/testing/headshot.png', 'thumbnail' => 'https://www.virtubrick.local/images/testing/headshot.png',

View File

@ -65,7 +65,7 @@ class ShopController extends Controller
$assets = self::getAssets($valid['assetTypeId'], (isset($valid['gearGenreId']) ? $valid['gearGenreId'] : null)); $assets = self::getAssets($valid['assetTypeId'], (isset($valid['gearGenreId']) ? $valid['gearGenreId'] : null));
$assets = $assets->orderByDesc('created_at') $assets = $assets->orderByDesc('created_at')
->paginate(30); ->paginate(35);
$data = []; $data = [];
foreach($assets as $asset) { foreach($assets as $asset) {

View File

@ -31,11 +31,10 @@ class ThumbnailController extends Controller
private function userValidationRules() private function userValidationRules()
{ {
// TODO: Fail validation if user is moderated.
return [ return [
'id' => [ 'id' => [
'required', 'required',
Rule::exists('App\Models\User', 'id') Rule::exists('App\Models\User', 'id'),
], ],
'position' => ['sometimes', 'regex:/(Full|Bust)/i'], 'position' => ['sometimes', 'regex:/(Full|Bust)/i'],
'type' => 'regex:/(3D|2D)/i' 'type' => 'regex:/(3D|2D)/i'
@ -55,7 +54,13 @@ class ThumbnailController extends Controller
$valid['type'] = strtolower($valid['type']); $valid['type'] = strtolower($valid['type']);
if($renderType == 'User') { if($renderType == 'User') {
if($valid['position'] == null) if($model->hasActivePunishment() && $model->getPunishment()->isDeletion())
{
$validator->errors()->add('id', 'User is moderated');
return ValidationHelper::generateValidatorError($validator);
}
if(!array_key_exists('position', $valid))
$valid['position'] = 'Full'; $valid['position'] = 'Full';
$valid['position'] = strtolower($valid['position']); $valid['position'] = strtolower($valid['position']);
@ -109,9 +114,9 @@ class ThumbnailController extends Controller
return $this->handleRender($request, 'Asset'); return $this->handleRender($request, 'Asset');
} }
public function renderUser() public function renderUser(Request $request)
{ {
return handleRender($request, 'User'); return $this->handleRender($request, 'User');
} }
public function tryAsset() public function tryAsset()

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Controllers\Web\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class UserModerationController extends Controller
{
public function create()
{
return view('web.auth.moderated');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\Web;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ModerationController extends Controller
{
public function notice(Request $request)
{
return view('web.auth.moderated')->with('punishment', Auth::user()->getPunishment());
}
public function reactivate(Request $request)
{
$punishment = Auth::user()->getPunishment();
if(!$punishment || !$punishment->expired())
return redirect()->back();
$punishment->active = false;
$punishment->save();
return redirect()->back();
}
}

View File

@ -11,6 +11,9 @@ class ProfileController extends Controller
{ {
protected function index(Request $request, User $user) protected function index(Request $request, User $user)
{ {
if($user->hasActivePunishment() && $user->getPunishment()->isDeletion())
abort(404);
return view('web.user.profile')->with([ return view('web.user.profile')->with([
'title' => sprintf('Profile of %s', $user->username), 'title' => sprintf('Profile of %s', $user->username),
'user' => $user 'user' => $user

View File

@ -25,7 +25,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
]; ];
/** /**
@ -43,14 +43,15 @@ class Kernel extends HttpKernel
// XlXi: Yeah no, the double session protector was stupid. // XlXi: Yeah no, the double session protector was stupid.
//\App\Http\Middleware\DoubleSessionProtector::class, // Prevents DDoS attacks. //\App\Http\Middleware\DoubleSessionProtector::class, // Prevents DDoS attacks.
\App\Http\Middleware\DailyReward::class, \App\Http\Middleware\DailyReward::class,
\App\Http\Middleware\LastSeenMiddleware::class \App\Http\Middleware\LastSeenMiddleware::class,
\App\Http\Middleware\UserPunishmentMiddleware::class
], ],
'api' => [ 'api' => [
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\UserPunishmentMiddleware::class
], ],
]; ];
@ -74,7 +75,6 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'roleset' => \App\Http\Middleware\Roleset::class, 'roleset' => \App\Http\Middleware\Roleset::class,
'banned' => \App\Http\Middleware\CheckBan::class,
'lastseen' => \App\Http\Middleware\LastSeenMiddleware::class, 'lastseen' => \App\Http\Middleware\LastSeenMiddleware::class,
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, 'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
]; ];

View File

@ -1,31 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckBan
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if(Auth::check() && Auth::user()->banId != null) {
if($request->route()->getName() != 'moderation.notice' && $request->route()->getName() != 'logout') {
return redirect()
->to(route('moderation.notice', [], 302));
}
} else {
return redirect('/', 302);
}
return $next($request);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
class UserPunishmentMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$isPunishmentRoute = str_starts_with($request->route()->getName(), 'punishment.');
if(Auth::user() && Auth::user()->hasActivePunishment())
{
if($isPunishmentRoute || $request->route()->getName() == 'auth.logout')
return $next($request);
if(in_array('api', $request->route()->middleware()))
{
if($request->route()->getName() == 'content') // cdn.virtubrick.net
return $next($request);
return response(['errors' => [['code' => 0, 'message' => 'User is moderated']]], 403)
->header('Cache-Control', 'private')
->header('Content-Type', 'application/json; charset=utf-8');
}
// Not an API route.
if(!$isPunishmentRoute)
return redirect()->route('punishment.notice', ['ReturnUrl' => url()->full()]);
}
elseif($isPunishmentRoute)
{
$returnUrl = $request->input('ReturnUrl');
if(!$returnUrl)
$returnUrl = '/';
return redirect($returnUrl);
}
return $next($request);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Punishment extends Model
{
use HasFactory;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'expiration' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
public function punishment_type()
{
return $this->belongsTo(PunishmentType::class, 'punishment_type_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function moderator()
{
return $this->belongsTo(User::class, 'moderator_id');
}
public function pardoner()
{
return $this->belongsTo(User::class, 'pardoner_id');
}
public function context()
{
return $this->hasMany(PunishmentContext::class, 'punishment_id');
}
public function expired()
{
if($this->user->hasRoleset('Owner'))
return true;
if(!$this->expiration)
return false;
return !$this->isDeletion() && Carbon::now()->greaterThan($this->expiration);
}
public function isDeletion()
{
return $this->punishment_type->time === null;
}
public function reviewed()
{
return $this->created_at->isoFormat('lll');
}
public function expirationStr()
{
if(!$this->expiration)
return 'Never';
return $this->created_at->isoFormat('lll');
}
public static function activeFor($userId)
{
return self::where('user_id', $userId)
->where('active', true)
->orderByDesc('id');
}
}

View File

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

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PunishmentType extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'label',
'time'
];
}

View File

@ -132,6 +132,21 @@ class User extends Authenticatable implements MustVerifyEmail
return false; return false;
} }
public function hasActivePunishment()
{
return Punishment::activeFor($this->id)->exists();
}
public function getPunishment()
{
return Punishment::activeFor($this->id)->first();
}
public function punishments()
{
return $this->hasMany(Punishment::class, 'user_id');
}
public function _hasRolesetInternal($roleName) public function _hasRolesetInternal($roleName)
{ {
$roleset = Roleset::where('Name', $roleName)->first(); $roleset = Roleset::where('Name', $roleName)->first();

View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Components\Admin;
use Illuminate\View\Component;
class ModerationStatus 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.moderation-status');
}
}

View File

@ -20,7 +20,6 @@ return new class extends Migration
$table->dateTime('email_verified_at')->nullable(); $table->dateTime('email_verified_at')->nullable();
$table->string('password'); $table->string('password');
$table->rememberToken(); $table->rememberToken();
$table->unsignedBigInteger('banId')->nullable();
$table->string('biography')->nullable(); $table->string('biography')->nullable();

View File

@ -0,0 +1,40 @@
<?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('punishments', function (Blueprint $table) {
$table->id();
$table->unsignedTinyInteger('punishment_type_id');
$table->boolean('active');
$table->string('user_note');
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('moderator_id');
$table->unsignedBigInteger('pardoner_id')->nullable();
$table->timestamp('expiration')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('punishments');
}
};

View File

@ -0,0 +1,35 @@
<?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('punishment_types', function (Blueprint $table) {
$table->id();
$table->string('label');
$table->integer('time')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('punishment_types');
}
};

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('punishment_contexts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('punishment_id');
$table->string('user_note');
$table->longText('description')->nullable();
$table->string('content_hash')->nullable()->comment('Will display an image from the CDN.');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('punishment_contexts');
}
};

View File

@ -18,7 +18,8 @@ class DatabaseSeeder extends Seeder
WebConfigurationSeeder::class, WebConfigurationSeeder::class,
AssetTypeSeeder::class, AssetTypeSeeder::class,
UsageCounterSeeder::class, UsageCounterSeeder::class,
RolesetSeeder::class RolesetSeeder::class,
PunishmentTypeSeeder::class
//FFlagSeeder::class //FFlagSeeder::class
]); ]);
} }

View File

@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\PunishmentType;
class PunishmentTypeSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
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']);
}
}

View File

@ -150,7 +150,9 @@ class ShopCategoryButton extends Component {
} }
handleClick() { handleClick() {
this.props.navigateCategory(this.props.id, this.data); this.props.setPage(1, true, () => {
this.props.navigateCategory(this.props.id, this.data);
});
} }
render() { render() {
@ -177,7 +179,7 @@ class ShopCategories extends Component {
return ( return (
<div className="virtubrick-shop-categories"> <div className="virtubrick-shop-categories">
<h5>Category</h5> <h5>Category</h5>
<ShopCategoryButton id="all" label="All Items" getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} /> <ShopCategoryButton id="all" label="All Items" getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} setPage={this.props.setPage} />
<ul className="list-unstyled ps-0"> <ul className="list-unstyled ps-0">
{ {
Object.keys(shopCategories).map((categoryName, index) => Object.keys(shopCategories).map((categoryName, index) =>
@ -185,10 +187,10 @@ class ShopCategories extends Component {
<a className="text-decoration-none fw-normal align-items-center virtubrick-list-dropdown" data-bs-toggle="collapse" data-bs-target={`#${makeCategoryId(categoryName, 'collapse')}`} aria-expanded={(index === 0 ? 'true' : 'false')} href="#">{ categoryName }</a> <a className="text-decoration-none fw-normal align-items-center virtubrick-list-dropdown" data-bs-toggle="collapse" data-bs-target={`#${makeCategoryId(categoryName, 'collapse')}`} aria-expanded={(index === 0 ? 'true' : 'false')} href="#">{ categoryName }</a>
<div className={classNames({'collapse': true, 'show': (index === 0)})} id={makeCategoryId(categoryName, 'collapse')}> <div className={classNames({'collapse': true, 'show': (index === 0)})} id={makeCategoryId(categoryName, 'collapse')}>
<ul className="btn-toggle-nav list-unstyled fw-normal small"> <ul className="btn-toggle-nav list-unstyled fw-normal small">
<li><ShopCategoryButton id={makeCategoryId(`all-${categoryName}`, 'type')} label={`All ${categoryName}`} categoryName={categoryName} getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} /></li> <li><ShopCategoryButton id={makeCategoryId(`all-${categoryName}`, 'type')} label={`All ${categoryName}`} categoryName={categoryName} getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} setPage={this.props.setPage} /></li>
{ {
shopCategories[categoryName].map(({label, assetTypeId, gearGenreId}, index) => shopCategories[categoryName].map(({label, assetTypeId, gearGenreId}, index) =>
<li><ShopCategoryButton id={makeCategoryId(`${label}-${categoryName}`, 'type')} label={label} categoryName={categoryName} getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} /></li> <li><ShopCategoryButton id={makeCategoryId(`${label}-${categoryName}`, 'type')} label={label} categoryName={categoryName} getCategoryAssetTypeByLabel={this.props.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.props.getCategoryAssetTypeIds} navigateCategory={this.props.navigateCategory} shopState={this.props.shopState} setPage={this.props.setPage} /></li>
) )
} }
</ul> </ul>
@ -271,10 +273,13 @@ class Shop extends Component {
pageLoaded: true, pageLoaded: true,
pageNumber: null, pageNumber: null,
pageCount: null, pageCount: null,
error: false error: false,
dataMem: false
}; };
this.navigateCategory = this.navigateCategory.bind(this); this.navigateCategory = this.navigateCategory.bind(this);
this.incrementPage = this.incrementPage.bind(this);
this.setPage = this.setPage.bind(this);
} }
getCategoryAssetTypeIds(categoryName) { getCategoryAssetTypeIds(categoryName) {
@ -304,14 +309,23 @@ class Shop extends Component {
return assetType; return assetType;
} }
navigateCategory(categoryId, data) { navigateCategory(categoryId, dataraw) {
this.setState({selectedCategoryId: categoryId, pageLoaded: false}); if(this.state.pageLoaded == false) return;
this.setState({selectedCategoryId: categoryId, dataMem: dataraw, pageLoaded: false});
let url = buildGenericApiUrl('api', 'shop/v1/list-json'); let url = buildGenericApiUrl('api', 'shop/v1/list-json');
if (this.state.pageNumber == null || this.state.pageNumber == 1)
this.setState({pageNumber: 1});
let paramIterator = 0; let paramIterator = 0;
let data = {...dataraw, page: this.state.pageNumber};
Object.keys(data).filter(key => { Object.keys(data).filter(key => {
if (key == 'label') if (key == 'label')
return false; return false;
else if (key == 'page' && (data[key] == null || data[key] == 1))
return false;
return true; return true;
}).map(key => { }).map(key => {
url += ((paramIterator++ == 0 ? '?' : '&') + `${key}=${data[key]}`); url += ((paramIterator++ == 0 ? '?' : '&') + `${key}=${data[key]}`);
@ -321,7 +335,7 @@ class Shop extends Component {
.then(res => { .then(res => {
const items = res.data; const items = res.data;
this.setState({ pageItems: items.data, pageCount: items.pages, pageNumber: 1, pageLoaded: true, error: false }); this.setState({ pageItems: items.data, pageCount: items.pages, pageLoaded: true, error: false });
}).catch(err => { }).catch(err => {
const data = err.response.data; const data = err.response.data;
@ -334,6 +348,22 @@ class Shop extends Component {
}); });
} }
incrementPage(amount) {
this.setPage(this.state.pageNumber + amount);
}
setPage(page, bypass = false, callback) {
if(!bypass && this.state.pageLoaded == false) return;
this.setState({pageNumber: page}, () => {
if(callback)
callback();
if(!bypass)
this.navigateCategory(this.state.selectedCategoryId, this.state.dataMem);
});
}
render() { render() {
return ( return (
<div className="container-lg my-2"> <div className="container-lg my-2">
@ -351,7 +381,7 @@ class Shop extends Component {
</div> </div>
<div className="row"> <div className="row">
<div className="col-md-2"> <div className="col-md-2">
<ShopCategories getCategoryAssetTypeByLabel={this.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.getCategoryAssetTypeIds} navigateCategory={this.navigateCategory} shopState={this.state} /> <ShopCategories getCategoryAssetTypeByLabel={this.getCategoryAssetTypeByLabel} getCategoryAssetTypeIds={this.getCategoryAssetTypeIds} navigateCategory={this.navigateCategory} shopState={this.state} setPage={this.setPage} />
</div> </div>
<div className="col-md-10 d-flex flex-column"> <div className="col-md-10 d-flex flex-column">
<div className="card p-3"> <div className="card p-3">
@ -386,7 +416,7 @@ class Shop extends Component {
this.state.pageCount > 1 ? this.state.pageCount > 1 ?
<ul className="list-inline mx-auto mt-3"> <ul className="list-inline mx-auto mt-3">
<li className="list-inline-item"> <li className="list-inline-item">
<button className="btn btn-secondary" disabled={(this.state.pageNumber <= 1) ? true : null}><i className="fa-solid fa-angle-left"></i></button> <button className="btn btn-secondary" disabled={(this.state.pageNumber <= 1) ? true : null} onClick={ () => this.incrementPage(-1) }><i className="fa-solid fa-angle-left"></i></button>
</li> </li>
<li className="list-inline-item virtubrick-paginator"> <li className="list-inline-item virtubrick-paginator">
<span>Page&nbsp;</span> <span>Page&nbsp;</span>
@ -394,7 +424,7 @@ class Shop extends Component {
<span>&nbsp;of { this.state.pageCount || '???' }</span> <span>&nbsp;of { this.state.pageCount || '???' }</span>
</li> </li>
<li className="list-inline-item"> <li className="list-inline-item">
<button className="btn btn-secondary" disabled={(this.state.pageNumber >= this.state.pageCount) ? true : null}><i className="fa-solid fa-angle-right"></i></button> <button className="btn btn-secondary" disabled={(this.state.pageNumber >= this.state.pageCount) ? true : null} onClick={ () => this.incrementPage(1) }><i className="fa-solid fa-angle-right"></i></button>
</li> </li>
</ul> </ul>
: :

View File

@ -0,0 +1,19 @@
@props([
'user'
])
@php
$color = 'text-';
$label = 'Unknown';
if($user->hasActivePunishment())
{
$color .= 'danger';
$label = $user->getPunishment()->punishment_type->label;
}
else
{
$color .= 'success';
$label = 'OK';
}
@endphp
<p class="{{ $color }}">{{ $label }}</p>

View File

@ -61,7 +61,7 @@
@endif @endif
<x-admin.user-admin-label label="Username">{{ $user->username }}</x-admin.user-admin-label> <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> <x-admin.user-admin-label label="Previous User Names"><b>TODO</b></x-admin.user-admin-label>
<x-admin.user-admin-label label="Moderation Status"><span class="text-success">OK (TODO)</span></x-admin.user-admin-label> <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-admin-label label="User Id">
<x-admin.user-search-input id="userid" definition="User ID" :value="$user->id" :nolabel=true /> <x-admin.user-search-input id="userid" definition="User ID" :value="$user->id" :nolabel=true />
</x-admin.user-admin-label> </x-admin.user-admin-label>
@ -74,7 +74,7 @@
<img src="{{ asset('/images/testing/avatar.png') }}" width="200" height="200" class="img-fluid vb-charimg" /> <img src="{{ asset('/images/testing/avatar.png') }}" width="200" height="200" class="img-fluid vb-charimg" />
</div> </div>
<div class="col-6"> <div class="col-6">
<a href="#" class="text-decoration-none">User Homepage</a><br/> <a href="{{ $user->getProfileUrl() }}" class="text-decoration-none">User Homepage</a><br/>
<a href="#" class="text-decoration-none">Moderate User</a> <a href="#" class="text-decoration-none">Moderate User</a>
</div> </div>
</div> </div>
@ -90,16 +90,24 @@
<th scope="col">Moderator</th> <th scope="col">Moderator</th>
<th scope="col">Created</th> <th scope="col">Created</th>
<th scope="col">Expiration</th> <th scope="col">Expiration</th>
<th scope="col">Acknowledged</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> @foreach($user->punishments as $punishment)
<th scope="col">1</th> <tr>
<th scope="col">1 day, perm, etc...</th> <th scope="col">{{ $punishment->id }}</th>
<th scope="col">Joe</th> <th scope="col">{{ $punishment->punishment_type->label }}</th>
<th scope="col">1/2/3 4:5 6</th> <th scope="col">
<th scope="col">1/2/3 4:5 6</th> <a href="{{ route('admin.useradmin', ['ID' => $punishment->moderator->id]) }}" class="text-decoration-none">
</tr> <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>
@endforeach
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -56,7 +56,7 @@
</th> </th>
<th scope="col">{{ $user->id }}</th> <th scope="col">{{ $user->id }}</th>
<th scope="col">{{ Auth::user()->hasRoleset('Owner') ? $user->email : $user->getCensoredEmail() }}</th> <th scope="col">{{ Auth::user()->hasRoleset('Owner') ? $user->email : $user->getCensoredEmail() }}</th>
<th scope="col"><p class="text-success">OK (TODO)</p></th> <th scope="col"><x-admin.moderation-status :user="$user" /></th>
<th scope="col"> <th scope="col">
@if($rolesetCount > 0) @if($rolesetCount > 0)
@php @php

View File

@ -1,42 +1,62 @@
@php
$noFooter = true;
$noNav = true;
@endphp
@extends('layouts.app') @extends('layouts.app')
@section('theme', 'light')
@section('title', 'Moderation Notice') @nonav
@nofooter
@section('content') @section('content')
<div class="container m-auto"> <div class="container m-auto">
<x-card class="virtubrick-moderation-card"> <div class="card p-3 virtubrick-moderation-card">
<x-slot name="title"> <h3>{{ $punishment->punishment_type->label }}</h3>
MODERATION NOTICE <p>
</x-slot> Your account has been {{ $punishment->isDeletion() ? 'closed ' : 'temporarily restricted' }} for violating our Terms of Service.
<x-slot name="body"> @if(!$punishment->isDeletion())
<div class="p-2 mb-2 d-flex flex-column justify-content-center"> Your account will be terminated if you do not abide by the rules.
<p>Your account has been suspended for violating our Terms of Service.</p> @endif
<div class="my-3"> </p>
<p><b>Suspention Date:</b> 5/6/2022 9:35 PM</p>
<p><b>Note:</b> testing</p> <div class="my-3">
</div> <p><b>Reviewed:</b> {{ $punishment->reviewed() }}</p>
</div> <p><b>Moderator Note:</b> {{ $punishment->user_note }}</p>
</x-slot> </div>
<x-slot name="footer">
<p>By checking the "I Agree" checkbox below, you agree to abide by {{ config('app.name') }}'s Terms of Service. Your account will be permantently suspended if you continue breaking the Terms of Service.</p> @foreach($punishment->context as $context)
<form> <div class="card bg-secondary p-2 mb-2 border-1">
<div class="my-2"> <p><b>Reason:</b> {{ $context->user_note }}</p>
<input class="form-check-input" type="checkbox" value="" id="agree" name="agree"> @if($context->description)
<label class="form-check-label" for="agree"> <p><b>Offensive Item:</b> {{ $context->description }}</p>
I Agree @endif
</label> @if($context->content_hash)
</div> <img src="{{ route('content', $context->content_hash) }}" class="img-fluid" width="210" height="210"/>
<button class="btn btn-primary">REACTIVATE</button> @endif
</form> </div>
@endforeach
<div class="text-center">
@if($punishment->expired())
<p>By checking the "I Agree" checkbox below, you agree to abide by {{ config('app.name') }}'s Terms of Service.</p>
<form method="POST" action="{{ route('punishment.reactivate') }}" class="mt-2">
@csrf
<div class="mb-2">
<input class="form-check-input" type="checkbox" value="" id="vb-reactivation-checkbox">
<label class="form-check-label" for="vb-reactivation-checkbox">
I Agree
</label>
</div>
<button class="btn btn-success" id="vb-reactivation-button" disabled>Re-activate My Account</button>
</form>
<script>
document.getElementById('vb-reactivation-checkbox').addEventListener('change', (event) => {
document.getElementById('vb-reactivation-button').disabled = !event.currentTarget.checked;
});
</script>
@elseif(!$punishment->isDeletion())
<p>You will be able to reactivate your account in <b>{{ $punishment->expiration->diffForHumans(['syntax' => Carbon\CarbonInterface::DIFF_ABSOLUTE]) }}</b>.</p>
@endif
<a class="btn btn-primary my-2" href="{{ route('auth.logout') }}">Logout</a>
<p>You will be able to reactivate your account in <b>0 Seconds</b>.</p>
<p class="text-muted">If you believe you have been unfairly moderated, please contact us at contact us at <a href="mailto:support@virtubrick.net" class="fw-bold text-decoration-none">support@virtubrick.net</a> and we'll be happy to help.</p> <p class="text-muted">If you believe you have been unfairly moderated, please contact us at contact us at <a href="mailto:support@virtubrick.net" class="fw-bold text-decoration-none">support@virtubrick.net</a> and we'll be happy to help.</p>
</x-slot> </div>
</x-card> </div>
</div> </div>
@endsection @endsection

View File

@ -74,12 +74,10 @@ Route::group(['as' => 'admin.', 'prefix' => 'admin'], function() {
}); });
Route::group(['as' => 'auth.', 'namespace' => 'Auth'], function() { Route::group(['as' => 'auth.', 'namespace' => 'Auth'], function() {
Route::group(['as' => 'protection.', 'prefix' => 'request-blocked'], function() { //Route::group(['as' => 'protection.', 'prefix' => 'request-blocked'], function() {
Route::get('/', 'DoubleSessionBlockController@index')->name('index'); // Route::get('/', 'DoubleSessionBlockController@index')->name('index');
Route::post('/', 'DoubleSessionBlockController@store')->name('bypass'); // Route::post('/', 'DoubleSessionBlockController@store')->name('bypass');
}); //});
Route::get('/moderation-notice', 'UserModerationController@index')->middleware(['auth', 'banned'])->name('moderation.notice');
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
Route::group(['as' => 'register.', 'prefix' => 'register'], function() { Route::group(['as' => 'register.', 'prefix' => 'register'], function() {
@ -120,6 +118,12 @@ Route::group(['as' => 'auth.', 'namespace' => 'Auth'], function() {
}); });
}); });
Route::group(['as' => 'punishment.', 'prefix' => 'membership'], function() {
Route::middleware('auth')->group(function () {
Route::get('/not-approved', 'ModerationController@notice')->name('notice');
Route::post('/not-approved', 'ModerationController@reactivate')->name('reactivate');
});
});
Route::withoutMiddleware(['csrf'])->group(function () { Route::withoutMiddleware(['csrf'])->group(function () {
Route::group(['as' => 'client.'], function() { Route::group(['as' => 'client.'], function() {
@ -130,3 +134,7 @@ Route::withoutMiddleware(['csrf'])->group(function () {
}); });
}); });
}); });
Route::fallback(function() {
return view('errors.404');
});