Add leader management functionality for Meetups

-  Introduced `leaders`, `promoteLeader`, and `demoteLeader` methods in `Meetup` model for consistent handling of meetup leadership.
- ⚙️ Refactored `MeetupLeaderController` to use new leadership methods, improving reusability and maintainability.
- 👮‍♂️ Added `ValidNpub` validation rule for npub input standardization.
- 🧪 Added feature tests for leadership delegation and permissions.
- 🖼️ Implemented leader management UI in meetup edit page with flash messaging for actions.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-16 23:11:24 +02:00
parent 9f8fda294a
commit ffcee850ca
6 changed files with 289 additions and 27 deletions
@@ -48,9 +48,7 @@ class MeetupLeaderController extends Controller
{
$user = NostrLogin::findOrCreateUser($request->string('npub')->toString());
$meetup->users()->syncWithoutDetaching([
$user->getKey() => ['is_leader' => true],
]);
$meetup->promoteLeader($user);
return response()->json(['data' => $this->leaders($meetup)], HttpResponse::HTTP_CREATED);
}
@@ -69,7 +67,7 @@ class MeetupLeaderController extends Controller
abort_if($user->getKey() === $meetup->created_by, HttpResponse::HTTP_FORBIDDEN, __('Der Ersteller des Meetups kann nicht entzogen werden.'));
$meetup->users()->updateExistingPivot($user->getKey(), ['is_leader' => false]);
$meetup->demoteLeader($user);
return response()->json(['data' => $this->leaders($meetup)]);
}
@@ -81,9 +79,7 @@ class MeetupLeaderController extends Controller
*/
private function leaders(Meetup $meetup): array
{
return $meetup->users()
->wherePivot('is_leader', true)
->get()
return $meetup->leaders()
->map(fn (User $user): array => [
'id' => $user->getKey(),
'name' => $user->name,
@@ -91,8 +87,6 @@ class MeetupLeaderController extends Controller
'avatar' => $user->profile_photo_url,
'is_creator' => $user->getKey() === $meetup->created_by,
])
->sortByDesc('is_creator')
->values()
->all();
}
}
@@ -2,9 +2,8 @@
namespace App\Http\Requests\Api;
use Closure;
use App\Rules\ValidNpub;
use Illuminate\Foundation\Http\FormRequest;
use swentel\nostr\Key\Key as NostrKey;
/**
* Setzt einen weiteren Leader für ein Meetup per Nostr-npub ein. Nur ein
@@ -24,18 +23,7 @@ class StoreMeetupLeaderRequest extends FormRequest
public function rules(): array
{
return [
'npub' => [
'required',
'string',
'starts_with:npub1',
function (string $attribute, mixed $value, Closure $fail): void {
try {
(new NostrKey)->convertToHex((string) $value);
} catch (\Throwable) {
$fail(__('Das ist kein gültiger npub.'));
}
},
],
'npub' => ['required', 'string', new ValidNpub],
];
}
}
+40
View File
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
@@ -198,6 +199,45 @@ class Meetup extends Model implements HasMedia
->exists();
}
/**
* Aktuelle Leader dieses Meetups (meetup_user.is_leader = true), Ersteller
* zuerst. Gemeinsame Quelle für REST-API (MeetupLeaderController) und das
* Portal-Frontend (meetups.edit).
*
* @return Collection<int, User>
*/
public function leaders(): Collection
{
return $this->users()
->wherePivot('is_leader', true)
->get()
->sortByDesc(fn (User $user): bool => $user->getKey() === $this->created_by)
->values();
}
/**
* Nutzer zum Leader befördern (meetup_user.is_leader = true). Idempotent.
* Geteilt von REST-Controller, MCP-Tools und Portal-Frontend.
*/
public function promoteLeader(User $user): void
{
$this->users()->syncWithoutDetaching([$user->getKey() => ['is_leader' => true]]);
}
/**
* Nutzer die Leader-Rolle entziehen (Demote; bleibt Mitglied). Der Ersteller
* des Meetups ist geschützt und wird nie entzogen (Domain-Invariante)
* Aufrufer dürfen zusätzlich mit 403 antworten.
*/
public function demoteLeader(User $user): void
{
if ($user->getKey() === $this->created_by) {
return;
}
$this->users()->updateExistingPivot($user->getKey(), ['is_leader' => false]);
}
/**
* Den Nutzer als Mitglied (nicht Leader) zu „Meine Meetups" hinzufügen.
* Idempotent: ein bereits hinzugefügter Nutzer bleibt unverändert. Gibt
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use swentel\nostr\Key\Key as NostrKey;
/**
* Validiert einen Nostr-npub (bech32-kodierter öffentlicher Schlüssel).
* Geteilt von der REST-API (StoreMeetupLeaderRequest) und der Leader-
* Verwaltung im Portal-Frontend, damit beide Wege dieselbe Prüfung nutzen.
*/
class ValidNpub implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (! is_string($value) || ! str_starts_with($value, 'npub1')) {
$fail(__('Das ist kein gültiger npub.'));
return;
}
try {
(new NostrKey)->convertToHex($value);
} catch (\Throwable) {
$fail(__('Das ist kein gültiger npub.'));
}
}
}