Files
WebAPP/app/Http/Controllers/Admin/UserController.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

313 lines
11 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class UserController extends Controller
{
public function index(Request $request): View
{
$query = User::with('children');
// Sortierung
$sortable = ['name', 'email', 'role', 'last_login_at', 'is_active', 'created_at'];
$sort = in_array($request->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]
);
}
}