Merge branch 'next' into add/rivet-dev
This commit is contained in:
commit
439ecc277d
17 changed files with 838 additions and 64 deletions
|
|
@ -4,11 +4,42 @@
|
|||
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* Skip host validation for certain routes:
|
||||
* - Terminal auth routes (called by realtime container)
|
||||
* - API routes (use token-based authentication, not host validation)
|
||||
* - Webhook endpoints (use cryptographic signature validation)
|
||||
*/
|
||||
public function handle(Request $request, $next)
|
||||
{
|
||||
// Skip host validation for these routes
|
||||
if ($request->is(
|
||||
'terminal/auth',
|
||||
'terminal/auth/ips',
|
||||
'api/*',
|
||||
'webhooks/*'
|
||||
)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip host validation if no FQDN is configured (initial setup)
|
||||
$fqdnHost = Cache::get('instance_settings_fqdn_host');
|
||||
if ($fqdnHost === '' || $fqdnHost === null) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// For all other routes, use parent's host validation
|
||||
return parent::handle($request, $next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
|
|
@ -44,6 +75,19 @@ public function hosts(): array
|
|||
$trustedHosts[] = $fqdnHost;
|
||||
}
|
||||
|
||||
// Trust the APP_URL host itself (not just subdomains)
|
||||
$appUrl = config('app.url');
|
||||
if ($appUrl) {
|
||||
try {
|
||||
$appUrlHost = parse_url($appUrl, PHP_URL_HOST);
|
||||
if ($appUrlHost && ! in_array($appUrlHost, $trustedHosts, true)) {
|
||||
$trustedHosts[] = $appUrlHost;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Trust all subdomains of APP_URL as fallback
|
||||
$trustedHosts[] = $this->allSubdomainsOfApplicationUrl();
|
||||
|
||||
|
|
|
|||
|
|
@ -517,6 +517,10 @@ private function deploy_dockerimage_buildpack()
|
|||
$this->generate_image_names();
|
||||
$this->prepare_builder_image();
|
||||
$this->generate_compose_file();
|
||||
|
||||
// Save runtime environment variables (including empty .env file if no variables defined)
|
||||
$this->save_runtime_environment_variables();
|
||||
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
|
|
@ -1222,9 +1226,9 @@ private function save_runtime_environment_variables()
|
|||
|
||||
// Handle empty environment variables
|
||||
if ($environment_variables->isEmpty()) {
|
||||
// For Docker Compose, we need to create an empty .env file
|
||||
// For Docker Compose and Docker Image, we need to create an empty .env file
|
||||
// because we always reference it in the compose file
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerimage') {
|
||||
$this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).');
|
||||
|
||||
// Create empty .env file
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public function submit()
|
|||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncData(false);
|
||||
$this->syncFromModel();
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
|
|
@ -96,7 +96,7 @@ public function submit()
|
|||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncData(false);
|
||||
$this->syncFromModel();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -1804,7 +1804,9 @@ public function getFilesFromServer(bool $isInit = false)
|
|||
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
||||
{
|
||||
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
||||
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
|
||||
|
||||
if ($hasHealthcheck && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$healthcheckCommand = null;
|
||||
$lines = $dockerfile->toArray();
|
||||
foreach ($lines as $line) {
|
||||
|
|
@ -1845,6 +1847,14 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
|
|||
$this->save();
|
||||
}
|
||||
}
|
||||
} elseif (! $hasHealthcheck && $this->custom_healthcheck_found) {
|
||||
// HEALTHCHECK was removed from Dockerfile, reset to defaults
|
||||
$this->custom_healthcheck_found = false;
|
||||
$this->health_check_interval = 5;
|
||||
$this->health_check_timeout = 5;
|
||||
$this->health_check_retries = 10;
|
||||
$this->health_check_start_period = 5;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.436',
|
||||
'version' => '4.0.0-beta.438',
|
||||
'helper_version' => '1.0.11',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
306
openapi.json
306
openapi.json
|
|
@ -3356,6 +3356,137 @@
|
|||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Databases"
|
||||
],
|
||||
"summary": "Create Backup",
|
||||
"description": "Create a new scheduled backup configuration for a database",
|
||||
"operationId": "create-database-backup",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the database.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Backup configuration data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"frequency"
|
||||
],
|
||||
"properties": {
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the backup is enabled",
|
||||
"default": true
|
||||
},
|
||||
"save_s3": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to save backups to S3",
|
||||
"default": false
|
||||
},
|
||||
"s3_storage_uuid": {
|
||||
"type": "string",
|
||||
"description": "S3 storage UUID (required if save_s3 is true)"
|
||||
},
|
||||
"databases_to_backup": {
|
||||
"type": "string",
|
||||
"description": "Comma separated list of databases to backup"
|
||||
},
|
||||
"dump_all": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to dump all databases",
|
||||
"default": false
|
||||
},
|
||||
"backup_now": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to trigger backup immediately after creation"
|
||||
},
|
||||
"database_backup_retention_amount_locally": {
|
||||
"type": "integer",
|
||||
"description": "Number of backups to retain locally"
|
||||
},
|
||||
"database_backup_retention_days_locally": {
|
||||
"type": "integer",
|
||||
"description": "Number of days to retain backups locally"
|
||||
},
|
||||
"database_backup_retention_max_storage_locally": {
|
||||
"type": "integer",
|
||||
"description": "Max storage (MB) for local backups"
|
||||
},
|
||||
"database_backup_retention_amount_s3": {
|
||||
"type": "integer",
|
||||
"description": "Number of backups to retain in S3"
|
||||
},
|
||||
"database_backup_retention_days_s3": {
|
||||
"type": "integer",
|
||||
"description": "Number of days to retain backups in S3"
|
||||
},
|
||||
"database_backup_retention_max_storage_s3": {
|
||||
"type": "integer",
|
||||
"description": "Max storage (MB) for S3 backups"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Backup configuration created successfully",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "550e8400-e29b-41d4-a716-446655440000"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Backup configuration created successfully."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/databases\/{uuid}": {
|
||||
|
|
@ -5381,6 +5512,96 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/deployments\/{uuid}\/cancel": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Deployments"
|
||||
],
|
||||
"summary": "Cancel",
|
||||
"description": "Cancel a deployment by UUID.",
|
||||
"operationId": "cancel-deployment-by-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "Deployment UUID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deployment cancelled successfully.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Deployment cancelled successfully."
|
||||
},
|
||||
"deployment_uuid": {
|
||||
"type": "string",
|
||||
"example": "cm37r6cqj000008jm0veg5tkm"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "cancelled-by-user"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Deployment cannot be cancelled (already finished\/failed\/cancelled).",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Deployment cannot be cancelled. Current status: finished"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"403": {
|
||||
"description": "User doesn't have permission to cancel this deployment.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "You do not have permission to cancel this deployment."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/deploy": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -5538,6 +5759,91 @@
|
|||
}
|
||||
},
|
||||
"\/github-apps": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"GitHub Apps"
|
||||
],
|
||||
"summary": "List",
|
||||
"description": "List all GitHub apps.",
|
||||
"operationId": "list-github-apps",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of GitHub apps.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"html_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"custom_user": {
|
||||
"type": "string"
|
||||
},
|
||||
"custom_port": {
|
||||
"type": "integer"
|
||||
},
|
||||
"app_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"installation_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"private_key_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"is_system_wide": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"team_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"GitHub Apps"
|
||||
|
|
|
|||
160
openapi.yaml
160
openapi.yaml
|
|
@ -2130,6 +2130,94 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- Databases
|
||||
summary: 'Create Backup'
|
||||
description: 'Create a new scheduled backup configuration for a database'
|
||||
operationId: create-database-backup
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the database.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
description: 'Backup configuration data'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- frequency
|
||||
properties:
|
||||
frequency:
|
||||
type: string
|
||||
description: 'Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)'
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'Whether the backup is enabled'
|
||||
default: true
|
||||
save_s3:
|
||||
type: boolean
|
||||
description: 'Whether to save backups to S3'
|
||||
default: false
|
||||
s3_storage_uuid:
|
||||
type: string
|
||||
description: 'S3 storage UUID (required if save_s3 is true)'
|
||||
databases_to_backup:
|
||||
type: string
|
||||
description: 'Comma separated list of databases to backup'
|
||||
dump_all:
|
||||
type: boolean
|
||||
description: 'Whether to dump all databases'
|
||||
default: false
|
||||
backup_now:
|
||||
type: boolean
|
||||
description: 'Whether to trigger backup immediately after creation'
|
||||
database_backup_retention_amount_locally:
|
||||
type: integer
|
||||
description: 'Number of backups to retain locally'
|
||||
database_backup_retention_days_locally:
|
||||
type: integer
|
||||
description: 'Number of days to retain backups locally'
|
||||
database_backup_retention_max_storage_locally:
|
||||
type: integer
|
||||
description: 'Max storage (MB) for local backups'
|
||||
database_backup_retention_amount_s3:
|
||||
type: integer
|
||||
description: 'Number of backups to retain in S3'
|
||||
database_backup_retention_days_s3:
|
||||
type: integer
|
||||
description: 'Number of days to retain backups in S3'
|
||||
database_backup_retention_max_storage_s3:
|
||||
type: integer
|
||||
description: 'Max storage (MB) for S3 backups'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
description: 'Backup configuration created successfully'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
uuid: { type: string, format: uuid, example: 550e8400-e29b-41d4-a716-446655440000 }
|
||||
message: { type: string, example: 'Backup configuration created successfully.' }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/databases/{uuid}':
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -3532,6 +3620,55 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/deployments/{uuid}/cancel':
|
||||
post:
|
||||
tags:
|
||||
- Deployments
|
||||
summary: Cancel
|
||||
description: 'Cancel a deployment by UUID.'
|
||||
operationId: cancel-deployment-by-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'Deployment UUID'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Deployment cancelled successfully.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Deployment cancelled successfully.' }
|
||||
deployment_uuid: { type: string, example: cm37r6cqj000008jm0veg5tkm }
|
||||
status: { type: string, example: cancelled-by-user }
|
||||
type: object
|
||||
'400':
|
||||
description: 'Deployment cannot be cancelled (already finished/failed/cancelled).'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Deployment cannot be cancelled. Current status: finished' }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'403':
|
||||
description: "User doesn't have permission to cancel this deployment."
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'You do not have permission to cancel this deployment.' }
|
||||
type: object
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/deploy:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -3631,6 +3768,29 @@ paths:
|
|||
-
|
||||
bearerAuth: []
|
||||
/github-apps:
|
||||
get:
|
||||
tags:
|
||||
- 'GitHub Apps'
|
||||
summary: List
|
||||
description: 'List all GitHub apps.'
|
||||
operationId: list-github-apps
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of GitHub apps.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
properties: { id: { type: integer }, uuid: { type: string }, name: { type: string }, organization: { type: string, nullable: true }, api_url: { type: string }, html_url: { type: string }, custom_user: { type: string }, custom_port: { type: integer }, app_id: { type: integer }, installation_id: { type: integer }, client_id: { type: string }, private_key_id: { type: integer }, is_system_wide: { type: boolean }, is_public: { type: boolean }, team_id: { type: integer }, type: { type: string } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- 'GitHub Apps'
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ @utility select {
|
|||
@apply w-full;
|
||||
@apply input-select;
|
||||
@apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23000000'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1rem 1rem;
|
||||
padding-right: 2.5rem;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23ffffff'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||
}
|
||||
}
|
||||
|
||||
@utility button {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden">
|
||||
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||
<livewire:switch-team />
|
||||
|
|
|
|||
|
|
@ -99,13 +99,11 @@
|
|||
<div class="relative">
|
||||
<button @click="dropdownOpen = !dropdownOpen"
|
||||
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
||||
title="Settings">
|
||||
<!-- Gear Icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Settings">
|
||||
title="Preferences">
|
||||
<!-- Sliders Icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Preferences">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
|
||||
<!-- Unread Count Badge -->
|
||||
|
|
|
|||
|
|
@ -1,43 +1,41 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
Terminal | Coolify
|
||||
</x-slot>
|
||||
<h1>Terminal</h1>
|
||||
<div class="flex gap-2 items-end subtitle">
|
||||
<div>Execute commands on your servers and containers without leaving the browser.</div>
|
||||
<x-helper
|
||||
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
|
||||
</div>
|
||||
<div x-init="$wire.loadContainers()">
|
||||
@if ($isLoadingContainers)
|
||||
<div class="pt-1">
|
||||
<x-loading text="Loading servers and containers..." />
|
||||
</div>
|
||||
@else
|
||||
@if ($servers->count() > 0)
|
||||
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
|
||||
wire:submit="$dispatchSelf('connectToContainer')">
|
||||
<x-forms.datalist id="selected_uuid" required wire:model.live="selected_uuid" placeholder="Search servers or containers...">
|
||||
@foreach ($servers as $server)
|
||||
@if ($loop->first)
|
||||
<option disabled value="default">Select a server or container</option>
|
||||
@endif
|
||||
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
||||
@foreach ($containers as $container)
|
||||
@if ($container['server_uuid'] == $server->uuid)
|
||||
<option value="{{ $container['uuid'] }}">
|
||||
{{ $server->name }} -> {{ $container['name'] }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-forms.datalist>
|
||||
<x-forms.button type="submit">Connect</x-forms.button>
|
||||
</form>
|
||||
</x-slot>
|
||||
<h1>Terminal</h1>
|
||||
<div class="flex gap-2 items-end subtitle">
|
||||
<div>Execute commands on your servers and containers without leaving the browser.</div>
|
||||
<x-helper
|
||||
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
|
||||
</div>
|
||||
<div x-init="$wire.loadContainers()">
|
||||
@if ($isLoadingContainers)
|
||||
<div class="pt-1">
|
||||
<x-loading text="Loading servers and containers..." />
|
||||
</div>
|
||||
@else
|
||||
<div>No servers with terminal access found.</div>
|
||||
@if ($servers->count() > 0)
|
||||
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
|
||||
wire:submit="$dispatchSelf('connectToContainer')">
|
||||
<x-forms.select id="selected_uuid" required wire:model.live="selected_uuid">
|
||||
<option value="default">Select a server or container</option>
|
||||
@foreach ($servers as $server)
|
||||
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
||||
@foreach ($containers as $container)
|
||||
@if ($container['server_uuid'] == $server->uuid)
|
||||
<option value="{{ $container['uuid'] }}">
|
||||
{{ $server->name }} -> {{ $container['name'] }}
|
||||
</option>
|
||||
@endif
|
||||
@endforeach
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
<x-forms.button type="submit">Connect</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
<div>No servers with terminal access found.</div>
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
<livewire:project.shared.terminal />
|
||||
</div>
|
||||
</div>
|
||||
<livewire:project.shared.terminal />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# ignore: true
|
||||
# documentation: https://min.io/docs/minio/container/index.html
|
||||
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
||||
# category: storage
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -227,3 +227,84 @@
|
|||
// Should only contain APP_URL pattern, not any FQDN
|
||||
expect($hosts2)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('skips host validation for terminal auth routes', function () {
|
||||
// These routes should be accessible with any Host header (for internal container communication)
|
||||
$response = $this->postJson('/terminal/auth', [], [
|
||||
'Host' => 'coolify:8080', // Internal Docker host
|
||||
]);
|
||||
|
||||
// Should not get 400 Bad Host (might get 401 Unauthorized instead)
|
||||
expect($response->status())->not->toBe(400);
|
||||
});
|
||||
|
||||
it('skips host validation for terminal auth ips route', function () {
|
||||
// These routes should be accessible with any Host header (for internal container communication)
|
||||
$response = $this->postJson('/terminal/auth/ips', [], [
|
||||
'Host' => 'soketi:6002', // Another internal Docker host
|
||||
]);
|
||||
|
||||
// Should not get 400 Bad Host (might get 401 Unauthorized instead)
|
||||
expect($response->status())->not->toBe(400);
|
||||
});
|
||||
|
||||
it('still enforces host validation for non-terminal routes', function () {
|
||||
InstanceSettings::updateOrCreate(
|
||||
['id' => 0],
|
||||
['fqdn' => 'https://coolify.example.com']
|
||||
);
|
||||
|
||||
// Regular routes should still validate Host header
|
||||
$response = $this->get('/', [
|
||||
'Host' => 'evil.com',
|
||||
]);
|
||||
|
||||
// Should get 400 Bad Host for untrusted host
|
||||
expect($response->status())->toBe(400);
|
||||
});
|
||||
|
||||
it('skips host validation for API routes', function () {
|
||||
// All API routes use token-based auth (Sanctum), not host validation
|
||||
// They should be accessible from any host (mobile apps, CLI tools, scripts)
|
||||
|
||||
// Test health check endpoint
|
||||
$response = $this->get('/api/health', [
|
||||
'Host' => 'internal-lb.local',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
|
||||
// Test v1 health check
|
||||
$response = $this->get('/api/v1/health', [
|
||||
'Host' => '10.0.0.5',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
|
||||
// Test feedback endpoint
|
||||
$response = $this->post('/api/feedback', [], [
|
||||
'Host' => 'mobile-app.local',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
});
|
||||
|
||||
it('skips host validation for webhook endpoints', function () {
|
||||
// All webhook routes are under /webhooks/* prefix (see RouteServiceProvider)
|
||||
// and use cryptographic signature validation instead of host validation
|
||||
|
||||
// Test GitHub webhook
|
||||
$response = $this->post('/webhooks/source/github/events', [], [
|
||||
'Host' => 'github-webhook-proxy.local',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
|
||||
// Test GitLab webhook
|
||||
$response = $this->post('/webhooks/source/gitlab/events/manual', [], [
|
||||
'Host' => 'gitlab.example.com',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
|
||||
// Test Stripe webhook
|
||||
$response = $this->post('/webhooks/payments/stripe/events', [], [
|
||||
'Host' => 'stripe-webhook-forwarder.local',
|
||||
]);
|
||||
expect($response->status())->not->toBe(400);
|
||||
});
|
||||
|
|
|
|||
63
tests/Unit/ApplicationDeploymentEmptyEnvTest.php
Normal file
63
tests/Unit/ApplicationDeploymentEmptyEnvTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Test to verify that empty .env files are created for build packs that require them.
|
||||
*
|
||||
* This test verifies the fix for the issue where deploying a Docker image without
|
||||
* environment variables would fail because Docker Compose expects a .env file
|
||||
* when env_file: ['.env'] is specified in the compose file.
|
||||
*
|
||||
* The fix ensures that for 'dockerimage' and 'dockercompose' build packs,
|
||||
* an empty .env file is created even when there are no environment variables defined.
|
||||
*/
|
||||
it('determines which build packs require empty .env file creation', function () {
|
||||
// Build packs that set env_file: ['.env'] in the generated compose file
|
||||
// and thus require an empty .env file even when no environment variables are defined
|
||||
$buildPacksRequiringEnvFile = ['dockerimage', 'dockercompose'];
|
||||
|
||||
// Build packs that don't use env_file in the compose file
|
||||
$buildPacksNotRequiringEnvFile = ['dockerfile', 'nixpacks', 'static'];
|
||||
|
||||
foreach ($buildPacksRequiringEnvFile as $buildPack) {
|
||||
// Verify the condition matches our fix
|
||||
$requiresEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||
expect($requiresEnvFile)->toBeTrue("Build pack '{$buildPack}' should require empty .env file");
|
||||
}
|
||||
|
||||
foreach ($buildPacksNotRequiringEnvFile as $buildPack) {
|
||||
// These build packs also use env_file but call save_runtime_environment_variables()
|
||||
// after generate_compose_file(), so they handle empty env files themselves
|
||||
$requiresEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||
expect($requiresEnvFile)->toBeFalse("Build pack '{$buildPack}' should not match the condition");
|
||||
}
|
||||
});
|
||||
|
||||
it('verifies dockerimage build pack is included in empty env file creation logic', function () {
|
||||
$buildPack = 'dockerimage';
|
||||
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||
|
||||
expect($shouldCreateEmptyEnvFile)->toBeTrue(
|
||||
'dockerimage build pack should create empty .env file when no environment variables are defined'
|
||||
);
|
||||
});
|
||||
|
||||
it('verifies dockercompose build pack is included in empty env file creation logic', function () {
|
||||
$buildPack = 'dockercompose';
|
||||
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||
|
||||
expect($shouldCreateEmptyEnvFile)->toBeTrue(
|
||||
'dockercompose build pack should create empty .env file when no environment variables are defined'
|
||||
);
|
||||
});
|
||||
|
||||
it('verifies other build packs are not included in empty env file creation logic', function () {
|
||||
$otherBuildPacks = ['dockerfile', 'nixpacks', 'static', 'buildpack'];
|
||||
|
||||
foreach ($otherBuildPacks as $buildPack) {
|
||||
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||
|
||||
expect($shouldCreateEmptyEnvFile)->toBeFalse(
|
||||
"Build pack '{$buildPack}' should not create empty .env file in save_runtime_environment_variables()"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.436"
|
||||
"version": "4.0.0-beta.438"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.437"
|
||||
"version": "4.0.0-beta.439"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
|
|
|
|||
Loading…
Reference in a new issue