mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-06-17 16:40:31 +00:00
✨ 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:
@@ -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],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user