- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er) - Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link - Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest - Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen) - Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
265 lines
8.8 KiB
PHP
265 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Enums\UserRole;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\ActivityLog;
|
|
use App\Models\Comment;
|
|
use App\Models\Event;
|
|
use App\Models\EventCatering;
|
|
use App\Models\EventParticipant;
|
|
use App\Models\EventTimekeeper;
|
|
use App\Models\Player;
|
|
use App\Models\User;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\View\View;
|
|
|
|
class ActivityLogController extends Controller
|
|
{
|
|
public function index(Request $request): View
|
|
{
|
|
if (!auth()->user()->canViewActivityLog()) {
|
|
abort(403);
|
|
}
|
|
|
|
$request->validate([
|
|
'category' => ['nullable', 'string', 'in:auth,users,players,events,files,settings,dsgvo'],
|
|
'from' => ['nullable', 'date'],
|
|
'to' => ['nullable', 'date'],
|
|
]);
|
|
|
|
$query = ActivityLog::with('user')->latest('created_at');
|
|
|
|
// Filter: Kategorie
|
|
if ($request->filled('category')) {
|
|
$actionMap = [
|
|
'auth' => ['login', 'logout', 'login_failed', 'registered'],
|
|
'users' => ['updated', 'toggled_active', 'role_changed', 'password_reset', 'deleted', 'restored'],
|
|
'players' => ['created', 'updated', 'deleted', 'restored', 'parent_assigned', 'parent_removed'],
|
|
'events' => ['created', 'updated', 'deleted', 'participant_status_changed'],
|
|
'files' => ['uploaded', 'deleted'],
|
|
'settings' => ['updated'],
|
|
'dsgvo' => ['dsgvo_consent_uploaded', 'dsgvo_consent_confirmed', 'dsgvo_consent_revoked', 'dsgvo_consent_removed', 'dsgvo_consent_rejected', 'account_self_deleted', 'child_auto_deactivated'],
|
|
];
|
|
|
|
$category = $request->input('category');
|
|
if (isset($actionMap[$category])) {
|
|
if ($category === 'auth' || $category === 'dsgvo') {
|
|
$query->whereIn('action', $actionMap[$category]);
|
|
} else {
|
|
$modelTypeMap = [
|
|
'users' => 'User',
|
|
'players' => 'Player',
|
|
'events' => 'Event',
|
|
'files' => 'File',
|
|
'settings' => 'Setting',
|
|
];
|
|
$query->where('model_type', $modelTypeMap[$category] ?? null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter: Datum von
|
|
if ($request->filled('from')) {
|
|
$query->whereDate('created_at', '>=', $request->input('from'));
|
|
}
|
|
|
|
// Filter: Datum bis
|
|
if ($request->filled('to')) {
|
|
$query->whereDate('created_at', '<=', $request->input('to'));
|
|
}
|
|
|
|
$logs = $query->paginate(30)->withQueryString();
|
|
|
|
return view('admin.activity-logs.index', compact('logs'));
|
|
}
|
|
|
|
public function revert(ActivityLog $log): RedirectResponse
|
|
{
|
|
if (!auth()->user()->canViewActivityLog()) {
|
|
abort(403);
|
|
}
|
|
|
|
$old = $log->properties['old'] ?? [];
|
|
|
|
$revertableActions = ['deleted', 'toggled_active', 'role_changed', 'status_changed', 'participant_status_changed'];
|
|
if (!in_array($log->action, $revertableActions) || !$log->model_id) {
|
|
return back()->with('error', __('admin.log_revert_not_possible'));
|
|
}
|
|
|
|
$success = match ($log->action) {
|
|
'deleted' => $this->revertDelete($log),
|
|
'toggled_active' => $this->revertToggleActive($log, $old),
|
|
'role_changed' => $this->revertRoleChange($log, $old),
|
|
'status_changed' => $this->revertStatusChange($log, $old),
|
|
'participant_status_changed' => $this->revertParticipantStatus($log, $old),
|
|
default => false,
|
|
};
|
|
|
|
if (!$success) {
|
|
return back()->with('error', __('admin.log_revert_not_possible'));
|
|
}
|
|
|
|
ActivityLog::log('reverted', __('admin.log_reverted', ['desc' => Str::limit($log->description, 80)]), $log->model_type, $log->model_id);
|
|
|
|
return back()->with('success', __('admin.log_revert_success'));
|
|
}
|
|
|
|
private function revertDelete(ActivityLog $log): bool
|
|
{
|
|
return match ($log->model_type) {
|
|
'User' => $this->restoreModel(User::class, $log->model_id),
|
|
'Player' => $this->restoreModel(Player::class, $log->model_id),
|
|
'Event' => $this->restoreEvent($log->model_id),
|
|
'Comment' => $this->restoreComment($log->model_id),
|
|
default => false,
|
|
};
|
|
}
|
|
|
|
private function restoreModel(string $class, int $id): bool
|
|
{
|
|
$model = $class::onlyTrashed()->find($id);
|
|
if (!$model) {
|
|
return false;
|
|
}
|
|
$model->restore();
|
|
return true;
|
|
}
|
|
|
|
private function restoreEvent(int $id): bool
|
|
{
|
|
$event = Event::onlyTrashed()->find($id);
|
|
if (!$event) {
|
|
return false;
|
|
}
|
|
$event->deleted_by = null;
|
|
$event->save();
|
|
$event->restore();
|
|
return true;
|
|
}
|
|
|
|
private function restoreComment(int $id): bool
|
|
{
|
|
$comment = Comment::find($id);
|
|
if (!$comment || !$comment->isDeleted()) {
|
|
return false;
|
|
}
|
|
$comment->deleted_at = null;
|
|
$comment->deleted_by = null;
|
|
$comment->save();
|
|
return true;
|
|
}
|
|
|
|
private function revertToggleActive(ActivityLog $log, array $old): bool
|
|
{
|
|
$model = match ($log->model_type) {
|
|
'User' => User::withTrashed()->find($log->model_id),
|
|
'Player' => Player::withTrashed()->find($log->model_id),
|
|
default => null,
|
|
};
|
|
|
|
if (!$model || !isset($old['is_active'])) {
|
|
return false;
|
|
}
|
|
|
|
$model->is_active = filter_var($old['is_active'], FILTER_VALIDATE_BOOLEAN);
|
|
$model->save();
|
|
return true;
|
|
}
|
|
|
|
private function revertRoleChange(ActivityLog $log, array $old): bool
|
|
{
|
|
if ($log->model_type !== 'User' || !isset($old['role'])) {
|
|
return false;
|
|
}
|
|
|
|
// Enum-Validierung: Nur gültige Rollen-Werte akzeptieren (V02)
|
|
$validRoles = array_column(UserRole::cases(), 'value');
|
|
if (!in_array($old['role'], $validRoles, true)) {
|
|
return false;
|
|
}
|
|
|
|
$user = User::withTrashed()->find($log->model_id);
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
// Selbst-Rollen-Änderung verhindern
|
|
if ($user->id === auth()->id()) {
|
|
return false;
|
|
}
|
|
|
|
// Nicht-Admins dürfen keine Admin-Rolle zuweisen oder Admin-Rollen rückgängig machen
|
|
if (!auth()->user()->isAdmin() && ($user->isAdmin() || $old['role'] === 'admin')) {
|
|
return false;
|
|
}
|
|
|
|
$user->role = $old['role'];
|
|
$user->save();
|
|
return true;
|
|
}
|
|
|
|
private function revertStatusChange(ActivityLog $log, array $old): bool
|
|
{
|
|
if (!isset($old['status']) || $log->model_type !== 'Event') {
|
|
return false;
|
|
}
|
|
|
|
$userId = $log->properties['old']['user_id'] ?? $log->properties['new']['user_id'] ?? null;
|
|
if (!$userId) {
|
|
return false;
|
|
}
|
|
|
|
$catering = EventCatering::where('event_id', $log->model_id)->where('user_id', $userId)->first();
|
|
if ($catering) {
|
|
$catering->update(['status' => $old['status']]);
|
|
return true;
|
|
}
|
|
|
|
$timekeeper = EventTimekeeper::where('event_id', $log->model_id)->where('user_id', $userId)->first();
|
|
if ($timekeeper) {
|
|
$timekeeper->update(['status' => $old['status']]);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function revertParticipantStatus(ActivityLog $log, array $old): bool
|
|
{
|
|
if (!isset($old['status']) || $log->model_type !== 'Event') {
|
|
return false;
|
|
}
|
|
|
|
// Spieler-ID bevorzugen, Namens-Suche als Fallback (DB-agnostisch)
|
|
$participant = EventParticipant::where('event_id', $log->model_id)
|
|
->when(
|
|
isset($old['participant_id']),
|
|
fn ($q) => $q->where('id', $old['participant_id']),
|
|
fn ($q) => $q->when(
|
|
isset($old['player']),
|
|
fn ($q2) => $q2->whereHas('player', function ($pq) use ($old) {
|
|
$parts = explode(' ', $old['player'], 2);
|
|
if (count($parts) === 2) {
|
|
$pq->where('first_name', $parts[0])->where('last_name', $parts[1]);
|
|
}
|
|
})
|
|
)
|
|
)
|
|
->first();
|
|
|
|
if (!$participant) {
|
|
return false;
|
|
}
|
|
|
|
$participant->status = $old['status'];
|
|
$participant->set_by_user_id = auth()->id();
|
|
$participant->responded_at = now();
|
|
$participant->save();
|
|
return true;
|
|
}
|
|
}
|