Add restore_point functionality to Meetups

- 💾 Introduced `restore_point` JSON column in `meetups` table for saving and restoring master data.
- 🛠️ Added methods `captureRestorePoint` and `restoreFromRestorePoint` to `Meetup` model for managing restore points.
- 🔒 Implemented authorization for updating meetups via `updateViaPortal` policy to include pivot members.
- 🔗 Created Artisan commands `meetups:snapshot` and `meetups:restore` for managing restore points from CLI.
- 🚦 Added rate limiter to restrict excessive update attempts in Livewire meetup editing.
-  Developed exhaustive feature tests for snapshot and restore actions, portal editing rules, and rate limiting.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-10 10:56:38 +02:00
parent 8c68b19138
commit f5cf85b438
9 changed files with 350 additions and 6 deletions
@@ -5,6 +5,7 @@ use App\Models\City;
use App\Models\Country;
use App\Models\Meetup;
use App\Traits\SeoTrait;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -84,14 +85,15 @@ class extends Component {
}
/**
* Stammdaten eines Meetups dürfen ausschließlich vom Ersteller (created_by) oder
* einem Super-Admin bearbeitet werden einheitlich mit MeetupPolicy, der REST-API
* und den MCP-Tools. Reine Mitglieder (meetup_user-Pivot) dürfen nur Termine anlegen
* (siehe meetups.create-edit-events), nicht aber die Stammdaten ändern.
* Portal-Frontend nutzt die gelockerte updateViaPortal-Ability: Ersteller,
* Super-Admins UND Mitglieder der meetup_user-Pivot („Meine Meetups") dürfen
* die Stammdaten bearbeiten. REST-API und MCP-Tools bleiben auf der strikten
* update()-Ability (nur Ersteller/Super-Admin). Übergangslösung, bis ein
* echtes Rollen-/Freigabekonzept existiert.
*/
protected function authorizeAccess(): void
{
if (auth()->guest() || auth()->user()->cannot('update', $this->meetup)) {
if (auth()->guest() || auth()->user()->cannot('updateViaPortal', $this->meetup)) {
abort(403);
}
}
@@ -163,6 +165,15 @@ class extends Component {
{
$this->authorizeAccess();
$rateLimiterKey = Meetup::updateRateLimitKey(auth()->id());
if (RateLimiter::tooManyAttempts($rateLimiterKey, 10)) {
$this->addError('rateLimit',
__('Zu viele Änderungen in kurzer Zeit. Bitte versuche es in einer Stunde erneut.'));
return;
}
$validated = $this->validate([
'name' => ['required', 'string', 'max:255', Rule::unique('meetups')->ignore($this->meetup->id)],
'city_id' => ['nullable', 'exists:cities,id'],
@@ -178,6 +189,8 @@ class extends Component {
'github_data' => ['nullable', 'json'],
]);
RateLimiter::hit($rateLimiterKey, 3600);
$validated['github_data'] = $this->sanitizeGithubData($validated['github_data'] ?? null);
$this->meetup->update($validated);
@@ -415,6 +428,12 @@ class extends Component {
</flux:text>
@endif
@error('rateLimit')
<flux:text class="text-red-600 dark:text-red-400 font-medium">
{{ $message }}
</flux:text>
@enderror
<flux:button class="cursor-pointer" variant="primary" type="submit">
{{ __('Meetup aktualisieren') }}
</flux:button>