heading('Support API Tokens') ->query(fn (): Builder => $this->getTokenQuery()) ->defaultSort('created_at', 'desc') ->columns([ Tables\Columns\TextColumn::make('name') ->label('Name') ->sortable() ->searchable(), Tables\Columns\TextColumn::make('abilities') ->label('Abilities') ->formatStateUsing(fn ($state): string => $this->formatAbilities($state)) ->wrap(), Tables\Columns\TextColumn::make('last_used_at') ->label('Last used') ->since() ->placeholder('—'), Tables\Columns\TextColumn::make('expires_at') ->label('Expires') ->dateTime('Y-m-d H:i') ->placeholder('—'), Tables\Columns\TextColumn::make('created_at') ->label('Created') ->since(), ]) ->headerActions([ Action::make('create_support_token') ->label('Create token') ->icon('heroicon-o-key') ->form([ TextInput::make('name') ->label('Token name') ->default($this->defaultTokenName()) ->required() ->maxLength(255) ->helperText('Existing tokens with the same name will be revoked.'), CheckboxList::make('abilities') ->label('Abilities') ->options($this->abilityOptions()) ->columns(2) ->required() ->default($this->defaultAbilities()), DateTimePicker::make('expires_at') ->label('Expires at') ->displayFormat('Y-m-d H:i') ->seconds(false), ]) ->action(function (array $data): void { $user = $this->getUser(); if (! $user) { return; } $name = $this->normalizeTokenName($data['name'] ?? null); $abilities = $this->normalizeAbilities($data['abilities'] ?? []); $expiresAt = $this->normalizeExpiresAt($data['expires_at'] ?? null); $user->tokens()->where('name', $name)->delete(); $token = $user->createToken($name, $abilities, $expiresAt); $this->recordTokenCreated($token, $abilities, $user); Notification::make() ->success() ->title('Token created') ->body('Copy this token now. It will not be shown again: '.$token->plainTextToken) ->persistent() ->send(); }), ]) ->actions([ Action::make('revoke') ->label('Revoke') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->visible(fn (PersonalAccessToken $record): bool => $this->ownsToken($record)) ->action(function (PersonalAccessToken $record): void { if (! $this->ownsToken($record)) { return; } app(SuperAdminAuditLogger::class)->record( 'support-api-token.revoked', $record, ['fields' => ['name', 'abilities', 'expires_at']], actor: $this->getUser(), source: static::class ); $record->delete(); Notification::make() ->success() ->title('Token revoked') ->send(); }), ]) ->emptyStateHeading('No support API tokens') ->emptyStateDescription('Create a token for external support tooling.'); } private function getTokenQuery(): Builder { $user = $this->getUser(); if (! $user) { return PersonalAccessToken::query()->whereRaw('1 = 0'); } return PersonalAccessToken::query() ->where('tokenable_id', $user->getKey()) ->where('tokenable_type', $user->getMorphClass()); } private function getUser(): ?User { $user = Filament::auth()->user(); return $user instanceof User ? $user : null; } private function formatAbilities(mixed $state): string { if (is_array($state)) { return implode(', ', $state); } if (is_string($state)) { return $state; } return ''; } /** * @return array */ private function defaultAbilities(): array { $abilities = config('support-api.token.default_abilities', []); if (! is_array($abilities)) { return ['support-admin']; } $abilities = array_values(array_filter($abilities, fn ($ability) => is_string($ability) && $ability !== '')); if (! in_array('support-admin', $abilities, true)) { $abilities[] = 'support-admin'; } return array_values(array_unique($abilities)); } /** * @return array */ private function abilityOptions(): array { $options = []; foreach ($this->defaultAbilities() as $ability) { $options[$ability] = $ability; } return $options; } /** * @param array $abilities * @return array */ private function normalizeAbilities(array $abilities): array { $allowed = $this->defaultAbilities(); $filtered = array_values(array_intersect($abilities, $allowed)); if (! in_array('support-admin', $filtered, true)) { $filtered[] = 'support-admin'; } sort($filtered); return $filtered; } private function defaultTokenName(): string { $name = config('support-api.token.name'); if (is_string($name) && $name !== '') { return $name; } return 'support-api'; } private function normalizeTokenName(?string $name): string { $name = $name ? trim($name) : ''; return $name !== '' ? $name : $this->defaultTokenName(); } private function normalizeExpiresAt(mixed $expiresAt): ?Carbon { if ($expiresAt instanceof Carbon) { return $expiresAt; } if ($expiresAt instanceof \DateTimeInterface) { return Carbon::instance($expiresAt); } if (is_string($expiresAt) && $expiresAt !== '') { return Carbon::parse($expiresAt); } return null; } private function recordTokenCreated(NewAccessToken $token, array $abilities, User $user): void { $actionLog = app(SuperAdminAuditLogger::class); $actionLog->record( 'support-api-token.created', $token->accessToken, [ 'fields' => ['name', 'abilities', 'expires_at'], 'abilities' => $abilities, ], actor: $user, source: static::class ); } private function ownsToken(PersonalAccessToken $token): bool { $user = $this->getUser(); if (! $user) { return false; } return (int) $token->tokenable_id === (int) $user->getKey() && $token->tokenable_type === $user->getMorphClass(); } }