diff --git a/.env.example b/.env.example index 85b9973f..df4e1cc6 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,7 @@ SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null +SESSION_SECURE_COOKIE=true BROADCAST_CONNECTION=log FILESYSTEM_DISK=local diff --git a/.github/workflows/protect-prod-branch.yml b/.github/workflows/protect-prod-branch.yml new file mode 100644 index 00000000..fa323692 --- /dev/null +++ b/.github/workflows/protect-prod-branch.yml @@ -0,0 +1,21 @@ +name: "Protect Prod Branch" +on: + pull_request: + branches: [ "prod" ] + +permissions: {} + +jobs: + check-source: + runs-on: ubuntu-latest + steps: + - name: Validate Source Branch + run: | + SRC="${{ github.head_ref }}" + if [[ "$SRC" == "dev" || "$SRC" == hotfix* ]]; then + echo "Source branch $SRC is valid." + exit 0 + else + echo "Error: Only 'dev' or 'hotfix*' branches can target 'prod'." + exit 1 + fi diff --git a/.github/workflows/release-button.yml b/.github/workflows/release-button.yml new file mode 100644 index 00000000..cbb3d6c8 --- /dev/null +++ b/.github/workflows/release-button.yml @@ -0,0 +1,28 @@ +name: "Trigger Release PR" +on: + workflow_dispatch: + +jobs: + create-release: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Open PR from Dev to Prod + env: + GH_TOKEN: ${{ github.token }} + run: | + # Pulls unique commits in dev not yet in prod + LOG=$(git log origin/prod..origin/dev --oneline --no-merges) + DATE=$(date +'%Y-%m-%d') + + gh pr create \ + --base prod \ + --head dev \ + --title "Release ($DATE)" \ + --body "### Changes in this release: + $LOG" diff --git a/README.md b/README.md index a8eff248..a0162963 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ Polydock Engine is a Laravel-based application management and deployment platfor For detailed documentation including features, technical details, setup instructions, and more, please see the [Documentation](docs/README.md). +## Development Workflow + +- **Branching**: Always branch from the `dev` branch for new features, bug fixes, or improvements. +- **Pull Requests**: Create Pull Requests (PRs) back into the `dev` branch for review and testing. +- **Releases**: Releases are performed by merging the `dev` branch into the `prod` branch. + ## Sponsoring Organizations - [Workshop Orange](https://www.workshoporange.co) - Project Delivery Professionals diff --git a/app/Console/Commands/RemoveAppInstancesByEmail.php b/app/Console/Commands/RemoveAppInstancesByEmail.php index b572920a..d1c4a858 100644 --- a/app/Console/Commands/RemoveAppInstancesByEmail.php +++ b/app/Console/Commands/RemoveAppInstancesByEmail.php @@ -37,9 +37,10 @@ public function handle() // Check if this is a pattern (contains %) or exact email $isPattern = str_contains($email, '%'); - + if (! $isPattern && ! filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->error('Invalid email address provided. Use % for wildcard patterns (e.g., %@example.com).'); + return 1; } @@ -60,6 +61,7 @@ public function handle() if ($instances->isEmpty()) { $this->info("No app instances found matching {$searchType}: {$email}"); + return 0; } diff --git a/app/Enums/PolydockStoreAppStatusEnum.php b/app/Enums/PolydockStoreAppStatusEnum.php index 89bb4230..f5da28c5 100644 --- a/app/Enums/PolydockStoreAppStatusEnum.php +++ b/app/Enums/PolydockStoreAppStatusEnum.php @@ -4,12 +4,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum PolydockStoreAppStatusEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case AVAILABLE = 'available'; case UNAVAILABLE = 'unavailable'; diff --git a/app/Enums/PolydockStoreStatusEnum.php b/app/Enums/PolydockStoreStatusEnum.php index 98374b9e..dcb1d08b 100644 --- a/app/Enums/PolydockStoreStatusEnum.php +++ b/app/Enums/PolydockStoreStatusEnum.php @@ -4,12 +4,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum PolydockStoreStatusEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case UNAVAILABLE = 'unavailable'; case PUBLIC = 'public'; case PRIVATE = 'private'; diff --git a/app/Enums/PolydockStoreWebhookCallStatusEnum.php b/app/Enums/PolydockStoreWebhookCallStatusEnum.php index 40cfeaf4..5ce653e3 100644 --- a/app/Enums/PolydockStoreWebhookCallStatusEnum.php +++ b/app/Enums/PolydockStoreWebhookCallStatusEnum.php @@ -4,12 +4,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum PolydockStoreWebhookCallStatusEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case PENDING = 'pending'; case PROCESSING = 'processing'; case SUCCESS = 'success'; diff --git a/app/Enums/PolydockVariableScopeEnum.php b/app/Enums/PolydockVariableScopeEnum.php index 95f73166..79f99a52 100644 --- a/app/Enums/PolydockVariableScopeEnum.php +++ b/app/Enums/PolydockVariableScopeEnum.php @@ -4,12 +4,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum PolydockVariableScopeEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case GLOBAL = 'global'; case BUILD = 'build'; case RUNTIME = 'runtime'; diff --git a/app/Enums/UserGroupRoleEnum.php b/app/Enums/UserGroupRoleEnum.php index d8a58f1c..b2d50bcb 100644 --- a/app/Enums/UserGroupRoleEnum.php +++ b/app/Enums/UserGroupRoleEnum.php @@ -2,12 +2,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum UserGroupRoleEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case OWNER = 'owner'; case MEMBER = 'member'; case VIEWER = 'viewer'; @@ -43,12 +46,4 @@ public static function getValues(): array { return array_column(self::cases(), 'value'); } - - public static function getOptions(): array - { - return collect(self::cases()) - ->mapWithKeys(fn ($role) => [ - $role->value => $role->getLabel(), - ])->all(); - } } diff --git a/app/Enums/UserRemoteRegistrationStatusEnum.php b/app/Enums/UserRemoteRegistrationStatusEnum.php index 6438aed6..f762c627 100644 --- a/app/Enums/UserRemoteRegistrationStatusEnum.php +++ b/app/Enums/UserRemoteRegistrationStatusEnum.php @@ -2,12 +2,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum UserRemoteRegistrationStatusEnum: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case PENDING = 'pending'; case PROCESSING = 'processing'; case SUCCESS = 'success'; @@ -47,12 +50,4 @@ public static function getValues(): array { return array_column(self::cases(), 'value'); } - - public static function getOptions(): array - { - return collect(self::cases()) - ->mapWithKeys(fn ($status) => [ - $status->value => $status->getLabel(), - ])->all(); - } } diff --git a/app/Enums/UserRemoteRegistrationType.php b/app/Enums/UserRemoteRegistrationType.php index 1a82f568..ee8efae2 100644 --- a/app/Enums/UserRemoteRegistrationType.php +++ b/app/Enums/UserRemoteRegistrationType.php @@ -4,12 +4,15 @@ namespace App\Enums; +use App\Traits\HasEnumOptions; use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasLabel; enum UserRemoteRegistrationType: string implements HasColor, HasIcon, HasLabel { + use HasEnumOptions; + case TEST_FAIL = 'TEST_FAIL'; case REQUEST_TRIAL = 'REQUEST_TRIAL'; case REQUEST_TRIAL_UNLISTED_REGION = 'REQUEST_TRIAL_UNLISTED_REGION'; diff --git a/app/Filament/Admin/Resources/PolydockAppInstanceResource.php b/app/Filament/Admin/Resources/PolydockAppInstanceResource.php index ae0bac29..29245803 100644 --- a/app/Filament/Admin/Resources/PolydockAppInstanceResource.php +++ b/app/Filament/Admin/Resources/PolydockAppInstanceResource.php @@ -12,6 +12,7 @@ use Filament\Forms; use Filament\Forms\Form; use Filament\Infolists\Components\Grid; +use Filament\Infolists\Components\KeyValueEntry; use Filament\Infolists\Components\Section; use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Infolist; @@ -245,6 +246,15 @@ public static function infolist(Infolist $infolist): Infolist ), ), ]), + Grid::make(2) + ->schema([ + TextEntry::make('app_url') + ->label('App URL') + ->url(fn ($record) => $record->app_url) + ->openUrlInNewTab() + ->icon('heroicon-m-link') + ->iconColor('primary'), + ]), ]) ->columnSpan(2), @@ -266,14 +276,14 @@ public static function infolist(Infolist $infolist): Infolist Section::make('Instance Configuration') ->description('Instance-specific settings configured at creation.') - ->schema(fn ($record) => self::getRenderedInstanceConfigForRecord($record)) - ->visible(fn ($record) => self::hasInstanceConfigFields($record)) + ->schema(self::getRenderedInstanceConfigForRecord(...)) + ->visible(self::hasInstanceConfigFields(...)) ->columnSpan(3) ->collapsible(), Section::make('Instance Data') ->description('Safe data that can be shared with webhooks') - ->schema(fn ($record) => self::getRenderedSafeDataForRecord($record)) + ->schema(self::getRenderedSafeDataForRecord(...)) ->columnSpan(3) ->collapsible(), ]) @@ -282,35 +292,46 @@ public static function infolist(Infolist $infolist): Infolist public static function getRenderedSafeDataForRecord(PolydockAppInstance $record): array { - $safeData = $record->data; - $sensitiveKeys = $record->getSensitiveDataKeys(); - $renderedArray = []; - foreach ($safeData as $key => $value) { - if ($record->shouldFilterKey($key, $sensitiveKeys)) { - $value = 'REDACTED'; - } - - if ($value === null) { - $value = 'N/A'; - } - - $renderKey = 'webhook_data_'.$key; - $renderedItem = TextEntry::make($renderKey) - ->label($key) - ->markdown() - ->columnSpanFull() - ->bulleted(); - - if (is_array($value)) { - $renderedItem->state($value); - } else { - $renderedItem->state([$value]); - } - - $renderedArray[] = $renderedItem; - } - - return $renderedArray; + return [ + Grid::make() + ->schema([ + KeyValueEntry::make('data') + ->label('') + ->state(function (PolydockAppInstance $record) { + $safeData = $record->data ?? []; + $sensitiveKeys = $record->getSensitiveDataKeys(); + $redactedData = []; + + foreach ($safeData as $key => $value) { + // Split combined key-value strings like "instance_config_VAR=VALUE" + // but skip if it's a URL or if the key is already a non-numeric string. + if (\is_int($key) && \is_string($value) && str_contains($value, '=') && ! str_starts_with($value, 'http')) { + [$newKey, $newValue] = explode('=', (string) $value, 2); + $key = $newKey; + $value = $newValue; + } + + if ($record->shouldFilterKey($key, $sensitiveKeys)) { + $value = 'REDACTED'; + } + + if ($value === null) { + $value = 'N/A'; + } + + if (\is_array($value)) { + $value = json_encode($value); + } + + $redactedData[$key] = $value; + } + + return $redactedData; + }) + ->columnSpan(2), + ]) + ->columns(3), + ]; } /** @@ -363,7 +384,7 @@ public static function getRenderedInstanceConfigForRecord(PolydockAppInstance $r // Check if value should be masked (for encrypted fields) $isEncrypted = $record->isPolydockVariableEncrypted($fieldName); - $renderedItem = TextEntry::make('instance_config_display_'.$fieldName) + $renderedItem = TextEntry::make("instance_config_display_{$fieldName}") ->label($labelName); if ($isEncrypted && $value !== null && $value !== '') { diff --git a/app/Filament/Admin/Resources/PolydockAppInstanceResource/Pages/CreatePolydockAppInstance.php b/app/Filament/Admin/Resources/PolydockAppInstanceResource/Pages/CreatePolydockAppInstance.php index d0fb3e03..c1f4d133 100644 --- a/app/Filament/Admin/Resources/PolydockAppInstanceResource/Pages/CreatePolydockAppInstance.php +++ b/app/Filament/Admin/Resources/PolydockAppInstanceResource/Pages/CreatePolydockAppInstance.php @@ -4,7 +4,6 @@ use App\Enums\PolydockStoreAppStatusEnum; use App\Filament\Admin\Resources\PolydockAppInstanceResource; -use App\Jobs\ProcessUserRemoteRegistration; use App\Models\PolydockStoreApp; use App\Models\UserRemoteRegistration; use App\Services\PolydockAppClassDiscovery; @@ -217,9 +216,6 @@ public function create(): void 'registration_uuid' => $registration->uuid, ]); - // Dispatch the job to process the registration - ProcessUserRemoteRegistration::dispatch($registration); - Notification::make() ->title('Instance creation started') ->body('The app instance is being created. You can track its progress in the App Instances list.') diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 7775a81b..2518edd0 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -6,7 +6,11 @@ use App\Enums\PolydockStoreAppStatusEnum; use App\Enums\PolydockStoreStatusEnum; +use App\Enums\PolydockStoreWebhookCallStatusEnum; +use App\Enums\PolydockVariableScopeEnum; use App\Enums\UserGroupRoleEnum; +use App\Enums\UserRemoteRegistrationStatusEnum; +use App\Enums\UserRemoteRegistrationType; use App\Http\Controllers\Controller; use App\Models\PolydockAppInstance; use App\Models\PolydockStoreApp; @@ -57,6 +61,31 @@ public function getStoreApps(): JsonResponse ]); } + /** + * Get selected enums + * + * Retrieve a list of selected enums used in the Polydock API and their possible values/labels. + * + * @group External API + * + * @subgroup Meta + */ + public function getEnums(): JsonResponse + { + return response()->json([ + 'data' => [ + // 'PolydockAppInstanceStatus' => PolydockAppInstanceStatus::getEnumOptions(), + // 'PolydockStoreAppStatus' => PolydockStoreAppStatusEnum::getEnumOptions(), + // 'PolydockStoreStatus' => PolydockStoreStatusEnum::getEnumOptions(), + 'PolydockStoreWebhookCallStatus' => PolydockStoreWebhookCallStatusEnum::getEnumOptions(), + 'PolydockVariableScope' => PolydockVariableScopeEnum::getEnumOptions(), + 'UserGroupRole' => UserGroupRoleEnum::getEnumOptions(), + 'UserRemoteRegistrationStatus' => UserRemoteRegistrationStatusEnum::getEnumOptions(), + 'UserRemoteRegistrationType' => UserRemoteRegistrationType::getEnumOptions(), + ], + ]); + } + /** * Get instances by user email * @@ -114,12 +143,24 @@ public function getInstances(Request $request): JsonResponse * * @bodyParam email email required The email address of the user. Example: new.user@example.com * @bodyParam storeAppId string required The UUID of the store app to provision. Example: 3a105da1-9c87-43ca-9ac8-72787fc5e315 - * @bodyParam config object optional Key-value overrides or configurations for this individual deployment. Example: {"lagoon_auto_idle": "1"} + * @bodyParam name string optional The display name for this instance. Defaults to lagoon-project-name if not provided. Example: "My awesome instance" + * @bodyParam secret object optional Sensitive AI and VectorDB credentials. Example: {"ai": {"llm_url": "https://llm", "api_key": "sk-123"}, "vector": {"db_host": "localhost", "db_port": 5432, "db_name": "db_d1234", "db_user": "admin", "db_pass": "pass"}} + * @bodyParam secret.ai object optional AI LLM configuration. + * @bodyParam secret.ai.llm_url string optional The LLM API base URL. Example: https://llm.local + * @bodyParam secret.ai.api_key string optional The LLM API key. Example: sk-123... + * @bodyParam secret.vector object optional Vector Database configuration. + * @bodyParam secret.vector.db_host string optional The database host. Example: localhost + * @bodyParam secret.vector.db_port int optional The database port. Example: 5432 + * @bodyParam secret.vector.db_name string optional The database name. Example: db_d1234 + * @bodyParam secret.vector.db_user string optional The database username. Example: admin + * @bodyParam secret.vector.db_pass string optional The database password. Example: secret-pass + * @bodyParam config object optional Key-value overrides or configurations for this individual deployment. Example: {"lagoon-auto-idle": "1"} * * @response 201 { * "message": "Instance provisioned", * "data": { * "uuid": "3a105da1-9c87-43ca-9ac8-72787fc5e315", + * "name": "My awesome instance", * "status": "new" * } * } @@ -129,12 +170,32 @@ public function createInstance(Request $request): JsonResponse $request->validate([ 'email' => 'required|email', 'storeAppId' => 'required|string|exists:polydock_store_apps,uuid', + 'name' => 'nullable|string|max:255', + 'secret' => 'nullable|array', + 'secret.ai' => 'nullable|array', + 'secret.ai.llm_url' => 'nullable|string', + 'secret.ai.api_key' => 'nullable|string', + 'secret.vector' => 'nullable|array', + 'secret.vector.db_host' => 'nullable|string', + 'secret.vector.db_port' => 'nullable|integer', + 'secret.vector.db_name' => 'nullable|string', + 'secret.vector.db_user' => 'nullable|string', + 'secret.vector.db_pass' => 'nullable|string', 'config' => 'nullable|array', 'config.*' => [ 'nullable', function ($attribute, $value, $fail) { - if (is_array($value) || is_object($value)) { - $fail('The ' . $attribute . ' must be a scalar value.'); + // Allow 'secret' to be an array if passed within config (legacy support) + if (str_ends_with($attribute, '.secret')) { + if (! \is_array($value)) { + $fail("The {$attribute} must be an array."); + } + + return; + } + + if (\is_array($value) || \is_object($value)) { + $fail("The {$attribute} must be a scalar value."); } }, ], @@ -163,15 +224,28 @@ function ($attribute, $value, $fail) { $storeApp = PolydockStoreApp::where('uuid', $request->input('storeAppId'))->firstOrFail(); - // Use the existing allocation mechanism or create a new instance - $instance = UserGroup::getNewAppInstanceForThisAppForThisGroup($storeApp, $primaryGroup); - // Apply config if provided $config = $request->input('config', []); + + // Use name from request, or fallback to lagoon-project-name from config if provided + $name = $request->input('name') ?? $config['lagoon-project-name'] ?? null; + + // Use the existing allocation mechanism or create a new instance + $instance = UserGroup::getNewAppInstanceForThisAppForThisGroup($storeApp, $primaryGroup, $name ? (string) $name : null); + + // Handle top-level secret if provided + if ($request->filled('secret')) { + $instance->storeKeyValue('secret', $request->input('secret')); + } + if (! empty($config)) { foreach ($config as $key => $value) { // If it's a known root column we could update it, otherwise data blob key-value store - $instance->storeKeyValue((string) $key, (string) $value); + // Skip if we already stored it from top-level secret to avoid overwriting with potentially partial data + if ((string) $key === 'secret' && $request->filled('secret')) { + continue; + } + $instance->storeKeyValue((string) $key, $value === null ? '' : $value); } } @@ -179,6 +253,7 @@ function ($attribute, $value, $fail) { 'message' => 'Instance provisioned', 'data' => [ 'uuid' => $instance->uuid, + 'name' => $instance->name, 'status' => $instance->status?->value, ], ], 201); @@ -199,7 +274,9 @@ function ($attribute, $value, $fail) { * "data": { * "uuid": "3a105da1-9c87-43ca-9ac8-72787fc5e315", * "status": "running-healthy-claimed", - * "status_message": "Instance is running smoothly." + * "status_message": "Instance is running smoothly.", + * "lagoon_claim_script": "/lagoon/polydock_claim.sh", + * "lagoon_project_name": "example-project" * } * } */ @@ -212,6 +289,8 @@ public function getInstanceStatus(string $uuid): JsonResponse 'uuid' => $instance->uuid, 'status' => $instance->status?->value, 'status_message' => $instance->status_message, + 'lagoon_claim_script' => $instance->getKeyValue(key: 'lagoon-claim-script'), + 'lagoon_project_name' => $instance->getKeyValue(key: 'lagoon-project-name'), ], ]); } diff --git a/app/Http/Controllers/Api/RegionsController.php b/app/Http/Controllers/Api/RegionsController.php index e811c4b0..0f4ef952 100644 --- a/app/Http/Controllers/Api/RegionsController.php +++ b/app/Http/Controllers/Api/RegionsController.php @@ -25,7 +25,6 @@ class RegionsController extends Controller * "data": { * "regions": [ * { - * "uuid": null, * "id": 1, * "label": "Demo US East Region Store", * "apps": [ @@ -61,7 +60,6 @@ public function index(): JsonResponse }]) ->get() ->map(fn ($store) => [ - 'uuid' => null, // Stores don't have UUIDs, using ID as identifier 'id' => $store->id, 'label' => $store->name, 'apps' => $store->apps->map(fn ($app) => [ diff --git a/app/Models/PolydockAppInstance.php b/app/Models/PolydockAppInstance.php index 195c497d..27039b0c 100644 --- a/app/Models/PolydockAppInstance.php +++ b/app/Models/PolydockAppInstance.php @@ -111,11 +111,6 @@ class PolydockAppInstance extends Model implements PolydockAppInstanceInterface */ private PolydockAppLoggerInterface $logger; - /** - * The name of the app instance - */ - private string $name; - // Add default sensitive keys specific to app instances protected array $sensitiveDataKeys = [ // Exact matches @@ -283,7 +278,15 @@ protected static function boot() // Set the app type using the store app's class $model->setAppType($storeApp->polydock_app_class); - $model->name = $model->generateUniqueProjectName($storeApp->lagoon_deploy_project_prefix); + if (empty($model->name)) { + $model->name = $model->generateUniqueProjectName($storeApp->lagoon_deploy_project_prefix); + } + + // Ensure name uniqueness + $baseName = $model->name; + while (static::where('name', $model->name)->exists()) { + $model->name = $baseName.'-'.strtolower(Str::random(4)); + } // Fill the UUID $model->uuid = Str::uuid()->toString(); @@ -423,7 +426,7 @@ public function getApp(): PolydockAppInterface */ public function setName(string $name): self { - $this->name = $name; + $this->attributes['name'] = $name; return $this; } @@ -542,10 +545,10 @@ public function getStatusMessage(): string * Store a key-value pair for the app instance * * @param string $key The key to store - * @param string $value The value to store - * @return self Returns the instance for method chaining + * @param mixed $value The value to store + * @return PolydockAppInstanceInterface Returns the instance for method chaining */ - public function storeKeyValue(string $key, string $value): self + public function storeKeyValue(string $key, $value): PolydockAppInstanceInterface { $resultData = $this->data ?? []; data_set($resultData, $key, $value); @@ -559,11 +562,11 @@ public function storeKeyValue(string $key, string $value): self * Get a stored value by key * * @param string $key The key to retrieve - * @return string The stored value, or empty string if not found + * @return mixed The stored value, or empty string if not found */ - public function getKeyValue(string $key): string + public function getKeyValue(string $key): mixed { - return data_get($this->data, $key) ?? ''; + return data_get($this->data, $key, ''); } /** diff --git a/app/Models/PolydockStoreApp.php b/app/Models/PolydockStoreApp.php index d5c3e239..8a11277c 100644 --- a/app/Models/PolydockStoreApp.php +++ b/app/Models/PolydockStoreApp.php @@ -219,7 +219,7 @@ public function getLagoonDeployProjectPrefixAttribute(): string /** * Get the Lagoon deploy private key attribute */ - public function getLagoonDeployPrivateKeyAttribute(): string + public function getLagoonDeployPrivateKeyAttribute(): ?string { return $this->store->lagoon_deploy_private_key; } diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php index 357e6b60..5116ef67 100644 --- a/app/Models/UserGroup.php +++ b/app/Models/UserGroup.php @@ -17,159 +17,102 @@ class UserGroup extends Model protected $fillable = [ 'name', + 'slug', ]; /** - * Get all users in this group - * - * @return BelongsToMany + * Get the users associated with the group. */ - public function users() + public function users(): BelongsToMany { - return $this->belongsToMany(User::class, 'user_user_group', 'user_group_id', 'user_id') + return $this->belongsToMany(User::class, 'user_user_group') ->withPivot('role') ->withTimestamps(); } /** - * Get all users with 'owner' role in this group - * - * @return BelongsToMany + * Get the users who are owners of the group. */ - public function owners() + public function owners(): BelongsToMany { - return $this->belongsToMany(User::class, 'user_user_group') - ->wherePivot('role', UserGroupRoleEnum::OWNER->value); + return $this->users()->wherePivot('role', UserGroupRoleEnum::OWNER->value); } /** - * Get all users with 'member' role in this group - * - * @return BelongsToMany + * Get the users who are members of the group. */ - public function members() + public function members(): BelongsToMany { - return $this->belongsToMany(User::class, 'user_user_group') - ->wherePivot('role', UserGroupRoleEnum::MEMBER->value); + return $this->users()->wherePivot('role', UserGroupRoleEnum::MEMBER->value); } /** - * Get all users with 'viewer' role in this group - * - * @return BelongsToMany + * Get the users who are viewers of the group. */ - public function viewers() + public function viewers(): BelongsToMany { - return $this->belongsToMany(User::class, 'user_user_group') - ->wherePivot('role', UserGroupRoleEnum::VIEWER->value); + return $this->users()->wherePivot('role', UserGroupRoleEnum::VIEWER->value); } /** - * Get all app instances for this group + * Get the app instances associated with the group. */ public function appInstances(): HasMany { return $this->hasMany(PolydockAppInstance::class); } - /** - * Get pending app instances - */ - public function appInstancesPending(): HasMany - { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$pendingStatuses); - } - - /** - * Get completed app instances - */ - public function appInstancesCompleted(): HasMany - { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$completedStatuses); - } - - /** - * Get failed app instances - */ - public function appInstancesFailed(): HasMany + public function appInstancesStageCreate(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$failedStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$stageCreateStatuses); } - /** - * Get polling app instances - */ - public function appInstancesPolling(): HasMany + public function appInstancesStageDeploy(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$pollingStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$stageDeployStatuses); } - /** - * Get create stage app instances - */ - public function appInstancesStageCreate(): HasMany + public function appInstancesStageUpgrade(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$stageCreateStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$stageUpgradeStatuses); } - /** - * Get deploy stage app instances - */ - public function appInstancesStageDeploy(): HasMany + public function appInstancesStageRemove(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$stageDeployStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$stageRemoveStatuses); } - /** - * Get remove stage app instances - */ - public function appInstancesStageRemove(): HasMany + public function appInstancesStageRunning(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$stageRemoveStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$stageRunningStatuses); } - /** - * Get upgrade stage app instances - */ - public function appInstancesStageUpgrade(): HasMany + public function appInstancesFailed(): HasMany { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$stageUpgradeStatuses); + return $this->appInstances()->whereIn('status', PolydockAppInstance::$failedStatuses); } /** - * Get running stage app instances + * Get the group name. */ - public function appInstancesStageRunning(): HasMany + public function getName(): string { - return $this->appInstances() - ->whereIn('status', PolydockAppInstance::$stageRunningStatuses); + return $this->name; } - /** - * Boot the model - */ - #[\Override] - protected static function booted() + protected static function boot() { - static::saving(function ($userGroup) { - $userGroup->name = preg_replace('/\s+/', ' ', trim((string) $userGroup->name)); - }); + parent::boot(); static::creating(function ($userGroup) { - if (! $userGroup->slug) { + if (empty($userGroup->slug)) { $slug = Str::slug($userGroup->name); + $originalSlug = $slug; $count = 1; while (static::where('slug', $slug)->exists()) { - $slug = Str::slug($userGroup->name).'-'.$count++; + $slug = $originalSlug.'-'.$count; + $count++; } $userGroup->slug = $slug; @@ -177,33 +120,40 @@ protected static function booted() }); } - public function getNewAppInstanceForThisApp(PolydockStoreApp $storeApp): PolydockAppInstance + public function getNewAppInstanceForThisApp(PolydockStoreApp $storeApp, ?string $name = null): PolydockAppInstance { - return self::getNewAppInstanceForThisAppForThisGroup($storeApp, $this); + return self::getNewAppInstanceForThisAppForThisGroup($storeApp, $this, $name); } public static function getNewAppInstanceForThisAppForThisGroup( PolydockStoreApp $storeApp, UserGroup $userGroup, + ?string $name = null, ): PolydockAppInstance { Log::info('Creating unallocated instance', [ 'app_id' => $storeApp->id, 'app_name' => $storeApp->name, + 'requested_name' => $name, ]); $allocationLock = Str::uuid()->toString(); - // Attempt to lock a single unallocated instance for this store app - $lockedRows = PolydockAppInstance::where('polydock_store_app_id', $storeApp->id) - ->whereNull('user_group_id') - ->whereNull('allocation_lock') - ->where('status', PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED) - ->limit(1) - ->update(['allocation_lock' => $allocationLock, 'user_group_id' => $userGroup->id]); - - // Check if we got a lock by querying for the instance with our lock - $lockedInstance = PolydockAppInstance::where('polydock_store_app_id', $storeApp->id) - ->where('allocation_lock', $allocationLock) - ->first(); + $lockedInstance = null; + + // If no custom name is requested, attempt to grab an unallocated instance + if ($name === null) { + // Attempt to lock a single unallocated instance for this store app + PolydockAppInstance::where('polydock_store_app_id', $storeApp->id) + ->whereNull('user_group_id') + ->whereNull('allocation_lock') + ->where('status', PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED) + ->limit(1) + ->update(['allocation_lock' => $allocationLock, 'user_group_id' => $userGroup->id]); + + // Check if we got a lock by querying for the instance with our lock + $lockedInstance = PolydockAppInstance::where('polydock_store_app_id', $storeApp->id) + ->where('allocation_lock', $allocationLock) + ->first(); + } if ($lockedInstance) { Log::info('Grabbed unallocated instance via lock', [ @@ -240,6 +190,7 @@ public static function getNewAppInstanceForThisAppForThisGroup( return $lockedInstance; } else { $appInstance = PolydockAppInstance::create([ + 'name' => $name, 'polydock_store_app_id' => $storeApp->id, 'user_group_id' => $userGroup->id, 'allocation_lock' => $allocationLock, diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c175c47d..edb72a11 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -6,6 +6,8 @@ use App\Services\PolydockAppClassDiscovery; use Dedoc\Scramble\Scramble; +use Dedoc\Scramble\Support\Generator\OpenApi; +use Dedoc\Scramble\Support\Generator\SecurityScheme; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Routing\Route; use Illuminate\Support\Facades\Gate; @@ -34,7 +36,12 @@ public function boot(): void Scramble::configure()->expose( ui: '/api', document: '/api/openapi.json', - ); + ) + ->withDocumentTransformers(function (OpenApi $openApi) { + $openApi->secure( + SecurityScheme::http('bearer')->as('BearerAuth') + ); + }); Scramble::routes(fn (Route $route) => str_starts_with($route->uri(), 'api/')); } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 96c828ef..d0d0d356 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -17,7 +17,7 @@ use Filament\Support\Colors\Color; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; +use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; @@ -61,7 +61,7 @@ public function panel(Panel $panel): Panel StartSession::class, AuthenticateSession::class, ShareErrorsFromSession::class, - VerifyCsrfToken::class, + ValidateCsrfToken::class, SubstituteBindings::class, DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index 15f0d63b..99893ec3 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -16,7 +16,7 @@ use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; -use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; +use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; @@ -49,7 +49,7 @@ public function panel(Panel $panel): Panel StartSession::class, AuthenticateSession::class, ShareErrorsFromSession::class, - VerifyCsrfToken::class, + ValidateCsrfToken::class, SubstituteBindings::class, DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, diff --git a/app/Traits/HasEnumOptions.php b/app/Traits/HasEnumOptions.php new file mode 100644 index 00000000..d7c3c8c8 --- /dev/null +++ b/app/Traits/HasEnumOptions.php @@ -0,0 +1,28 @@ +mapWithKeys(function ($case) { + // Fallback to title-cased value if the Enum doesn't implement HasLabel + $label = ($case instanceof HasLabel) + ? $case->getLabel() + : str($case->value)->title()->toString(); + + return [$case->value => $label]; + })->all(); + } + + public static function getValues(): array + { + return array_column(static::cases(), 'value'); + } +} diff --git a/app/Traits/HasWebhookSensitiveData.php b/app/Traits/HasWebhookSensitiveData.php index 9f71469d..c7ada2b9 100644 --- a/app/Traits/HasWebhookSensitiveData.php +++ b/app/Traits/HasWebhookSensitiveData.php @@ -20,14 +20,14 @@ public function getSensitiveDataKeys(): array 'recaptcha', // Regex patterns (starting with /) - '/^.*_key.*$/', // Anything containing _key - '/^.*private.*$/', // Anything containing private + '/^.*_key.*$/', // Anything containing _key + '/^.*private.*$/', // Anything containing private '/^.*secret.*$/', // Anything containing secret - '/^.*pass.*$/', // Anything containing pass - '/^.*username.*$/', // Anything containing username - '/^.*token.*$/', // Anything containing token - '/^.*api.*$/', // Anything containing api - '/^.*ssh.*$/', // Anything containing ssh + '/^.*pass.*$/', // Anything containing pass + '/^.*username.*$/', // Anything containing username + '/^.*token.*$/', // Anything containing token + '/^.*api.*$/', // Anything containing api + '/^.*ssh.*$/', // Anything containing ssh ]; } @@ -36,10 +36,11 @@ public function getSensitiveDataKeys(): array */ public function registerSensitiveDataKeys(array|string $keys): self { - $keys = is_array($keys) ? $keys : [$keys]; - $this->sensitiveDataKeys = array_unique( - array_merge($this->getSensitiveDataKeys(), $keys), - ); + $keys = \is_array($keys) ? $keys : [$keys]; + $this->sensitiveDataKeys = array_unique([ + ...$this->getSensitiveDataKeys(), + ...$keys, + ]); return $this; } @@ -54,7 +55,7 @@ public function shouldFilterKey(string $key, array $sensitiveKeys): bool foreach ($sensitiveKeys as $sensitiveKey) { // If it's a regex pattern if (str_starts_with((string) $sensitiveKey, '/')) { - if (preg_match($sensitiveKey, $key)) { + if (preg_match($sensitiveKey, $lowercaseKey)) { return true; } diff --git a/bootstrap/app.php b/bootstrap/app.php index a93eaa1a..57501693 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use App\Http\Middleware\EnsureInstancesReadAbility; +use App\Http\Middleware\EnsureInstancesWriteAbility; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -14,9 +16,12 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware) { + $trustedProxies = env('TRUSTED_PROXIES', '*'); + $middleware->trustProxies(at: $trustedProxies); + $middleware->alias([ - 'instances.read.ability' => \App\Http\Middleware\EnsureInstancesReadAbility::class, - 'instances.write.ability' => \App\Http\Middleware\EnsureInstancesWriteAbility::class, + 'instances.read.ability' => EnsureInstancesReadAbility::class, + 'instances.write.ability' => EnsureInstancesWriteAbility::class, ]); }) ->withExceptions(function (Exceptions $exceptions) { diff --git a/composer.json b/composer.json index 58d84754..e4379dd9 100644 --- a/composer.json +++ b/composer.json @@ -14,14 +14,14 @@ "amazeeio/lagoon-logs": "^0.0.5", "amazeeio/polydock-app-amazeeclaw": "^0.1", "amazeeio/polydock-app-amazeeio-privategpt": "^0.1", - "dedoc/scramble": "^0.13.14", + "dedoc/scramble": "^0.13.16", "evanschleret/lara-mjml": "^0.3.0", "filament/filament": "^3.2", "freedomtech-hosting/ft-lagoon-php": "^0.0.16", "freedomtech-hosting/polydock-amazeeai-backend-client-php": "^0.1", - "freedomtech-hosting/polydock-app": "^0.0.31", + "freedomtech-hosting/polydock-app": "^0.0.33", "freedomtech-hosting/polydock-app-amazeeio-generic": "^0.1", - "laravel/framework": "^11.31", + "laravel/framework": "^12.0", "laravel/horizon": "^5.30", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9", @@ -32,15 +32,15 @@ "fakerphp/faker": "^1.23", "hotmeteor/spectator": "^2.1", "larastan/larastan": "^3.9", - "laravel/pail": "^1.1", - "laravel/pint": "^1.13", - "laravel/sail": "^1.26", + "laravel/pail": "^1.2", + "laravel/pint": "^1.21", + "laravel/sail": "^1.41", "mockery/mockery": "^1.6", - "nunomaduro/collision": "^8.1", - "phpunit/phpunit": "^11.0.1", + "nunomaduro/collision": "^8.6", + "phpunit/phpunit": "^11.5", "rector/rector": "^2.3", "squizlabs/php_codesniffer": "^4.0", - "uselagoon/sailonlagoon": "^0.2.0" + "uselagoon/sailonlagoon": "^0.3.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 87938e56..f088b32a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "845a7c0612806aecef24ddc2a8656180", + "content-hash": "9014550b8b9f780e4a2ff235bf4dbcca", "packages": [ { "name": "amazeeio/lagoon-logs", @@ -55,20 +55,19 @@ }, { "name": "amazeeio/polydock-app-amazeeclaw", - "version": "v0.1.7", + "version": "v0.1.8", "source": { "type": "git", "url": "https://github.com/amazeeio/polydock-app-amazeeclaw.git", - "reference": "603312a19812e89ff332304f58940b68a76a45ab" + "reference": "c43274ae5042acd1a6a36167d74f8ab59ce63338" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeclaw/zipball/603312a19812e89ff332304f58940b68a76a45ab", - "reference": "603312a19812e89ff332304f58940b68a76a45ab", + "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeclaw/zipball/c43274ae5042acd1a6a36167d74f8ab59ce63338", + "reference": "c43274ae5042acd1a6a36167d74f8ab59ce63338", "shasum": "" }, "require": { - "freedomtech-hosting/polydock-amazeeai-backend-client-php": "^0.1", "freedomtech-hosting/polydock-app-amazeeio-generic": "^0.1" }, "type": "library", @@ -84,28 +83,28 @@ "description": "Polydock App - AmazeeClaw AI", "support": { "issues": "https://github.com/amazeeio/polydock-app-amazeeclaw/issues", - "source": "https://github.com/amazeeio/polydock-app-amazeeclaw/tree/v0.1.7" + "source": "https://github.com/amazeeio/polydock-app-amazeeclaw/tree/v0.1.8" }, - "time": "2026-02-24T01:49:32+00:00" + "time": "2026-03-20T22:03:00+00:00" }, { "name": "amazeeio/polydock-app-amazeeio-privategpt", - "version": "v0.1.4", + "version": "v0.1.6", "source": { "type": "git", "url": "https://github.com/amazeeio/polydock-app-amazeeio-privategpt.git", - "reference": "b9538c96346299bd2cbf668b702fbf79e5ed24db" + "reference": "94d4dd33cc50150a8cba603173a8a69fd7f0a4ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeio-privategpt/zipball/b9538c96346299bd2cbf668b702fbf79e5ed24db", - "reference": "b9538c96346299bd2cbf668b702fbf79e5ed24db", + "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeio-privategpt/zipball/94d4dd33cc50150a8cba603173a8a69fd7f0a4ef", + "reference": "94d4dd33cc50150a8cba603173a8a69fd7f0a4ef", "shasum": "" }, "require": { "cuyz/valinor": "^2.3.2", "freedomtech-hosting/ft-lagoon-php": "^0.0.16", - "freedomtech-hosting/polydock-app": "^0.0.31", + "freedomtech-hosting/polydock-app": "^0.0.33", "freedomtech-hosting/polydock-app-amazeeio-generic": "^0.1", "guzzlehttp/guzzle": "^7.10.0" }, @@ -137,35 +136,34 @@ "description": "Polydock App - amazee.io PrivateGPT with Direct API Integration", "support": { "issues": "https://github.com/amazeeio/polydock-app-amazeeio-privategpt/issues", - "source": "https://github.com/amazeeio/polydock-app-amazeeio-privategpt/tree/v0.1.4" + "source": "https://github.com/amazeeio/polydock-app-amazeeio-privategpt/tree/v0.1.6" }, - "time": "2026-03-11T22:00:43+00:00" + "time": "2026-03-20T22:28:16+00:00" }, { "name": "anourvalar/eloquent-serialize", - "version": "1.3.5", + "version": "1.3.6", "source": { "type": "git", "url": "https://github.com/AnourValar/eloquent-serialize.git", - "reference": "1a7dead8d532657e5358f8f27c0349373517681e" + "reference": "42645e5515d6105c0e6db202f93cab3d36cdce74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/1a7dead8d532657e5358f8f27c0349373517681e", - "reference": "1a7dead8d532657e5358f8f27c0349373517681e", + "url": "https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/42645e5515d6105c0e6db202f93cab3d36cdce74", + "reference": "42645e5515d6105c0e6db202f93cab3d36cdce74", "shasum": "" }, "require": { - "laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0", + "laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^7.4|^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.26", "laravel/legacy-factories": "^1.1", - "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.5|^10.5|^11.0", - "psalm/plugin-laravel": "^2.8|^3.0", "squizlabs/php_codesniffer": "^3.7" }, "type": "library", @@ -203,9 +201,9 @@ ], "support": { "issues": "https://github.com/AnourValar/eloquent-serialize/issues", - "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.5" + "source": "https://github.com/AnourValar/eloquent-serialize/tree/1.3.6" }, - "time": "2025-12-04T13:38:21+00:00" + "time": "2026-03-18T15:36:03+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -669,16 +667,16 @@ }, { "name": "dedoc/scramble", - "version": "v0.13.15", + "version": "v0.13.16", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "8101fb042c178fa8506740380b8067d571951a2c" + "reference": "43a5dd62b6a5d7d6fd0125092985d78ae22f0ffe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/8101fb042c178fa8506740380b8067d571951a2c", - "reference": "8101fb042c178fa8506740380b8067d571951a2c", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/43a5dd62b6a5d7d6fd0125092985d78ae22f0ffe", + "reference": "43a5dd62b6a5d7d6fd0125092985d78ae22f0ffe", "shasum": "" }, "require": { @@ -737,7 +735,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.13.15" + "source": "https://github.com/dedoc/scramble/tree/v0.13.16" }, "funding": [ { @@ -745,7 +743,7 @@ "type": "github" } ], - "time": "2026-03-12T13:16:40+00:00" + "time": "2026-03-17T21:42:41+00:00" }, { "name": "dflydev/dot-access-data", @@ -824,16 +822,16 @@ }, { "name": "doctrine/dbal", - "version": "4.4.2", + "version": "4.4.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722" + "reference": "61e730f1658814821a85f2402c945f3883407dec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/476f7f0fa6ea4aa5364926db7fabdf6049075722", - "reference": "476f7f0fa6ea4aa5364926db7fabdf6049075722", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61e730f1658814821a85f2402c945f3883407dec", + "reference": "61e730f1658814821a85f2402c945f3883407dec", "shasum": "" }, "require": { @@ -910,7 +908,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/4.4.2" + "source": "https://github.com/doctrine/dbal/tree/4.4.3" }, "funding": [ { @@ -926,7 +924,7 @@ "type": "tidelift" } ], - "time": "2026-02-26T12:12:19+00:00" + "time": "2026-03-20T08:52:12+00:00" }, { "name": "doctrine/deprecations", @@ -1858,16 +1856,16 @@ }, { "name": "freedomtech-hosting/polydock-app", - "version": "v0.0.31", + "version": "v0.0.33", "source": { "type": "git", "url": "https://github.com/amazeeio/polydock-app-lib.git", - "reference": "cf234a3d6722cd01f0296e0fc01c34e3553c9340" + "reference": "ccebea110db942a5dac4f545116cb7d1ed15db0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amazeeio/polydock-app-lib/zipball/cf234a3d6722cd01f0296e0fc01c34e3553c9340", - "reference": "cf234a3d6722cd01f0296e0fc01c34e3553c9340", + "url": "https://api.github.com/repos/amazeeio/polydock-app-lib/zipball/ccebea110db942a5dac4f545116cb7d1ed15db0f", + "reference": "ccebea110db942a5dac4f545116cb7d1ed15db0f", "shasum": "" }, "require": { @@ -1892,28 +1890,28 @@ "description": "Library for Polydock Apps", "support": { "issues": "https://github.com/amazeeio/polydock-app-lib/issues", - "source": "https://github.com/amazeeio/polydock-app-lib/tree/v0.0.31" + "source": "https://github.com/amazeeio/polydock-app-lib/tree/v0.0.33" }, - "time": "2026-02-20T18:31:16+00:00" + "time": "2026-03-20T22:21:32+00:00" }, { "name": "freedomtech-hosting/polydock-app-amazeeio-generic", - "version": "v0.1.3", + "version": "v0.1.5", "source": { "type": "git", "url": "https://github.com/amazeeio/polydock-app-amazeeio-generic.git", - "reference": "39e7dd7829f97251006f3038cc25dd20553db9f5" + "reference": "3dff1aaaf781862bd877440438ed12e90c31bb3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeio-generic/zipball/39e7dd7829f97251006f3038cc25dd20553db9f5", - "reference": "39e7dd7829f97251006f3038cc25dd20553db9f5", + "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeio-generic/zipball/3dff1aaaf781862bd877440438ed12e90c31bb3a", + "reference": "3dff1aaaf781862bd877440438ed12e90c31bb3a", "shasum": "" }, "require": { "freedomtech-hosting/ft-lagoon-php": "^0.0.16", "freedomtech-hosting/polydock-amazeeai-backend-client-php": "^0.1", - "freedomtech-hosting/polydock-app": "^0.0.31" + "freedomtech-hosting/polydock-app": "^0.0.33" }, "type": "library", "autoload": { @@ -1934,9 +1932,9 @@ "description": "Polydock App - amazee.io Generic", "support": { "issues": "https://github.com/amazeeio/polydock-app-amazeeio-generic/issues", - "source": "https://github.com/amazeeio/polydock-app-amazeeio-generic/tree/v0.1.3" + "source": "https://github.com/amazeeio/polydock-app-amazeeio-generic/tree/v0.1.5" }, - "time": "2026-03-11T21:59:42+00:00" + "time": "2026-03-20T22:24:56+00:00" }, { "name": "fruitcake/php-cors", @@ -2485,28 +2483,28 @@ }, { "name": "kirschbaum-development/eloquent-power-joins", - "version": "4.2.11", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git", - "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588" + "reference": "dbf2dfaa1900152f2e3dc42b30b67f67a82e7c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/0e3e3372992e4bf82391b3c7b84b435c3db73588", - "reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588", + "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/dbf2dfaa1900152f2e3dc42b30b67f67a82e7c36", + "reference": "dbf2dfaa1900152f2e3dc42b30b67f67a82e7c36", "shasum": "" }, "require": { - "illuminate/database": "^11.42|^12.0", - "illuminate/support": "^11.42|^12.0", + "illuminate/database": "^11.42|^12.0|^13.0", + "illuminate/support": "^11.42|^12.0|^13.0", "php": "^8.2" }, "require-dev": { "friendsofphp/php-cs-fixer": "dev-master", - "laravel/legacy-factories": "^1.0@dev", - "orchestra/testbench": "^9.0|^10.0", - "phpunit/phpunit": "^10.0|^11.0" + "laravel/legacy-factories": "^1.0@dev|dev-master", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.0|^11.0|^12.0" }, "type": "library", "extra": { @@ -2542,26 +2540,26 @@ ], "support": { "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues", - "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.11" + "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.3.0" }, - "time": "2025-12-17T00:37:48+00:00" + "time": "2026-03-17T16:43:01+00:00" }, { "name": "laravel/framework", - "version": "v11.49.0", + "version": "v12.55.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "5a05faf641be4edc92b42990d8673561bcd3f63a" + "reference": "6d9185a248d101b07eecaf8fd60b18129545fd33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/5a05faf641be4edc92b42990d8673561bcd3f63a", - "reference": "5a05faf641be4edc92b42990d8673561bcd3f63a", + "url": "https://api.github.com/repos/laravel/framework/zipball/6d9185a248d101b07eecaf8fd60b18129545fd33", + "reference": "6d9185a248d101b07eecaf8fd60b18129545fd33", "shasum": "" }, "require": { - "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12|^0.13|^0.14", + "brick/math": "^0.11|^0.12|^0.13|^0.14", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -2576,32 +2574,34 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", + "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.72.6|^3.8.4", + "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^7.0.3", - "symfony/error-handler": "^7.0.3", - "symfony/finder": "^7.0.3", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", "symfony/http-foundation": "^7.2.0", - "symfony/http-kernel": "^7.0.3", - "symfony/mailer": "^7.0.3", - "symfony/mime": "^7.0.3", - "symfony/polyfill-php83": "^1.31", - "symfony/process": "^7.0.3", - "symfony/routing": "^7.0.3", - "symfony/uid": "^7.0.3", - "symfony/var-dumper": "^7.0.3", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.6.1", "voku/portable-ascii": "^2.0.2" @@ -2633,6 +2633,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", "illuminate/mail": "self.version", @@ -2642,6 +2643,7 @@ "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", @@ -2665,17 +2667,18 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^9.18.0", - "pda/pheanstalk": "^5.0.6", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", - "phpstan/phpstan": "2.1.41", - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", - "predis/predis": "^2.3", - "resend/resend-php": "^0.10.0", - "symfony/cache": "^7.0.3", - "symfony/http-client": "^7.0.3", - "symfony/psr-http-message-bridge": "^7.0.3", - "symfony/translation": "^7.0.3" + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", @@ -2690,7 +2693,7 @@ "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", @@ -2701,22 +2704,22 @@ "mockery/mockery": "Required to use mocking (^1.6).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -2727,6 +2730,7 @@ "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], @@ -2735,7 +2739,8 @@ "Illuminate\\Support\\": [ "src/Illuminate/Macroable/", "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/" + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" ] } }, @@ -2759,20 +2764,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-03-17T14:12:52+00:00" + "time": "2026-03-18T14:28:59+00:00" }, { "name": "laravel/horizon", - "version": "v5.45.3", + "version": "v5.45.4", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "80c4de73c56227c625e5a3c6bfaddff760ab0962" + "reference": "b2b32e3f6013081e0176307e9081cd085f0ad4d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/80c4de73c56227c625e5a3c6bfaddff760ab0962", - "reference": "80c4de73c56227c625e5a3c6bfaddff760ab0962", + "url": "https://api.github.com/repos/laravel/horizon/zipball/b2b32e3f6013081e0176307e9081cd085f0ad4d6", + "reference": "b2b32e3f6013081e0176307e9081cd085f0ad4d6", "shasum": "" }, "require": { @@ -2837,9 +2842,9 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.45.3" + "source": "https://github.com/laravel/horizon/tree/v5.45.4" }, - "time": "2026-03-11T14:28:04+00:00" + "time": "2026-03-18T14:14:59+00:00" }, { "name": "laravel/prompts", @@ -3151,16 +3156,16 @@ }, { "name": "league/commonmark", - "version": "2.8.1", + "version": "2.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "84b1ca48347efdbe775426f108622a42735a6579" + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", - "reference": "84b1ca48347efdbe775426f108622a42735a6579", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", "shasum": "" }, "require": { @@ -3254,7 +3259,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T21:37:03+00:00" + "time": "2026-03-19T13:16:38+00:00" }, { "name": "league/config", @@ -4867,16 +4872,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.49", + "version": "3.0.50", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", + "reference": "aa6ad8321ed103dc3624fb600a25b66ebf78ec7b", "shasum": "" }, "require": { @@ -4957,7 +4962,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.50" }, "funding": [ { @@ -4973,7 +4978,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T09:17:28+00:00" + "time": "2026-03-19T02:57:58+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -5762,33 +5767,33 @@ }, { "name": "ryangjchandler/blade-capture-directive", - "version": "v1.1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/ryangjchandler/blade-capture-directive.git", - "reference": "bbb1513dfd89eaec87a47fe0c449a7e3d4a1976d" + "reference": "3f9e80b56ff60b78755ef320e3e16d88850101d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/bbb1513dfd89eaec87a47fe0c449a7e3d4a1976d", - "reference": "bbb1513dfd89eaec87a47fe0c449a7e3d4a1976d", + "url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/3f9e80b56ff60b78755ef320e3e16d88850101d6", + "reference": "3f9e80b56ff60b78755ef320e3e16d88850101d6", "shasum": "" }, "require": { - "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", "php": "^8.1", "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { "nunomaduro/collision": "^7.0|^8.0", "nunomaduro/larastan": "^2.0|^3.0", - "orchestra/testbench": "^8.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.7", - "pestphp/pest-plugin-laravel": "^2.0|^3.1", + "orchestra/testbench": "^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.7|^4.1", + "pestphp/pest-plugin-laravel": "^2.0|^3.1|^v4.1.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0|^2.0", "phpstan/phpstan-phpunit": "^1.0|^2.0", - "phpunit/phpunit": "^10.0|^11.5.3", + "phpunit/phpunit": "^10.0|^11.5.3|^12.0", "spatie/laravel-ray": "^1.26" }, "type": "library", @@ -5828,7 +5833,7 @@ ], "support": { "issues": "https://github.com/ryangjchandler/blade-capture-directive/issues", - "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.1.0" + "source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.1.1" }, "funding": [ { @@ -5836,7 +5841,7 @@ "type": "github" } ], - "time": "2025-02-25T09:09:36+00:00" + "time": "2026-03-19T10:36:26+00:00" }, { "name": "softonic/graphql-client", @@ -8074,6 +8079,86 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, { "name": "symfony/polyfill-php85", "version": "v1.33.0", @@ -12260,30 +12345,29 @@ }, { "name": "uselagoon/sailonlagoon", - "version": "v0.2.0", + "version": "v0.3.0", "source": { "type": "git", "url": "https://github.com/uselagoon/sailonlagoon.git", - "reference": "1757300224866f8489ef02fc81800f2d10665110" + "reference": "b8dcd1c6099d231c958c13ad3e79d3d8606be209" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/uselagoon/sailonlagoon/zipball/1757300224866f8489ef02fc81800f2d10665110", - "reference": "1757300224866f8489ef02fc81800f2d10665110", + "url": "https://api.github.com/repos/uselagoon/sailonlagoon/zipball/b8dcd1c6099d231c958c13ad3e79d3d8606be209", + "reference": "b8dcd1c6099d231c958c13ad3e79d3d8606be209", "shasum": "" }, "require": { - "illuminate/contracts": "^10.0||^11.0", + "illuminate/contracts": "^11.0|^12.0", "php": "^8.1", "spatie/laravel-package-tools": "^1.14.0" }, "require-dev": { - "laravel/pint": "^1.0", - "nunomaduro/collision": "^7.8", - "orchestra/testbench": "^8.8", - "pestphp/pest": "^2.20", - "pestphp/pest-plugin-arch": "^2.5", - "pestphp/pest-plugin-laravel": "^2.0" + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" }, "type": "library", "extra": { @@ -12322,7 +12406,7 @@ ], "support": { "issues": "https://github.com/uselagoon/sailonlagoon/issues", - "source": "https://github.com/uselagoon/sailonlagoon/tree/v0.2.0" + "source": "https://github.com/uselagoon/sailonlagoon/tree/v0.3.0" }, "funding": [ { @@ -12330,7 +12414,7 @@ "type": "github" } ], - "time": "2024-06-27T02:13:55+00:00" + "time": "2025-08-20T20:41:45+00:00" } ], "aliases": [], diff --git a/config/cache.php b/config/cache.php index 925f7d2e..c2d927d7 100644 --- a/config/cache.php +++ b/config/cache.php @@ -103,6 +103,6 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), ]; diff --git a/config/database.php b/config/database.php index 125949ed..5fd0838f 100644 --- a/config/database.php +++ b/config/database.php @@ -147,7 +147,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), ], 'default' => [ diff --git a/config/horizon.php b/config/horizon.php index 9f439c7e..90ae9de1 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -56,7 +56,7 @@ 'prefix' => env( 'HORIZON_PREFIX', - Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + Str::slug((string) env('APP_NAME', 'laravel')).'-horizon-' ), /* diff --git a/config/session.php b/config/session.php index ba0aa60b..3fb57d54 100644 --- a/config/session.php +++ b/config/session.php @@ -129,7 +129,7 @@ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_').'_session' + Str::slug((string) env('APP_NAME', 'laravel')).'-session' ), /* diff --git a/package-lock.json b/package-lock.json index 189e986f..ba88e54a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1028,9 +1028,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", "dev": true, "funding": [ { @@ -1049,7 +1049,7 @@ "license": "MIT", "dependencies": { "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", + "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -1065,25 +1065,34 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { - "version": "2.9.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz", - "integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==", + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/binary-extensions": { @@ -1104,6 +1113,15 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1185,9 +1203,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", "dev": true, "funding": [ { @@ -1642,9 +1660,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "dev": true, "license": "ISC" }, @@ -1796,9 +1814,9 @@ } }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -1961,6 +1979,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -2399,12 +2418,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", - "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -2413,32 +2432,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimatch/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2929,9 +2927,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -3115,9 +3113,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -3463,9 +3461,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3608,12 +3606,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" diff --git a/routes/api.php b/routes/api.php index ebc5cdd3..664809ab 100644 --- a/routes/api.php +++ b/routes/api.php @@ -17,6 +17,7 @@ Route::get('/store-apps', [AuthenticatedApiController::class, 'getStoreApps'])->name('api.store-apps'); Route::get('/instances', [AuthenticatedApiController::class, 'getInstances'])->name('api.instances.get'); Route::get('/instance/{uuid}/status', [AuthenticatedApiController::class, 'getInstanceStatus'])->name('api.instance.status'); + Route::get('/enums', [AuthenticatedApiController::class, 'getEnums'])->name('api.enums'); }); // Routes consumed by MoaD - write operations diff --git a/tests/Feature/Api/AuthenticatedApiTest.php b/tests/Feature/Api/AuthenticatedApiTest.php index 10bc8669..01f01a65 100644 --- a/tests/Feature/Api/AuthenticatedApiTest.php +++ b/tests/Feature/Api/AuthenticatedApiTest.php @@ -67,6 +67,31 @@ protected function setUp(): void // The storeApp needs valid app_class that exists, or we might hit PolydockEngineAppNotFoundException. Let's ensure a mock or valid class exists. } + public function test_get_enums_returns_all_enum_options(): void + { + Sanctum::actingAs($this->user, ['instances.read']); + + $response = $this->getJson('/api/enums'); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + // 'PolydockAppInstanceStatus', + // 'PolydockStoreAppStatus', + // 'PolydockStoreStatus', + 'PolydockStoreWebhookCallStatus', + 'PolydockVariableScope', + 'UserGroupRole', + 'UserRemoteRegistrationStatus', + 'UserRemoteRegistrationType', + ], + ]); + + // $this->assertIsArray($response->json('data.PolydockAppInstanceStatus')); + // $this->assertArrayHasKey('new', $response->json('data.PolydockAppInstanceStatus')); + // $this->assertEquals('New', $response->json('data.PolydockAppInstanceStatus.new')); + } + public function test_unauthenticated_requests_are_rejected(): void { $response = $this->getJson('/api/store-apps'); @@ -155,6 +180,8 @@ public function test_get_instance_status(): void 'polydock_store_app_id' => $this->storeApp->id, ]); + $instance->storeKeyValue('lagoon-project-name', 'test-lagoon-name'); + $instance->storeKeyValue('lagoon-claim-script', '/app/.lagoon/scripts/polydock_claim.sh'); $instance->setStatus(PolydockAppInstanceStatus::PRE_CREATE_RUNNING, 'Creating...'); $instance->save(); @@ -163,6 +190,8 @@ public function test_get_instance_status(): void $response->assertOk(); $this->assertEquals($instance->status->value, $response->json('data.status')); $this->assertEquals('Creating...', $response->json('data.status_message')); + $this->assertEquals('/app/.lagoon/scripts/polydock_claim.sh', $response->json('data.lagoon_claim_script')); + $this->assertEquals('test-lagoon-name', $response->json('data.lagoon_project_name')); } public function test_delete_instance_sets_pre_remove_status(): void @@ -184,4 +213,88 @@ public function test_delete_instance_sets_pre_remove_status(): void $instance->refresh(); $this->assertEquals(PolydockAppInstanceStatus::PENDING_PRE_REMOVE, $instance->status); } + + public function test_create_instance_with_custom_name_and_secrets(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $customName = 'My Custom Instance'; + $secret = [ + 'llm_key' => 'secret-api-key', + 'llm_url' => 'https://llm.local', + ]; + + $response = $this->postJson('/api/instance', [ + 'email' => $this->user->email, + 'storeAppId' => $this->storeApp->uuid, + 'name' => $customName, + 'config' => [ + 'secret' => $secret, + ], + ]); + + $response->assertCreated(); + $this->assertEquals($customName, $response->json('data.name')); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($customName, $instance->name); + $this->assertEquals($secret, $instance->getKeyValue('secret')); + } + + public function test_create_instance_ensures_unique_name(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $duplicateName = 'duplicate-name'; + + // Create first instance + PolydockAppInstance::create([ + 'polydock_store_app_id' => $this->storeApp->id, + 'name' => $duplicateName, + ]); + + // Create second instance with same name + $response = $this->postJson('/api/instance', [ + 'email' => $this->user->email, + 'storeAppId' => $this->storeApp->uuid, + 'name' => $duplicateName, + ]); + + $response->assertCreated(); + $newName = $response->json('data.name'); + + $this->assertNotEquals($duplicateName, $newName); + $this->assertStringContainsString($duplicateName, $newName); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($newName, $instance->name); + $this->assertEquals($newName, $instance->getKeyValue('lagoon-project-name')); + } + + public function test_create_instance_with_nested_secrets(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $secret = [ + 'ai' => [ + 'llm_url' => 'https://ai.example.com', + 'api_key' => 'ai-secret-123', + ], + 'vector' => [ + 'db_host' => 'vectordb.example.com', + 'db_port' => 5432, + ], + ]; + + $response = $this->postJson('/api/instance', [ + 'email' => $this->user->email, + 'storeAppId' => $this->storeApp->uuid, + 'secret' => $secret, + ]); + + $response->assertCreated(); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($secret, $instance->getKeyValue('secret')); + } } diff --git a/tests/Feature/Api/RegionsApiTest.php b/tests/Feature/Api/RegionsApiTest.php index d6f14d45..2ee70633 100644 --- a/tests/Feature/Api/RegionsApiTest.php +++ b/tests/Feature/Api/RegionsApiTest.php @@ -46,7 +46,6 @@ public function test_can_get_public_regions_with_available_apps() 'data' => [ 'regions' => [ '*' => [ - 'uuid', 'id', 'label', 'apps' => [ diff --git a/tests/Feature/Filament/CreatePolydockAppInstanceTest.php b/tests/Feature/Filament/CreatePolydockAppInstanceTest.php index be4ae70b..7a2d8843 100644 --- a/tests/Feature/Filament/CreatePolydockAppInstanceTest.php +++ b/tests/Feature/Filament/CreatePolydockAppInstanceTest.php @@ -72,8 +72,9 @@ public function test_admin_can_create_app_instance(): void 'email' => 'newuser@example.com', ]); - // Verify job was dispatched + // Verify job was dispatched exactly once Queue::assertPushed(ProcessUserRemoteRegistration::class); + Queue::assertPushedTimes(ProcessUserRemoteRegistration::class, 1); } public function test_create_form_validates_required_fields(): void diff --git a/tests/Unit/Traits/HasWebhookSensitiveDataTest.php b/tests/Unit/Traits/HasWebhookSensitiveDataTest.php new file mode 100644 index 00000000..46008f43 --- /dev/null +++ b/tests/Unit/Traits/HasWebhookSensitiveDataTest.php @@ -0,0 +1,71 @@ +traitObject = new class + { + use HasWebhookSensitiveData; + + public $sensitiveDataKeys; + }; + } + + public function test_get_sensitive_data_keys_returns_defaults() + { + $keys = $this->traitObject->getSensitiveDataKeys(); + + $this->assertIsArray($keys); + $this->assertContains('private_key', $keys); + $this->assertContains('secret', $keys); + } + + public function test_register_sensitive_data_keys_merges_new_keys() + { + $this->traitObject->registerSensitiveDataKeys('new_key'); + $keys = $this->traitObject->getSensitiveDataKeys(); + + $this->assertContains('new_key', $keys); + $this->assertContains('private_key', $keys); + } + + public function test_register_sensitive_data_keys_with_array() + { + $this->traitObject->registerSensitiveDataKeys(['key1', 'key2']); + $keys = $this->traitObject->getSensitiveDataKeys(); + + $this->assertContains('key1', $keys); + $this->assertContains('key2', $keys); + $this->assertContains('private_key', $keys); + } + + public function test_should_filter_key_exact_match() + { + $sensitiveKeys = ['password']; + $this->assertTrue($this->traitObject->shouldFilterKey('password', $sensitiveKeys)); + $this->assertTrue($this->traitObject->shouldFilterKey('PASSWORD', $sensitiveKeys)); + $this->assertFalse($this->traitObject->shouldFilterKey('username', $sensitiveKeys)); + } + + public function test_should_filter_key_regex_match() + { + $sensitiveKeys = ['/^.*_key.*$/']; + $this->assertTrue($this->traitObject->shouldFilterKey('api_key', $sensitiveKeys)); + $this->assertTrue($this->traitObject->shouldFilterKey('some_key_here', $sensitiveKeys)); + $this->assertFalse($this->traitObject->shouldFilterKey('token', $sensitiveKeys)); + } + + public function test_should_filter_key_case_insensitive_regex() + { + $sensitiveKeys = ['/^.*_key.*$/']; + $this->assertTrue($this->traitObject->shouldFilterKey('AMAZEEAI_API_KEY', $sensitiveKeys)); + } +}