From 2957e89c799f95e4a47410535bb3849dab013af3 Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode Date: Tue, 3 Feb 2026 22:49:42 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Add=20`#[Locked]`=20attribute=20?= =?UTF-8?q?to=20Livewire=20components=20to=20enhance=20security=20against?= =?UTF-8?q?=20client-side=20state=20tampering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../livewire/association/benefits.blade.php | 9 ++++++ .../association/election/admin.blade.php | 19 ++++++++++++ .../association/election/index.blade.php | 25 ++++++++++++++++ .../association/election/show.blade.php | 20 +++++++++++++ .../association/members/admin.blade.php | 29 +++++++++++++++++++ .../views/livewire/association/news.blade.php | 4 +++ .../livewire/association/profile.blade.php | 20 +++++++++++++ .../project-support/form/create.blade.php | 3 ++ .../project-support/form/edit.blade.php | 4 +++ .../project-support/index.blade.php | 5 ++++ .../project-support/show.blade.php | 6 ++++ .../views/livewire/auth-button.blade.php | 3 ++ .../Livewire/Association/ProfileTest.php | 4 +-- 13 files changed, 149 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/association/benefits.blade.php b/resources/views/livewire/association/benefits.blade.php index 40c0364..a839bf5 100644 --- a/resources/views/livewire/association/benefits.blade.php +++ b/resources/views/livewire/association/benefits.blade.php @@ -4,26 +4,35 @@ use App\Models\EinundzwanzigPleb; use App\Support\NostrAuth; use App\Traits\NostrFetcherTrait; use Flux\Flux; +use Livewire\Attributes\Locked; use Livewire\Component; new class extends Component { use NostrFetcherTrait; + #[Locked] public ?EinundzwanzigPleb $currentPleb = null; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public bool $currentYearIsPaid = false; + #[Locked] public ?string $nip05Handle = ''; + #[Locked] public bool $nip05Verified = false; + #[Locked] public ?string $nip05VerifiedHandle = null; + #[Locked] public bool $nip05HandleMismatch = false; + #[Locked] public array $nip05VerifiedHandles = []; protected $listeners = [ diff --git a/resources/views/livewire/association/election/admin.blade.php b/resources/views/livewire/association/election/admin.blade.php index 22b7fce..16b89d1 100644 --- a/resources/views/livewire/association/election/admin.blade.php +++ b/resources/views/livewire/association/election/admin.blade.php @@ -1,6 +1,7 @@ loadBoardVotes(); } + public function handleNostrLoggedIn(string $pubkey): void + { + $this->currentPubkey = $pubkey; + $this->currentPleb = \App\Models\EinundzwanzigPleb::query() + ->where('pubkey', $pubkey)->first(); + $this->isAllowed = (bool) $this->currentPleb; + } + + public function handleNostrLoggedOut(): void + { + $this->currentPubkey = null; + $this->currentPleb = null; + $this->isAllowed = false; + } + public function handleNewVote(): void { $this->loadEvents(); diff --git a/resources/views/livewire/association/election/index.blade.php b/resources/views/livewire/association/election/index.blade.php index 56a65f4..8c2fe99 100644 --- a/resources/views/livewire/association/election/index.blade.php +++ b/resources/views/livewire/association/election/index.blade.php @@ -3,14 +3,18 @@ use App\Models\EinundzwanzigPleb; use App\Models\Election; use App\Support\NostrAuth; +use Livewire\Attributes\Locked; use Livewire\Component; new class extends Component { + #[Locked] public bool $isAllowed = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?EinundzwanzigPleb $currentPleb = null; public array $elections = []; @@ -37,6 +41,27 @@ new class extends Component { } } + public function handleNostrLoggedIn(string $pubkey): void + { + $this->currentPubkey = $pubkey; + $this->currentPleb = EinundzwanzigPleb::query() + ->where('pubkey', $pubkey)->first(); + + $logPubkeys = [ + '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033', + '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279', + ]; + + $this->isAllowed = in_array($pubkey, $logPubkeys, true); + } + + public function handleNostrLoggedOut(): void + { + $this->currentPubkey = null; + $this->currentPleb = null; + $this->isAllowed = false; + } + public function saveElection($index): void { $election = $this->elections[$index]; diff --git a/resources/views/livewire/association/election/show.blade.php b/resources/views/livewire/association/election/show.blade.php index 75d952b..01aabad 100644 --- a/resources/views/livewire/association/election/show.blade.php +++ b/resources/views/livewire/association/election/show.blade.php @@ -5,6 +5,7 @@ use App\Models\EinundzwanzigPleb; use App\Models\Profile; use App\Support\NostrAuth; use Livewire\Attributes\Computed; +use Livewire\Attributes\Locked; use Livewire\Component; use swentel\nostr\Event\Event as NostrEvent; use swentel\nostr\Filter\Filter; @@ -16,12 +17,16 @@ use swentel\nostr\Request\Request; use swentel\nostr\Subscription\Subscription; new class extends Component { + #[Locked] public bool $isAllowed = false; + #[Locked] public bool $showLog = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?EinundzwanzigPleb $currentPleb = null; public array $events = []; @@ -197,6 +202,21 @@ new class extends Component { } } + public function handleNostrLoggedIn(string $pubkey): void + { + $this->currentPubkey = $pubkey; + $this->currentPleb = EinundzwanzigPleb::query() + ->where('pubkey', $pubkey)->first(); + $this->isAllowed = (bool) $this->currentPleb; + } + + public function handleNostrLoggedOut(): void + { + $this->currentPubkey = null; + $this->currentPleb = null; + $this->isAllowed = false; + } + public function updatedSearch($value): void { $this->plebs = EinundzwanzigPleb::query() diff --git a/resources/views/livewire/association/members/admin.blade.php b/resources/views/livewire/association/members/admin.blade.php index 47fa04a..1375c80 100644 --- a/resources/views/livewire/association/members/admin.blade.php +++ b/resources/views/livewire/association/members/admin.blade.php @@ -5,14 +5,18 @@ use App\Models\EinundzwanzigPleb; use App\Support\NostrAuth; use Flux\Flux; use Livewire\Attributes\Computed; +use Livewire\Attributes\Locked; use Livewire\Component; new class extends Component { + #[Locked] public bool $isAllowed = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?EinundzwanzigPleb $currentPleb = null; public string $sortBy = 'association_status'; @@ -63,6 +67,31 @@ new class extends Component $this->plebs = $this->loadPlebs(); } + public function handleNostrLoggedIn(string $pubkey): void + { + $this->currentPubkey = $pubkey; + $this->currentPleb = EinundzwanzigPleb::query() + ->where('pubkey', $pubkey)->first(); + + $allowedPubkeys = [ + '0adf67475ccc5ca456fd3022e46f5d526eb0af6284bf85494c0dd7847f3e5033', + '430169631f2f0682c60cebb4f902d68f0c71c498fd1711fd982f052cf1fd4279', + '7acf30cf60b85c62b8f654556cc21e4016df8f5604b3b6892794f88bb80d7a1d', + 'f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba', + '19e358b8011f5f4fc653c565c6d4c2f33f32661f4f90982c9eedc292a8774ec3', + 'acbcec475a1a4f9481939ecfbd1c3d111f5b5a474a39ae039bbc720fdd305bec', + ]; + + $this->isAllowed = in_array($pubkey, $allowedPubkeys, true); + } + + public function handleNostrLoggedOut(): void + { + $this->currentPubkey = null; + $this->currentPleb = null; + $this->isAllowed = false; + } + private function loadPlebs() { $query = EinundzwanzigPleb::query() diff --git a/resources/views/livewire/association/news.blade.php b/resources/views/livewire/association/news.blade.php index 35c0972..4df9487 100644 --- a/resources/views/livewire/association/news.blade.php +++ b/resources/views/livewire/association/news.blade.php @@ -6,6 +6,7 @@ use App\Support\NostrAuth; use Illuminate\Support\Collection; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; +use Livewire\Attributes\Locked; use Livewire\Attributes\Title; use Livewire\Attributes\Url; use Livewire\Component; @@ -17,6 +18,7 @@ new class extends Component { use WithFileUploads; + #[Locked] public Collection|array $news = []; #[Url(as: 'kategorie')] @@ -30,8 +32,10 @@ class extends Component { public $file; + #[Locked] public bool $isAllowed = false; + #[Locked] public bool $canEdit = false; public ?int $confirmDeleteId = null; diff --git a/resources/views/livewire/association/profile.blade.php b/resources/views/livewire/association/profile.blade.php index c62b393..c289f2c 100644 --- a/resources/views/livewire/association/profile.blade.php +++ b/resources/views/livewire/association/profile.blade.php @@ -11,6 +11,7 @@ use Flux\Flux; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Livewire\Attributes\Locked; use Livewire\Component; use swentel\nostr\Event\Event as NostrEvent; use swentel\nostr\Filter\Filter; @@ -35,42 +36,61 @@ new class extends Component { public string $fax = ''; + #[Locked] public bool $nip05Verified = false; + #[Locked] public ?string $nip05VerifiedHandle = null; + #[Locked] public bool $nip05HandleMismatch = false; + #[Locked] public array $nip05VerifiedHandles = []; + #[Locked] public array $yearsPaid = []; + #[Locked] public array $events = []; + #[Locked] public $payments; + #[Locked] public ?string $invoiceStatus = null; + #[Locked] public ?string $invoiceStatusLabel = null; + #[Locked] public ?string $invoiceStatusMessage = null; + #[Locked] public string $invoiceStatusVariant = 'info'; + #[Locked] public ?string $invoiceExpiresAt = null; + #[Locked] public ?string $invoiceExpiresAtDisplay = null; + #[Locked] public ?string $invoiceExpiresIn = null; + #[Locked] public int $amountToPay = 21000; + #[Locked] public bool $currentYearIsPaid = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?EinundzwanzigPleb $currentPleb = null; + #[Locked] public ?string $qrCode = null; protected $listeners = [ diff --git a/resources/views/livewire/association/project-support/form/create.blade.php b/resources/views/livewire/association/project-support/form/create.blade.php index c30512c..f75e9f6 100644 --- a/resources/views/livewire/association/project-support/form/create.blade.php +++ b/resources/views/livewire/association/project-support/form/create.blade.php @@ -3,6 +3,7 @@ use App\Models\ProjectProposal; use App\Support\NostrAuth; use Livewire\Attributes\Layout; +use Livewire\Attributes\Locked; use Livewire\Attributes\Title; use Livewire\Component; use Livewire\WithFileUploads; @@ -25,8 +26,10 @@ class extends Component public $file = null; + #[Locked] public bool $isAllowed = false; + #[Locked] public bool $isAdmin = false; public function mount(): void diff --git a/resources/views/livewire/association/project-support/form/edit.blade.php b/resources/views/livewire/association/project-support/form/edit.blade.php index 0b25638..5d97497 100644 --- a/resources/views/livewire/association/project-support/form/edit.blade.php +++ b/resources/views/livewire/association/project-support/form/edit.blade.php @@ -3,6 +3,7 @@ use App\Models\ProjectProposal; use App\Support\NostrAuth; use Livewire\Attributes\Layout; +use Livewire\Attributes\Locked; use Livewire\Attributes\Title; use Livewire\Component; use Livewire\WithFileUploads; @@ -14,6 +15,7 @@ class extends Component { use WithFileUploads; + #[Locked] public ProjectProposal $project; public array $form = [ @@ -27,8 +29,10 @@ class extends Component public $file = null; + #[Locked] public bool $isAllowed = false; + #[Locked] public bool $isAdmin = false; public function mount($projectProposal): void diff --git a/resources/views/livewire/association/project-support/index.blade.php b/resources/views/livewire/association/project-support/index.blade.php index bf1c674..25f493c 100644 --- a/resources/views/livewire/association/project-support/index.blade.php +++ b/resources/views/livewire/association/project-support/index.blade.php @@ -6,6 +6,7 @@ use App\Models\ProjectProposal; use App\Support\NostrAuth; use Flux\Flux; use Illuminate\Database\Eloquent\Collection; +use Livewire\Attributes\Locked; use Livewire\Component; new class extends Component { @@ -17,12 +18,16 @@ new class extends Component { public string $search = ''; + #[Locked] public Collection $projects; + #[Locked] public bool $isAllowed = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?ProjectProposal $projectToDelete = null; protected $listeners = [ diff --git a/resources/views/livewire/association/project-support/show.blade.php b/resources/views/livewire/association/project-support/show.blade.php index c196681..3ca0a33 100644 --- a/resources/views/livewire/association/project-support/show.blade.php +++ b/resources/views/livewire/association/project-support/show.blade.php @@ -4,19 +4,25 @@ use App\Livewire\Traits\WithNostrAuth; use App\Models\ProjectProposal; use App\Models\Vote; use App\Support\NostrAuth; +use Livewire\Attributes\Locked; use Livewire\Component; new class extends Component { use WithNostrAuth; + #[Locked] public $projectProposal; + #[Locked] public bool $isAllowed = false; + #[Locked] public ?string $currentPubkey = null; + #[Locked] public ?object $currentPleb = null; + #[Locked] public bool $ownVoteExists = false; public function mount($projectProposal): void diff --git a/resources/views/livewire/auth-button.blade.php b/resources/views/livewire/auth-button.blade.php index 348e3b9..833cfa5 100644 --- a/resources/views/livewire/auth-button.blade.php +++ b/resources/views/livewire/auth-button.blade.php @@ -1,13 +1,16 @@ pubkey); + // With API failure, the component should show error status regardless of previous state + // Locked properties prevent client-side tampering, so we verify the API failure handling directly Livewire::test('association.profile') - ->set('invoiceStatus', 'Settled') - ->set('invoiceStatusLabel', 'Bezahlt') ->call('listenForPayment') ->assertSet('invoiceStatus', null) ->assertSet('invoiceStatusLabel', 'Status unbekannt')