authorize('secrets.manage'); } public function save(string $key): void { $this->guard(); $field = self::field($key); $value = trim((string) ($this->entered[$field] ?? '')); if ($value === '') { $this->addError('entered.'.$field, __('secrets.empty')); return; } try { app(SecretVault::class)->put($key, $value, Auth::guard('operator')->user()); } catch (Throwable $e) { $this->addError('entered.'.$field, $e->getMessage()); return; } // Out of the component state as soon as it is stored. $this->entered[$field] = ''; $this->check = null; $this->dispatch('notify', message: __('secrets.saved')); } /** * The save button opens ConfirmSaveSecret instead of calling save() * directly (R23) — that modal cannot see `entered` itself, so its confirm * button dispatches back here rather than mutating anything on its own. */ #[On('secret-save-confirmed')] public function onSaveConfirmed(string $key): void { $this->save($key); } public function forget(string $key): void { $this->guard(); app(SecretVault::class)->forget($key); $this->check = null; $this->dispatch('notify', message: __('secrets.removed')); } /** See onSaveConfirmed() — same reasoning, for ConfirmForgetSecret. */ #[On('secret-forget-confirmed')] public function onForgetConfirmed(string $key): void { $this->forget($key); } /** * Try the key that is in force — or the one being typed, before storing it. * * Checking the candidate first is the point: a key that is saved and wrong * fails later, somewhere else, usually in front of a customer. */ public function test(string $key): void { $this->guard(); // The checker named by THIS entry, not a fixed one. When the area held a // single Stripe key a hard-coded StripeCheck was the same thing; with // four entries it would have reported on Stripe while the operator was // looking at the DNS token. $checker = SecretVault::REGISTRY[$key]['check'] ?? null; abort_if($checker === null, 404); $candidate = trim((string) ($this->entered[self::field($key)] ?? '')) ?: null; $this->check = app($checker)->run($candidate); } /** The dotless form key for a registry key. */ public static function field(string $key): string { return str_replace('.', '_', $key); } /** Capability AND a recent password, on every single action. */ private function guard(): void { $this->authorize('secrets.manage'); abort_unless($this->passwordRecentlyConfirmed(), 403); } public function render() { $vault = app(SecretVault::class); $unlocked = $this->passwordRecentlyConfirmed(); return view('livewire.admin.secrets', [ 'unlocked' => $unlocked, 'usable' => $vault->isUsable(), 'entries' => collect(SecretVault::REGISTRY) ->map(fn (array $meta, string $key) => [ 'key' => $key, 'field' => self::field($key), 'label' => __($meta['label']), // Only where a checker exists. A test button that cannot // actually test is a promise the page does not keep. 'testable' => isset($meta['check']), 'source' => $vault->source($key), // Only ever an outline, and only once unlocked. 'outline' => $unlocked ? $vault->outline($key) : null, 'updated_at' => $unlocked ? $vault->updatedAt($key) : null, // The SSH identity is a multi-line PEM key: a single-line // password field would mangle it on paste. Everything else // here is a one-line token. 'multiline' => $key === 'ssh.private_key', ]) ->values() ->all(), ]); } }