Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
github-actions[bot]
2026-10-30 11:16:06 +00:00
15 changed files with 307 additions and 78 deletions
@@ -154,12 +154,7 @@ class ApplicationsController extends Controller
$application->settings?->makeHidden(['id', 'application_id', 'created_at', 'updated_at']);
}
$payload = serializeApiResponse($application);
if ($application instanceof Application && filled(data_get($payload, 'docker_compose'))) {
$payload['docker_compose'] = redactLockedValuesInGeneratedCompose($payload['docker_compose'], $application);
}
return $payload;
return redactLockedApiResourceValues($application, serializeApiResponse($application));
}
private function applicationSettingsFromRequest(Request $request): array
@@ -5087,20 +5082,25 @@ class ApplicationsController extends Controller
], 422);
}
// Validate ports_mappings
if ($request->has('ports_mappings')) {
$ports = [];
// Validate that each host binding (IP, port, protocol) of ports_mappings is unique
if (is_string($request->ports_mappings)) {
$hostBindings = [];
foreach (explode(',', $request->ports_mappings) as $portMapping) {
$port = explode(':', $portMapping);
if (in_array($port[0], $ports)) {
// The container port is after the last ":", an empty host port makes Docker pick a free port
$hostBinding = preg_replace('#/(tcp|udp|sctp)#', '', substr($portMapping, 0, (int) strrpos($portMapping, ':')));
if ($hostBinding === '' || str_ends_with($hostBinding, ':')) {
continue;
}
$protocol = preg_match('#/(udp|sctp)#', $portMapping, $matches) === 1 ? $matches[1] : 'tcp';
if (in_array("{$hostBinding}/{$protocol}", $hostBindings, true)) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
'ports_mappings' => 'The first number before : should be unique between mappings.',
'ports_mappings' => 'Each host IP, port, and protocol can only be used once.',
],
], 422);
}
$ports[] = $port[0];
$hostBindings[] = "{$hostBinding}/{$protocol}";
}
}
// Validate custom_labels
@@ -258,19 +258,7 @@ class DatabasesController extends Controller
$this->hideNestedServerSecrets($database, $loadNestedServerSecrets);
}
$payload = serializeApiResponse($database);
// A locked password variable is never shown again, also not inside the connection URLs.
if (request()->attributes->get('can_read_sensitive', false) === true
&& method_exists($database, 'isCredentialLocked') && $database->isCredentialLocked()) {
foreach (['internal_db_url', 'external_db_url'] as $key) {
if ($payload->has($key)) {
$payload->put($key, null);
}
}
}
return $payload;
return redactLockedApiResourceValues($database, serializeApiResponse($database));
}
private function hideNestedServerSecrets(Model $model, bool $loadRelations = false): void
+10 -6
View File
@@ -7,6 +7,7 @@ use App\Models\Project;
use App\Support\ValidationPatterns;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use OpenApi\Attributes as OA;
@@ -168,13 +169,16 @@ class ProjectController extends Controller
return response()->json(['message' => 'Environment not found.'], 404);
}
// Redis connection URLs read REDIS_PASSWORD and REDIS_USERNAME; load the variables once, not per database.
$environment = $environment->load(['applications', 'postgresqls', 'redis.runtime_environment_variables', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
collect(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services'])
->flatMap(fn (string $relation) => $environment->{$relation})
->each(fn ($resource) => exposeSensitiveFields($resource));
$environment->redis->each(fn ($redis) => $redis->makeHidden('runtime_environment_variables'));
$relations = ['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services'];
$environment = $environment->load([...$relations, 'redis.runtime_environment_variables']);
$payload = $environment->withoutRelations()->toArray();
foreach ($relations as $relation) {
$payload[$relation] = $environment->{$relation}
->map(fn ($resource) => Arr::except(serializeApiResource($resource), 'runtime_environment_variables'))
->all();
}
return response()->json(serializeApiResponse($environment));
return response()->json(serializeApiResponse($payload));
}
#[OA\Post(
@@ -69,8 +69,7 @@ class ResourcesController extends Controller
}
$resources = $resources->flatten();
$resources = $resources->map(function ($resource) {
exposeSensitiveFields($resource);
$payload = $resource->toArray();
$payload = serializeApiResource($resource);
$payload['status'] = $resource->status;
$payload['type'] = $resource->type();
@@ -78,12 +78,7 @@ class ServicesController extends Controller
$service->makeHidden(['value', 'real_value']);
}
$payload = serializeApiResponse($service);
if ($service instanceof Service && filled(data_get($payload, 'docker_compose'))) {
$payload['docker_compose'] = redactLockedValuesInGeneratedCompose($payload['docker_compose'], $service);
}
return $payload;
return redactLockedApiResourceValues($service, serializeApiResponse($service));
}
/**