Files
WebAPP/app/Http/Controllers/Admin/ActivityLogController.php
Rhino 2e24a40d68 Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill
- 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>
2026-03-02 07:30:37 +01:00

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