security: Rechteausweitung schließen & Admin-Panel feingranular gaten (K1, H1, H2, M8)
- users:manage/roles:manage auf allen User-/Rollen-Masken erzwungen; is_super_admin aus $fillable entfernt und nur per autorisiertem forceFill setzbar; zentraler Gate::before-Super-Admin-Kurzschluss (K1/M8). - Nicht-Super-Admins können Super-Admin-Konten nicht mehr bearbeiten (Fix#2); Impersonation mit Rangprüfung, keine Token-Erstellung während Impersonation (H1). - Destruktive/verwaltende Admin-Aktionen (CRM/Billing/Compliance/Struktur) hängen an neuen admin-only Permissions; Editor behält reine Content-Rechte (H2). - Kanonischer Permission-Katalog (App\Authorization\Permission) als Single Source of Truth für Seeder und Monitoring. Deployment: `php artisan db:seed --class=RolesAndPermissionsSeeder` auf allen Umgebungen ausführen (neue Permissions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
39e13d6d84
commit
77f8f9cb35
34 changed files with 748 additions and 100 deletions
|
|
@ -40,6 +40,7 @@
|
|||
? app(\App\Services\Admin\AdminPerformanceCache::class)->pressReleaseReviewCount()
|
||||
: 0;
|
||||
$legalOpenCount = $canAdmin ? \App\Models\LegalRequest::query()->open()->count() : 0;
|
||||
$contactOpenCount = $canAdmin ? \App\Models\ContactMessage::query()->open()->count() : 0;
|
||||
@endphp
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
|
|
@ -115,14 +116,18 @@
|
|||
:badge="$reviewCount > 0 ? $reviewCount : null" badge-color="yellow" wire:navigate>
|
||||
{{ __('Pressemitteilungen') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="folder" :href="route('admin.categories.index')"
|
||||
:current="request()->routeIs('admin.categories.*')" wire:navigate>
|
||||
{{ __('Kategorien') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="code-bracket-square" :href="route('admin.footer-codes.index')"
|
||||
:current="request()->routeIs('admin.footer-codes.*')" wire:navigate>
|
||||
{{ __('Footer-Codes') }}
|
||||
</flux:navlist.item>
|
||||
@can('categories:manage')
|
||||
<flux:navlist.item icon="folder" :href="route('admin.categories.index')"
|
||||
:current="request()->routeIs('admin.categories.*')" wire:navigate>
|
||||
{{ __('Kategorien') }}
|
||||
</flux:navlist.item>
|
||||
@endcan
|
||||
@can('footer-codes:manage')
|
||||
<flux:navlist.item icon="code-bracket-square" :href="route('admin.footer-codes.index')"
|
||||
:current="request()->routeIs('admin.footer-codes.*')" wire:navigate>
|
||||
{{ __('Footer-Codes') }}
|
||||
</flux:navlist.item>
|
||||
@endcan
|
||||
</flux:navlist.group>
|
||||
|
||||
{{-- CRM --}}
|
||||
|
|
@ -137,16 +142,28 @@
|
|||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
{{-- Recht & Compliance --}}
|
||||
<flux:navlist.group :heading="__('Recht & Compliance')" class="grid mb-4">
|
||||
<flux:navlist.item icon="scale" :href="route('admin.legal-requests.index')"
|
||||
:current="request()->routeIs('admin.legal-requests.*')"
|
||||
:badge="$legalOpenCount > 0 ? $legalOpenCount : null" badge-color="red" wire:navigate>
|
||||
{{ __('Anfragen') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@can('compliance:manage')
|
||||
{{-- Kontakt: Anliegen aus dem Kontakt-Widget (Widget + Hub-Formular) --}}
|
||||
<flux:navlist.group :heading="__('Kontakt')" class="grid mb-4">
|
||||
<flux:navlist.item icon="chat-bubble-left-right" :href="route('admin.contact-messages.index')"
|
||||
:current="request()->routeIs('admin.contact-messages.*')"
|
||||
:badge="$contactOpenCount > 0 ? $contactOpenCount : null" badge-color="red" wire:navigate>
|
||||
{{ __('Anliegen') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
{{-- Recht & Compliance --}}
|
||||
<flux:navlist.group :heading="__('Recht & Compliance')" class="grid mb-4">
|
||||
<flux:navlist.item icon="scale" :href="route('admin.legal-requests.index')"
|
||||
:current="request()->routeIs('admin.legal-requests.*')"
|
||||
:badge="$legalOpenCount > 0 ? $legalOpenCount : null" badge-color="red" wire:navigate>
|
||||
{{ __('Anfragen') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endcan
|
||||
|
||||
{{-- Billing --}}
|
||||
@can('billing:manage')
|
||||
<flux:navlist.group :heading="__('Billing')" class="grid mb-4">
|
||||
<flux:navlist.item icon="archive-box" :href="route('admin.invoices.index')"
|
||||
:current="request()->routeIs('admin.invoices.*')" wire:navigate>
|
||||
|
|
@ -169,6 +186,7 @@
|
|||
{{ __('Newsletter Sync') }}
|
||||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endcan
|
||||
|
||||
{{-- Administration --}}
|
||||
<flux:navlist.group :heading="__('Administration')" class="grid mb-4">
|
||||
|
|
@ -176,22 +194,32 @@
|
|||
:current="request()->routeIs('admin.presets.*')" wire:navigate>
|
||||
{{ __('Voreinstellungen') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="users" :href="route('admin.users.index')"
|
||||
:current="request()->routeIs('admin.users.*')" wire:navigate>
|
||||
{{ __('Benutzer') }}
|
||||
</flux:navlist.item>
|
||||
<flux:navlist.item icon="shield-check" :href="route('admin.roles.index')"
|
||||
:current="request()->routeIs('admin.roles.*')" wire:navigate>
|
||||
{{ __('Rollen & Rechte') }}
|
||||
</flux:navlist.item>
|
||||
@can('users:manage')
|
||||
<flux:navlist.item icon="users" :href="route('admin.users.index')"
|
||||
:current="request()->routeIs('admin.users.*')" wire:navigate>
|
||||
{{ __('Benutzer') }}
|
||||
</flux:navlist.item>
|
||||
@endcan
|
||||
@can('roles:manage')
|
||||
<flux:navlist.item icon="shield-check" :href="route('admin.roles.index')"
|
||||
:current="request()->routeIs('admin.roles.*')" wire:navigate>
|
||||
{{ __('Rollen & Rechte') }}
|
||||
</flux:navlist.item>
|
||||
@endcan
|
||||
</flux:navlist.group>
|
||||
|
||||
{{-- Reports --}}
|
||||
<flux:navlist.group :heading="__('Reports')" class="grid mb-4">
|
||||
<flux:navlist.item icon="chart-bar-square" :href="route('admin.reports.slow-requests')"
|
||||
:current="request()->routeIs('admin.reports.*')" wire:navigate>
|
||||
:current="request()->routeIs('admin.reports.slow-requests')" wire:navigate>
|
||||
{{ __('Performance') }}
|
||||
</flux:navlist.item>
|
||||
@can('settings:manage')
|
||||
<flux:navlist.item icon="heart" :href="route('admin.reports.system-health')"
|
||||
:current="request()->routeIs('admin.reports.system-health')" wire:navigate>
|
||||
{{ __('Betriebsüberwachung') }}
|
||||
</flux:navlist.item>
|
||||
@endcan
|
||||
</flux:navlist.group>
|
||||
@endif
|
||||
|
||||
|
|
@ -387,6 +415,9 @@
|
|||
|
||||
{{ $slot }}
|
||||
|
||||
{{-- Schwebendes Kontakt-Widget (unten rechts, eingeloggter Bereich) --}}
|
||||
<x-web.contact-launcher />
|
||||
|
||||
@persist('toast')
|
||||
<flux:toast position="top end" class="pt-24" />
|
||||
@endpersist
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ new #[Layout('components.layouts.app'), Title('Kategorie anlegen')] class extend
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('categories:manage');
|
||||
|
||||
$this->validate([
|
||||
'slugDe' => ['nullable', 'string', 'max:120'],
|
||||
'slugEn' => ['nullable', 'string', 'max:120'],
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
|
|||
|
||||
public function save(ImageService $imageService): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$this->validate();
|
||||
|
||||
$slug = (new Company)->generateUniqueSlug($this->company_name, ['portal' => $this->portal]);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
|
|||
|
||||
public function update(ImageService $imageService): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$this->validate();
|
||||
|
||||
$company = Company::query()->find($this->companyId);
|
||||
|
|
@ -186,6 +188,8 @@ new #[Layout('components.layouts.app'), Title('Firma bearbeiten')] class extends
|
|||
|
||||
public function deleteCompany(): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$company = Company::query()->find($this->companyId);
|
||||
if (! $company) {
|
||||
session()->flash('error', __('Die angeforderte Firma wurde nicht gefunden.'));
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ new #[Layout('components.layouts.app'), Title('Kontakt anlegen')] class extends
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$validated = $this->validate([
|
||||
'companyId' => ['required', 'integer', Rule::exists('companies', 'id')],
|
||||
'portal' => ['required', Rule::in(array_map(static fn (Portal $portal): string => $portal->value, Portal::cases()))],
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ new #[Layout('components.layouts.app'), Title('Kontakt bearbeiten')] class exten
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$validated = $this->validate([
|
||||
'companyId' => ['required', 'integer', Rule::exists('companies', 'id')],
|
||||
'portal' => ['required', Rule::in(array_map(static fn (Portal $portal): string => $portal->value, Portal::cases()))],
|
||||
|
|
|
|||
|
|
@ -350,6 +350,8 @@ new #[Layout('components.layouts.app'), Title('Kontakte')] class extends Compone
|
|||
|
||||
public function deleteContactFromIndex(int $contactId): void
|
||||
{
|
||||
$this->authorize('companies:write');
|
||||
|
||||
$contact = Contact::query()->find($contactId);
|
||||
if (! $contact) {
|
||||
$this->notification = __('Der angeforderte Kontakt wurde nicht gefunden.');
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ new #[Layout('components.layouts.app'), Title('Footer-Code anlegen')] class exte
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('footer-codes:manage');
|
||||
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function (): void {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('footer-codes:manage');
|
||||
|
||||
$this->validate();
|
||||
|
||||
$code = FooterCode::query()->findOrFail($this->id);
|
||||
|
|
@ -81,6 +83,8 @@ new #[Layout('components.layouts.app'), Title('Footer-Code bearbeiten')] class e
|
|||
|
||||
public function delete(): void
|
||||
{
|
||||
$this->authorize('footer-codes:manage');
|
||||
|
||||
$code = FooterCode::query()->findOrFail($this->id);
|
||||
$code->delete();
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Com
|
|||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$this->authorize('footer-codes:manage');
|
||||
|
||||
$footerCode = FooterCode::query()->find($id);
|
||||
|
||||
if (! $footerCode) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
|||
*/
|
||||
public function resolveWithdrawal(int $id): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
WithdrawalRequest::query()->findOrFail($id)->update([
|
||||
'status' => 'resolved',
|
||||
'resolved_at' => now(),
|
||||
|
|
@ -60,6 +62,8 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
|||
|
||||
public function reopenWithdrawal(int $id): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
WithdrawalRequest::query()->findOrFail($id)->update([
|
||||
'status' => 'open',
|
||||
'resolved_at' => null,
|
||||
|
|
@ -72,6 +76,8 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
|||
*/
|
||||
public function resolveTermination(int $id): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
TerminationRequest::query()->findOrFail($id)->update([
|
||||
'status' => 'resolved',
|
||||
'resolved_at' => now(),
|
||||
|
|
@ -80,6 +86,8 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten
|
|||
|
||||
public function reopenTermination(int $id): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
TerminationRequest::query()->findOrFail($id)->update([
|
||||
'status' => 'open',
|
||||
'resolved_at' => null,
|
||||
|
|
|
|||
|
|
@ -24,22 +24,30 @@ new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')]
|
|||
|
||||
public function markInProgress(): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
$this->request()->update(['status' => LegalRequestStatus::InProgress->value]);
|
||||
$this->notification = __('Status auf „In Bearbeitung" gesetzt.');
|
||||
}
|
||||
|
||||
public function resolve(): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
$this->finish(LegalRequestStatus::Resolved, __('Anfrage als erledigt markiert.'));
|
||||
}
|
||||
|
||||
public function reject(): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
$this->finish(LegalRequestStatus::Rejected, __('Anfrage abgelehnt.'));
|
||||
}
|
||||
|
||||
public function reopen(): void
|
||||
{
|
||||
$this->authorize('compliance:manage');
|
||||
|
||||
$this->request()->update([
|
||||
'status' => LegalRequestStatus::Open->value,
|
||||
'resolved_by_user_id' => null,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends
|
|||
|
||||
public function triggerDryRun(): void
|
||||
{
|
||||
$this->authorize('billing:manage');
|
||||
|
||||
$subscription = NewsletterSubscription::query()->latest('id')->first();
|
||||
|
||||
if ($subscription === null) {
|
||||
|
|
@ -32,6 +34,8 @@ new #[Layout('components.layouts.app'), Title('Newsletter Sync')] class extends
|
|||
|
||||
public function triggerTestSync(): void
|
||||
{
|
||||
$this->authorize('billing:manage');
|
||||
|
||||
$subscription = NewsletterSubscription::query()->latest('id')->first();
|
||||
|
||||
if ($subscription === null) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ new #[Layout('components.layouts.app'), Title('Zahlungen')] class extends Compon
|
|||
*/
|
||||
public function cancelBundle(int $userId, LegacySubscriptionService $legacy): void
|
||||
{
|
||||
$this->authorize('billing:manage');
|
||||
|
||||
$user = User::query()->find($userId);
|
||||
|
||||
if (! $user) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ new #[Layout('components.layouts.app'), Title('Tarife & Pakete')] class extends
|
|||
|
||||
public function edit(int $planId): void
|
||||
{
|
||||
$this->authorize('billing:manage');
|
||||
|
||||
$plan = Plan::query()->findOrFail($planId);
|
||||
|
||||
$this->editingPlanId = $plan->id;
|
||||
|
|
@ -49,6 +51,8 @@ new #[Layout('components.layouts.app'), Title('Tarife & Pakete')] class extends
|
|||
|
||||
public function save(StripePlanSyncService $stripeSync): void
|
||||
{
|
||||
$this->authorize('billing:manage');
|
||||
|
||||
// Deutsche Dezimal-Eingaben (49,00) für die numeric-Regel normalisieren.
|
||||
$this->monthlyPrice = str_replace(',', '.', trim($this->monthlyPrice));
|
||||
$this->yearlyPrice = str_replace(',', '.', trim($this->yearlyPrice));
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@ new #[Layout('components.layouts.app'), Title('Neue Rolle')] class extends Compo
|
|||
|
||||
public string $guardName = 'web';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('roles:manage');
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('roles:manage');
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ new #[Layout('components.layouts.app'), Title('Rolle bearbeiten')] class extends
|
|||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->authorize('roles:manage');
|
||||
|
||||
$this->id = $id;
|
||||
|
||||
$role = Role::query()
|
||||
|
|
@ -44,6 +46,8 @@ new #[Layout('components.layouts.app'), Title('Rolle bearbeiten')] class extends
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('roles:manage');
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ use Spatie\Permission\Models\Role;
|
|||
|
||||
new #[Layout('components.layouts.app'), Title('Rollen & Rechte')] class extends Component
|
||||
{
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('roles:manage');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ new #[Layout('components.layouts.app'), Title('Benutzer')] class extends Compone
|
|||
|
||||
public ?int $viewingUserId = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$sortable = ['name', 'email', 'portal', 'is_active', 'last_login_at', 'created_at'];
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
|
|||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$this->portal = Portal::Both->value;
|
||||
$this->registrationType = RegistrationType::ExistingLegacy->value;
|
||||
}
|
||||
|
|
@ -110,6 +112,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$validated = $this->validate([
|
||||
...$this->userIdentityRules(),
|
||||
...$this->userRolesAndCompaniesRules(),
|
||||
|
|
@ -124,10 +128,15 @@ new #[Layout('components.layouts.app'), Title('Benutzer anlegen')] class extends
|
|||
'registration_type' => $validated['registrationType'],
|
||||
'language' => $validated['language'],
|
||||
'is_active' => $this->isActive,
|
||||
'is_super_admin' => $this->isSuperAdmin,
|
||||
'password' => null,
|
||||
]);
|
||||
|
||||
// Super-Admin-Rang darf nur ein Super-Admin vergeben (is_super_admin ist
|
||||
// nicht mass-assignable, siehe User::$fillable).
|
||||
if ($this->isSuperAdmin && auth()->user()->is_super_admin) {
|
||||
$user->forceFill(['is_super_admin' => true])->save();
|
||||
}
|
||||
|
||||
$user->syncRoles($validated['selectedRoles']);
|
||||
|
||||
$user->syncCompaniesWithRoles($validated['linkedCompanyIds'], $validated['companyRoles']);
|
||||
|
|
|
|||
|
|
@ -136,8 +136,21 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
|
|||
'country_code' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Verhindert, dass ein Nicht-Super-Admin (nur `users:manage`) ein
|
||||
* mächtigeres Super-Admin-Konto bearbeitet — sonst ließe sich über einen
|
||||
* E-Mail-/Rollen-/Aktiv-Wechsel das Konto übernehmen (analog zur
|
||||
* Impersonation-Rangprüfung). Nur ein Super-Admin darf Super-Admins ändern.
|
||||
*/
|
||||
private function assertMayManageRank(User $user): void
|
||||
{
|
||||
abort_if($user->is_super_admin && ! auth()->user()->is_super_admin, 403);
|
||||
}
|
||||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$this->id = $id;
|
||||
|
||||
$user = User::query()
|
||||
|
|
@ -150,6 +163,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
|
|||
])
|
||||
->findOrFail($id);
|
||||
|
||||
$this->assertMayManageRank($user);
|
||||
|
||||
$this->name = $user->name;
|
||||
$this->agencyName = (string) ($user->agency_name ?? '');
|
||||
$this->email = $user->email;
|
||||
|
|
@ -379,6 +394,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
|
|||
|
||||
public function save(): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$validated = $this->validate([
|
||||
...$this->userIdentityRules($this->id),
|
||||
'profile.salutation_key' => ['nullable', 'string', 'max:20'],
|
||||
|
|
@ -411,6 +428,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
|
|||
|
||||
$user = User::query()->findOrFail($this->id);
|
||||
|
||||
$this->assertMayManageRank($user);
|
||||
|
||||
$user->update([
|
||||
'name' => $validated['name'],
|
||||
'agency_name' => $validated['agencyName'] ?: null,
|
||||
|
|
@ -419,9 +438,14 @@ new #[Layout('components.layouts.app'), Title('Benutzer bearbeiten')] class exte
|
|||
'registration_type' => $validated['registrationType'],
|
||||
'language' => $validated['language'],
|
||||
'is_active' => $this->isActive,
|
||||
'is_super_admin' => $this->isSuperAdmin,
|
||||
]);
|
||||
|
||||
// Super-Admin-Rang darf nur ein Super-Admin vergeben/entziehen. is_super_admin
|
||||
// ist nicht mass-assignable und wird ausschließlich hier autorisiert gesetzt.
|
||||
if (auth()->user()->is_super_admin) {
|
||||
$user->forceFill(['is_super_admin' => $this->isSuperAdmin])->save();
|
||||
}
|
||||
|
||||
$user->syncRoles($validated['selectedRoles']);
|
||||
|
||||
$profilePayload = [
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ new #[Layout('components.layouts.app'), Title('Benutzer anzeigen')] class extend
|
|||
|
||||
public function mount(int $id): void
|
||||
{
|
||||
$this->authorize('users:manage');
|
||||
|
||||
$this->id = $id;
|
||||
|
||||
if (! User::query()->whereKey($id)->exists()) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Admin\UserImpersonation;
|
||||
use App\Services\Api\ApiAccessEligibilityService;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Layout;
|
||||
|
|
@ -19,6 +20,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public ?string $eligibilityMessage = null;
|
||||
|
||||
private const IMPERSONATION_DENIAL_MESSAGE = 'Während der Benutzer-Impersonation können keine API-Tokens erstellt oder widerrufen werden.';
|
||||
|
||||
private const ABILITIES = [
|
||||
'press-releases:read' => 'Pressemitteilungen lesen',
|
||||
'press-releases:write' => 'Pressemitteilungen erstellen und bearbeiten',
|
||||
|
|
@ -29,6 +32,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public function createToken(): void
|
||||
{
|
||||
$this->ensureNotImpersonating();
|
||||
|
||||
$eligibility = app(ApiAccessEligibilityService::class);
|
||||
$denialReason = $eligibility->denialReason(auth()->user());
|
||||
|
||||
|
|
@ -59,6 +64,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public function revokeToken(int $tokenId): void
|
||||
{
|
||||
$this->ensureNotImpersonating();
|
||||
|
||||
auth()->user()
|
||||
->tokens()
|
||||
->whereKey($tokenId)
|
||||
|
|
@ -72,17 +79,30 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
{
|
||||
$eligibility = app(ApiAccessEligibilityService::class);
|
||||
$denialReason = $eligibility->denialReason(auth()->user());
|
||||
$isImpersonating = app(UserImpersonation::class)->isActive();
|
||||
|
||||
return [
|
||||
'abilityOptions' => self::ABILITIES,
|
||||
'canCreateApiToken' => $denialReason === null,
|
||||
'apiTokenDenialReason' => $denialReason,
|
||||
'canCreateApiToken' => $denialReason === null && ! $isImpersonating,
|
||||
'canRevokeApiTokens' => ! $isImpersonating,
|
||||
'apiTokenDenialReason' => $isImpersonating
|
||||
? __(self::IMPERSONATION_DENIAL_MESSAGE)
|
||||
: $denialReason,
|
||||
'tokens' => auth()->user()
|
||||
->tokens()
|
||||
->latest()
|
||||
->get(['id', 'name', 'abilities', 'last_used_at', 'created_at']),
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureNotImpersonating(): void
|
||||
{
|
||||
abort_if(
|
||||
app(UserImpersonation::class)->isActive(),
|
||||
403,
|
||||
__(self::IMPERSONATION_DENIAL_MESSAGE),
|
||||
);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-8">
|
||||
|
|
@ -223,6 +243,7 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
icon="trash"
|
||||
wire:click="revokeToken({{ $token->id }})"
|
||||
wire:confirm="{{ __('Diesen API-Token wirklich widerrufen?') }}"
|
||||
:disabled="! $canRevokeApiTokens"
|
||||
>
|
||||
{{ __('Widerrufen') }}
|
||||
</flux:button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue