Merge branch 'next' into improve-resource-route-handling

This commit is contained in:
Andras Bacsai committed 2026-07-02 13:25:47 +02:00
commit a121386ab4
570 files changed
+18896 -5156

No files matched your search

+5 -4
View File
@@ -5,6 +5,7 @@ namespace App\Actions\Stripe;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Support\Collection;
use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class CancelSubscription
@@ -21,7 +22,7 @@ class CancelSubscription
$this->isDryRun = $isDryRun;
if (! $isDryRun && isCloud()) {
$this->stripe = new StripeClient(config('subscription.stripe_api_key'));
$this->stripe = app(StripeClient::class);
}
}
@@ -64,7 +65,7 @@ class CancelSubscription
];
}
$stripe = new StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$subscriptions = $this->getSubscriptionsPreview();
$verified = collect();
@@ -88,7 +89,7 @@ class CancelSubscription
'reason' => "Status in Stripe: {$stripeSubscription->status}",
]);
}
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
// Subscription doesn't exist in Stripe
$notFound->push([
'subscription' => $subscription,
@@ -181,7 +182,7 @@ class CancelSubscription
return false;
}
$stripe = new StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$stripe->subscriptions->cancel($subscriptionId, []);
// Update local record if exists
@@ -3,6 +3,7 @@
namespace App\Actions\Stripe;
use App\Models\Team;
use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class CancelSubscriptionAtPeriodEnd
@@ -11,7 +12,7 @@ class CancelSubscriptionAtPeriodEnd
public function __construct(?StripeClient $stripe = null)
{
$this->stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
$this->stripe = $stripe ?? app(StripeClient::class);
}
/**
@@ -47,7 +48,7 @@ class CancelSubscriptionAtPeriodEnd
\Log::info("Subscription {$subscription->stripe_subscription_id} set to cancel at period end for team {$team->name}");
return ['success' => true, 'error' => null];
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
\Log::error("Stripe cancel at period end error for team {$team->id}: ".$e->getMessage());
return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+6 -4
View File
@@ -3,6 +3,8 @@
namespace App\Actions\Stripe;
use App\Models\Team;
use Carbon\Carbon;
use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class RefundSubscription
@@ -13,7 +15,7 @@ class RefundSubscription
public function __construct(?StripeClient $stripe = null)
{
$this->stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
$this->stripe = $stripe ?? app(StripeClient::class);
}
/**
@@ -39,7 +41,7 @@ class RefundSubscription
try {
$stripeSubscription = $this->stripe->subscriptions->retrieve($subscription->stripe_subscription_id);
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
return $this->ineligible('Subscription not found in Stripe.');
}
@@ -49,7 +51,7 @@ class RefundSubscription
return $this->ineligible("Subscription status is '{$stripeSubscription->status}'.", $currentPeriodEnd);
}
$startDate = \Carbon\Carbon::createFromTimestamp($stripeSubscription->start_date);
$startDate = Carbon::createFromTimestamp($stripeSubscription->start_date);
$daysSinceStart = (int) $startDate->diffInDays(now());
$daysRemaining = self::REFUND_WINDOW_DAYS - $daysSinceStart;
@@ -130,7 +132,7 @@ class RefundSubscription
\Log::info("Refunded and cancelled subscription {$subscription->stripe_subscription_id} for team {$team->name}");
return ['success' => true, 'error' => null];
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
\Log::error("Stripe refund error for team {$team->id}: ".$e->getMessage());
return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
+3 -2
View File
@@ -3,6 +3,7 @@
namespace App\Actions\Stripe;
use App\Models\Team;
use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class ResumeSubscription
@@ -11,7 +12,7 @@ class ResumeSubscription
public function __construct(?StripeClient $stripe = null)
{
$this->stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
$this->stripe = $stripe ?? app(StripeClient::class);
}
/**
@@ -43,7 +44,7 @@ class ResumeSubscription
\Log::info("Subscription {$subscription->stripe_subscription_id} resumed for team {$team->name}");
return ['success' => true, 'error' => null];
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
\Log::error("Stripe resume subscription error for team {$team->id}: ".$e->getMessage());
return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
@@ -17,7 +17,7 @@ class UpdateSubscriptionQuantity
public function __construct(?StripeClient $stripe = null)
{
$this->stripe = $stripe ?? new StripeClient(config('subscription.stripe_api_key'));
$this->stripe = $stripe ?? app(StripeClient::class);
}
/**
@@ -4,6 +4,8 @@ namespace App\Console\Commands\Cloud;
use App\Models\Team;
use Illuminate\Console\Command;
use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class CloudFixSubscription extends Command
{
@@ -31,7 +33,7 @@ class CloudFixSubscription extends Command
*/
public function handle()
{
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
if ($this->option('verify-all')) {
return $this->verifyAllActiveSubscriptions($stripe);
@@ -111,7 +113,7 @@ class CloudFixSubscription extends Command
/**
* Fix canceled subscriptions in the database
*/
private function fixCanceledSubscriptions(\Stripe\StripeClient $stripe)
private function fixCanceledSubscriptions(StripeClient $stripe)
{
$isDryRun = $this->option('dry-run');
$checkOne = $this->option('one');
@@ -220,7 +222,7 @@ class CloudFixSubscription extends Command
break;
}
}
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
if ($e->getStripeCode() === 'resource_missing') {
$toFixCount++;
@@ -326,7 +328,7 @@ class CloudFixSubscription extends Command
/**
* Verify all active subscriptions against Stripe API
*/
private function verifyAllActiveSubscriptions(\Stripe\StripeClient $stripe)
private function verifyAllActiveSubscriptions(StripeClient $stripe)
{
$isDryRun = $this->option('dry-run');
$shouldFix = $this->option('fix-verified');
@@ -570,7 +572,7 @@ class CloudFixSubscription extends Command
break;
}
} catch (\Stripe\Exception\InvalidRequestException $e) {
} catch (InvalidRequestException $e) {
$this->error(' → Error: '.$e->getMessage());
if ($e->getStripeCode() === 'resource_missing' || $e->getHttpStatus() === 404) {
@@ -730,7 +732,7 @@ class CloudFixSubscription extends Command
/**
* Search for subscriptions by customer ID
*/
private function searchSubscriptionsByCustomer(\Stripe\StripeClient $stripe, $customerId, $requireActive = false)
private function searchSubscriptionsByCustomer(StripeClient $stripe, $customerId, $requireActive = false)
{
try {
$subscriptions = $stripe->subscriptions->all([
@@ -770,7 +772,7 @@ class CloudFixSubscription extends Command
/**
* Search for subscriptions by team member emails
*/
private function searchSubscriptionsByEmails(\Stripe\StripeClient $stripe, $emails)
private function searchSubscriptionsByEmails(StripeClient $stripe, $emails)
{
$this->line(' → Searching by team member emails...');
+1
View File
@@ -18,6 +18,7 @@ use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Str;
use Mail;
use function Laravel\Prompts\confirm;
@@ -4,6 +4,7 @@ namespace App\Console\Commands\Generate;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Process;
use Symfony\Component\Yaml\Yaml;
class Services extends Command
@@ -77,6 +78,7 @@ class Services extends Command
'category' => $data->get('category'),
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
];
if ($port = $data->get('port')) {
@@ -99,6 +101,26 @@ class Services extends Command
return $payload;
}
private function templateLastUpdatedAt(string $file): ?string
{
$process = Process::path(base_path())->run([
'git',
'log',
'-1',
'--format=%cI',
'--',
"templates/compose/{$file}",
]);
if ($process->failed()) {
return null;
}
$timestamp = trim($process->output());
return $timestamp === '' ? null : $timestamp;
}
private function generateServiceTemplatesWithFqdn(): void
{
$serviceTemplatesWithFqdn = collect(array_merge(
@@ -155,6 +177,7 @@ class Services extends Command
'category' => $data->get('category'),
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
];
if ($port = $data->get('port')) {
@@ -232,6 +255,7 @@ class Services extends Command
'category' => $data->get('category'),
'logo' => $data->get('logo', 'svgs/default.webp'),
'minversion' => $data->get('minversion', '0.0.0'),
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
];
if ($port = $data->get('port')) {
@@ -30,7 +30,6 @@ use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class ApplicationsController extends Controller
{
@@ -59,6 +58,10 @@ class ApplicationsController extends Controller
]);
}
if ($application->is_shown_once ?? false) {
$application->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($application);
}
@@ -1193,7 +1196,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1432,7 +1435,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1641,7 +1644,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1687,7 +1690,7 @@ class ApplicationsController extends Controller
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
$request->offsetSet('name', 'dockerfile-'.new_public_id());
}
$return = $this->validateDataApplications($request, $server);
@@ -1761,7 +1764,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1805,7 +1808,7 @@ class ApplicationsController extends Controller
], 422);
}
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
$request->offsetSet('name', 'docker-image-'.new_public_id());
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof JsonResponse) {
@@ -1880,7 +1883,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -2678,7 +2681,7 @@ class ApplicationsController extends Controller
]);
if ($instantDeploy) {
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -2873,8 +2876,12 @@ class ApplicationsController extends Controller
$this->authorize('manageEnvironment', $application);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -3097,12 +3104,18 @@ class ApplicationsController extends Controller
], 400);
}
$bulk_data = collect($bulk_data)->map(function ($item) {
return collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment']);
$item = collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime', 'comment']);
if ($item->has('key')) {
$item->put('key', ValidationPatterns::normalizeEnvironmentVariableKey((string) $item->get('key')));
}
return $item;
});
$returnedEnvs = collect();
foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -3299,8 +3312,12 @@ class ApplicationsController extends Controller
$this->authorize('manageEnvironment', $application);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_preview' => 'boolean',
'is_literal' => 'boolean',
@@ -3585,7 +3602,7 @@ class ApplicationsController extends Controller
$this->authorize('deploy', $application);
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -3783,7 +3800,7 @@ class ApplicationsController extends Controller
$this->authorize('deploy', $application);
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -177,6 +177,7 @@ class CloudProviderTokensController extends Controller
if (is_null($token)) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
return response()->json($this->removeSensitiveData($token));
}
@@ -243,6 +244,7 @@ class CloudProviderTokensController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [CloudProviderToken::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -394,6 +396,7 @@ class CloudProviderTokensController extends Controller
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
$this->authorize('update', $token);
$token->update(array_intersect_key($body, array_flip($allowedFields)));
@@ -475,6 +478,7 @@ class CloudProviderTokensController extends Controller
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
$this->authorize('delete', $token);
if ($token->hasServers()) {
return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400);
@@ -545,9 +549,18 @@ class CloudProviderTokensController extends Controller
if (! $cloudToken) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
$this->authorize('view', $cloudToken);
$validation = $this->validateProviderToken($cloudToken->provider, $cloudToken->token);
auditLog('api.cloud_token.validated', [
'team_id' => $teamId,
'cloud_token_uuid' => $cloudToken->uuid,
'cloud_token_name' => $cloudToken->name,
'provider' => $cloudToken->provider,
'valid' => $validation['valid'],
]);
return response()->json([
'valid' => $validation['valid'],
'message' => $validation['valid'] ? 'Token is valid.' : $validation['error'],
@@ -3133,8 +3133,12 @@ class DatabasesController extends Controller
$this->authorize('manageEnvironment', $database);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -3281,8 +3285,12 @@ class DatabasesController extends Controller
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
if (array_key_exists('key', $item)) {
$item['key'] = ValidationPatterns::normalizeEnvironmentVariableKey((string) $item['key']);
}
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -3399,8 +3407,12 @@ class DatabasesController extends Controller
$this->authorize('manageEnvironment', $database);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -15,7 +15,6 @@ use App\Models\Tag;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
class DeployController extends Controller
{
@@ -511,7 +510,7 @@ class DeployController extends Controller
if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') {
return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null];
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
@@ -183,6 +183,7 @@ class GithubController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [GithubApp::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
@@ -564,6 +565,7 @@ class GithubController extends Controller
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
$this->authorize('update', $githubApp);
// Define allowed fields for update
$allowedFields = [
@@ -737,6 +739,7 @@ class GithubController extends Controller
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
$this->authorize('delete', $githubApp);
// Check if the GitHub app is being used by any applications
if ($githubApp->applications->isNotEmpty()) {
@@ -116,6 +116,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -237,6 +238,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -336,6 +338,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -445,6 +448,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -550,6 +554,7 @@ class HetznerController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [Server::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -620,6 +625,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
$this->authorize('view', $token);
// Validate private key
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
@@ -97,6 +97,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$this->authorize('view', $project);
$project->load(['environments']);
@@ -233,6 +234,7 @@ class ProjectController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [Project::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -385,6 +387,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$this->authorize('update', $project);
$project->update($request->only($allowedFields));
@@ -469,6 +472,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$this->authorize('delete', $project);
if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
@@ -652,6 +656,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$this->authorize('update', $project);
$existingEnvironment = $project->environments()->where('name', $request->name)->first();
if ($existingEnvironment) {
@@ -746,6 +751,7 @@ class ProjectController extends Controller
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$this->authorize('delete', $environment);
if (! $environment->isEmpty()) {
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
@@ -110,6 +110,7 @@ class SecurityController extends Controller
'message' => 'Private Key not found.',
], 404);
}
$this->authorize('view', $key);
return response()->json($this->removeSensitiveData($key));
}
@@ -176,6 +177,7 @@ class SecurityController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [PrivateKey::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
@@ -338,6 +340,7 @@ class SecurityController extends Controller
'message' => 'Private Key not found.',
], 404);
}
$this->authorize('update', $foundKey);
$foundKey->update($request->only($allowedFields));
auditLog('api.private_key.updated', [
@@ -421,6 +424,7 @@ class SecurityController extends Controller
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
$this->authorize('delete', $key);
if ($key->isInUse()) {
return response()->json([
@@ -97,12 +97,12 @@ class SentinelController extends Controller
if ($this->shouldDispatchUpdate($server, $data)) {
PushServerUpdateJob::dispatch($server, $data);
}
auditLog('sentinel.metrics_pushed', [
'server_uuid' => $server->uuid,
'team_id' => $server->team_id,
]);
auditLog('sentinel.metrics_pushed', [
'server_uuid' => $server->uuid,
'team_id' => $server->team_id,
]);
}
return response()->json(['message' => 'ok'], 200);
}
@@ -148,6 +148,7 @@ class ServersController extends Controller
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('view', $server);
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
@@ -477,6 +478,7 @@ class ServersController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
$this->authorize('create', [ModelsServer::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -701,6 +703,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('update', $server);
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
@@ -825,6 +828,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('delete', $server);
$force = filter_var($request->query('force', false), FILTER_VALIDATE_BOOLEAN);
@@ -924,6 +928,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('update', $server);
ValidateServer::dispatch($server);
auditLog('api.server.validated', [
@@ -39,6 +39,10 @@ class ServicesController extends Controller
]);
}
if ($service->is_shown_once ?? false) {
$service->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($service);
}
@@ -1247,8 +1251,12 @@ class ServicesController extends Controller
$this->authorize('manageEnvironment', $service);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -1396,8 +1404,12 @@ class ServicesController extends Controller
$updatedEnvs = collect();
foreach ($bulk_data as $item) {
if (array_key_exists('key', $item)) {
$item['key'] = ValidationPatterns::normalizeEnvironmentVariableKey((string) $item['key']);
}
$validator = customApiValidator($item, [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -1515,8 +1527,12 @@ class ServicesController extends Controller
$this->authorize('manageEnvironment', $service);
if ($request->has('key')) {
$request->merge(['key' => ValidationPatterns::normalizeEnvironmentVariableKey((string) $request->key)]);
}
$validator = customApiValidator($request->all(), [
'key' => 'string|required',
'key' => ValidationPatterns::environmentVariableKeyRules(),
'value' => 'string|nullable',
'is_literal' => 'boolean',
'is_multiline' => 'boolean',
@@ -110,6 +110,7 @@ class TeamController extends Controller
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$this->authorize('view', $team);
$team = $this->removeSensitiveData($team);
return response()->json(
@@ -168,6 +169,7 @@ class TeamController extends Controller
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$this->authorize('view', $team);
$members = $team->members;
$members->makeHidden([
'pivot',
+9 -40
View File
@@ -2,6 +2,8 @@
namespace App\Http\Controllers;
use App\Support\DatabaseBackupFileValidator;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Controller as BaseController;
@@ -11,26 +13,11 @@ use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
class UploadController extends BaseController
{
use AuthorizesRequests;
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
private const ALLOWED_EXTENSIONS = [
'sql',
'sql.gz',
'gz',
'zip',
'tar',
'tar.gz',
'tgz',
'dump',
'bak',
'bson',
'bson.gz',
'archive',
'archive.gz',
'bz2',
'xz',
'dmp',
];
private const ALLOWED_EXTENSIONS = DatabaseBackupFileValidator::ALLOWED_EXTENSIONS;
public function upload(Request $request)
{
@@ -40,6 +27,8 @@ class UploadController extends BaseController
return response()->json(['error' => 'You do not have permission for this database'], 500);
}
$this->authorize('uploadBackup', $resource);
$chunk = $request->file('file');
$originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null;
if (blank($originalName) || ! self::hasAllowedExtension($originalName)) {
@@ -80,10 +69,7 @@ class UploadController extends BaseController
protected function saveFile(UploadedFile $file, string $resourceIdentifier)
{
$originalName = $file->getClientOriginalName();
$size = $file->getSize();
if (! self::hasAllowedExtension($originalName) || $size === false || $size > self::MAX_BYTES) {
if (! DatabaseBackupFileValidator::isUploadAllowed($file, self::MAX_BYTES)) {
@unlink($file->getPathname());
return response()->json([
@@ -103,24 +89,7 @@ class UploadController extends BaseController
private static function hasAllowedExtension(string $name): bool
{
$lower = strtolower($name);
$suffixes = array_map(fn ($ext) => '.'.$ext, self::ALLOWED_EXTENSIONS);
usort($suffixes, fn ($a, $b) => strlen($b) <=> strlen($a));
foreach ($suffixes as $suffix) {
if (! str_ends_with($lower, $suffix)) {
continue;
}
$stem = substr($lower, 0, -strlen($suffix));
if ($stem !== '' && ! str_ends_with($stem, '.')) {
return true;
}
return false;
}
return false;
return DatabaseBackupFileValidator::hasAllowedExtension($name);
}
private static function formatMaxSize(): string
+2 -3
View File
@@ -10,7 +10,6 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Bitbucket extends Controller
{
@@ -141,7 +140,7 @@ class Bitbucket extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -192,7 +191,7 @@ class Bitbucket extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
+2 -3
View File
@@ -11,7 +11,6 @@ use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Gitea extends Controller
{
@@ -127,7 +126,7 @@ class Gitea extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -194,7 +193,7 @@ class Gitea extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
+12 -3
View File
@@ -17,7 +17,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Github extends Controller
{
@@ -144,7 +143,7 @@ class Github extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -262,6 +261,16 @@ class Github extends Controller
return response('Nothing to do. No GitHub App found.');
}
$webhook_secret = data_get($github_app, 'webhook_secret');
if (empty($webhook_secret)) {
auditLogWebhookFailure('github', 'webhook_secret_missing', [
'mode' => 'app',
'github_app_id' => $github_app->id,
'github_app_name' => $github_app->name,
'installation_target_id' => $x_github_hook_installation_target_id,
]);
return response('Invalid signature.');
}
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
if (config('app.env') !== 'local') {
if (! hash_equals($x_hub_signature_256, $hmac)) {
@@ -362,7 +371,7 @@ class Github extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
+2 -3
View File
@@ -11,7 +11,6 @@ use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class Gitlab extends Controller
{
@@ -168,7 +167,7 @@ class Gitlab extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -236,7 +235,7 @@ class Gitlab extends Controller
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
+25
View File
@@ -7,9 +7,34 @@ use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
class ApiAbility extends CheckForAnyAbility
{
/**
* Permissions that only admins/owners may use.
*/
private const MEMBER_DISALLOWED_ABILITIES = [
'root',
'write',
'write:sensitive',
'deploy',
'read:sensitive',
];
public function handle($request, $next, ...$abilities)
{
try {
$token = $request->user()->currentAccessToken();
$teamId = data_get($token, 'team_id');
if ($teamId !== null && ! $request->user()->isAdminOfTeam((int) $teamId)) {
$tokenAbilities = $token->abilities ?? [];
$disallowed = array_intersect($tokenAbilities, self::MEMBER_DISALLOWED_ABILITIES);
if (! empty($disallowed)) {
return response()->json([
'message' => 'This API token has permissions ('.implode(', ', $disallowed).') that exceed your current role as a team member. Members are restricted to read-only API access. Please revoke this token and create a new one with only read permissions.',
], 403);
}
}
if ($request->user()->tokenCan('root')) {
return $next($request);
}
+5 -2
View File
@@ -10,10 +10,13 @@ class ApiSensitiveData
public function handle(Request $request, Closure $next)
{
$token = $request->user()->currentAccessToken();
$hasTokenPermission = $token->can('root') || $token->can('read:sensitive');
$teamId = (int) data_get($token, 'team_id');
$isAdmin = $teamId ? $request->user()->isAdminOfTeam($teamId) : false;
// Allow access to sensitive data if token has root or read:sensitive permission
// Allow access to sensitive data only if token has permission AND user is admin/owner
$request->attributes->add([
'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
'can_read_sensitive' => $hasTokenPermission && $isAdmin,
]);
return $next($request);
+5 -6
View File
@@ -12,15 +12,14 @@ class CanCreateResources
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request);
// if (! Gate::allows('createAnyResource')) {
// abort(403, 'You do not have permission to create resources.');
// }
if (! Gate::allows('createAnyResource')) {
abort(403, 'You do not have permission to create resources.');
}
// return $next($request);
return $next($request);
}
}
+55 -13
View File
@@ -37,7 +37,6 @@ use JsonException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
use Visus\Cuid2\Cuid2;
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -1834,8 +1833,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]
);
}
} elseif ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerfile') {
// For Docker Compose and Dockerfile, create an empty .env file even if there are no build-time variables
} elseif (in_array($this->build_pack, ['dockercompose', 'dockerfile', 'railpack'], true)) {
// For build packs that source the build-time .env file, create an empty file even if there are no build-time variables
// This ensures the file exists when referenced in build commands
$this->application_deployment_queue->addLogEntry('Creating empty build-time .env file in /artifacts (no build-time variables defined).', hidden: true);
@@ -2207,7 +2206,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
@@ -2307,6 +2306,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
[
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
'hidden' => true,
'skip_command_log' => true,
],
[
executeInDocker($this->deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
@@ -2366,12 +2367,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
$this->execute_remote_command(
[
$importCommands,
'hidden' => true,
]
);
$this->execute_remote_command(...$this->gitCommandDefinitions($importCommands));
$this->create_workdir();
$this->execute_remote_command(
[
@@ -2401,6 +2397,39 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return $commands;
}
private function gitCommandDefinitions(Collection|array|string $commands): array
{
if (is_string($commands)) {
return [
[
$commands,
'hidden' => true,
],
];
}
return collect($commands)
->map(function ($command): array {
if (is_string($command)) {
return [
$command,
'hidden' => true,
];
}
if (is_array($command)) {
return $command + ['hidden' => true];
}
return [
'command' => $command,
'hidden' => true,
];
})
->values()
->all();
}
private function cleanup_git()
{
$this->execute_remote_command(
@@ -2668,12 +2697,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$cacheArgs .= ' --build-arg secrets-hash='.$this->generate_secrets_hash($variables);
}
$environmentPrefix = $this->railpack_build_environment_prefix($variables);
// Build-time variables reach the build through the sourced build-time .env file
// (written by save_buildtime_environment_variables), which interpolates shell-style
// references such as BETTER_AUTH_URL=$COOLIFY_URL. Passing them inline via `env`
// would forward the literal `$COOLIFY_URL` because each value is single-quoted and
// `env` does not interpolate its own assignments. Only buildpack control variables
// (NIXPACKS_/RAILPACK_) — which are excluded from the build-time .env file and never
// need interpolation — are still passed inline.
$controlVariables = $variables->filter(
fn ($value, $key) => str($key)->startsWith(EnvironmentVariable::BUILDPACK_CONTROL_VARIABLE_PREFIXES)
);
$environmentPrefix = $this->railpack_build_environment_prefix($controlVariables);
$secretFlags = $this->railpack_build_secret_flags($variables);
$frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version');
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
." && {$environmentPrefix}docker buildx build --builder coolify-railpack"
$buildxBuildCommand = "{$environmentPrefix}docker buildx build --builder coolify-railpack"
." {$this->addHosts} --network host"
." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\""
." {$cacheArgs}"
@@ -2683,6 +2722,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
.' --load'
." -t {$imageName}"
." {$this->workdir}";
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
.' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand);
}
private function decode_railpack_config(string $config, string $source): array
+1 -2
View File
@@ -27,7 +27,6 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -309,7 +308,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
// Generate unique UUID for each database backup execution
$attempts = 0;
do {
$this->backup_log_uuid = (string) new Cuid2;
$this->backup_log_uuid = new_public_id();
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
$attempts++;
if ($attempts >= 3 && $exists) {
+18 -9
View File
@@ -14,7 +14,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Visus\Cuid2\Cuid2;
use Throwable;
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
{
@@ -71,16 +71,25 @@ class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
->first();
if ($found) {
ApplicationPullRequestUpdateJob::dispatchSync(
application: $application,
preview: $found,
status: ProcessStatus::CLOSED
);
CleanupPreviewDeployment::run($application, $this->pullRequestId, $found);
try {
$this->dispatchPullRequestClosedUpdate($application, $found);
} catch (Throwable $e) {
report($e);
} finally {
CleanupPreviewDeployment::run($application, $this->pullRequestId, $found);
}
}
}
protected function dispatchPullRequestClosedUpdate(Application $application, ApplicationPreview $preview): void
{
ApplicationPullRequestUpdateJob::dispatchSync(
application: $application,
preview: $preview,
status: ProcessStatus::CLOSED
);
}
private function handleOpenAction(Application $application, ?GithubApp $githubApp): void
{
if (! $application->isPRDeployable()) {
@@ -156,7 +165,7 @@ class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
}
// Queue the deployment
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
queue_application_deployment(
application: $application,
pull_request_id: $this->pullRequestId,
+7 -4
View File
@@ -36,7 +36,7 @@ class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
$data = data_get($this->event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
@@ -100,7 +100,7 @@ class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
if ($subscription->stripe_subscription_id) {
try {
$stripe = new StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$stripeSubscription = $stripe->subscriptions->retrieve(
$subscription->stripe_subscription_id
);
@@ -166,7 +166,7 @@ class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
// Verify payment status with Stripe API before sending failure notification
if ($paymentIntentId) {
try {
$stripe = new StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$paymentIntent = $stripe->paymentIntents->retrieve($paymentIntentId);
if (in_array($paymentIntent->status, ['processing', 'succeeded', 'requires_action', 'requires_confirmation'])) {
@@ -260,7 +260,10 @@ class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue
$comment = data_get($data, 'cancellation_details.comment');
$lookup_key = data_get($data, 'items.data.0.price.lookup_key');
if (str($lookup_key)->contains('dynamic')) {
$quantity = min((int) data_get($data, 'items.data.0.quantity', 2), UpdateSubscriptionQuantity::MAX_SERVER_LIMIT);
$quantity = max(
UpdateSubscriptionQuantity::MIN_SERVER_LIMIT,
min((int) data_get($data, 'items.data.0.quantity', 2), UpdateSubscriptionQuantity::MAX_SERVER_LIMIT)
);
$team = data_get($subscription, 'team');
if ($team) {
$team->update([
+2 -1
View File
@@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Stripe\StripeClient;
class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -27,7 +28,7 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
$subscription = $this->team->subscription;
if ($subscription && $subscription->stripe_customer_id) {
try {
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
if ($subscription->stripe_subscription_id) {
$stripeSubscription = $stripe->subscriptions->retrieve($subscription->stripe_subscription_id);
+4 -3
View File
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Stripe\StripeClient;
class SyncStripeSubscriptionsJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -33,7 +34,7 @@ class SyncStripeSubscriptionsJob implements ShouldBeEncrypted, ShouldQueue
->where('stripe_invoice_paid', true)
->get();
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
// Bulk fetch all valid subscription IDs from Stripe (active + past_due)
$validStripeIds = $this->fetchValidStripeSubscriptionIds($stripe, $onProgress);
@@ -123,7 +124,7 @@ class SyncStripeSubscriptionsJob implements ShouldBeEncrypted, ShouldQueue
*
* @return array{email: string, customer_id: string, subscription_id: string, status: string}|null
*/
private function findActiveSubscriptionByEmail(\Stripe\StripeClient $stripe, string $customerId): ?array
private function findActiveSubscriptionByEmail(StripeClient $stripe, string $customerId): ?array
{
try {
$customer = $stripe->customers->retrieve($customerId);
@@ -177,7 +178,7 @@ class SyncStripeSubscriptionsJob implements ShouldBeEncrypted, ShouldQueue
*
* @return array<string>
*/
private function fetchValidStripeSubscriptionIds(\Stripe\StripeClient $stripe, ?\Closure $onProgress = null): array
private function fetchValidStripeSubscriptionIds(StripeClient $stripe, ?\Closure $onProgress = null): array
{
$validIds = [];
$fetched = 0;
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Stripe\StripeClient;
class VerifyStripeSubscriptionStatusJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -29,7 +30,7 @@ class VerifyStripeSubscriptionStatusJob implements ShouldBeEncrypted, ShouldQueu
if (! $this->subscription->stripe_subscription_id &&
$this->subscription->stripe_customer_id) {
try {
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$subscriptions = $stripe->subscriptions->all([
'customer' => $this->subscription->stripe_customer_id,
'limit' => 1,
@@ -50,7 +51,7 @@ class VerifyStripeSubscriptionStatusJob implements ShouldBeEncrypted, ShouldQueu
}
try {
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$stripe = app(StripeClient::class);
$stripeSubscription = $stripe->subscriptions->retrieve(
$this->subscription->stripe_subscription_id
);
+3
View File
@@ -54,6 +54,9 @@ class Index extends Component
public function getSubscribers()
{
if (Auth::id() !== 0 && ! session('impersonating')) {
return redirect()->route('dashboard');
}
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
}
+14 -2
View File
@@ -9,13 +9,15 @@ use App\Models\Server;
use App\Models\Team;
use App\Services\ConfigurationRepository;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\Url;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Index extends Component
{
use AuthorizesRequests;
protected $listeners = [
'refreshBoardingIndex' => 'validateServer',
'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
@@ -174,6 +176,9 @@ class Index extends Component
public function skipBoarding()
{
if (auth()->user()?->isMember()) {
return redirect()->route('dashboard');
}
Team::find(currentTeam()->id)->update([
'show_boarding' => false,
]);
@@ -276,6 +281,7 @@ class Index extends Component
]);
try {
$this->authorize('create', PrivateKey::class);
$privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
@@ -294,6 +300,12 @@ class Index extends Component
{
$this->validate();
try {
$this->authorize('create', Server::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->privateKey = formatPrivateKey($this->privateKey);
$foundServer = Server::whereIp($this->remoteServerHost)->first();
if ($foundServer) {
@@ -457,7 +469,7 @@ class Index extends Component
$this->createdProject = Project::create([
'name' => 'My first project',
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
]);
$this->currentState = 'create-resource';
}
+2 -3
View File
@@ -9,7 +9,6 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Docker extends Component
{
@@ -35,7 +34,7 @@ class Docker extends Component
public function mount(?string $server_id = null): void
{
$this->network = (string) new Cuid2;
$this->network = new_public_id();
$this->servers = Server::isUsable()->get();
if (filled($server_id)) {
@@ -68,7 +67,7 @@ class Docker extends Component
public function generateName(): void
{
$name = data_get($this->selectedServer, 'name', new Cuid2);
$name = data_get($this->selectedServer, 'name', new_public_id());
$this->name = str("{$name}-{$this->network}")->kebab();
}
+5
View File
@@ -3,6 +3,7 @@
namespace App\Livewire\Destination;
use App\Models\StandaloneDocker;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -31,8 +32,12 @@ class Show extends Component
if (! $destination) {
return redirect()->route('destination.index');
}
$this->authorize('view', $destination);
$this->destination = $destination;
$this->syncData();
} catch (AuthorizationException) {
abort(403);
} catch (\Throwable $e) {
return handleError($e, $this);
}
+8
View File
@@ -1053,6 +1053,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new postgresql)',
'type' => 'postgresql',
'category' => 'Databases',
'logo' => 'svgs/postgresql.svg',
'resourceType' => 'database',
]);
@@ -1062,6 +1063,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new mysql)',
'type' => 'mysql',
'category' => 'Databases',
'logo' => 'svgs/mysql.svg',
'resourceType' => 'database',
]);
@@ -1071,6 +1073,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new mariadb)',
'type' => 'mariadb',
'category' => 'Databases',
'logo' => 'svgs/mariadb.svg',
'resourceType' => 'database',
]);
@@ -1080,6 +1083,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new redis)',
'type' => 'redis',
'category' => 'Databases',
'logo' => 'svgs/redis.svg',
'resourceType' => 'database',
]);
@@ -1089,6 +1093,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new keydb)',
'type' => 'keydb',
'category' => 'Databases',
'logo' => 'svgs/keydb.svg',
'resourceType' => 'database',
]);
@@ -1098,6 +1103,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new dragonfly)',
'type' => 'dragonfly',
'category' => 'Databases',
'logo' => 'svgs/dragonfly.svg',
'resourceType' => 'database',
]);
@@ -1107,6 +1113,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new mongodb)',
'type' => 'mongodb',
'category' => 'Databases',
'logo' => 'svgs/mongodb.svg',
'resourceType' => 'database',
]);
@@ -1116,6 +1123,7 @@ class GlobalSearch extends Component
'quickcommand' => '(type: new clickhouse)',
'type' => 'clickhouse',
'category' => 'Databases',
'logo' => 'svgs/clickhouse-icon.svg',
'resourceType' => 'database',
]);
}
+1 -2
View File
@@ -4,7 +4,6 @@ namespace App\Livewire;
// use Livewire\Component;
use Illuminate\View\Component;
use Visus\Cuid2\Cuid2;
class MonacoEditor extends Component
{
@@ -40,7 +39,7 @@ class MonacoEditor extends Component
public function render()
{
if (is_null($this->id)) {
$this->id = new Cuid2;
$this->id = new_public_id();
}
if (is_null($this->name)) {
+32 -20
View File
@@ -2,12 +2,16 @@
namespace App\Livewire;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
use AuthorizesRequests;
public $team;
public function mount()
@@ -17,27 +21,35 @@ class NavbarDeleteTeam extends Component
public function delete($password, $selectedActions = [])
{
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
try {
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
}
$currentTeam = currentTeam();
$this->authorize('delete', $currentTeam);
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
$session = DB::table('sessions')->where('user_id', $user->id)->first();
if ($session) {
DB::table('sessions')->where('id', $session->id)->delete();
}
});
Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id);
$currentTeam->delete();
$newTeam = Auth::user()->teams()->first();
refreshSession($newTeam);
return redirect()->route('team.index');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$currentTeam = currentTeam();
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
$session = DB::table('sessions')->where('user_id', $user->id)->first();
if ($session) {
DB::table('sessions')->where('id', $session->id)->delete();
}
});
refreshSession();
return redirectRoute($this, 'team.index');
}
public function render()
+3 -1
View File
@@ -110,7 +110,9 @@ class Discord extends Component
refreshSession();
} else {
$this->discordEnabled = $this->settings->discord_enabled;
$this->discordWebhookUrl = $this->settings->discord_webhook_url;
$this->discordWebhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->discord_webhook_url
: null;
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
+10 -2
View File
@@ -170,11 +170,15 @@ class Email extends Component
$this->smtpPort = $this->settings->smtp_port;
$this->smtpEncryption = $this->settings->smtp_encryption;
$this->smtpUsername = $this->settings->smtp_username;
$this->smtpPassword = $this->settings->smtp_password;
$this->smtpPassword = auth()->user()->can('update', $this->settings)
? $this->settings->smtp_password
: null;
$this->smtpTimeout = $this->settings->smtp_timeout;
$this->resendEnabled = $this->settings->resend_enabled;
$this->resendApiKey = $this->settings->resend_api_key;
$this->resendApiKey = auth()->user()->can('update', $this->settings)
? $this->settings->resend_api_key
: null;
$this->useInstanceEmailSettings = $this->settings->use_instance_email_settings;
@@ -242,6 +246,8 @@ class Email extends Component
public function submitSmtp()
{
$this->authorize('update', $this->settings);
try {
$this->resetErrorBag();
$this->validate([
@@ -289,6 +295,8 @@ class Email extends Component
public function submitResend()
{
$this->authorize('update', $this->settings);
try {
$this->resetErrorBag();
$this->validate([
+7 -2
View File
@@ -113,8 +113,13 @@ class Pushover extends Component
refreshSession();
} else {
$this->pushoverEnabled = $this->settings->pushover_enabled;
$this->pushoverUserKey = $this->settings->pushover_user_key;
$this->pushoverApiToken = $this->settings->pushover_api_token;
if (auth()->user()->can('update', $this->settings)) {
$this->pushoverUserKey = $this->settings->pushover_user_key;
$this->pushoverApiToken = $this->settings->pushover_api_token;
} else {
$this->pushoverUserKey = null;
$this->pushoverApiToken = null;
}
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;
+3 -1
View File
@@ -110,7 +110,9 @@ class Slack extends Component
refreshSession();
} else {
$this->slackEnabled = $this->settings->slack_enabled;
$this->slackWebhookUrl = $this->settings->slack_webhook_url;
$this->slackWebhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->slack_webhook_url
: null;
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
+7 -2
View File
@@ -169,8 +169,13 @@ class Telegram extends Component
$this->settings->save();
} else {
$this->telegramEnabled = $this->settings->telegram_enabled;
$this->telegramToken = $this->settings->telegram_token;
$this->telegramChatId = $this->settings->telegram_chat_id;
if (auth()->user()->can('update', $this->settings)) {
$this->telegramToken = $this->settings->telegram_token;
$this->telegramChatId = $this->settings->telegram_chat_id;
} else {
$this->telegramToken = null;
$this->telegramChatId = null;
}
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
+3 -1
View File
@@ -105,7 +105,9 @@ class Webhook extends Component
refreshSession();
} else {
$this->webhookEnabled = $this->settings->webhook_enabled;
$this->webhookUrl = $this->settings->webhook_url;
$this->webhookUrl = auth()->user()->can('update', $this->settings)
? $this->settings->webhook_url
: null;
$this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications;
$this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications;
+5 -2
View File
@@ -4,11 +4,13 @@ namespace App\Livewire\Project;
use App\Models\Project;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class AddEmpty extends Component
{
use AuthorizesRequests;
public string $name;
public string $description = '';
@@ -29,12 +31,13 @@ class AddEmpty extends Component
public function submit()
{
try {
$this->authorize('create', Project::class);
$this->validate();
$project = Project::create([
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
]);
$productionEnvironment = $project->environments()->where('name', 'production')->first();
@@ -59,7 +59,9 @@ class Show extends Component
$this->application_deployment_queue = $application_deployment_queue;
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->deployment_uuid = $deploymentUuid;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_debug_enabled = auth()->user()->isMember()
? false
: $this->application->settings->is_debug_enabled;
$this->isKeepAliveOn();
}
@@ -110,6 +112,8 @@ class Show extends Component
public function downloadAllLogs(): string
{
$this->authorize('update', $this->application);
$logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true)
->map(function ($line) {
$prefix = '';
@@ -6,11 +6,14 @@ use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Carbon;
use Livewire\Component;
class DeploymentNavbar extends Component
{
use AuthorizesRequests;
public ApplicationDeploymentQueue $application_deployment_queue;
public Application $application;
@@ -25,7 +28,9 @@ class DeploymentNavbar extends Component
{
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
$this->server = $this->application->destination->server;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_debug_enabled = auth()->user()->isMember()
? false
: $this->application->settings->is_debug_enabled;
}
public function deploymentFinished()
@@ -35,15 +40,21 @@ class DeploymentNavbar extends Component
public function show_debug()
{
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
$this->application->settings->save();
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue');
try {
$this->authorize('update', $this->application);
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
$this->application->settings->save();
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->dispatch('refreshQueue');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function force_start()
{
try {
$this->authorize('deploy', $this->application);
force_start_deployment($this->application_deployment_queue);
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -58,10 +69,15 @@ class DeploymentNavbar extends Component
return '';
}
$isMember = auth()->user()->isMember();
$markdown = "# Deployment Logs\n\n";
$markdown .= "```\n";
foreach ($logs as $log) {
if ($isMember && ! empty($log['hidden'])) {
continue;
}
if (isset($log['output'])) {
$markdown .= $log['output']."\n";
}
@@ -74,6 +90,11 @@ class DeploymentNavbar extends Component
public function cancel()
{
try {
$this->authorize('deploy', $this->application);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$kill_command = "docker rm -f {$deployment_uuid}";
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;
+1 -2
View File
@@ -13,7 +13,6 @@ use Illuminate\Support\Collection;
use Livewire\Component;
use Livewire\Features\SupportEvents\Event;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class General extends Component
{
@@ -549,7 +548,7 @@ class General extends Component
try {
$this->authorize('update', $this->application);
$uuid = new Cuid2;
$uuid = new_public_id();
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
+92 -77
View File
@@ -7,7 +7,6 @@ use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Heading extends Component
{
@@ -65,107 +64,123 @@ class Heading extends Component
public function force_deploy_without_cache()
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$this->deploy(force_rebuild: true);
$this->deploy(force_rebuild: true);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function deploy(bool $force_rebuild = false)
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
return;
return;
}
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return;
}
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
return;
}
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('error', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return;
}
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
return;
}
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('error', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
}
protected function setDeploymentUuid()
{
$this->deploymentUuid = new Cuid2;
$this->deploymentUuid = new_public_id();
$this->parameters['deployment_uuid'] = $this->deploymentUuid;
}
public function stop()
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restart()
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
}
public function render()
+22 -17
View File
@@ -9,7 +9,6 @@ use App\Models\ApplicationPreview;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Previews extends Component
{
@@ -234,31 +233,38 @@ class Previews extends Component
public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null)
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$dockerRegistryImageTag = null;
if ($this->application->build_pack === 'dockerimage') {
$dockerRegistryImageTag = $this->application->previews()
->where('pull_request_id', $pull_request_id)
->value('docker_registry_image_tag');
$dockerRegistryImageTag = null;
if ($this->application->build_pack === 'dockerimage') {
$dockerRegistryImageTag = $this->application->previews()
->where('pull_request_id', $pull_request_id)
->value('docker_registry_image_tag');
}
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
}
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null)
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$this->setDeploymentUuid();
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
@@ -305,7 +311,7 @@ class Previews extends Component
protected function setDeploymentUuid()
{
$this->deployment_uuid = new Cuid2;
$this->deployment_uuid = new_public_id();
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
@@ -350,9 +356,8 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
$server = $this->application->destination->server;
if ($this->application->destination->server->isSwarm()) {
@@ -6,7 +6,6 @@ use App\Models\ApplicationPreview;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
class PreviewsCompose extends Component
{
@@ -64,7 +63,7 @@ class PreviewsCompose extends Component
if (empty($domain_string)) {
$server = $this->preview->application->destination->server;
$template = $this->preview->application->preview_url_template;
$random = new Cuid2;
$random = new_public_id();
// Generate a unique domain like main app services do
$generated_fqdn = generateUrl(server: $server, random: $random);
@@ -79,7 +78,7 @@ class PreviewsCompose extends Component
$domain_list = explode(',', $domain_string);
$preview_fqdns = [];
$template = $this->preview->application->preview_url_template;
$random = new Cuid2;
$random = new_public_id();
foreach ($domain_list as $single_domain) {
$single_domain = trim($single_domain);
@@ -6,7 +6,6 @@ use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Rollback extends Component
{
@@ -52,7 +51,7 @@ class Rollback extends Component
$commit = validateGitRef($commit, 'rollback commit');
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $this->application,
@@ -3,11 +3,14 @@
namespace App\Livewire\Project\Application;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Swarm extends Component
{
use AuthorizesRequests;
public Application $application;
#[Validate('required')]
@@ -51,6 +54,7 @@ class Swarm extends Component
public function instantSave()
{
try {
$this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
@@ -61,6 +65,7 @@ class Swarm extends Component
public function submit()
{
try {
$this->authorize('update', $this->application);
$this->syncData(true);
$this->dispatch('success', 'Swarm settings updated.');
} catch (\Throwable $e) {
+12 -9
View File
@@ -11,11 +11,13 @@ use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneMe extends Component
{
use AuthorizesRequests;
public string $project_uuid;
public string $environment_uuid;
@@ -61,7 +63,7 @@ class CloneMe extends Component
->servers()
->get()
->reject(fn ($server) => $server->isBuildServer());
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
$this->newName = str($this->project->name.'-clone-'.new_public_id())->slug();
}
public function toggleVolumeCloning(bool $value)
@@ -91,6 +93,7 @@ class CloneMe extends Component
public function clone(string $type)
{
try {
$this->authorize('create', Project::class);
$this->validate([
'selectedDestination' => 'required',
'newName' => ValidationPatterns::nameRules(),
@@ -108,7 +111,7 @@ class CloneMe extends Component
if ($this->environment->name !== 'production') {
$project->environments()->create([
'name' => $this->environment->name,
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
]);
}
$environment = $project->environments->where('name', $this->environment->name)->first();
@@ -120,7 +123,7 @@ class CloneMe extends Component
$project = $this->project;
$environment = $this->project->environments()->create([
'name' => $this->newName,
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
]);
}
$applications = $this->environment->applications;
@@ -134,7 +137,7 @@ class CloneMe extends Component
}
foreach ($databases as $database) {
$uuid = (string) new Cuid2;
$uuid = new_public_id();
$newDatabase = $database->replicate([
'id',
'created_at',
@@ -225,7 +228,7 @@ class CloneMe extends Component
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$uuid = new_public_id();
$newBackup = $backup->replicate([
'id',
'created_at',
@@ -254,7 +257,7 @@ class CloneMe extends Component
}
foreach ($services as $service) {
$uuid = (string) new Cuid2;
$uuid = new_public_id();
$newService = $service->replicate([
'id',
'created_at',
@@ -278,7 +281,7 @@ class CloneMe extends Component
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
]);
@@ -409,7 +412,7 @@ class CloneMe extends Component
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$uuid = new_public_id();
$newBackup = $backup->replicate([
'id',
'created_at',
+52 -6
View File
@@ -2,10 +2,13 @@
namespace App\Livewire\Project\Database;
use App\Jobs\DatabaseBackupJob;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -17,7 +20,7 @@ class BackupEdit extends Component
public ScheduledDatabaseBackup $backup;
#[Locked]
public $s3s;
public $availableS3Storages;
#[Locked]
public $parameters;
@@ -68,7 +71,7 @@ class BackupEdit extends Component
public bool $disableLocalBackup = false;
#[Validate(['nullable', 'integer'])]
public ?int $s3StorageId = 1;
public ?int $s3StorageId = null;
#[Validate(['nullable', 'string'])]
public ?string $databasesToBackup = null;
@@ -128,7 +131,7 @@ class BackupEdit extends Component
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
$this->saveS3 = $this->backup->save_s3;
$this->disableLocalBackup = $this->backup->disable_local_backup ?? false;
$this->s3StorageId = $this->backup->s3_storage_id;
$this->s3StorageId = $this->backup->s3_storage_id ?? $this->availableS3StorageIds()->first();
$this->databasesToBackup = $this->backup->databases_to_backup;
$this->dumpAll = $this->backup->dump_all;
$this->timeout = $this->backup->timeout;
@@ -190,6 +193,18 @@ class BackupEdit extends Component
}
}
public function backupNow()
{
try {
$this->authorize('manageBackups', $this->backup->database);
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
@@ -202,6 +217,11 @@ class BackupEdit extends Component
}
}
public function updatedS3StorageId(): void
{
$this->instantSave();
}
private function customValidate()
{
if (! is_numeric($this->backup->s3_storage_id)) {
@@ -209,10 +229,14 @@ class BackupEdit extends Component
}
// S3 backup cannot be enabled without a valid S3 storage owned by the team
$availableS3Ids = collect($this->s3s)->pluck('id');
if ($this->backup->save_s3 && ! $availableS3Ids->contains($this->backup->s3_storage_id)) {
$this->backup->save_s3 = $this->saveS3 = false;
$availableS3Ids = $this->availableS3StorageIds();
if ($availableS3Ids->isEmpty()) {
$this->backup->s3_storage_id = $this->s3StorageId = null;
if ($this->backup->save_s3) {
$this->backup->save_s3 = $this->saveS3 = false;
}
} elseif (! $availableS3Ids->contains($this->backup->s3_storage_id)) {
$this->backup->s3_storage_id = $this->s3StorageId = $availableS3Ids->first();
}
// Validate that disable_local_backup can only be true when S3 backup is enabled
@@ -227,6 +251,28 @@ class BackupEdit extends Component
$this->validate();
}
private function availableS3StorageIds(): Collection
{
$storages = collect($this->availableS3Storages);
$storageIds = $storages->pluck('id')->filter()->all();
if (empty($storageIds)) {
return collect();
}
$teamIds = $storages->pluck('team_id')->reject(fn ($teamId) => $teamId === null)->unique()->values()->all();
if (empty($teamIds)) {
return collect();
}
return S3Storage::query()
->whereKey($storageIds)
->whereIn('team_id', $teamIds)
->where('is_usable', true)
->pluck('id');
}
public function submit()
{
try {
@@ -3,12 +3,16 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class BackupExecutions extends Component
{
use AuthorizesRequests;
public ?ScheduledDatabaseBackup $backup = null;
public $database;
@@ -44,29 +48,45 @@ class BackupExecutions extends Component
public function cleanupFailed()
{
if ($this->backup) {
$this->backup->executions()->where('status', 'failed')->delete();
$this->refreshBackupExecutions();
$this->dispatch('success', 'Failed backups cleaned up.');
try {
$this->authorize('manageBackups', $this->database);
if ($this->backup) {
$this->backup->executions()->where('status', 'failed')->delete();
$this->refreshBackupExecutions();
$this->dispatch('success', 'Failed backups cleaned up.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function cleanupDeleted()
{
if ($this->backup) {
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
if ($deletedCount > 0) {
$this->backup->executions()->where('local_storage_deleted', true)->delete();
$this->refreshBackupExecutions();
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
} else {
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
try {
$this->authorize('manageBackups', $this->database);
if ($this->backup) {
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
if ($deletedCount > 0) {
$this->backup->executions()->where('local_storage_deleted', true)->delete();
$this->refreshBackupExecutions();
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
} else {
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
}
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function deleteBackup($executionId, $password, $selectedActions = [])
{
try {
$this->authorize('manageBackups', $this->database);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
}
@@ -78,7 +98,7 @@ class BackupExecutions extends Component
return;
}
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === ServiceDatabase::class
? $execution->scheduledDatabaseBackup->database->service->destination->server
: $execution->scheduledDatabaseBackup->database->destination->server;
@@ -185,7 +205,7 @@ class BackupExecutions extends Component
if ($this->database) {
$server = null;
if ($this->database instanceof \App\Models\ServiceDatabase) {
if ($this->database instanceof ServiceDatabase) {
$server = $this->database->service->destination->server;
} elseif ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
+7 -3
View File
@@ -14,9 +14,13 @@ class BackupNow extends Component
public function backupNow()
{
$this->authorize('manageBackups', $this->backup->database);
try {
$this->authorize('manageBackups', $this->backup->database);
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
DatabaseBackupJob::dispatch($this->backup);
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}
@@ -42,6 +42,8 @@ class General extends Component
public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array
{
$user = Auth::user();
@@ -72,6 +74,11 @@ class General extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->clickhouseAdminPassword = '';
}
}
protected function rules(): array
@@ -40,6 +40,8 @@ class General extends Component
public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array
{
$user = Auth::user();
@@ -70,6 +72,11 @@ class General extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->dragonflyPassword = '';
}
}
protected function rules(): array
+16 -6
View File
@@ -90,18 +90,28 @@ class Heading extends Component
public function restart()
{
$this->authorize('manage', $this->database);
try {
$this->authorize('manage', $this->database);
$activity = RestartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
$activity = RestartDatabase::run($this->database);
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function start()
{
$this->authorize('manage', $this->database);
try {
$this->authorize('manage', $this->database);
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
$activity = StartDatabase::run($this->database);
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
@@ -14,6 +14,7 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Support\DatabaseBackupFileValidator;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Storage;
@@ -451,10 +452,20 @@ EOD;
// Check if an uploaded file exists first (takes priority over custom location)
if (Storage::exists($backupFileName)) {
$path = Storage::path($backupFileName);
// Reject malicious PostgreSQL payloads before transferring the file anywhere.
if ($this->isPostgresqlRestore() && DatabaseBackupFileValidator::fileContainsPostgresqlProgramExecution($path)) {
Storage::delete($backupFileName);
$this->dispatch('error', 'The uploaded backup contains disallowed PostgreSQL restore directives (COPY ... PROGRAM or psql shell commands) and was rejected.');
return true;
}
$tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resourceUuid;
instant_scp($path, $tmpPath, $this->server);
Storage::delete($backupFileName);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
$this->addRestoreSafetyCheckCommand($this->importCommands, $tmpPath);
} elseif (filled($this->customLocation)) {
// Validate the custom location to prevent command injection
if (! $this->validateServerPath($this->customLocation)) {
@@ -465,6 +476,7 @@ EOD;
$tmpPath = '/tmp/restore_'.$this->resourceUuid;
$escapedCustomLocation = escapeshellarg($this->customLocation);
$this->importCommands[] = "docker cp {$escapedCustomLocation} {$this->container}:{$tmpPath}";
$this->addRestoreSafetyCheckCommand($this->importCommands, $tmpPath);
} else {
$this->dispatch('error', 'The file does not exist or has been deleted.');
@@ -721,6 +733,7 @@ EOD;
// 6. Copy from helper to server, then immediately to database container
$commands[] = "docker cp {$escapedHelperContainerPath} {$escapedServerTmpPath}";
$commands[] = "docker cp {$escapedServerTmpPath} {$escapedDatabaseContainerTmpPath}";
$this->addRestoreSafetyCheckCommand($commands, $containerTmpPath);
// 7. Cleanup helper container and server temp file immediately (no longer needed)
$commands[] = "docker rm -f {$containerName} 2>/dev/null || true";
@@ -765,6 +778,69 @@ EOD;
return true;
}
public function buildRestoreSafetyCheckCommand(string $tmpPath): ?string
{
$script = $this->buildPostgresRestoreScanScript($tmpPath);
if ($script === null) {
return null;
}
return "docker exec {$this->container} sh -c ".escapeshellarg($script);
}
/**
* Build the POSIX shell snippet that aborts (exit 1) when a PostgreSQL
* backup contains directives leading to OS command execution.
*
* Hardened against bypasses:
* - decompresses gzip backups before scanning,
* - strips `--` line comments and flattens newlines so multi-line and
* comment-separated payloads (e.g. `FROM/**​/PROGRAM`) are caught,
* - matches a literal `\!` shell escape and `\o|`/`\g|` pipe redirects.
*/
public function buildPostgresRestoreScanScript(string $tmpPath): ?string
{
if (! $this->isPostgresqlRestore()) {
return null;
}
$escapedTmpPath = escapeshellarg($tmpPath);
// Token separator PostgreSQL treats as whitespace: real whitespace or a
// /* ... */ block comment (used to split keywords like FROM/**/PROGRAM).
$sep = '([[:space:]]|/\\*[^*]*\\*/)';
$pattern = implode('|', [
"copy{$sep}+[^;]*(from|to){$sep}+program",
'(^|[[:space:]])\\\\!',
"(^|[[:space:]])\\\\(o|g){$sep}*\\|",
]);
$escapedPattern = escapeshellarg($pattern);
return "if (gunzip -cf {$escapedTmpPath} 2>/dev/null || cat {$escapedTmpPath}) | sed 's/--.*//' | tr '\n\r\t' ' ' | grep -Eiq {$escapedPattern}; then echo 'Blocked PostgreSQL restore: COPY ... PROGRAM and psql shell commands are not allowed.'; exit 1; fi";
}
private function addRestoreSafetyCheckCommand(array &$commands, string $tmpPath): void
{
$command = $this->buildRestoreSafetyCheckCommand($tmpPath);
if ($command !== null) {
$commands[] = $command;
}
}
private function isPostgresqlRestore(): bool
{
$morphClass = $this->resource->getMorphClass();
if ($morphClass === ServiceDatabase::class) {
return str_contains($this->resource->databaseType(), 'postgres');
}
return $morphClass === StandalonePostgresql::class || $morphClass === 'postgresql';
}
public function buildRestoreCommand(string $tmpPath): string
{
$escapedTmpPath = escapeshellarg($tmpPath);
@@ -2,13 +2,20 @@
namespace App\Livewire\Project\Database;
use App\Models\StandalonePostgresql;
use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Component;
class InitScript extends Component
{
use AuthorizesRequests;
#[Locked]
public StandalonePostgresql $database;
#[Locked]
public array $script;
@@ -35,6 +42,7 @@ class InitScript extends Component
public function submit()
{
try {
$this->authorize('update', $this->database);
$this->validate();
$this->script['index'] = $this->index;
$this->script['content'] = $this->content;
@@ -48,6 +56,7 @@ class InitScript extends Component
public function delete()
{
try {
$this->authorize('update', $this->database);
$this->dispatch('delete_init_script', $this->script);
} catch (Exception $e) {
return handleError($e, $this);
@@ -42,6 +42,8 @@ class General extends Component
public bool $isLogDrainEnabled = false;
public bool $isPasswordHiddenForMember = false;
public function getListeners(): array
{
$user = Auth::user();
@@ -72,6 +74,11 @@ class General extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->keydbPassword = '';
}
}
protected function rules(): array
@@ -47,6 +47,8 @@ class General extends Component
public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array
{
return [
@@ -126,6 +128,12 @@ class General extends Component
} catch (Exception $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mariadbRootPassword = '';
$this->mariadbPassword = '';
}
}
public function syncData(bool $toModel = false)
@@ -45,6 +45,8 @@ class General extends Component
public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array
{
return [
@@ -119,6 +121,11 @@ class General extends Component
} catch (Exception $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mongoInitdbRootPassword = '';
}
}
public function syncData(bool $toModel = false)
@@ -47,6 +47,8 @@ class General extends Component
public ?string $customDockerRunOptions = null;
public bool $isPasswordHiddenForMember = false;
protected function rules(): array
{
return [
@@ -126,6 +128,12 @@ class General extends Component
} catch (Exception $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->mysqlRootPassword = '';
$this->mysqlPassword = '';
}
}
public function syncData(bool $toModel = false)
@@ -55,6 +55,8 @@ class General extends Component
public string $new_content;
public bool $isPasswordHiddenForMember = false;
protected $listeners = [
'save_init_script',
'delete_init_script',
@@ -140,6 +142,11 @@ class General extends Component
} catch (Exception $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->postgresPassword = '';
}
}
public function syncData(bool $toModel = false)
@@ -45,6 +45,8 @@ class General extends Component
public string $redisVersion;
public bool $isPasswordHiddenForMember = false;
protected $listeners = [
'envsUpdated' => 'refresh',
];
@@ -118,6 +120,11 @@ class General extends Component
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->redisPassword = '';
}
}
public function syncData(bool $toModel = false)
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@@ -34,7 +35,7 @@ class ScheduledBackups extends Component
$this->setSelectedBackup($this->selectedBackupId, true);
}
$this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
if ($this->database->getMorphClass() === ServiceDatabase::class) {
$this->type = 'service-database';
} else {
$this->type = 'database';
@@ -56,22 +57,30 @@ class ScheduledBackups extends Component
public function setCustomType()
{
$this->authorize('update', $this->database);
try {
$this->authorize('update', $this->database);
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function delete($scheduled_backup_id): void
{
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
$this->authorize('manageBackups', $this->database);
try {
$this->authorize('manageBackups', $this->database);
$backup->delete();
$this->dispatch('success', 'Scheduled backup deleted.');
$this->refreshScheduledBackups();
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
$backup->delete();
$this->dispatch('success', 'Scheduled backup deleted.');
$this->refreshScheduledBackups();
} catch (\Throwable $e) {
handleError($e, $this);
}
}
public function refreshScheduledBackups(?int $id = null): void
+14 -10
View File
@@ -28,18 +28,22 @@ class DeleteEnvironment extends Component
public function delete()
{
$this->validate([
'environment_id' => 'required|int',
]);
$environment = Environment::ownedByCurrentTeam()->findOrFail($this->environment_id);
$this->authorize('delete', $environment);
try {
$this->validate([
'environment_id' => 'required|int',
]);
$environment = Environment::ownedByCurrentTeam()->findOrFail($this->environment_id);
$this->authorize('delete', $environment);
if ($environment->isEmpty()) {
$environment->delete();
if ($environment->isEmpty()) {
$environment->delete();
return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
}
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
}
}
+14 -10
View File
@@ -26,18 +26,22 @@ class DeleteProject extends Component
public function delete()
{
$this->validate([
'project_id' => 'required|int',
]);
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
$this->authorize('delete', $project);
try {
$this->validate([
'project_id' => 'required|int',
]);
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
$this->authorize('delete', $project);
if ($project->isEmpty()) {
$project->delete();
if ($project->isEmpty()) {
$project->delete();
return redirectRoute($this, 'project.index');
return redirectRoute($this, 'project.index');
}
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
}
}
+4
View File
@@ -4,10 +4,13 @@ namespace App\Livewire\Project;
use App\Models\Project;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Edit extends Component
{
use AuthorizesRequests;
public Project $project;
public string $name;
@@ -54,6 +57,7 @@ class Edit extends Component
public function submit()
{
try {
$this->authorize('update', $this->project);
$this->syncData(true);
$this->dispatch('success', 'Project updated.');
} catch (\Throwable $e) {
+4
View File
@@ -5,11 +5,14 @@ namespace App\Livewire\Project;
use App\Models\Application;
use App\Models\Project;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Component;
class EnvironmentEdit extends Component
{
use AuthorizesRequests;
public Project $project;
public Application $application;
@@ -62,6 +65,7 @@ class EnvironmentEdit extends Component
public function submit()
{
try {
$this->authorize('update', $this->environment);
$this->syncData(true);
redirectRoute($this, 'project.environment.edit', [
'environment_uuid' => $this->environment->uuid,
@@ -5,11 +5,14 @@ namespace App\Livewire\Project\New;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
class DockerCompose extends Component
{
use AuthorizesRequests;
public string $dockerComposeRaw = '';
public string $envFile = '';
@@ -30,6 +33,8 @@ class DockerCompose extends Component
public function submit()
{
try {
$this->authorize('create', Service::class);
$this->validate([
'dockerComposeRaw' => 'required',
]);
+6 -2
View File
@@ -6,11 +6,13 @@ use App\Models\Application;
use App\Models\Project;
use App\Services\DockerImageParser;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class DockerImage extends Component
{
use AuthorizesRequests;
public string $imageName = '';
public string $imageTag = '';
@@ -81,6 +83,8 @@ class DockerImage extends Component
public function submit()
{
$this->authorize('create', Application::class);
$this->validate([
'imageName' => ValidationPatterns::dockerImageNameRules(required: true),
'imageTag' => ValidationPatterns::dockerImageTagRules(),
@@ -130,7 +134,7 @@ class DockerImage extends Component
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
$application = Application::create([
'name' => 'docker-image-'.new Cuid2,
'name' => 'docker-image-'.new_public_id(),
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main',
+6 -2
View File
@@ -3,17 +3,21 @@
namespace App\Livewire\Project\New;
use App\Models\Project;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class EmptyProject extends Component
{
use AuthorizesRequests;
public function createEmptyProject()
{
$this->authorize('create', Project::class);
$project = Project::create([
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
'uuid' => new_public_id(),
]);
return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
@@ -7,6 +7,7 @@ use App\Models\GithubApp;
use App\Models\Project;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Attributes\Locked;
@@ -14,6 +15,8 @@ use Livewire\Component;
class GithubPrivateRepository extends Component
{
use AuthorizesRequests;
public $current_step = 'github_apps';
public $github_apps;
@@ -169,6 +172,8 @@ class GithubPrivateRepository extends Component
public function submit()
{
try {
$this->authorize('create', Application::class);
// Validate git repository parts and branch
$validator = validator([
'selected_repository_owner' => $this->selected_repository_owner,
@@ -10,12 +10,15 @@ use App\Models\Project;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
class GithubPrivateRepositoryDeployKey extends Component
{
use AuthorizesRequests;
public $current_step = 'private_keys';
public $parameters;
@@ -128,6 +131,8 @@ class GithubPrivateRepositoryDeployKey extends Component
public function submit()
{
$this->authorize('create', Application::class);
$this->validate();
try {
$destination_uuid = $this->query['destination'] ?? null;
@@ -6,16 +6,18 @@ use App\Models\Application;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\Project;
use App\Models\Service;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Spatie\Url\Url;
class PublicGitRepository extends Component
{
use AuthorizesRequests;
public string $repository_url;
public int $port = 3000;
@@ -260,6 +262,8 @@ class PublicGitRepository extends Component
public function submit()
{
try {
$this->authorize('create', Application::class);
$this->validate();
// Additional validation for git repository and branch
@@ -295,33 +299,6 @@ class PublicGitRepository extends Component
$project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$environment = $project->environments()->where('uuid', $environment_uuid)->firstOrFail();
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
$server = $destination->server;
$new_service = [
'name' => 'service'.str()->random(10),
'docker_compose_raw' => 'coolify',
'environment_id' => $environment->id,
'server_id' => $server->id,
];
if ($this->git_source === 'other') {
$new_service['git_repository'] = $this->git_repository;
$new_service['git_branch'] = $this->git_branch;
} else {
$new_service['git_repository'] = $this->git_repository;
$new_service['git_branch'] = $this->git_branch;
$new_service['source_id'] = $this->git_source->id;
$new_service['source_type'] = $this->git_source->getMorphClass();
}
$service = Service::create($new_service);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid,
]);
return;
}
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
+24 -13
View File
@@ -6,7 +6,6 @@ use App\Models\Project;
use App\Models\Server;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Select extends Component
@@ -107,7 +106,7 @@ class Select extends Component
public function loadServices()
{
$services = get_service_templates();
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services);
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
$default_logo = 'images/default.webp';
@@ -279,19 +278,31 @@ class Select extends Component
return $this->formatLastModified($this->serviceTemplatesPath());
}
private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
private function serviceTemplateLastUpdatedMap(Collection $services): array
{
$bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
return $services
->mapWithKeys(fn ($service, $serviceName) => [
(string) $serviceName => $this->serviceTemplateLastUpdatedFromPayload($service)
?? $this->serviceTemplateLastUpdated((string) $serviceName),
])
->all();
}
return Cache::remember(
"service-template-last-updated-map:{$bundleMtime}",
now()->addDay(),
fn () => $serviceNames
->mapWithKeys(fn ($serviceName) => [
(string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
])
->all()
);
private function serviceTemplateLastUpdatedFromPayload(mixed $service): ?string
{
$timestamp = data_get($service, 'template_last_updated_at');
if (! is_string($timestamp) || $timestamp === '') {
return null;
}
try {
return CarbonImmutable::parse($timestamp)
->timezone(config('app.timezone'))
->format('M j, Y H:i');
} catch (\Throwable) {
return null;
}
}
private function serviceTemplateLastUpdated(string $serviceName): ?string
@@ -5,11 +5,13 @@ namespace App\Livewire\Project\New;
use App\Models\Application;
use App\Models\GithubApp;
use App\Models\Project;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class SimpleDockerfile extends Component
{
use AuthorizesRequests;
public string $dockerfile = '';
public array $parameters;
@@ -30,6 +32,8 @@ CMD ["nginx", "-g", "daemon off;"]
public function submit()
{
$this->authorize('create', Application::class);
$this->validate([
'dockerfile' => 'required',
]);
@@ -48,7 +52,7 @@ CMD ["nginx", "-g", "daemon off;"]
$port = 80;
}
$application = Application::create([
'name' => 'dockerfile-'.new Cuid2,
'name' => 'dockerfile-'.new_public_id(),
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main',
+4
View File
@@ -4,16 +4,20 @@ namespace App\Livewire\Project\Resource;
use App\Models\EnvironmentVariable;
use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class Create extends Component
{
use AuthorizesRequests;
public $type;
public $project;
public function mount()
{
$this->authorize('createAnyResource');
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
+22 -9
View File
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Service;
use App\Models\Service;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class EditCompose extends Component
{
use AuthorizesRequests;
public Service $service;
public $serviceId;
@@ -72,19 +75,29 @@ class EditCompose extends Component
public function saveEditedCompose()
{
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->dispatch('refreshStorages');
try {
$this->authorize('update', $this->service);
$this->dispatch('info', 'Saving new docker compose...');
$this->dispatch('saveCompose', $this->dockerComposeRaw);
$this->dispatch('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
$this->validate([
'isContainerLabelEscapeEnabled' => 'required',
]);
$this->syncData(true);
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
$this->dispatch('success', 'Service updated successfully');
try {
$this->authorize('update', $this->service);
$this->validate([
'isContainerLabelEscapeEnabled' => 'required',
]);
$this->syncData(true);
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
$this->dispatch('success', 'Service updated successfully');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
+1 -2
View File
@@ -110,8 +110,7 @@ class FileStorage extends Component
public function loadStorageOnServer()
{
try {
// Loading content is a read operation, so we use 'view' permission
$this->authorize('view', $this->resource);
$this->authorize('update', $this->resource);
$this->fileStorage->loadStorageOnServer();
$this->syncData();
+39 -28
View File
@@ -110,17 +110,20 @@ class Heading extends Component
public function start()
{
$this->authorizeService('deploy');
$activity = StartService::run($this->service, pullLatestImages: true);
$this->dispatch('activityMonitor', $activity->id);
try {
$this->authorizeService('deploy');
$activity = StartService::run($this->service, pullLatestImages: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function forceDeploy()
{
$this->authorizeService('deploy');
try {
$this->authorizeService('deploy');
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
@@ -131,49 +134,57 @@ class Heading extends Component
$activity->save();
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function stop()
{
$this->authorizeService('stop');
try {
$this->authorizeService('stop');
StopService::dispatch($this->service, false, $this->docker_cleanup);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restart()
{
$this->authorizeService('deploy');
try {
$this->authorizeService('deploy');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
return;
}
$activity = StartService::run($this->service, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$activity = StartService::run($this->service, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
$this->authorizeService('deploy');
try {
$this->authorizeService('deploy');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
return;
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
}
private function authorizeService(string $ability): void
+25 -3
View File
@@ -4,16 +4,21 @@ namespace App\Livewire\Project\Service;
use App\Models\Service;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class StackForm extends Component
{
use AuthorizesRequests;
public Service $service;
public Collection $fields;
public bool $isPasswordHiddenForMember = false;
protected $listeners = ['saveCompose'];
// Explicit properties
@@ -118,6 +123,17 @@ class StackForm extends Component
})->flatMap(function ($group) {
return $group;
});
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
if ($this->isPasswordHiddenForMember) {
$this->fields = $this->fields->map(function ($field) {
if (data_get($field, 'isPassword')) {
$field['value'] = null;
}
return $field;
});
}
}
public function saveCompose($raw)
@@ -128,14 +144,20 @@ class StackForm extends Component
public function instantSave()
{
$this->syncData(true);
$this->service->save();
$this->dispatch('success', 'Service settings saved.');
try {
$this->authorize('update', $this->service);
$this->syncData(true);
$this->service->save();
$this->dispatch('success', 'Service settings saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit($notify = true)
{
try {
$this->authorize('update', $this->service);
$this->validate();
$this->syncData(true);
+1 -2
View File
@@ -8,7 +8,6 @@ use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Danger extends Component
{
@@ -39,7 +38,7 @@ class Danger extends Component
public function mount()
{
$parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->modalId = new_public_id();
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentUuid = data_get($parameters, 'environment_uuid');
+9 -4
View File
@@ -7,12 +7,14 @@ use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Destination extends Component
{
use AuthorizesRequests;
public $resource;
public Collection $networks;
@@ -59,6 +61,7 @@ class Destination extends Component
public function stop($serverId)
{
try {
$this->authorize('deploy', $this->resource);
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
@@ -70,12 +73,13 @@ class Destination extends Component
public function redeploy(int $network_id, int $server_id)
{
try {
$this->authorize('deploy', $this->resource);
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$deployment_uuid = new Cuid2;
$deployment_uuid = new_public_id();
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
$result = queue_application_deployment(
@@ -128,7 +132,7 @@ class Destination extends Component
});
$this->resource->refresh();
$this->refreshServers();
} catch (\Exception $e) {
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -149,7 +153,7 @@ class Destination extends Component
$this->resource->additional_networks()->attach($network->id, ['server_id' => $server->id]);
$this->dispatch('refresh');
} catch (\Exception $e) {
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@@ -157,6 +161,7 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password, $selectedActions = [])
{
try {
$this->authorize('update', $this->resource);
if (! verifyPasswordConfirmation($password, $this)) {
return 'The provided password is incorrect.';
}
@@ -111,7 +111,7 @@ class All extends Component
$query->orderBy('order');
}
return $query->get();
return $this->nullLockedValues($query->get());
}
private function searchTerm(): string
@@ -127,6 +127,20 @@ class All extends Component
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty();
}
private function nullLockedValues($envs)
{
$isMember = auth()->user()?->isMember();
$envs->each(function ($env) use ($isMember) {
if ($env->is_shown_once || $isMember) {
$env->value = null;
$env->real_value = null;
}
});
return $envs;
}
public function getIsSearchActiveProperty(): bool
{
return $this->searchTerm() !== '';
@@ -204,7 +218,12 @@ class All extends Component
private function formatEnvironmentVariables($variables)
{
return $variables->map(function ($item) {
$isMember = auth()->user()?->isMember();
return $variables->map(function ($item) use ($isMember) {
if ($isMember) {
return "$item->key=(Hidden, only admins can view)";
}
if ($item->is_shown_once) {
return "$item->key=(Locked Secret, delete and add again to change)";
}
@@ -61,6 +61,8 @@ class Show extends Component
public bool $is_redis_credential = false;
public bool $isValueHidden = false;
public array $problematicVariables = [];
protected $listeners = [
@@ -160,6 +162,13 @@ class Show extends Component
$this->is_really_required = $this->env->is_really_required ?? false;
$this->is_shared = $this->env->is_shared ?? false;
$this->real_value = $this->env->real_value;
if ($this->env->is_shown_once || auth()->user()?->isMember()) {
$this->value = null;
$this->real_value = null;
}
$this->isValueHidden = auth()->user()?->isMember() ?? false;
}
}
@@ -6,12 +6,15 @@ use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Livewire\Attributes\On;
use Livewire\Component;
class ExecuteContainerCommand extends Component
{
use AuthorizesRequests;
public $selected_container = 'default';
public Collection $containers;
@@ -40,6 +43,7 @@ class ExecuteContainerCommand extends Component
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
@@ -56,6 +60,7 @@ class ExecuteContainerCommand extends Component
abort(404);
}
$this->resource = $resource;
$this->authorize('view', $this->resource);
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
@@ -63,6 +68,7 @@ class ExecuteContainerCommand extends Component
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
@@ -70,6 +76,7 @@ class ExecuteContainerCommand extends Component
} elseif (data_get($this->parameters, 'server_uuid')) {
$this->type = 'server';
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
$this->authorize('view', $this->resource);
$this->servers = $this->servers->push($this->resource);
}
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
@@ -152,7 +159,9 @@ class ExecuteContainerCommand extends Component
public function connectToServer()
{
try {
$this->authorize('canAccessTerminal');
$server = $this->servers->first();
$this->authorize('view', $server);
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
@@ -181,6 +190,7 @@ class ExecuteContainerCommand extends Component
return;
}
try {
$this->authorize('canAccessTerminal');
// Validate container name format
if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
throw new \InvalidArgumentException('Invalid container name format');
@@ -198,6 +208,8 @@ class ExecuteContainerCommand extends Component
throw new \RuntimeException('Invalid server configuration.');
}
$this->authorize('view', $server);
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
+6 -2
View File
@@ -78,8 +78,12 @@ class HealthChecks extends Component
public function mount()
{
$this->authorize('view', $this->resource);
$this->syncData();
try {
$this->authorize('view', $this->resource);
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function syncData(bool $toModel = false): void
+252 -249
View File
@@ -22,7 +22,6 @@ use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class ResourceOperations extends Component
{
@@ -56,224 +55,86 @@ class ResourceOperations extends Component
public function cloneTo($destination_id)
{
$this->authorize('update', $this->resource);
try {
$this->authorize('update', $this->resource);
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
if (! $new_destination) {
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
}
if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.');
}
$uuid = (string) new Cuid2;
$server = $new_destination->server;
if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
$route = route('project.application.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'application_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif (
$this->resource->getMorphClass() === StandalonePostgresql::class ||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
$this->resource->getMorphClass() === StandaloneMysql::class ||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
$this->resource->getMorphClass() === StandaloneRedis::class ||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
$this->resource->getMorphClass() === StandaloneClickhouse::class
) {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
'started_at' => null,
'destination_id' => $new_destination->id,
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
if (! $new_destination) {
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
}
$new_resource->persistentStorages()->delete();
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$new_resource->uuid;
} else {
if (str_starts_with($volume->name, $this->resource->uuid)) {
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
} else {
$newName = $new_resource->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopDatabase::dispatch($this->resource);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $this->resource->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartDatabase::dispatch($this->resource);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
if (! $new_destination) {
return $this->addError('destination_id', 'Destination not found.');
}
$uuid = new_public_id();
$server = $new_destination->server;
$fileStorages = $this->resource->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
}
if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
$scheduledBackups = $this->resource->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
$route = route('project.application.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'application_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif (
$this->resource->getMorphClass() === StandalonePostgresql::class ||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
$this->resource->getMorphClass() === StandaloneMysql::class ||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
$this->resource->getMorphClass() === StandaloneRedis::class ||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
$this->resource->getMorphClass() === StandaloneClickhouse::class
) {
$uuid = new_public_id();
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
];
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill($payload);
$newEnvironmentVariable->save();
}
$route = route('project.database.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'database_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(),
'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$scheduledTasks = $this->resource->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$environmentVariables = $this->resource->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
foreach ($new_resource->applications() as $application) {
$application->fill([
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
])->save();
'started_at' => null,
'destination_id' => $new_destination->id,
]);
$new_resource->save();
$persistentVolumes = $application->persistentStorages()->get();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$new_resource->persistentStorages()->delete();
$persistentVolumes = $this->resource->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$new_resource->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$new_resource->uuid;
} else {
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
if (str_starts_with($volume->name, $this->resource->uuid)) {
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
} else {
$newName = $new_resource->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
@@ -283,80 +144,222 @@ class ResourceOperations extends Component
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $application->id,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($application);
StopDatabase::dispatch($this->resource);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->service->destination->server;
$sourceServer = $this->resource->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($application);
StartDatabase::dispatch($this->resource);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
}
foreach ($new_resource->databases() as $database) {
$database->fill([
'status' => 'exited',
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
} else {
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
$fileStorages = $this->resource->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $database->id,
'resource_id' => $new_resource->id,
]);
$newPersistentVolume->save();
$newStorage->save();
}
if ($this->cloneVolumeData) {
try {
StopService::dispatch($database->service);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->service->destination->server;
$targetServer = $new_resource->destination->server;
$scheduledBackups = $this->resource->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = new_public_id();
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
$environmentVaribles = $this->resource->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
];
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill($payload);
$newEnvironmentVariable->save();
}
StartService::dispatch($database->service);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
$route = route('project.database.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'database_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} elseif ($this->resource->type() === 'service') {
$uuid = new_public_id();
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
'destination_type' => $new_destination->getMorphClass(),
'server_id' => $new_destination->server_id,
]);
$new_resource->save();
$tags = $this->resource->tags;
foreach ($tags as $tag) {
$new_resource->tags()->attach($tag->id);
}
$scheduledTasks = $this->resource->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => new_public_id(),
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$environmentVariables = $this->resource->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
foreach ($new_resource->applications() as $application) {
$application->fill([
'status' => 'exited',
])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
} else {
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($application);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->service->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($application);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
}
foreach ($new_resource->databases() as $database) {
$database->fill([
'status' => 'exited',
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $volume->resource->uuid)) {
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
} else {
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
'uuid',
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($database->service);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->service->destination->server;
$targetServer = $new_resource->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($database->service);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
}
$new_resource->parse();
$route = route('project.service.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'service_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
}
$new_resource->parse();
$route = route('project.service.configuration', [
'project_uuid' => $this->projectUuid,
'environment_uuid' => $this->environmentUuid,
'service_uuid' => $new_resource->uuid,
]).'#resource-operations';
return redirect()->to($route);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+7
View File
@@ -5,11 +5,14 @@ namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\On;
use Livewire\Component;
class Terminal extends Component
{
use AuthorizesRequests;
public bool $hasShell = true;
public bool $isTerminalConnected = false;
@@ -32,7 +35,11 @@ class Terminal extends Component
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$this->authorize('canAccessTerminal');
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
$this->authorize('view', $server);
if (! $server->isTerminalEnabled() || $server->isForceDisabled()) {
abort(403, 'Terminal access is disabled on this server.');
}
+7 -5
View File
@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared;
use App\Models\Application;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
class UploadConfig extends Component
{
use AuthorizesRequests;
public $config;
public $applicationId;
@@ -29,13 +32,12 @@ class UploadConfig extends Component
public function uploadConfig()
{
try {
$application = Application::findOrFail($this->applicationId);
$application = Application::ownedByCurrentTeam()->findOrFail($this->applicationId);
$this->authorize('update', $application);
$application->setConfig($this->config);
$this->dispatch('success', 'Application settings updated');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
return;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+12 -7
View File
@@ -34,19 +34,24 @@ class Webhooks extends Component
{
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
if ($this->canViewSecrets()) {
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
}
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
$this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
}
public function canViewSecrets(): bool
{
return auth()->user()->can('update', $this->resource);
}
public function submit()
{
try {
Loaded 100 of 570 files, more files were not shown because too many files have changed in this diff. Show more