input('sort'), $sortable) ? $request->input('sort') : 'created_at'; $direction = $request->input('direction') === 'asc' ? 'asc' : 'desc'; match ($sort) { 'last_login_at' => $query->orderBy(DB::raw('CASE WHEN last_login_at IS NULL THEN 1 ELSE 0 END'))->orderBy('last_login_at', $direction), default => $query->orderBy($sort, $direction), }; $users = $query->paginate(20)->withQueryString(); $trashedUsers = User::onlyTrashed() ->where('deleted_at', '>=', now()->subDays(7)) ->latest('deleted_at') ->get(); return view('admin.users.index', compact('users', 'trashedUsers', 'sort', 'direction')); } public function edit(User $user): View { return view('admin.users.edit', compact('user')); } /** * Schutz: Coach darf keine Admin-Konten aendern (S01). */ private function guardAgainstCoachModifyingAdmin(User $user): void { if (!auth()->user()->isAdmin() && $user->isAdmin()) { abort(403, __('admin.cannot_modify_admin')); } } public function update(Request $request, User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 'phone' => ['nullable', 'string', 'max:30'], 'role' => ['required', 'in:admin,coach,parent_rep,user'], 'profile_picture' => ['nullable', 'image', 'max:2048', 'mimes:jpg,jpeg,png,gif,webp'], ]); // Selbstschutz: Eigene Rolle nicht ändern if ($user->id === auth()->id()) { unset($validated['role']); } elseif (isset($validated['role']) && $validated['role'] === 'admin' && !auth()->user()->isAdmin()) { abort(403, __('admin.cannot_assign_admin_role')); } // Handle profile picture upload if ($request->hasFile('profile_picture')) { if ($user->profile_picture) { Storage::disk('public')->delete($user->profile_picture); } $file = $request->file('profile_picture'); $storedName = 'avatars/' . Str::uuid() . '.' . $file->guessExtension(); Storage::disk('public')->putFileAs('', $file, $storedName); $validated['profile_picture'] = $storedName; } else { unset($validated['profile_picture']); } $oldData = ['name' => $user->name, 'email' => $user->email, 'role' => $user->role->value]; // Rolle separat setzen (nicht in $fillable fuer Mass-Assignment-Schutz) $newRole = $validated['role'] ?? null; unset($validated['role']); $user->update($validated); if ($newRole) { $user->role = $newRole; $user->save(); } $newData = ['name' => $user->name, 'email' => $user->email, 'role' => $user->role->value]; ActivityLog::logWithChanges('updated', __('admin.log_user_updated', ['name' => $user->name]), 'User', $user->id, $oldData, $newData); return redirect()->route('admin.users.index')->with('success', __('admin.user_updated')); } public function toggleActive(User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); if ($user->id === auth()->id()) { return back()->with('error', __('admin.cannot_deactivate_self')); } $oldActive = $user->is_active; $user->is_active = !$user->is_active; $user->save(); $status = $user->is_active ? __('admin.activated') : __('admin.deactivated'); ActivityLog::logWithChanges('toggled_active', __('admin.log_user_toggled', ['name' => $user->name, 'status' => $status]), 'User', $user->id, ['is_active' => $oldActive], ['is_active' => $user->is_active]); return back()->with('success', __('admin.user_toggled', ['status' => $status])); } public function updateRole(Request $request, User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); if ($user->id === auth()->id()) { return back()->with('error', __('admin.cannot_change_own_role')); } $validated = $request->validate([ 'role' => ['required', 'in:admin,coach,parent_rep,user'], ]); if ($validated['role'] === 'admin' && !auth()->user()->isAdmin()) { abort(403, __('admin.cannot_assign_admin_role')); } $oldRole = $user->role->value; $user->role = $validated['role']; $user->save(); ActivityLog::logWithChanges('role_changed', __('admin.log_role_changed', ['name' => $user->name, 'role' => $validated['role']]), 'User', $user->id, ['role' => $oldRole], ['role' => $validated['role']]); return back()->with('success', __('admin.role_updated')); } public function resetPassword(User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); if ($user->id === auth()->id()) { return back()->with('error', __('admin.cannot_reset_own_password')); } $status = Password::sendResetLink(['email' => $user->email]); ActivityLog::log('password_reset', __('admin.log_password_reset', ['name' => $user->name]), 'User', $user->id); if ($status === Password::RESET_LINK_SENT) { return redirect()->route('admin.users.edit', $user) ->with('success', __('admin.password_reset_link_sent')); } return redirect()->route('admin.users.edit', $user) ->with('error', __($status)); } public function removePicture(User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); if ($user->profile_picture) { Storage::disk('public')->delete($user->profile_picture); $user->update(['profile_picture' => null]); } return back()->with('success', __('admin.picture_removed')); } public function destroy(User $user): RedirectResponse { $this->guardAgainstCoachModifyingAdmin($user); // Schutz: nicht sich selbst und nicht User ID 1 löschen if ($user->id === auth()->id()) { return back()->with('error', __('admin.cannot_delete_self')); } if ($user->id === 1) { return back()->with('error', __('admin.cannot_delete_main_admin')); } $user->delete(); ActivityLog::logWithChanges('deleted', __('admin.log_user_deleted', ['name' => $user->name]), 'User', $user->id, ['name' => $user->name, 'email' => $user->email, 'role' => $user->role->value], null); return redirect()->route('admin.users.index')->with('success', __('admin.user_deleted')); } public function restore(int $id): RedirectResponse { $user = User::onlyTrashed()->findOrFail($id); if (! $user->isRestorable()) { return back()->with('error', __('admin.restore_expired')); } $user->restore(); ActivityLog::logWithChanges('restored', __('admin.log_user_restored', ['name' => $user->name]), 'User', $user->id, null, ['name' => $user->name, 'email' => $user->email]); return back()->with('success', __('admin.user_restored')); } public function toggleDsgvoConsent(User $user): RedirectResponse { if (!auth()->user()->isAdmin()) { abort(403); } if (!$user->hasDsgvoConsent()) { return back()->with('error', __('admin.dsgvo_no_file')); } if ($user->isDsgvoConfirmed()) { $old = [ 'dsgvo_accepted_at' => $user->dsgvo_accepted_at->toDateTimeString(), 'dsgvo_accepted_by' => $user->dsgvoAcceptedBy?->name ?? (string) $user->dsgvo_accepted_by, ]; $user->dsgvo_accepted_at = null; $user->dsgvo_accepted_by = null; $user->save(); ActivityLog::logWithChanges( 'dsgvo_consent_revoked', __('admin.log_dsgvo_revoked', ['name' => $user->name]), 'User', $user->id, $old, null ); } else { $user->dsgvo_accepted_at = now(); $user->dsgvo_accepted_by = auth()->id(); $user->save(); ActivityLog::logWithChanges( 'dsgvo_consent_confirmed', __('admin.log_dsgvo_confirmed', ['name' => $user->name]), 'User', $user->id, null, [ 'dsgvo_accepted_at' => now()->toDateTimeString(), 'dsgvo_accepted_by' => auth()->user()->name, ] ); } return back()->with('success', __('admin.dsgvo_toggled')); } public function rejectDsgvoConsent(User $user): RedirectResponse { if (!auth()->user()->isAdmin()) { abort(403); } if (!$user->hasDsgvoConsent()) { return back()->with('error', __('admin.dsgvo_no_file')); } // Path-Traversal-Schutz if ($user->dsgvo_consent_file && !str_starts_with($user->dsgvo_consent_file, 'dsgvo/')) { abort(403); } // Datei vom Disk löschen Storage::disk('local')->delete($user->dsgvo_consent_file); $user->dsgvo_consent_file = null; $user->dsgvo_accepted_at = null; $user->dsgvo_accepted_by = null; $user->save(); ActivityLog::log( 'dsgvo_consent_rejected', __('admin.log_dsgvo_rejected', ['name' => $user->name]), 'User', $user->id ); return back()->with('success', __('admin.dsgvo_rejected')); } public function viewDsgvoConsent(User $user) { if (! auth()->user()->isAdmin()) { abort(403); } if (!$user->dsgvo_consent_file || !str_starts_with($user->dsgvo_consent_file, 'dsgvo/') || !Storage::disk('local')->exists($user->dsgvo_consent_file)) { abort(404); } ActivityLog::log('dsgvo_document_viewed', __('admin.log_dsgvo_viewed', ['name' => $user->name]), 'User', $user->id); $mimeType = Storage::disk('local')->mimeType($user->dsgvo_consent_file); return response()->file( Storage::disk('local')->path($user->dsgvo_consent_file), ['Content-Type' => $mimeType] ); } }