Compare commits

...

4 Commits

Author SHA1 Message Date
w33b 09c08f3fea Add comment restore button 2026-05-06 21:15:15 +02:00
w33b 75f631c3e6 Add MogLog System 2026-05-06 21:08:51 +02:00
w33b fdf26604f3 Add ability to delete comments by moderators 2026-05-06 19:02:50 +02:00
w33b 59cb39ca77 Fix remove role function 2026-05-06 16:27:42 +02:00
12 changed files with 230 additions and 34 deletions
+4 -2
View File
@@ -17,11 +17,13 @@ class IsModerator
*/ */
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR)) { if (Auth::check() && (
Auth::user()->hasRole(UserRole::MODERATOR) ||
Auth::user()->hasRole(UserRole::ADMINISTRATOR))) {
return $next($request); return $next($request);
} }
session()->flash('error_msg', 'This resource is restricted to Administrators!'); session()->flash('error_msg', 'This resource is restricted to Moderators!');
return redirect()->route('home.index'); return redirect()->route('home.index');
} }
+37 -1
View File
@@ -2,7 +2,9 @@
namespace App\Livewire; namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Episode; use App\Models\Episode;
use App\Models\ModLog;
use App\Models\User; use App\Models\User;
use App\Notifications\CommentNotification; use App\Notifications\CommentNotification;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -43,7 +45,7 @@ class Comment extends Component
'replyState.body' => 'reply', 'replyState.body' => 'reply',
]; ];
public function updatedIsEditing($isEditing) public function updatedIsEditing(bool $isEditing)
{ {
if (! $isEditing) { if (! $isEditing) {
return; return;
@@ -67,11 +69,45 @@ class Comment extends Component
{ {
$this->authorize('destroy', $this->comment); $this->authorize('destroy', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Deleted comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = $user->id;
$this->comment->save();
$this->dispatch('refresh');
return;
}
$this->comment->delete(); $this->comment->delete();
$this->dispatch('refresh'); $this->dispatch('refresh');
} }
public function restoreComment()
{
$this->authorize('restore', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Restored comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = null;
$this->comment->save();
$this->dispatch('refresh');
}
}
public function postReply() public function postReply()
{ {
if (! ($this->comment->depth() < 2)) { if (! ($this->comment->depth() < 2)) {
+8
View File
@@ -72,4 +72,12 @@ class Comment extends Model
{ {
return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count()); return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count());
} }
/**
* Returns wether or not comment has been removed by moderation
*/
public function isDeletedByModerator(): bool
{
return $this->deleted_by_moderator_id !== null;
}
} }
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ModLog extends Model
{
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'moderator',
'data',
];
}
+5 -1
View File
@@ -157,7 +157,11 @@ class User extends Authenticatable implements HasPasskeys
return; return;
} }
$this->roles = array_diff($this->roles, [$role->value]); $this->roles = collect($this->roles)
->reject(fn ($value) => $value === $role->value)
->values()
->all();
$this->save(); $this->save();
} }
} }
+21
View File
@@ -2,6 +2,7 @@
namespace App\Policies; namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Comment; use App\Models\Comment;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Auth\Access\HandlesAuthorization;
@@ -17,6 +18,26 @@ class CommentPolicy
public function destroy(User $user, Comment $comment): bool public function destroy(User $user, Comment $comment): bool
{ {
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return $user->id === $comment->user_id; return $user->id === $comment->user_id;
} }
public function restore(User $user, Comment $comment): bool
{
// Comment not deleted
if ($comment->deleted_by_moderator_id === null) {
return false;
}
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return false;
}
} }
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->bigInteger('deleted_by_moderator_id')
->nullable()
->after('parent_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->dropColumn('deleted_by_moderator_id');
});
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('mod_logs', function (Blueprint $table) {
$table->id();
$table->string('moderator');
$table->text('data');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('mod_logs');
}
};
+5 -1
View File
@@ -1,15 +1,19 @@
@auth @auth
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR)) @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR))
<div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10"> <div class="relative p-5 bg-white dark:bg-neutral-700/40 rounded-lg overflow-hidden z-10">
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<div class="float-left"> <div class="float-left">
<a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap"> <a data-te-toggle="modal" data-te-target="#modalUploadEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
<i class="fa-solid fa-plus pr-[6px]"></i> Add Episode <i class="fa-solid fa-plus pr-[6px]"></i> Add Episode
</a> </a>
</div> </div>
@endif
<div class="float-right"> <div class="float-right">
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="modal" data-te-target="#modalAddSubtitles" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap"> <a data-te-toggle="modal" data-te-target="#modalAddSubtitles" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
<i class="fa-solid fa-plus pr-[6px]"></i> Add Subtitles <i class="fa-solid fa-plus pr-[6px]"></i> Add Subtitles
</a> </a>
@endif
<a data-te-toggle="modal" data-te-target="#modalEditEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap"> <a data-te-toggle="modal" data-te-target="#modalEditEpisode" class="text-xl text-gray-800 dark:text-gray-200 leading-tight cursor-pointer whitespace-nowrap">
<i class="fa-solid fa-pen pr-[6px]"></i> Edit Episode <i class="fa-solid fa-pen pr-[6px]"></i> Edit Episode
</a> </a>
+52 -20
View File
@@ -1,40 +1,62 @@
<div> <div>
<div class="flex" id="comment-{{ $comment->id }}"> <div class="flex" id="comment-{{ $comment->id }}">
<div class="flex-shrink-0 mr-4"> <div class="flex-shrink-0 mr-4">
@if($comment->isDeletedByModerator())
<img class="h-10 w-10 rounded-full" src="{{ asset('images/default-avatar.webp') }}" alt="Deleted comment">
@else
<img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}"> <img class="h-10 w-10 rounded-full" src="{{ $comment->user->getAvatar() }}" alt="{{ $comment->user->name }}">
@endif
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex gap-2"> <div class="flex gap-2">
@if($comment->isDeletedByModerator())
@if (Auth::check() && (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR)))
<p class="font-medium text-gray-900 dark:text-gray-100">Deleted ({{ $comment->user->name }})</p>
@else
<p class="font-medium text-gray-900 dark:text-gray-100">Deleted</p>
@endif
@else
<p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p> <p class="font-medium text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</p>
@endif
@if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR)) @if($comment->user->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
<a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a> <a data-te-toggle="tooltip" title="Admin"><i class="fa-solid fa-crown text-yellow-600"></i></a>
@endif @endif
@if($comment->user->hasRole(\App\Enums\UserRole::MODERATOR))
<a data-te-toggle="tooltip" title="Admin" class="text-rose-600">Moderator</a>
@endif
@if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER)) @if($comment->user->hasRole(\App\Enums\UserRole::SUPPORTER))
<a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a> <a data-te-toggle="tooltip" title="Badge of appreciation for the horny people supporting us! :3"><i class="fa-solid fa-hand-holding-heart text-rose-600"></i></a>
@endif @endif
</div> </div>
<div class="mt-1 flex-grow w-full"> <div class="mt-1 flex-grow w-full">
@if ($isEditing) @if($comment->isDeletedByModerator())
<form wire:submit.prevent="editComment"> <div class="text-gray-700 dark:text-gray-200">Deleted by moderation.</div>
<div> @if (Auth::check() && (Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR)))
<label for="comment" class="sr-only">Comment body</label> <div class="text-gray-700 dark:text-gray-300 pt-1">Original comment: {!! $comment->presenter()->markdownBody() !!}</div>
<textarea id="comment" name="comment" rows="3" @endif
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
@error('editState.body') border-red-500 @enderror"
placeholder="Write something" wire:model.defer="editState.body"></textarea>
@error('editState.body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-3 flex items-center justify-between">
<button type="submit"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
Edit
</button>
</div>
</form>
@else @else
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div> @if ($isEditing)
<form wire:submit.prevent="editComment">
<div>
<label for="comment" class="sr-only">Comment body</label>
<textarea id="comment" name="comment" rows="3"
class="bg-white dark:bg-neutral-700 shadow-sm block w-full focus:ring-rose-500 focus:border-rose-500 border-gray-300 dark:border-gray-400/40 text-gray-900 dark:text-gray-200 placeholder:text-gray-400 rounded-md
@error('editState.body') border-red-500 @enderror"
placeholder="Write something" wire:model.defer="editState.body"></textarea>
@error('editState.body')
<p class="mt-2 text-sm text-red-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-3 flex items-center justify-between">
<button type="submit"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md shadow-sm text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500">
Edit
</button>
</div>
</form>
@else
<div class="text-gray-700 dark:text-gray-200">{!! $comment->presenter()->markdownBody() !!}</div>
@endif
@endif @endif
</div> </div>
<div class="mt-2 space-x-2 flex flex-row"> <div class="mt-2 space-x-2 flex flex-row">
@@ -87,6 +109,16 @@
Delete Delete
</button> </button>
@endcan @endcan
@can ('restore', $comment)
<button
wire:click="restoreComment"
type="button"
class="text-gray-900 dark:text-gray-100 font-medium"
>
Restore
</button>
@endcan
@endauth @endauth
</div> </div>
</div> </div>
+10 -5
View File
@@ -46,11 +46,16 @@
@include('modals.share') @include('modals.share')
@auth @auth
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
@include('admin.modals.upload-episode') @if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR) || Auth::user()->hasRole(\App\Enums\UserRole::MODERATOR))
@include('admin.modals.add-subtitles') @include('admin.modals.edit-episode')
@include('admin.modals.edit-episode') @endif
@endif
@if(Auth::user()->hasRole(\App\Enums\UserRole::ADMINISTRATOR))
@include('admin.modals.upload-episode')
@include('admin.modals.add-subtitles')
@endif
@endauth @endauth
<!-- Player Script --> <!-- Player Script -->
@vite(['resources/js/player.js']) @vite(['resources/js/player.js'])
+11 -4
View File
@@ -51,12 +51,19 @@ Route::group(['middleware' => ['auth', 'auth.admin']], function () {
Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags'); Route::get('/admin/tags', [AdminApiController::class, 'getTags'])->name('admin.tags');
Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios'); Route::get('/admin/studios', [AdminApiController::class, 'getStudios'])->name('admin.studios');
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
// Subtitles // Subtitles
Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles'); Route::get('/admin/subtitles/{episode_id}', [AdminApiController::class, 'getSubtitles'])->name('admin.subtitles');
Route::post('/admin/add-new-subtitle', [SubtitleController::class, 'store'])->name('admin.add.new.subtitle'); Route::post('/admin/add-new-subtitle', [SubtitleController::class, 'store'])->name('admin.add.new.subtitle');
Route::post('/admin/update-subtitles', [SubtitleController::class, 'update'])->name('admin.update.subtitles'); Route::post('/admin/update-subtitles', [SubtitleController::class, 'update'])->name('admin.update.subtitles');
}); });
/*
|---------------------------------------------------------------------------------
| Moderator Routes
|---------------------------------------------------------------------------------
*/
Route::group(['middleware' => ['auth', 'auth.moderator']], function () {
// Get Tags for editing Episode
Route::get('/admin/tags/{episode_id}', [AdminApiController::class, 'getEpisodeTags'])->name('admin.tags.episode');
Route::get('/admin/studio/{episode_id}', [AdminApiController::class, 'getEpisodeStudio'])->name('admin.studio.episode');
});