Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 58 additions & 9 deletions modules/api-access/src/Support/IdempotencyStore.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<?php

declare(strict_types=1);

namespace Liberu\Foundation\ApiAccess\Support;

use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;

Expand All @@ -10,18 +13,64 @@ final class IdempotencyStore
public function begin(string $identity, string $key, string $requestBody): ?object
{
$hash = hash('sha256', $requestBody);
$existing = DB::table('api_idempotency_keys')->where('identity_ref', $identity)->where('key', $key)->where('expires_at', '>', now())->first();
if ($existing && ! hash_equals($existing->request_hash, $hash)) {
throw new RuntimeException('Idempotency key was reused with a different request.');
}if ($existing) {
return $existing;
}DB::table('api_idempotency_keys')->insert(['identity_ref' => $identity, 'key' => $key, 'request_hash' => $hash, 'expires_at' => now()->addHours((int) config('api-access.idempotency_hours', 24)), 'created_at' => now(), 'updated_at' => now()]);

return null;

return DB::transaction(function () use ($identity, $key, $hash): ?object {
$existing = DB::table('api_idempotency_keys')
->where('identity_ref', $identity)
->where('key', $key)
->lockForUpdate()
->first();

if ($existing && Carbon::parse($existing->expires_at)->isFuture()) {
if (! hash_equals($existing->request_hash, $hash)) {
throw new RuntimeException('Idempotency key was reused with a different request.');
}

return $existing;
}

$now = now();
$attributes = [
'identity_ref' => $identity,
'key' => $key,
'request_hash' => $hash,
'response_status' => null,
'response_body' => null,
'expires_at' => $now->copy()->addHours((int) config('api-access.idempotency_hours', 24)),
'created_at' => $now,
'updated_at' => $now,
];

if ($existing) {
DB::table('api_idempotency_keys')->where('id', $existing->id)->update($attributes);

return null;
}

$inserted = DB::table('api_idempotency_keys')->insertOrIgnore($attributes);
if ($inserted === 1) {
return null;
}

$created = DB::table('api_idempotency_keys')
->where('identity_ref', $identity)
->where('key', $key)
->lockForUpdate()
->first();

if ($created && ! hash_equals($created->request_hash, $hash)) {
throw new RuntimeException('Idempotency key was reused with a different request.');
}

return $created;
});
}

public function complete(string $identity, string $key, int $status, string $body): void
{
DB::table('api_idempotency_keys')->where('identity_ref', $identity)->where('key', $key)->update(['response_status' => $status, 'response_body' => $body, 'updated_at' => now()]);
DB::table('api_idempotency_keys')
->where('identity_ref', $identity)
->where('key', $key)
->update(['response_status' => $status, 'response_body' => $body, 'updated_at' => now()]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ paths:
operationId: control.panel.accounts.delegate
security: [{sanctum: []}]
responses: {'201': {description: Delegation created.}}
/api/v1/control-panel/accounts/{account}/archive:
post:
operationId: control.panel.accounts.archive
security: [{sanctum: []}]
responses: {'200': {description: Account archived.}}
/api/v1/control-panel/accounts/{account}/branding:
patch:
operationId: control.panel.accounts.branding.update
Expand Down
1 change: 1 addition & 0 deletions modules/control-panel-accounts-api/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Route::post('/', [AccountController::class, 'store'])->name('control-panel.accounts.store');
Route::post('{account}/suspend', [AccountController::class, 'suspend'])->name('control-panel.accounts.suspend');
Route::post('{account}/activate', [AccountController::class, 'activate'])->name('control-panel.accounts.activate');
Route::post('{account}/archive', [AccountController::class, 'archive'])->name('control-panel.accounts.archive');
Route::post('{account}/delegations', [AccountController::class, 'delegate'])->name('control-panel.accounts.delegate');
Route::patch('{account}/branding', [AccountController::class, 'branding'])->name('control-panel.accounts.branding');
Route::post('{account}/quota-check', [AccountController::class, 'quotaCheck'])->name('control-panel.accounts.quota-check');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Liberu\ControlPanel\Accounts\Actions\ActivateAccount;
use Liberu\ControlPanel\Accounts\Actions\ArchiveAccount;
use Liberu\ControlPanel\Accounts\Actions\CreateAccount;
use Liberu\ControlPanel\Accounts\Actions\CreateHostingPackage;
use Liberu\ControlPanel\Accounts\Actions\DelegateAccount;
Expand Down Expand Up @@ -54,6 +55,13 @@ public function activate(Request $request, Account $account, ActivateAccount $ac
return response()->json(['data' => self::resource($activate->execute($account))]);
}

public function archive(Request $request, Account $account, ArchiveAccount $archive): JsonResponse
{
$this->assertTeam($request, $account);

return response()->json(['data' => self::resource($archive->execute($account))]);
}

public function package(Request $request, CreateHostingPackage $create): JsonResponse
{
$teamId = $request->user()?->current_team_id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Liberu\ControlPanel\Accounts\Actions\ActivateAccount;
use Liberu\ControlPanel\Accounts\Actions\ArchiveAccount;
use Liberu\ControlPanel\Accounts\Actions\SuspendAccount;
use Liberu\ControlPanel\Accounts\Models\Account;
use Liberu\ControlPanel\AccountsFilament\Resources\AccountResource\Pages\CreateAccount;
Expand Down Expand Up @@ -71,6 +72,10 @@ public static function table(Table $table): Table
Action::make('activate')
->visible(fn (Account $record): bool => $record->status->value === 'suspended')
->action(fn (Account $record): Account => app(ActivateAccount::class)->execute($record)),
Action::make('archive')
->requiresConfirmation()
->visible(fn (Account $record): bool => $record->status->value !== 'archived')
->action(fn (Account $record): Account => app(ArchiveAccount::class)->execute($record)),
])->defaultSort('created_at', 'desc');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
@elseif ($account->status->value === 'suspended')
<button type="button" wire:click="activate('{{ $account->getKey() }}')">Activate</button>
@endif
@if ($account->status->value !== 'archived')
<button type="button" wire:click="archive('{{ $account->getKey() }}')">Archive</button>
@endif
</li>
@endforeach
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Liberu\ControlPanel\Accounts\Actions\ActivateAccount;
use Liberu\ControlPanel\Accounts\Actions\ArchiveAccount;
use Liberu\ControlPanel\Accounts\Actions\SuspendAccount;
use Liberu\ControlPanel\Accounts\Models\Account;
use Livewire\Component;
Expand Down Expand Up @@ -60,4 +61,13 @@ public function activate(string $accountId, ActivateAccount $activate): void
->firstOrFail();
$activate->execute($account);
}

public function archive(string $accountId, ArchiveAccount $archive): void
{
$account = Account::query()
->whereKey($accountId)
->where('team_id', auth()->user()?->current_team_id)
->firstOrFail();
$archive->execute($account);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Illuminate\Support\ServiceProvider;
use Liberu\ControlPanel\Accounts\Actions\ActivateAccount;
use Liberu\ControlPanel\Accounts\Actions\ArchiveAccount;
use Liberu\ControlPanel\Accounts\Actions\CreateAccount;
use Liberu\ControlPanel\Accounts\Actions\CreateHostingPackage;
use Liberu\ControlPanel\Accounts\Actions\DelegateAccount;
Expand All @@ -26,6 +27,7 @@ public function register(): void
$this->app->scoped(SuspendAccount::class);
$this->app->scoped(UpdateBranding::class);
$this->app->scoped(ActivateAccount::class);
$this->app->scoped(ArchiveAccount::class);
$this->app->scoped(ListAccounts::class);
$this->app->scoped(QuotaGuard::class);
$this->app->scoped(UpdateHostingPackage::class);
Expand Down
26 changes: 26 additions & 0 deletions modules/control-panel-accounts/src/Actions/ArchiveAccount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Liberu\ControlPanel\Accounts\Actions;

use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Liberu\ControlPanel\Accounts\Enums\AccountStatus;
use Liberu\ControlPanel\Accounts\Models\Account;

final class ArchiveAccount
{
public function execute(Account $account): Account
{
if ($account->status === AccountStatus::Archived) {
throw ValidationException::withMessages(['account' => 'The account is already archived.']);
}

return DB::transaction(function () use ($account): Account {
$account->forceFill(['status' => AccountStatus::Archived])->save();

return $account->refresh();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ paths:
post: {operationId: control.panel.api.and.automation.credentials.create, security: [{sanctum: []}], requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CredentialCreate'}}}}, responses: {'201': {$ref: '#/components/responses/Resource'}}}
/api/v1/control-panel/api-and-automation/webhooks:
post: {operationId: control.panel.api.and.automation.webhooks.create, security: [{sanctum: []}], requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/WebhookCreate'}}}}, responses: {'201': {$ref: '#/components/responses/Resource'}}}
/api/v1/control-panel/api-and-automation/webhooks/{webhook}/pause:
post: {operationId: control.panel.api.and.automation.webhooks.pause, security: [{sanctum: []}], responses: {'200': {description: Webhook paused.}, '422': {description: Webhook cannot be paused.}}}
/api/v1/control-panel/api-and-automation/webhooks/{webhook}/resume:
post: {operationId: control.panel.api.and.automation.webhooks.resume, security: [{sanctum: []}], responses: {'200': {description: Webhook resumed.}, '422': {description: Webhook cannot be resumed.}}}
/api/v1/control-panel/api-and-automation/templates/{template}/runs:
post:
operationId: control.panel.api.and.automation.runs.create
Expand Down
2 changes: 2 additions & 0 deletions modules/control-panel-api-and-automation-api/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
Route::post('/', [AutomationController::class, 'store'])->name('control-panel.api-and-automation.store');
Route::post('credentials', [AutomationController::class, 'credential'])->name('control-panel.api-and-automation.credentials.store');
Route::post('webhooks', [AutomationController::class, 'webhook'])->name('control-panel.api-and-automation.webhooks.store');
Route::post('webhooks/{webhook}/pause', [AutomationController::class, 'pauseWebhook'])->name('control-panel.api-and-automation.webhooks.pause');
Route::post('webhooks/{webhook}/resume', [AutomationController::class, 'resumeWebhook'])->name('control-panel.api-and-automation.webhooks.resume');
Route::post('templates/{template}/runs', [AutomationController::class, 'run'])->name('control-panel.api-and-automation.runs.store');
Route::post('templates', [AutomationController::class, 'template'])->name('control-panel.api-and-automation.templates.store');
Route::post('schedules', [AutomationController::class, 'schedule'])->name('control-panel.api-and-automation.schedules.store');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
use Illuminate\Validation\ValidationException;
use Liberu\ControlPanel\ApiAutomation\Actions\CreateAutomationSchedule;
use Liberu\ControlPanel\ApiAutomation\Actions\CreateAutomationTemplate;
use Liberu\ControlPanel\ApiAutomation\Actions\PauseWebhook;
use Liberu\ControlPanel\ApiAutomation\Actions\RecordBillingProvisioningEvent;
use Liberu\ControlPanel\ApiAutomation\Actions\RegisterApiCredential;
use Liberu\ControlPanel\ApiAutomation\Actions\RegisterAutomation;
use Liberu\ControlPanel\ApiAutomation\Actions\RegisterAutomationCommand;
use Liberu\ControlPanel\ApiAutomation\Actions\RegisterWebhook;
use Liberu\ControlPanel\ApiAutomation\Actions\ResumeWebhook;
use Liberu\ControlPanel\ApiAutomation\Actions\StartOrchestration;
use Liberu\ControlPanel\ApiAutomation\Models\AutomationDefinition;
use Liberu\ControlPanel\ApiAutomation\Models\AutomationTemplate;
use Liberu\ControlPanel\ApiAutomation\Models\WebhookEndpoint;
use Liberu\ControlPanel\ApiAutomation\Queries\ListAutomations;

final class AutomationController
Expand Down Expand Up @@ -69,6 +72,22 @@ public function webhook(Request $request, RegisterWebhook $register): JsonRespon
return response()->json(['data' => ['id' => $webhook->getKey(), 'type' => 'control-panel-automation-webhook', 'attributes' => $webhook->only(['name', 'url', 'events', 'status', 'retry_limit'])]], 201);
}

public function pauseWebhook(Request $request, string $webhook, PauseWebhook $pause): JsonResponse
{
$teamId = $request->user()?->current_team_id;
$item = WebhookEndpoint::query()->whereKey($webhook)->where('team_id', $teamId)->firstOrFail();

return response()->json(['data' => self::webhookResource($pause->execute($item))]);
}

public function resumeWebhook(Request $request, string $webhook, ResumeWebhook $resume): JsonResponse
{
$teamId = $request->user()?->current_team_id;
$item = WebhookEndpoint::query()->whereKey($webhook)->where('team_id', $teamId)->firstOrFail();

return response()->json(['data' => self::webhookResource($resume->execute($item))]);
}

public function run(Request $request, string $template, StartOrchestration $start): JsonResponse
{
$teamId = $request->user()?->current_team_id;
Expand Down Expand Up @@ -133,4 +152,9 @@ private static function templateResource(AutomationTemplate $item): array
{
return ['id' => $item->getKey(), 'type' => 'control-panel-automation-template', 'attributes' => $item->only(['name', 'version', 'description', 'inputs', 'steps', 'active'])];
}

private static function webhookResource(WebhookEndpoint $item): array
{
return ['id' => $item->getKey(), 'type' => 'control-panel-automation-webhook', 'attributes' => $item->only(['name', 'url', 'events', 'status', 'retry_limit', 'failure_count', 'last_delivered_at'])];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Liberu\ControlPanel\ApiAutomationFilament\Resources;

use Filament\Actions\Action;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
Expand All @@ -12,6 +13,8 @@
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Liberu\ControlPanel\ApiAutomation\Actions\PauseWebhook;
use Liberu\ControlPanel\ApiAutomation\Actions\ResumeWebhook;
use Liberu\ControlPanel\ApiAutomation\Models\WebhookEndpoint;
use Liberu\ControlPanel\ApiAutomationFilament\Resources\WebhookEndpointResource\Pages\CreateWebhookEndpoint;
use Liberu\ControlPanel\ApiAutomationFilament\Resources\WebhookEndpointResource\Pages\EditWebhookEndpoint;
Expand Down Expand Up @@ -39,7 +42,15 @@ public static function form(Schema $schema): Schema

public static function table(Table $table): Table
{
return $table->columns([TextColumn::make('name')->searchable(), TextColumn::make('url')->limit(40), TextColumn::make('status')->badge(), TextColumn::make('failure_count'), TextColumn::make('created_at')->dateTime()]);
return $table->columns([TextColumn::make('name')->searchable(), TextColumn::make('url')->limit(40), TextColumn::make('status')->badge(), TextColumn::make('failure_count'), TextColumn::make('created_at')->dateTime()])->recordActions([
Action::make('pause')
->requiresConfirmation()
->visible(fn (WebhookEndpoint $record): bool => $record->status === 'active')
->action(fn (WebhookEndpoint $record): WebhookEndpoint => app(PauseWebhook::class)->execute($record)),
Action::make('resume')
->visible(fn (WebhookEndpoint $record): bool => in_array($record->status, ['paused', 'failed'], true))
->action(fn (WebhookEndpoint $record): WebhookEndpoint => app(ResumeWebhook::class)->execute($record)),
]);
}

public static function getEloquentQuery(): Builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<section aria-labelledby="automation-webhook-inventory"><h2 id="automation-webhook-inventory">{{ __('Webhooks') }}</h2>
<label for="webhook-search">{{ __("Search webhooks") }}</label><input id="webhook-search" type="search" wire:model.live.debounce.300ms="search"><ul>@forelse($webhooks as $webhook)<li wire:key="automation-webhook-{{ $webhook->getKey() }}">{{ $webhook->name }} — {{ $webhook->status }}</li>@empty<li>{{ __('No webhooks found.') }}</li>@endforelse</ul>{{ $webhooks->links() }}</section>
<label for="webhook-search">{{ __("Search webhooks") }}</label><input id="webhook-search" type="search" wire:model.live.debounce.300ms="search"><ul>@forelse($webhooks as $webhook)<li wire:key="automation-webhook-{{ $webhook->getKey() }}">{{ $webhook->name }} — {{ $webhook->status }} @if ($webhook->status === 'active') <button type="button" wire:click="pause('{{ $webhook->getKey() }}')">{{ __('Pause') }}</button> @elseif (in_array($webhook->status, ['paused', 'failed'], true)) <button type="button" wire:click="resume('{{ $webhook->getKey() }}')">{{ __('Resume') }}</button> @endif</li>@empty<li>{{ __('No webhooks found.') }}</li>@endforelse</ul>{{ $webhooks->links() }}</section>
Loading
Loading