diff --git a/.github/workflows/pr-quality.yaml b/.github/workflows/pr-quality.yaml
index 594724fdb..45a695ddc 100644
--- a/.github/workflows/pr-quality.yaml
+++ b/.github/workflows/pr-quality.yaml
@@ -40,7 +40,10 @@ jobs:
max-emoji-count: 2
max-code-references: 5
require-linked-issue: false
- blocked-terms: "STRAWBERRY"
+ blocked-terms: |
+ STRAWBERRY
+ ๐ค Generated with Claude Code
+ Generated with Claude Code
blocked-issue-numbers: 8154
# PR Template Checks
@@ -97,7 +100,7 @@ jobs:
exempt-pr-milestones: ""
# PR Success Actions
- success-add-pr-labels: "quality/verified"
+ success-add-pr-labels: ""
# PR Failure Actions
failure-remove-pr-labels: ""
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87e8ae806..8cd7287f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1190,7 +1190,118 @@ ### ๐ Features
- *(service)* Update autobase to version 2.5 (#7923)
- *(service)* Add chibisafe template (#5808)
- *(ui)* Improve sidebar menu items styling (#7928)
-- *(service)* Improve open-archiver
+- *(template)* Add open archiver template (#6593)
+- *(service)* Add linkding template (#6651)
+- *(service)* Add glip template (#7937)
+- *(templates)* Add Sessy docker compose template (#7951)
+- *(api)* Add update urls support to services api
+- *(api)* Improve service urls update
+- *(api)* Add url update support to services api (#7929)
+- *(api)* Improve docker_compose_domains
+- *(api)* Add more allowed fields
+- *(notifications)* Add mattermost notifications (#7963)
+- *(templates)* Add ElectricSQL docker compose template
+- *(service)* Add back soketi-app-manager
+- *(service)* Upgrade checkmate to v3 (#7995)
+- *(service)* Update pterodactyl version (#7981)
+- *(service)* Add langflow template (#8006)
+- *(service)* Upgrade listmonk to v6
+- *(service)* Add alexandrie template (#8021)
+- *(service)* Upgrade formbricks to v4 (#8022)
+- *(service)* Add goatcounter template (#8029)
+- *(installer)* Add tencentos as a supported os
+- *(installer)* Update nightly install script
+- Update pr template to remove unnecessary quote blocks
+- *(service)* Add satisfactory game server (#8056)
+- *(service)* Disable mautic (#8088)
+- *(service)* Add bento-pdf (#8095)
+- *(ui)* Add official postgres 18 support
+- *(database)* Add official postgres 18 support
+- *(ui)* Use 2 column layout
+- *(database)* Add official postgres 18 and pgvector 18 support (#8143)
+- *(ui)* Improve global search with uuid and pr support (#7901)
+- *(openclaw)* Add Openclaw service with environment variables and health checks
+- *(service)* Disable maybe
+- *(service)* Disable maybe (#8167)
+- *(service)* Add sure
+- *(service)* Add sure (#8157)
+- *(docker)* Install PHP sockets extension in development environment
+- *(services)* Add Spacebot service with custom logo support (#8427)
+- Expose scheduled tasks to API
+- *(api)* Add OpenAPI for managing scheduled tasks for applications and services
+- *(api)* Add delete endpoints for scheduled tasks in applications and services
+- *(api)* Add update endpoints for scheduled tasks in applications and services
+- *(api)* Add scheduled tasks CRUD API with auth and validation (#8428)
+- *(monitoring)* Add scheduled job monitoring dashboard (#8433)
+- *(service)* Disable plane
+- *(service)* Disable plane (#8580)
+- *(service)* Disable pterodactyl panel and pterodactyl wings
+- *(service)* Disable pterodactyl panel and pterodactyl wings (#8512)
+- *(service)* Upgrade beszel and beszel-agent to v0.18
+- *(service)* Upgrade beszel and beszel-agent to v0.18 (#8513)
+- Add command healthcheck type
+- Require health check command for 'cmd' type with backend validation and frontend update
+- *(healthchecks)* Add command health checks with input validation
+- *(healthcheck)* Add command-based health check support (#8612)
+- *(jobs)* Optimize async job dispatches and enhance Stripe subscription sync
+- *(jobs)* Add queue delay resilience to scheduled job execution
+- *(scheduler)* Add pagination to skipped jobs and filter manager start events
+- Add comment field to environment variables
+- Limit comment field to 256 characters for environment variables
+- Enhance environment variable handling to support mixed formats and add comprehensive tests
+- Add comment field to shared environment variables
+- Show comment field for locked environment variables
+- Add function to extract inline comments from docker-compose YAML environment variables
+- Add magic variable detection and update UI behavior accordingly
+- Add comprehensive environment variable parsing with nested resolution and hardcoded variable detection
+- *(models)* Add is_required to EnvironmentVariable fillable array
+- Add comment field to environment variables (#7269)
+- *(service)* Pydio-cells.yml
+- Pydio cells svg
+- Pydio-cells.yml pin to stable version
+- *(service)* Add Pydio cells (#8323)
+- *(service)* Disable minio community edition
+- *(service)* Disable minio community edition (#8686)
+- *(subscription)* Add Stripe server limit quantity adjustment flow
+- *(subscription)* Add refunds and cancellation management (#8637)
+- Add configurable timeout for public database TCP proxy
+- Add configurable proxy timeout for public database TCP proxy (#8673)
+- *(jobs)* Implement encrypted queue jobs
+- *(proxy)* Add database-backed config storage with disk backups
+- *(proxy)* Add database-backed config storage with disk backups (#8905)
+- *(livewire)* Add selectedActions parameter and error handling to delete methods
+- *(gitlab)* Add GitLab source integration with SSH and HTTP basic auth
+- *(git-sources)* Add GitLab integration and URL encode credentials (#8910)
+- *(server)* Add server metadata collection and display
+- *(git-import)* Support custom ssh command for fetch, submodule, and lfs
+- *(ui)* Add log filter based on log level
+- *(ui)* Add log filter based on log level (#8784)
+- *(seeders)* Add GitHub deploy key example application
+- *(service)* Update n8n-with-postgres-and-worker to 2.10.4 (#8807)
+- *(service)* Add container label escape control to services API
+- *(server)* Allow force deletion of servers with resources
+- *(server)* Allow force deletion of servers with resources (#8962)
+- *(compose-preview)* Populate fqdn from docker_compose_domains
+- *(compose-preview)* Populate fqdn from docker_compose_domains (#8963)
+- *(server)* Auto-fetch server metadata after validation
+- *(server)* Auto-fetch server metadata after validation (#8964)
+- *(templates)* Add imgcompress service, for offline image processing (#8763)
+- *(service)* Add librespeed (#8626)
+- *(service)* Update databasus to v3.16.2 (#8586)
+- *(preview)* Add configurable PR suffix toggle for volumes
+- *(api)* Add storages endpoints for applications
+- *(api)* Expand update_storage to support name, mount_path, host_path, content fields
+- *(environment-variable)* Add placeholder hint for magic variables
+- *(subscription)* Display next billing date and billing interval
+- *(api)* Support comments in bulk environment variable endpoints
+- *(api)* Add database environment variable management endpoints
+- *(storage)* Add resources tab and improve S3 deletion handling
+- *(storage)* Group backups by database and filter by s3 status
+- *(storage)* Add storage management for backup schedules
+- *(jobs)* Add cache-based deduplication for delayed cron execution
+- *(storage)* Add storage endpoints and UUID support for databases and services
+- *(monitoring)* Add Laravel Nightwatch monitoring support
+- *(validation)* Make hostname validation case-insensitive and expand allowed characters
### ๐ Bug Fixes
@@ -3773,6 +3884,7 @@ ### ๐ Bug Fixes
- *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management
- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6
- *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy
+- *(git)* Tracking issue due to case sensitivity
- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.6 and 4.0.0-beta.420.7
- *(scheduling)* Remove unnecessary padding from scheduled task form layout for improved UI consistency
- *(horizon)* Update queue configuration to use environment variable for dynamic queue management
@@ -3798,7 +3910,6 @@ ### ๐ Bug Fixes
- *(application)* Add option to suppress toast notifications when loading compose file
- *(git)* Tracking issue due to case sensitivity
- *(git)* Tracking issue due to case sensitivity
-- *(git)* Tracking issue due to case sensitivity
- *(ui)* Delete button width on small screens (#6308)
- *(service)* Matrix entrypoint
- *(ui)* Add flex-wrap to prevent overflow on small screens (#6307)
@@ -4422,6 +4533,197 @@ ### ๐ Bug Fixes
- *(api)* Deprecate applications compose endpoint
- *(api)* Applications post and patch endpoints
- *(api)* Applications create and patch endpoints (#7917)
+- *(service)* Sftpgo port
+- *(env)* Only cat .env file in dev
+- *(api)* Encoding checks (#7944)
+- *(env)* Only show nixpacks plan variables section in dev
+- Switch custom labels check to UTF-8
+- *(api)* One click service name and description cannot be set during creation
+- *(ui)* Improve volume mount warning for compose applications (#7947)
+- *(api)* Show an error if the same 2 urls are provided
+- *(preview)* Docker compose preview URLs (#7959)
+- *(api)* Check domain conflicts within the request
+- *(api)* Include docker_compose_domains in domain conflict check
+- *(api)* Is_static and docker network missing
+- *(api)* If domains field is empty clear the fqdn column
+- *(api)* Application endpoint issues part 2 (#7948)
+- Optimize queries and caching for projects and environments
+- *(perf)* Eliminate N+1 queries from InstanceSettings and Server lookups (#7966)
+- Update version numbers to 4.0.0-beta.462 and 4.0.0-beta.463
+- *(service)* Update seaweedfs logo (#7971)
+- *(service)* Soju svg
+- *(service)* Autobase database is not persisted correctly (#7978)
+- *(ui)* Make tooltips a bit wider
+- *(ui)* Modal issues
+- *(validation)* Add @, / and & support to names and descriptions
+- *(backup)* Postgres restore arithmetic syntax error (#7997)
+- *(service)* Users unable to create their first ente account without SMTP (#7986)
+- *(ui)* Horizontal overflow on application and service headings (#7970)
+- *(service)* Supabase studio settings redirect loop (#7828)
+- *(env)* Skip escaping for valid JSON in environment variables (#6160)
+- *(service)* Disable kong response buffering and increase timeouts (#7864)
+- *(service)* Rocketchat fails to start due to database version incompatibility (#7999)
+- *(service)* N8n v2 with worker timeout error
+- *(service)* Elasticsearch-with-kibana not generating account token
+- *(service)* Elasticsearch-with-kibana not generating account token (#8067)
+- *(service)* Kimai fails to start (#8027)
+- *(service)* Reactive-resume template (#8048)
+- *(api)* Infinite loop with github app with many repos (#8052)
+- *(env)* Skip escaping for valid JSON in environment variables (#8080)
+- *(docker)* Update PostgreSQL version to 16 in Dockerfile
+- *(validation)* Enforce url validation for instance domain (#8078)
+- *(service)* Bluesky pds invite code doesn't generate (#8081)
+- *(service)* Bugsink login fails due to cors (#8083)
+- *(service)* Strapi doesn't start (#8084)
+- *(service)* Activepieces postgres 18 volume mount (#8098)
+- *(service)* Forgejo login failure (#8145)
+- *(database)* Pgvector 18 version is not parsed properly
+- *(labels)* Make sure name is slugified
+- *(parser)* Replace dashes and dots in auto generated envs
+- Stop database proxy when is_public changes to false (#8138)
+- *(docs)* Update documentation link for Openclaw service
+- *(api-docs)* Use proper schema references for environment variable endpoints (#8239)
+- *(ui)* Fix datalist border color and add repository selection watcher (#8240)
+- *(server)* Improve IP uniqueness validation with team-specific error messages
+- *(jobs)* Initialize status variable in checkHetznerStatus (#8359)
+- *(jobs)* Handle queue timeouts gracefully in Horizon (#8360)
+- *(push-server-job)* Skip containers with empty service subId (#8361)
+- *(database)* Disable proxy on port allocation failure (#8362)
+- *(sentry)* Use withScope for SSH retry event tracking (#8363)
+- *(api)* Add a newline to openapi.json
+- *(server)* Improve IP uniqueness validation with team-specific error messages
+- *(service)* Glitchtip webdashboard doesn't load
+- *(service)* Glitchtip webdashboard doesn't load (#8249)
+- *(api)* Improve scheduled tasks API with auth, validation, and execution endpoints
+- *(api)* Improve scheduled tasks validation and delete logic
+- *(security)* Harden deployment paths and deploy abilities (#8549)
+- *(service)* Always enable force https labels
+- *(traefik)* Respect force https in service labels (#8550)
+- *(team)* Include webhook notifications in enabled check (#8557)
+- *(service)* Resolve team lookup via service relationship
+- *(service)* Resolve team lookup via service relationship (#8559)
+- *(database)* Chown redis/keydb configs when custom conf set (#8561)
+- *(version)* Update coolify version to 4.0.0-beta.464 and nightly version to 4.0.0-beta.465
+- *(applications)* Treat zero private_key_id as deploy key (#8563)
+- *(deploy)* Split BuildKit and secrets detection (#8565)
+- *(auth)* Prevent CSRF redirect loop during 2FA challenge (#8596)
+- *(input)* Prevent eye icon flash on password fields before Alpine.js loads (#8599)
+- *(api)* Correct permission requirements for POST endpoints (#8600)
+- *(health-checks)* Prevent command injection in health check commands (#8611)
+- *(auth)* Prevent cross-tenant IDOR in resource cloning (#8613)
+- *(docker)* Centralize command escaping in executeInDocker helper (#8615)
+- *(api)* Add team authorization to domains_by_server endpoint (#8616)
+- *(ca-cert)* Prevent command injection via base64 encoding (#8617)
+- *(scheduler)* Add self-healing for stale Redis locks and detection in UI (#8618)
+- *(health-checks)* Sanitize and validate CMD healthcheck commands
+- *(healthchecks)* Remove redundant newline sanitization from CMD healthcheck
+- *(soketi)* Make host binding configurable for IPv6 support (#8619)
+- *(ssh)* Automatically fix SSH directory permissions during upgrade (#8635)
+- *(jobs)* Prevent non-due jobs firing on restart and enrich skip logs with resource links
+- *(database)* Close confirmation modal after import/restore
+- Application rollback uses correct commit sha
+- *(rollback)* Escape commit SHA to prevent shell injection
+- Save comment field when creating application environment variables
+- Allow editing comments on locked environment variables
+- Add Update button for locked environment variable comments
+- Remove duplicate delete button from locked environment variable view
+- Position Update button next to comment field for locked variables
+- Preserve existing comments in bulk update and always show save notification
+- Update success message logic to only show when changes are made
+- *(bootstrap)* Add bounds check to extractBalancedBraceContent
+- Pydio-cells svg path typo
+- *(database)* Handle PDO constant name change for PGSQL_ATTR_DISABLE_PREPARES
+- *(proxy)* Handle IPv6 CIDR notation in Docker network gateways (#8703)
+- *(ssh)* Prevent RCE via SSH command injection (#8748)
+- *(service)* Cloudreve doesn't persist data across restarts
+- *(service)* Cloudreve doesn't persist data across restarts (#8740)
+- Join link should be set correctly in the env variables
+- *(service)* Ente photos join link doesn't work (#8727)
+- *(subscription)* Harden quantity updates and proxy trust behavior
+- *(auth)* Resolve 419 session errors with domain-based access and Cloudflare Tunnels (#8749)
+- *(server)* Handle limit edge case and IPv6 allowlist dedupe
+- *(server-limit)* Re-enable force-disabled servers at limit
+- *(ip-allowlist)* Add IPv6 CIDR support for API access restrictions (#8750)
+- *(proxy)* Remove ipv6 cidr network remediation
+- Address review feedback on proxy timeout
+- *(proxy)* Add validation and normalization for database proxy timeout
+- *(proxy)* Mounting error for nginx.conf in dev
+- Enable preview deployment page for deploy key applications
+- *(application-source)* Support localhost key with id=0
+- Enable preview deployment page for deploy key applications (#8579)
+- *(docker-compose)* Respect preserveRepository setting when executing start command (#8848)
+- *(proxy)* Mounting error for nginx.conf in dev (#8662)
+- *(database)* Close confirmation modal after database import/restore (#8697)
+- *(subscription)* Use optional chaining for preview object access
+- *(parser)* Use firstOrCreate instead of updateOrCreate for environment variables
+- *(env-parser)* Capture clean variable names without trailing braces in bash-style defaults (#8855)
+- *(terminal)* Resolve WebSocket connection and host authorization issues (#8862)
+- *(docker-cleanup)* Respect keep for rollback setting for Nixpacks build images (#8859)
+- *(push-server)* Track last_online_at and reset database restart state
+- *(docker)* Prevent false container exits on failed docker queries (#8860)
+- *(api)* Require write permission for validation endpoints
+- *(sentinel)* Add token validation to prevent command injection
+- *(log-drain)* Prevent command injection by base64-encoding environment variables
+- *(git-ref-validation)* Prevent command injection via git references
+- *(docker)* Add path validation to prevent command injection in file locations
+- Prevent command injection and fix developer view shared variables error (#8889)
+- Build-time environment variables break Next.js (#8890)
+- *(modal)* Make confirmation modal close after dispatching Livewire actions (#8892)
+- *(parser)* Preserve user-saved env vars on Docker Compose redeploy (#8894)
+- *(security)* Sanitize newlines in health check commands to prevent RCE (#8898)
+- Prevent scheduled task input fields from losing focus
+- Prevent scheduled task input fields from losing focus (#8654)
+- *(api)* Add docker_cleanup parameter to stop endpoints
+- *(api)* Add docker_cleanup parameter to stop endpoints (#8899)
+- *(deployment)* Filter null and empty environment variables from nixpacks plan
+- *(deployment)* Filter null and empty environment variables from nixpacks plan (#8902)
+- *(livewire)* Add error handling and selectedActions to delete methods (#8909)
+- *(parsers)* Use firstOrCreate instead of updateOrCreate for environment variables
+- *(parsers)* Use firstOrCreate instead of updateOrCreate for environment variables (#8915)
+- *(ssh)* Remove undefined trackSshRetryEvent() method call (#8927)
+- *(validation)* Support scoped packages in file path validation (#8928)
+- *(parsers)* Resolve shared variables in compose environment
+- *(parsers)* Resolve shared variables in compose environment (#8930)
+- *(api)* Cast teamId to int in deployment authorization check
+- *(api)* Cast teamId to int in deployment authorization check (#8931)
+- *(git-import)* Ensure ssh key is used for fetch, submodule, and lfs operations (#8933)
+- *(ui)* Info logs were not highlighted with blue color
+- *(application)* Clarify deployment type precedence logic
+- *(git-import)* Explicitly specify ssh key and remove duplicate validation rules
+- *(application)* Clarify deployment type precedence logic (#8934)
+- *(git)* GitHub App webhook endpoint defaults to IPv4 instead of the instance domain
+- *(git)* GitHub App webhook endpoint defaults to IPv4 instead of the instance domain (#8948)
+- *(service)* Hoppscotch fails to start due to db unhealthy
+- *(service)* Hoppscotch fails to start due to db unhealthy (#8949)
+- *(api)* Allow is_container_label_escape_enabled in service operations (#8955)
+- *(docker-compose)* Respect preserveRepository when injecting --project-directory
+- *(docker-compose)* Respect preserveRepository when injecting --project-directory (#8956)
+- *(compose)* Include git branch in compose file not found error
+- *(template)* Fix heyform template
+- *(template)* Fix heyform template (#8747)
+- *(preview)* Exclude bind mounts from preview deployment suffix
+- *(preview)* Sync isPreviewSuffixEnabled property on file storage save
+- *(storages)* Hide PR suffix for services and fix instantSave logic
+- *(preview)* Enable per-volume control of PR suffix in preview deployments (#9006)
+- Prevent sporadic SSH permission denied by validating key content
+- *(ssh)* Handle chmod failures gracefully and simplify key management
+- Prevent sporadic SSH permission denied on key rotation (#8990)
+- *(stripe)* Add error handling and resilience to subscription operations
+- *(stripe)* Add error handling and resilience to subscription operations (#9030)
+- *(api)* Extract resource UUIDs from route parameters
+- *(backup)* Throw explicit error when S3 storage missing or deleted (#9038)
+- *(docker)* Skip cleanup stale warning on cloud instances
+- *(deployment)* Disable build server during restart operations
+- *(deployment)* Disable build server during restart operations (#9045)
+- *(docker)* Log failed cleanup attempts when server is not functional
+- *(environment-variable)* Guard refresh against missing or stale variables
+- *(github-webhook)* Handle unsupported event types gracefully
+- *(github-webhook)* Handle unsupported event types gracefully (#9119)
+- *(deployment)* Properly escape shell arguments in nixpacks commands
+- *(deployment)* Properly escape shell arguments in nixpacks commands (#9122)
+- *(validation)* Make hostname validation case-insensitive and expand allowed name characters (#9134)
+- *(team)* Resolve server limit checks for API token authentication (#9123)
+- *(subscription)* Prevent duplicate subscriptions with updateOrCreate
### ๐ผ Other
@@ -4886,6 +5188,12 @@ ### ๐ผ Other
- CVE-2025-55182 React2shell infected supabase/studio:2025.06.02-sha-8f2993d
- Bump superset to 6.0.0
- Trim whitespace from domain input in instance settings (#7837)
+- Upgrade postgres client to fix build error
+- Application rollback uses correct commit sha (#8576)
+- *(deps)* Bump rollup from 4.57.1 to 4.59.0
+- *(deps)* Bump rollup from 4.57.1 to 4.59.0 (#8691)
+- *(deps)* Bump league/commonmark from 2.8.0 to 2.8.1
+- *(deps)* Bump league/commonmark from 2.8.0 to 2.8.1 (#8793)
### ๐ Refactor
@@ -5510,6 +5818,23 @@ ### ๐ Refactor
- Move all env sorting to one place
- *(api)* Make docker_compose_raw description more clear
- *(api)* Update application create endpoints docs
+- *(api)* Application urls validation
+- *(services)* Improve some service slogans
+- *(ssh-retry)* Remove Sentry tracking from retry logic
+- *(ssh-retry)* Remove Sentry tracking from retry logic
+- *(jobs)* Split task skip checks into critical and runtime phases
+- Add explicit fillable array to EnvironmentVariable model
+- Replace inline note with callout component for consistency
+- *(application-source)* Use Laravel helpers for null checks
+- *(ssh)* Remove Sentry retry event tracking from ExecuteRemoteCommand
+- Consolidate file path validation patterns and support scoped packages
+- *(environment-variable)* Remove buildtime/runtime options and improve comment field
+- Remove verbose logging and use explicit exception types
+- *(breadcrumb)* Optimize queries and simplify state management
+- *(scheduler)* Extract cron scheduling logic to shared helper
+- *(team)* Make server limit methods accept optional team parameter
+- *(team)* Update serverOverflow to use static serverLimit
+- *(docker)* Simplify installation and remove version pinning
### ๐ Documentation
@@ -5616,7 +5941,6 @@ ### ๐ Documentation
- Update changelog
- *(tests)* Update testing guidelines for unit and feature tests
- *(sync)* Create AI Instructions Synchronization Guide and update CLAUDE.md references
-- Update changelog
- *(database-patterns)* Add critical note on mass assignment protection for new columns
- Clarify cloud-init script compatibility
- Update changelog
@@ -5647,7 +5971,27 @@ ### ๐ Documentation
- Update application architecture and database patterns for request-level caching best practices
- Remove git worktree symlink instructions from CLAUDE.md
- Remove git worktree symlink instructions from CLAUDE.md (#7908)
+- Add transcript lol link and logo to readme (#7331)
+- *(api)* Change domains to urls
+- *(api)* Improve domains API docs
- Update changelog
+- Update changelog
+- *(api)* Improve app endpoint deprecation description
+- Add Coolify design system reference
+- Add Coolify design system reference (#8237)
+- Update changelog
+- Update changelog
+- Update changelog
+- *(sponsors)* Add huge sponsors section and reorganize list
+- *(application)* Add comments explaining commit selection logic for rollback support
+- *(readme)* Add VPSDime to Big Sponsors list
+- *(readme)* Move MVPS to Huge Sponsors section
+- *(settings)* Clarify Do Not Track helper text
+- Update changelog
+- Update changelog
+- *(sponsors)* Add ScreenshotOne as a huge sponsor
+- *(sponsors)* Update Brand.dev to Context.dev
+- *(readme)* Add PetroSky Cloud to sponsors
### โก Performance
@@ -5658,6 +6002,7 @@ ### โก Performance
- Remove dead server filtering code from Kernel scheduler (#7585)
- *(server)* Optimize destinationsByServer query
- *(server)* Optimize destinationsByServer query (#7854)
+- *(breadcrumb)* Optimize queries and simplify navigation to fix OOM (#9048)
### ๐จ Styling
@@ -5670,6 +6015,7 @@ ### ๐จ Styling
- *(campfire)* Format environment variables for better readability in Docker Compose file
- *(campfire)* Update comment for DISABLE_SSL environment variable for clarity
- Update background colors to use gray-50 for consistency in auth views
+- *(modal-confirmation)* Improve mobile responsiveness
### ๐งช Testing
@@ -5686,6 +6032,14 @@ ### ๐งช Testing
- Add tests for shared environment variable spacing and resolution
- Add comprehensive preview deployment port and path tests
- Add comprehensive preview deployment port and path tests (#7677)
+- Add Pest browser testing with SQLite :memory: schema
+- Add dashboard test and improve browser test coverage
+- Migrate to SQLite :memory: and add Pest browser testing (#8364)
+- *(rollback)* Use full-length git commit SHA values in test fixtures
+- *(rollback)* Verify shell metacharacter escaping in git commit parameter
+- *(factories)* Add missing model factories for app test suite
+- *(magic-variables)* Add feature tests for SERVICE_URL/FQDN variable handling
+- Add behavioral ssh key stale-file regression
### โ๏ธ Miscellaneous Tasks
@@ -6293,10 +6647,10 @@ ### โ๏ธ Miscellaneous Tasks
- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files
- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively
- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively
-- *(service)* Update Nitropage template (#6181)
-- *(versions)* Update all version
- *(bump)* Update composer deps
- *(version)* Bump Coolify version to 4.0.0-beta.420.6
+- *(service)* Update Nitropage template (#6181)
+- *(versions)* Update all version
- *(service)* Improve matrix service
- *(service)* Format runner service
- *(service)* Improve sequin
@@ -6399,6 +6753,94 @@ ### โ๏ธ Miscellaneous Tasks
- *(services)* Upgrade service template json files
- *(api)* Update openapi json and yaml
- *(api)* Regenerate openapi docs
+- Prepare for PR
+- *(api)* Improve current request error message
+- *(api)* Improve current request error message
+- *(api)* Update openapi files
+- *(service)* Update service templates json
+- *(services)* Update service template json files
+- *(service)* Use major version for openpanel (#8053)
+- Prepare for PR
+- *(services)* Update service template json files
+- Bump coolify version
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(scheduler)* Fix scheduled job duration metric (#8551)
+- Prepare for PR
+- Prepare for PR
+- *(horizon)* Make max time configurable (#8560)
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(ui)* Widen project heading nav spacing (#8564)
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Add pr quality check workflow
+- Do not build or generate changelog on pr-quality changes
+- Add pr quality check via anti slop action (#8344)
+- Improve pr quality workflow
+- Delete label removal workflow
+- Improve pr quality workflow (#8374)
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(repo)* Improve contributor PR template
+- Add anti-slop v0.2 options to the pr-quality check
+- Improve pr template and quality check workflow (#8574)
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(ui)* Add labels header
+- *(ui)* Add container labels header (#8752)
+- *(templates)* Update n8n templates to 2.10.2 (#8679)
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(version)* Bump coolify, realtime, and sentinel versions
+- *(realtime)* Upgrade npm dependencies
+- *(realtime)* Upgrade coolify-realtime to 1.0.11
+- Prepare for PR
+- Prepare for PR
+- Prepare for PR
+- *(release)* Bump version to 4.0.0-beta.466
+- Prepare for PR
+- Prepare for PR
+- *(service)* Pin castopod service to a static version instead of latest
+- *(service)* Remove unused attributes on imgcompress service
+- *(service)* Pin imgcompress to a static version instead of latest
+- *(service)* Update SeaweedFS images to version 4.13 (#8738)
+- *(templates)* Bump databasus image version
+- Remove coolify-examples-1 submodule
+- *(versions)* Bump coolify, sentinel, and traefik versions
+- *(versions)* Bump sentinel to 0.0.21
+- *(service)* Disable Booklore service (#9105)
### โ๏ธ Revert
diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php
index 9f97dd0d4..7ea6a871e 100644
--- a/app/Actions/Fortify/CreateNewUser.php
+++ b/app/Actions/Fortify/CreateNewUser.php
@@ -37,12 +37,13 @@ public function create(array $input): User
if (User::count() == 0) {
// If this is the first user, make them the root user
// Team is already created in the database/seeders/ProductionSeeder.php
- $user = User::create([
+ $user = (new User)->forceFill([
'id' => 0,
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
+ $user->save();
$team = $user->teams()->first();
// Disable registration after first user is created
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index 2e08ec6ad..8bb85c7fc 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -49,7 +49,7 @@ public function handle(Server $server)
}');
$found = StandaloneDocker::where('server_id', $server->id);
if ($found->count() == 0 && $server->id) {
- StandaloneDocker::create([
+ StandaloneDocker::forceCreate([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php
index 0a20deae5..22c48aa89 100644
--- a/app/Actions/Server/ValidateServer.php
+++ b/app/Actions/Server/ValidateServer.php
@@ -30,7 +30,8 @@ public function handle(Server $server)
]);
['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection();
if (! $this->uptime) {
- $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Error: '.$error.'
';
+ $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
+ $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Error: '.$sanitizedError.'
';
$server->update([
'validation_logs' => $this->error,
]);
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 6b5e1d4ac..17948d93b 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -40,10 +40,10 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);
- $network = $service->destination->network;
+ $safeNetwork = escapeshellarg($service->destination->network);
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
- $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
+ $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} {$safeNetwork} {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
diff --git a/app/Actions/Stripe/UpdateSubscriptionQuantity.php b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
index a3eab4dca..d4d29af20 100644
--- a/app/Actions/Stripe/UpdateSubscriptionQuantity.php
+++ b/app/Actions/Stripe/UpdateSubscriptionQuantity.php
@@ -4,6 +4,7 @@
use App\Jobs\ServerLimitCheckJob;
use App\Models\Team;
+use Stripe\Exception\InvalidRequestException;
use Stripe\StripeClient;
class UpdateSubscriptionQuantity
@@ -42,6 +43,7 @@ public function fetchPricePreview(Team $team, int $quantity): array
}
$currency = strtoupper($item->price->currency ?? 'usd');
+ $billingInterval = $item->price->recurring->interval ?? 'month';
// Upcoming invoice gives us the prorated amount due now
$upcomingInvoice = $this->stripe->invoices->upcoming([
@@ -99,6 +101,7 @@ public function fetchPricePreview(Team $team, int $quantity): array
'tax_description' => $taxDescription,
'quantity' => $quantity,
'currency' => $currency,
+ 'billing_interval' => $billingInterval,
],
];
} catch (\Exception $e) {
@@ -184,7 +187,7 @@ public function execute(Team $team, int $quantity): array
\Log::info("Subscription {$subscription->stripe_subscription_id} quantity updated to {$quantity} for team {$team->name}");
return ['success' => true, 'error' => null];
- } catch (\Stripe\Exception\InvalidRequestException $e) {
+ } catch (InvalidRequestException $e) {
\Log::error("Stripe update quantity error for team {$team->id}: ".$e->getMessage());
return ['success' => false, 'error' => 'Stripe error: '.$e->getMessage()];
diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php
index acc6dc2f9..7daa6ba28 100644
--- a/app/Console/Commands/Dev.php
+++ b/app/Console/Commands/Dev.php
@@ -30,32 +30,32 @@ public function init()
// Generate APP_KEY if not exists
if (empty(config('app.key'))) {
- echo "Generating APP_KEY.\n";
+ echo " INFO Generating APP_KEY.\n";
Artisan::call('key:generate');
}
// Generate STORAGE link if not exists
if (! file_exists(public_path('storage'))) {
- echo "Generating STORAGE link.\n";
+ echo " INFO Generating storage link.\n";
Artisan::call('storage:link');
}
// Seed database if it's empty
$settings = InstanceSettings::find(0);
if (! $settings) {
- echo "Initializing instance, seeding database.\n";
+ echo " INFO Initializing instance, seeding database.\n";
Artisan::call('migrate --seed');
} else {
- echo "Instance already initialized.\n";
+ echo " INFO Instance already initialized.\n";
}
// Clean up stuck jobs and stale locks on development startup
try {
- echo "Cleaning up Redis (stuck jobs and stale locks)...\n";
+ echo " INFO Cleaning up Redis (stuck jobs and stale locks)...\n";
Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]);
- echo "Redis cleanup completed.\n";
+ echo " INFO Redis cleanup completed.\n";
} catch (\Throwable $e) {
- echo "Error in cleanup:redis: {$e->getMessage()}\n";
+ echo " ERROR Redis cleanup failed: {$e->getMessage()}\n";
}
try {
@@ -66,10 +66,10 @@ public function init()
]);
if ($updatedTaskCount > 0) {
- echo "Marked {$updatedTaskCount} stuck scheduled task executions as failed\n";
+ echo " INFO Marked {$updatedTaskCount} stuck scheduled task executions as failed.\n";
}
} catch (\Throwable $e) {
- echo "Could not cleanup stuck scheduled task executions: {$e->getMessage()}\n";
+ echo " ERROR Could not clean up stuck scheduled task executions: {$e->getMessage()}\n";
}
try {
@@ -80,10 +80,10 @@ public function init()
]);
if ($updatedBackupCount > 0) {
- echo "Marked {$updatedBackupCount} stuck database backup executions as failed\n";
+ echo " INFO Marked {$updatedBackupCount} stuck database backup executions as failed.\n";
}
} catch (\Throwable $e) {
- echo "Could not cleanup stuck database backup executions: {$e->getMessage()}\n";
+ echo " ERROR Could not clean up stuck database backup executions: {$e->getMessage()}\n";
}
CheckHelperImageJob::dispatch();
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index 43ba06804..462155142 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -136,7 +136,7 @@ public function handle()
$application = Application::all()->first();
$preview = ApplicationPreview::all()->first();
if (! $preview) {
- $preview = ApplicationPreview::create([
+ $preview = ApplicationPreview::forceCreate([
'application_id' => $application->id,
'pull_request_id' => 1,
'pull_request_html_url' => 'http://example.com',
diff --git a/app/Console/Commands/Horizon.php b/app/Console/Commands/Horizon.php
deleted file mode 100644
index d3e35ca5a..000000000
--- a/app/Console/Commands/Horizon.php
+++ /dev/null
@@ -1,23 +0,0 @@
-info('Horizon is enabled on this server.');
- $this->call('horizon');
- exit(0);
- } else {
- exit(0);
- }
- }
-}
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 66cb77838..e95c29f72 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -212,18 +212,19 @@ private function cleanupUnusedNetworkFromCoolifyProxy()
$removeNetworks = $allNetworks->diff($networks);
$commands = collect();
foreach ($removeNetworks as $network) {
- $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
+ $safe = escapeshellarg($network);
+ $out = instant_remote_process(["docker network inspect -f json {$safe} | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
if (empty($out)) {
- $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
- $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
} else {
$data = collect(json_decode($out, true));
if ($data->count() === 1) {
// If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
$isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
if ($isCoolifyProxyItself) {
- $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
- $commands->push("docker network rm $network >/dev/null 2>&1 || true");
+ $commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
+ $commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
}
}
}
diff --git a/app/Console/Commands/Nightwatch.php b/app/Console/Commands/Nightwatch.php
deleted file mode 100644
index 40fd86a81..000000000
--- a/app/Console/Commands/Nightwatch.php
+++ /dev/null
@@ -1,22 +0,0 @@
-info('Nightwatch is enabled on this server.');
- $this->call('nightwatch:agent');
- }
-
- exit(0);
- }
-}
diff --git a/app/Console/Commands/Scheduler.php b/app/Console/Commands/Scheduler.php
deleted file mode 100644
index ee64368c3..000000000
--- a/app/Console/Commands/Scheduler.php
+++ /dev/null
@@ -1,23 +0,0 @@
-info('Scheduler is enabled on this server.');
- $this->call('schedule:work');
- exit(0);
- } else {
- exit(0);
- }
- }
-}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index ad1f50ea2..77f4e626f 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -230,6 +230,7 @@ public function applications(Request $request)
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -395,6 +396,7 @@ public function create_public_application(Request $request)
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -560,6 +562,7 @@ public function create_private_gh_app_application(Request $request)
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -1006,7 +1009,7 @@ private function create_application(Request $request, $type)
if ($return instanceof JsonResponse) {
return $return;
}
- $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
+ $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -1055,6 +1058,7 @@ private function create_application(Request $request, $type)
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
+ $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled',false);
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -1158,7 +1162,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
@@ -1267,6 +1271,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1385,7 +1393,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1499,6 +1507,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1585,7 +1597,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1695,6 +1707,10 @@ private function create_application(Request $request, $type)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
+ if (isset($isPreserveRepositoryEnabled)) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1772,7 +1788,7 @@ private function create_application(Request $request, $type)
}
$application = new Application;
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->ports_exposes = $port;
$application->build_pack = 'dockerfile';
@@ -1884,7 +1900,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
- $application->fill($request->all());
+ $application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->build_pack = 'dockerimage';
$application->destination_id = $destination->id;
@@ -2000,7 +2016,7 @@ private function create_application(Request $request, $type)
$service = new Service;
removeUnnecessaryFieldsFromRequest($request);
- $service->fill($request->all());
+ $service->fill($request->only($allowedFields));
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
@@ -2390,6 +2406,7 @@ public function delete_by_uuid(Request $request)
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
+ 'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
],
)
),
@@ -2475,7 +2492,7 @@ public function update_by_uuid(Request $request)
$this->authorize('update', $application);
$server = $application->destination->server;
- $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
+ $allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
$validationRules = [
'name' => 'string|max:255',
@@ -2722,7 +2739,7 @@ public function update_by_uuid(Request $request)
$connectToDockerNetwork = $request->connect_to_docker_network;
$useBuildServer = $request->use_build_server;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
-
+ $isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@@ -2757,10 +2774,13 @@ public function update_by_uuid(Request $request)
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
-
+ if ($request->has('is_preserve_repository_enabled')) {
+ $application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
+ $application->settings->save();
+ }
removeUnnecessaryFieldsFromRequest($request);
- $data = $request->all();
+ $data = $request->only($allowedFields);
if ($requestHasDomains && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains);
}
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 660ed4529..1b5cd0d44 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -264,6 +264,7 @@ public function database_by_uuid(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -327,7 +328,7 @@ public function database_by_uuid(Request $request)
)]
public function update_by_uuid(Request $request)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -344,6 +345,7 @@ public function update_by_uuid(Request $request)
'image' => 'string',
'is_public' => 'boolean',
'public_port' => 'numeric|nullable',
+ 'public_port_timeout' => 'integer|nullable|min:1',
'limits_memory' => 'string',
'limits_memory_swap' => 'string',
'limits_memory_swappiness' => 'numeric',
@@ -375,7 +377,7 @@ public function update_by_uuid(Request $request)
}
switch ($database->type()) {
case 'standalone-postgresql':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
$validator = customApiValidator($request->all(), [
'postgres_user' => 'string',
'postgres_password' => 'string',
@@ -406,20 +408,20 @@ public function update_by_uuid(Request $request)
}
break;
case 'standalone-clickhouse':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
$validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string',
]);
break;
case 'standalone-dragonfly':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
$validator = customApiValidator($request->all(), [
'dragonfly_password' => 'string',
]);
break;
case 'standalone-redis':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
$validator = customApiValidator($request->all(), [
'redis_password' => 'string',
'redis_conf' => 'string',
@@ -446,7 +448,7 @@ public function update_by_uuid(Request $request)
}
break;
case 'standalone-keydb':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
$validator = customApiValidator($request->all(), [
'keydb_password' => 'string',
'keydb_conf' => 'string',
@@ -473,7 +475,7 @@ public function update_by_uuid(Request $request)
}
break;
case 'standalone-mariadb':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
$validator = customApiValidator($request->all(), [
'mariadb_conf' => 'string',
'mariadb_root_password' => 'string',
@@ -503,7 +505,7 @@ public function update_by_uuid(Request $request)
}
break;
case 'standalone-mongodb':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
@@ -533,7 +535,7 @@ public function update_by_uuid(Request $request)
break;
case 'standalone-mysql':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
@@ -1068,6 +1070,7 @@ public function update_backup(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1135,6 +1138,7 @@ public function create_database_postgresql(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1201,6 +1205,7 @@ public function create_database_clickhouse(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1268,6 +1273,7 @@ public function create_database_dragonfly(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1335,6 +1341,7 @@ public function create_database_redis(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1405,6 +1412,7 @@ public function create_database_keydb(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1475,6 +1483,7 @@ public function create_database_mariadb(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1542,6 +1551,7 @@ public function create_database_mysql(Request $request)
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'],
'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'],
+ 'public_port_timeout' => ['type' => 'integer', 'description' => 'Public port timeout in seconds (default: 3600)'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
@@ -1580,7 +1590,7 @@ public function create_database_mongodb(Request $request)
public function create_database(Request $request, NewDatabaseTypes $type)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1670,6 +1680,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
'destination_uuid' => 'string',
'is_public' => 'boolean',
'public_port' => 'numeric|nullable',
+ 'public_port_timeout' => 'integer|nullable|min:1',
'limits_memory' => 'string',
'limits_memory_swap' => 'string',
'limits_memory_swappiness' => 'numeric',
@@ -1696,7 +1707,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
}
if ($type === NewDatabaseTypes::POSTGRESQL) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
$validator = customApiValidator($request->all(), [
'postgres_user' => 'string',
'postgres_password' => 'string',
@@ -1740,7 +1751,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('postgres_conf', $postgresConf);
}
- $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -1755,7 +1766,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MARIADB) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
$validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string',
@@ -1795,7 +1806,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mariadb_conf', $mariadbConf);
}
- $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -1811,7 +1822,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
@@ -1854,7 +1865,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mysql_conf', $mysqlConf);
}
- $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -1870,7 +1881,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::REDIS) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
$validator = customApiValidator($request->all(), [
'redis_password' => 'string',
'redis_conf' => 'string',
@@ -1910,7 +1921,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('redis_conf', $redisConf);
}
- $database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -1926,7 +1937,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::DRAGONFLY) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
$validator = customApiValidator($request->all(), [
'dragonfly_password' => 'string',
]);
@@ -1947,7 +1958,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
removeUnnecessaryFieldsFromRequest($request);
- $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -1956,7 +1967,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
'uuid' => $database->uuid,
]))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::KEYDB) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
$validator = customApiValidator($request->all(), [
'keydb_password' => 'string',
'keydb_conf' => 'string',
@@ -1996,7 +2007,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('keydb_conf', $keydbConf);
}
- $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -2012,7 +2023,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
$validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string',
@@ -2032,7 +2043,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
- $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@@ -2048,7 +2059,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
@@ -2090,7 +2101,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mongo_conf', $mongoConf);
}
- $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
+ $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 85d532f62..6ff06c10a 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -4,12 +4,15 @@
use App\Actions\Database\StartDatabase;
use App\Actions\Service\StartService;
+use App\Enums\ApplicationDeploymentStatus;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\Service;
use App\Models\Tag;
+use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
@@ -228,8 +231,8 @@ public function cancel_deployment(Request $request)
// Check if deployment can be cancelled (must be queued or in_progress)
$cancellableStatuses = [
- \App\Enums\ApplicationDeploymentStatus::QUEUED->value,
- \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ApplicationDeploymentStatus::QUEUED->value,
+ ApplicationDeploymentStatus::IN_PROGRESS->value,
];
if (! in_array($deployment->status, $cancellableStatuses)) {
@@ -246,11 +249,11 @@ public function cancel_deployment(Request $request)
// Mark deployment as cancelled
$deployment->update([
- 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
+ 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
// Get the server
- $server = Server::find($build_server_id);
+ $server = Server::whereTeamId($teamId)->find($build_server_id);
if ($server) {
// Add cancellation log entry
@@ -304,6 +307,8 @@ public function cancel_deployment(Request $request)
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'pr', in: 'query', description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.', schema: new OA\Schema(type: 'integer')),
+ new OA\Parameter(name: 'pull_request_id', in: 'query', description: 'Preview deployment identifier. Alias of pr.', schema: new OA\Schema(type: 'integer')),
+ new OA\Parameter(name: 'docker_tag', in: 'query', description: 'Docker image tag for Docker Image preview deployments. Requires pull_request_id.', schema: new OA\Schema(type: 'string')),
],
responses: [
@@ -354,7 +359,9 @@ public function deploy(Request $request)
$uuids = $request->input('uuid');
$tags = $request->input('tag');
$force = $request->input('force') ?? false;
- $pr = $request->input('pr') ? max((int) $request->input('pr'), 0) : 0;
+ $pullRequestId = $request->input('pull_request_id', $request->input('pr'));
+ $pr = $pullRequestId ? max((int) $pullRequestId, 0) : 0;
+ $dockerTag = $request->string('docker_tag')->trim()->value() ?: null;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
@@ -362,16 +369,22 @@ public function deploy(Request $request)
if ($tags && $pr) {
return response()->json(['message' => 'You can only use tag or pr, not both.'], 400);
}
+ if ($dockerTag && $pr === 0) {
+ return response()->json(['message' => 'docker_tag requires pull_request_id.'], 400);
+ }
+ if ($dockerTag && $tags) {
+ return response()->json(['message' => 'You can only use tag or docker_tag, not both.'], 400);
+ }
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
- return $this->by_uuids($uuids, $teamId, $force, $pr);
+ return $this->by_uuids($uuids, $teamId, $force, $pr, $dockerTag);
}
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
}
- private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0)
+ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $pr = 0, ?string $dockerTag = null)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
@@ -384,15 +397,22 @@ private function by_uuids(string $uuid, int $teamId, bool $force = false, int $p
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
+ $dockerTagForResource = $dockerTag;
if ($pr !== 0) {
- $preview = $resource->previews()->where('pull_request_id', $pr)->first();
+ $preview = null;
+ if ($resource instanceof Application && $resource->build_pack === 'dockerimage') {
+ $preview = $this->upsertDockerImagePreview($resource, $pr, $dockerTag);
+ $dockerTagForResource = $preview?->docker_registry_image_tag;
+ } else {
+ $preview = $resource->previews()->where('pull_request_id', $pr)->first();
+ }
if (! $preview) {
$deployments->push(['message' => "Pull request {$pr} not found for this resource.", 'resource_uuid' => $uuid]);
continue;
}
}
- $result = $this->deploy_resource($resource, $force, $pr);
+ $result = $this->deploy_resource($resource, $force, $pr, $dockerTagForResource);
if (isset($result['status']) && $result['status'] === 429) {
return response()->json(['message' => $result['message']], 429)->header('Retry-After', 60);
}
@@ -465,7 +485,7 @@ public function by_tags(string $tags, int $team_id, bool $force = false)
return response()->json(['message' => 'No resources found with this tag.'], 404);
}
- public function deploy_resource($resource, bool $force = false, int $pr = 0): array
+ public function deploy_resource($resource, bool $force = false, int $pr = 0, ?string $dockerTag = null): array
{
$message = null;
$deployment_uuid = null;
@@ -477,9 +497,12 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar
// Check authorization for application deployment
try {
$this->authorize('deploy', $resource);
- } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ } catch (AuthorizationException $e) {
return ['message' => 'Unauthorized to deploy this application.', 'deployment_uuid' => null];
}
+ 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;
$result = queue_application_deployment(
application: $resource,
@@ -487,6 +510,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar
force_rebuild: $force,
pull_request_id: $pr,
is_api: true,
+ docker_registry_image_tag: $dockerTag,
);
if ($result['status'] === 'queue_full') {
return ['message' => $result['message'], 'deployment_uuid' => null, 'status' => 429];
@@ -500,7 +524,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar
// Check authorization for service deployment
try {
$this->authorize('deploy', $resource);
- } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ } catch (AuthorizationException $e) {
return ['message' => 'Unauthorized to deploy this service.', 'deployment_uuid' => null];
}
StartService::run($resource);
@@ -510,7 +534,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar
// Database resource - check authorization
try {
$this->authorize('manage', $resource);
- } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
+ } catch (AuthorizationException $e) {
return ['message' => 'Unauthorized to start this database.', 'deployment_uuid' => null];
}
StartDatabase::dispatch($resource);
@@ -525,6 +549,34 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0): ar
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
+ private function upsertDockerImagePreview(Application $application, int $pullRequestId, ?string $dockerTag): ?ApplicationPreview
+ {
+ $preview = $application->previews()->where('pull_request_id', $pullRequestId)->first();
+
+ if (! $preview && $dockerTag === null) {
+ return null;
+ }
+
+ if (! $preview) {
+ $preview = ApplicationPreview::create([
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pullRequestId,
+ 'pull_request_html_url' => '',
+ 'docker_registry_image_tag' => $dockerTag,
+ ]);
+ $preview->generate_preview_fqdn();
+
+ return $preview;
+ }
+
+ if ($dockerTag !== null && $preview->docker_registry_image_tag !== $dockerTag) {
+ $preview->docker_registry_image_tag = $dockerTag;
+ $preview->save();
+ }
+
+ return $preview;
+ }
+
#[OA\Get(
summary: 'List application deployments',
description: 'List application deployments by using the app uuid',
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index da553a68c..c8638be0d 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Models\Project;
use App\Support\ValidationPatterns;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use OpenApi\Attributes as OA;
@@ -234,7 +235,7 @@ public function create_project(Request $request)
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
$validator = Validator::make($request->all(), [
@@ -257,7 +258,7 @@ public function create_project(Request $request)
], 422);
}
- $project = Project::create([
+ $project = Project::forceCreate([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,
@@ -347,7 +348,7 @@ public function update_project(Request $request)
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
$validator = Validator::make($request->all(), [
@@ -600,7 +601,7 @@ public function create_environment(Request $request)
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
$validator = Validator::make($request->all(), [
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index e7b36cb9a..2c62928c2 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
+use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
@@ -176,7 +177,7 @@ public function create_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
@@ -300,7 +301,7 @@ public function update_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
+ if ($return instanceof JsonResponse) {
return $return;
}
@@ -330,7 +331,7 @@ public function update_key(Request $request)
'message' => 'Private Key not found.',
], 404);
}
- $foundKey->update($request->all());
+ $foundKey->update($request->only($allowedFields));
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index fbf4b9e56..6a742fe1b 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -432,7 +432,7 @@ public function create_service(Request $request)
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($servicePayload, 'connect_to_docker_network', true);
}
- $service = Service::create($servicePayload);
+ $service = Service::forceCreate($servicePayload);
$service->name = $request->name ?? "$oneClickServiceName-".$service->uuid;
$service->description = $request->description;
if ($request->has('is_container_label_escape_enabled')) {
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index fd0282d96..03b36e4e0 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -14,14 +14,6 @@ private function removeSensitiveData($team)
'custom_server_limit',
'pivot',
]);
- if (request()->attributes->get('can_read_sensitive', false) === false) {
- $team->makeHidden([
- 'smtp_username',
- 'smtp_password',
- 'resend_api_key',
- 'telegram_token',
- ]);
- }
return serializeApiResponse($team);
}
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index 183186711..e59bc6ead 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -119,7 +119,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -128,7 +128,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'bitbucket',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index a9d65eae6..6ba4b33cf 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -144,7 +144,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -153,7 +153,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitea',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index 08e5d7162..fe4f17d9e 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -177,7 +177,7 @@ public function manual(Request $request)
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
@@ -186,7 +186,7 @@ public function manual(Request $request)
]);
$pr_app->generate_preview_fqdn_compose();
} else {
- $pr_app = ApplicationPreview::create([
+ $pr_app = ApplicationPreview::forceCreate([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 785e8c8e3..833e6bfe8 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -76,6 +76,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $dockerImageTag = null;
+ private ?string $dockerImagePreviewTag = null;
+
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
@@ -208,6 +210,7 @@ public function __construct(public int $application_deployment_queue_id)
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
+ $this->dockerImagePreviewTag = $this->application_deployment_queue->docker_registry_image_tag;
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
@@ -246,6 +249,9 @@ public function __construct(public int $application_deployment_queue_id)
// Set preview fqdn
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
+ if ($this->application->build_pack === 'dockerimage' && str($this->dockerImagePreviewTag)->isEmpty()) {
+ $this->dockerImagePreviewTag = $this->preview?->docker_registry_image_tag;
+ }
if ($this->preview) {
if ($this->application->build_pack === 'dockercompose') {
$this->preview->generate_preview_fqdn_compose();
@@ -288,7 +294,8 @@ public function handle(): void
// Make sure the private key is stored in the filesystem
$this->server->privateKey->storeInFileSystem();
// Generate custom host<->ip mapping
- $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $allContainers = instant_remote_process(["docker network inspect {$safeNetwork} -f '{{json .Containers}}' "], $this->server);
if (! is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
@@ -465,14 +472,14 @@ private function decide_what_to_do()
$this->just_restart();
return;
+ } elseif ($this->application->build_pack === 'dockerimage') {
+ $this->deploy_dockerimage_buildpack();
} elseif ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} elseif ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} elseif ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
- } elseif ($this->application->build_pack === 'dockerimage') {
- $this->deploy_dockerimage_buildpack();
} elseif ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} elseif ($this->application->build_pack === 'static') {
@@ -553,11 +560,7 @@ private function deploy_simple_dockerfile()
private function deploy_dockerimage_buildpack()
{
$this->dockerImage = $this->application->docker_registry_image_name;
- if (str($this->application->docker_registry_image_tag)->isEmpty()) {
- $this->dockerImageTag = 'latest';
- } else {
- $this->dockerImageTag = $this->application->docker_registry_image_tag;
- }
+ $this->dockerImageTag = $this->resolveDockerImageTag();
// Check if this is an image hash deployment
$isImageHash = str($this->dockerImageTag)->startsWith('sha256-');
@@ -574,6 +577,19 @@ private function deploy_dockerimage_buildpack()
$this->rolling_update();
}
+ private function resolveDockerImageTag(): string
+ {
+ if ($this->pull_request_id !== 0 && str($this->dockerImagePreviewTag)->isNotEmpty()) {
+ return $this->dockerImagePreviewTag;
+ }
+
+ if (str($this->application->docker_registry_image_tag)->isNotEmpty()) {
+ return $this->application->docker_registry_image_tag;
+ }
+
+ return 'latest';
+ }
+
private function deploy_docker_compose_buildpack()
{
if (data_get($this->application, 'docker_compose_location')) {
@@ -1933,6 +1949,11 @@ private function query_logs()
private function deploy_pull_request()
{
+ if ($this->application->build_pack === 'dockerimage') {
+ $this->deploy_dockerimage_buildpack();
+
+ return;
+ }
if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
@@ -2015,9 +2036,11 @@ private function prepare_builder_image(bool $firstTry = true)
$runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
if ($this->dockerConfigFileExists === 'OK') {
- $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
- $runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ $safeNetwork = escapeshellarg($this->destination->network);
+ $runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
}
if ($firstTry) {
@@ -3046,28 +3069,29 @@ private function build_image()
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
+ $safeNetwork = escapeshellarg($this->destination->network);
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
}
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args
if ($this->force_rebuild) {
- $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
} else {
- $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
+ $build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 7f1feaa21..a2d08e1e8 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -678,6 +678,7 @@ private function upload_to_s3(): void
} else {
$network = $this->database->destination->network;
}
+ $safeNetwork = escapeshellarg($network);
$fullImageName = $this->getFullImageName();
@@ -689,13 +690,13 @@ private function upload_to_s3(): void
if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
- $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
+ $commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
// Escape S3 credentials to prevent command injection
diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php
index 041cd812c..01a512439 100644
--- a/app/Jobs/ProcessGithubPullRequestWebhook.php
+++ b/app/Jobs/ProcessGithubPullRequestWebhook.php
@@ -118,7 +118,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
if (! $found) {
if ($application->build_pack === 'dockercompose') {
- $preview = ApplicationPreview::create([
+ $preview = ApplicationPreview::forceCreate([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,
@@ -127,7 +127,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
]);
$preview->generate_preview_fqdn_compose();
} else {
- $preview = ApplicationPreview::create([
+ $preview = ApplicationPreview::forceCreate([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $this->pullRequestId,
diff --git a/app/Jobs/SendWebhookJob.php b/app/Jobs/SendWebhookJob.php
index 607fda3fe..9d2a94606 100644
--- a/app/Jobs/SendWebhookJob.php
+++ b/app/Jobs/SendWebhookJob.php
@@ -9,6 +9,8 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Validator;
class SendWebhookJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -40,6 +42,20 @@ public function __construct(
*/
public function handle(): void
{
+ $validator = Validator::make(
+ ['webhook_url' => $this->webhookUrl],
+ ['webhook_url' => ['required', 'url', new \App\Rules\SafeWebhookUrl]]
+ );
+
+ if ($validator->fails()) {
+ Log::warning('SendWebhookJob: blocked unsafe webhook URL', [
+ 'url' => $this->webhookUrl,
+ 'errors' => $validator->errors()->all(),
+ ]);
+
+ return;
+ }
+
if (isDev()) {
ray('Sending webhook notification', [
'url' => $this->webhookUrl,
diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php
index 288904471..ee8cf2797 100644
--- a/app/Jobs/ValidateAndInstallServerJob.php
+++ b/app/Jobs/ValidateAndInstallServerJob.php
@@ -45,7 +45,8 @@ public function handle(): void
// Validate connection
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if (! $uptime) {
- $errorMessage = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Error: '.$error;
+ $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
+ $errorMessage = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Error: '.$sanitizedError;
$this->server->update([
'validation_logs' => $errorMessage,
'is_validating' => false,
@@ -197,7 +198,7 @@ public function handle(): void
]);
$this->server->update([
- 'validation_logs' => 'An error occurred during validation: '.$e->getMessage(),
+ 'validation_logs' => 'An error occurred during validation: '.htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
'is_validating' => false,
]);
}
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index b5f6d2929..d1345e7bf 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -6,7 +6,6 @@
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Cache;
use Livewire\Component;
class Index extends Component
@@ -22,16 +21,15 @@ class Index extends Component
public function mount()
{
if (! isCloud() && ! isDev()) {
- return redirect()->route('dashboard');
- }
- if (Auth::id() !== 0 && ! session('impersonating')) {
- return redirect()->route('dashboard');
+ abort(403);
}
+ $this->authorizeAdminAccess();
$this->getSubscribers();
}
public function back()
{
+ $this->authorizeAdminAccess();
if (session('impersonating')) {
session()->forget('impersonating');
$user = User::find(0);
@@ -45,6 +43,7 @@ public function back()
public function submitSearch()
{
+ $this->authorizeAdminAccess();
if ($this->search !== '') {
$this->foundUsers = User::where(function ($query) {
$query->where('name', 'like', "%{$this->search}%")
@@ -61,19 +60,33 @@ public function getSubscribers()
public function switchUser(int $user_id)
{
- if (Auth::id() !== 0) {
- return redirect()->route('dashboard');
- }
+ $this->authorizeRootOnly();
session(['impersonating' => true]);
$user = User::find($user_id);
+ if (! $user) {
+ abort(404);
+ }
$team_to_switch_to = $user->teams->first();
- // Cache::forget("team:{$user->id}");
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
+ private function authorizeAdminAccess(): void
+ {
+ if (! Auth::check() || (Auth::id() !== 0 && ! session('impersonating'))) {
+ abort(403);
+ }
+ }
+
+ private function authorizeRootOnly(): void
+ {
+ if (! Auth::check() || Auth::id() !== 0) {
+ abort(403);
+ }
+ }
+
public function render()
{
return view('livewire.admin.index');
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 0f6f45d83..170f0cdea 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -9,6 +9,7 @@
use App\Models\Team;
use App\Services\ConfigurationRepository;
use Illuminate\Support\Collection;
+use Livewire\Attributes\Url;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -19,18 +20,18 @@ class Index extends Component
'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
];
- #[\Livewire\Attributes\Url(as: 'step', history: true)]
+ #[Url(as: 'step', history: true)]
public string $currentState = 'welcome';
- #[\Livewire\Attributes\Url(keep: true)]
+ #[Url(keep: true)]
public ?string $selectedServerType = null;
public ?Collection $privateKeys = null;
- #[\Livewire\Attributes\Url(keep: true)]
+ #[Url(keep: true)]
public ?int $selectedExistingPrivateKey = null;
- #[\Livewire\Attributes\Url(keep: true)]
+ #[Url(keep: true)]
public ?string $privateKeyType = null;
public ?string $privateKey = null;
@@ -45,7 +46,7 @@ class Index extends Component
public ?Collection $servers = null;
- #[\Livewire\Attributes\Url(keep: true)]
+ #[Url(keep: true)]
public ?int $selectedExistingServer = null;
public ?string $remoteServerName = null;
@@ -66,7 +67,7 @@ class Index extends Component
public Collection $projects;
- #[\Livewire\Attributes\Url(keep: true)]
+ #[Url(keep: true)]
public ?int $selectedProject = null;
public ?Project $createdProject = null;
@@ -121,7 +122,7 @@ public function mount()
}
if ($this->selectedExistingServer) {
- $this->createdServer = Server::find($this->selectedExistingServer);
+ $this->createdServer = Server::ownedByCurrentTeam()->find($this->selectedExistingServer);
if ($this->createdServer) {
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
@@ -145,7 +146,7 @@ public function mount()
}
if ($this->selectedProject) {
- $this->createdProject = Project::find($this->selectedProject);
+ $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
if (! $this->createdProject) {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
}
@@ -431,13 +432,16 @@ public function getProjects()
public function selectExistingProject()
{
- $this->createdProject = Project::find($this->selectedProject);
+ $this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
+ if (! $this->createdProject) {
+ return $this->dispatch('error', 'Project not found.');
+ }
$this->currentState = 'create-resource';
}
public function createNewProject()
{
- $this->createdProject = Project::create([
+ $this->createdProject = Project::forceCreate([
'name' => 'My first project',
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 70751fa03..141235590 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -24,7 +24,7 @@ class Docker extends Component
#[Validate(['required', 'string'])]
public string $name;
- #[Validate(['required', 'string'])]
+ #[Validate(['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['required', 'string'])]
@@ -77,7 +77,7 @@ public function submit()
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
- $docker = SwarmDocker::create([
+ $docker = SwarmDocker::forceCreate([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,
@@ -88,7 +88,7 @@ public function submit()
if ($found) {
throw new \Exception('Network already added to this server.');
} else {
- $docker = StandaloneDocker::create([
+ $docker = StandaloneDocker::forceCreate([
'name' => $this->name,
'network' => $this->network,
'server_id' => $this->selectedServer->id,
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 98cf72376..f2cdad074 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -20,7 +20,7 @@ class Show extends Component
#[Validate(['string', 'required'])]
public string $name;
- #[Validate(['string', 'required'])]
+ #[Validate(['string', 'required', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['string', 'required'])]
@@ -84,8 +84,9 @@ public function delete()
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}
- instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
- instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
+ $safeNetwork = escapeshellarg($this->destination->network);
+ instant_remote_process(["docker network disconnect {$safeNetwork} coolify-proxy"], $this->destination->server, throwError: false);
+ instant_remote_process(["docker network rm -f {$safeNetwork}"], $this->destination->server);
}
$this->destination->delete();
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index f910110dc..154748b47 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -1203,7 +1203,7 @@ public function selectServer($serverId, $shouldProgress = true)
public function loadDestinations()
{
$this->loadingDestinations = true;
- $server = Server::find($this->selectedServerId);
+ $server = Server::ownedByCurrentTeam()->find($this->selectedServerId);
if (! $server) {
$this->loadingDestinations = false;
@@ -1280,7 +1280,7 @@ public function selectProject($projectUuid, $shouldProgress = true)
public function loadEnvironments()
{
$this->loadingEnvironments = true;
- $project = Project::where('uuid', $this->selectedProjectUuid)->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->selectedProjectUuid)->first();
if (! $project) {
$this->loadingEnvironments = false;
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index b914fbd94..ab3884320 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -5,6 +5,7 @@
use App\Models\DiscordNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
+use App\Rules\SafeWebhookUrl;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -20,7 +21,7 @@ class Discord extends Component
#[Validate(['boolean'])]
public bool $discordEnabled = false;
- #[Validate(['url', 'nullable'])]
+ #[Validate(['nullable', new SafeWebhookUrl])]
public ?string $discordWebhookUrl = null;
#[Validate(['boolean'])]
diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php
index fa8c97ae9..f870b3986 100644
--- a/app/Livewire/Notifications/Slack.php
+++ b/app/Livewire/Notifications/Slack.php
@@ -5,6 +5,7 @@
use App\Models\SlackNotificationSettings;
use App\Models\Team;
use App\Notifications\Test;
+use App\Rules\SafeWebhookUrl;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -25,7 +26,7 @@ class Slack extends Component
#[Validate(['boolean'])]
public bool $slackEnabled = false;
- #[Validate(['url', 'nullable'])]
+ #[Validate(['nullable', new SafeWebhookUrl])]
public ?string $slackWebhookUrl = null;
#[Validate(['boolean'])]
diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php
index 8af70c6eb..630d422a9 100644
--- a/app/Livewire/Notifications/Webhook.php
+++ b/app/Livewire/Notifications/Webhook.php
@@ -5,6 +5,7 @@
use App\Models\Team;
use App\Models\WebhookNotificationSettings;
use App\Notifications\Test;
+use App\Rules\SafeWebhookUrl;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -20,7 +21,7 @@ class Webhook extends Component
#[Validate(['boolean'])]
public bool $webhookEnabled = false;
- #[Validate(['url', 'nullable'])]
+ #[Validate(['nullable', new SafeWebhookUrl])]
public ?string $webhookUrl = null;
#[Validate(['boolean'])]
diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php
index 974f0608a..a2581a5c9 100644
--- a/app/Livewire/Project/AddEmpty.php
+++ b/app/Livewire/Project/AddEmpty.php
@@ -30,7 +30,7 @@ public function submit()
{
try {
$this->validate();
- $project = Project::create([
+ $project = Project::forceCreate([
'name' => $this->name,
'description' => $this->description,
'team_id' => currentTeam()->id,
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 5c186af70..6fd063cf3 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -146,9 +146,9 @@ protected function rules(): array
'gitRepository' => 'required',
'gitBranch' => 'required',
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
- 'installCommand' => 'nullable',
- 'buildCommand' => 'nullable',
- 'startCommand' => 'nullable',
+ 'installCommand' => ValidationPatterns::shellSafeCommandRules(),
+ 'buildCommand' => ValidationPatterns::shellSafeCommandRules(),
+ 'startCommand' => ValidationPatterns::shellSafeCommandRules(),
'buildPack' => 'required',
'staticImage' => 'required',
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
@@ -200,6 +200,9 @@ protected function messages(): array
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
+ 'installCommand.regex' => 'The install command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
+ 'buildCommand.regex' => 'The build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
+ 'startCommand.regex' => 'The start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
'preDeploymentCommandContainer.regex' => 'The pre-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
'postDeploymentCommandContainer.regex' => 'The post-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
'name.required' => 'The Name field is required.',
@@ -732,6 +735,7 @@ public function setRedirect()
$this->authorize('update', $this->application);
try {
+ $this->application->redirect = $this->redirect;
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.
Please add www to your domain list and as an A DNS record (if applicable).');
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 41f352c14..c61a4e4a7 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -35,8 +35,17 @@ class Previews extends Component
public array $previewFqdns = [];
+ public array $previewDockerTags = [];
+
+ public ?int $manualPullRequestId = null;
+
+ public ?string $manualDockerTag = null;
+
protected $rules = [
'previewFqdns.*' => 'string|nullable',
+ 'previewDockerTags.*' => 'string|nullable',
+ 'manualPullRequestId' => 'integer|min:1|nullable',
+ 'manualDockerTag' => 'string|nullable',
];
public function mount()
@@ -53,12 +62,17 @@ private function syncData(bool $toModel = false): void
$preview = $this->application->previews->get($key);
if ($preview) {
$preview->fqdn = $fqdn;
+ if ($this->application->build_pack === 'dockerimage') {
+ $preview->docker_registry_image_tag = $this->previewDockerTags[$key] ?? null;
+ }
}
}
} else {
$this->previewFqdns = [];
+ $this->previewDockerTags = [];
foreach ($this->application->previews as $key => $preview) {
$this->previewFqdns[$key] = $preview->fqdn;
+ $this->previewDockerTags[$key] = $preview->docker_registry_image_tag;
}
}
}
@@ -174,7 +188,7 @@ public function generate_preview($preview_id)
}
}
- public function add(int $pull_request_id, ?string $pull_request_html_url = null)
+ public function add(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
{
try {
$this->authorize('update', $this->application);
@@ -182,7 +196,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
$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)) {
- $found = ApplicationPreview::create([
+ $found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
@@ -195,13 +209,18 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
} else {
$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)) {
- $found = ApplicationPreview::create([
+ if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
+ $found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
+ 'pull_request_html_url' => $pull_request_html_url ?? '',
+ 'docker_registry_image_tag' => $docker_registry_image_tag,
]);
}
+ if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
+ $found->docker_registry_image_tag = $docker_registry_image_tag;
+ $found->save();
+ }
$found->generate_preview_fqdn();
$this->application->refresh();
$this->syncData(false);
@@ -217,37 +236,50 @@ public function force_deploy_without_cache(int $pull_request_id, ?string $pull_r
{
$this->authorize('deploy', $this->application);
- $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
+ $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);
}
- public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
+ 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);
- $this->add($pull_request_id, $pull_request_html_url);
- $this->deploy($pull_request_id, $pull_request_html_url);
+ $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);
}
- public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
+ 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->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)) {
- ApplicationPreview::create([
+ if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
+ $found = ApplicationPreview::forceCreate([
'application_id' => $this->application->id,
'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
+ 'pull_request_html_url' => $pull_request_html_url ?? '',
+ 'docker_registry_image_tag' => $docker_registry_image_tag,
]);
}
+ if ($found && $this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()) {
+ $found->docker_registry_image_tag = $docker_registry_image_tag;
+ $found->save();
+ }
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: $force_rebuild,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
+ docker_registry_image_tag: $docker_registry_image_tag,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
@@ -277,6 +309,32 @@ protected function setDeploymentUuid()
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
}
+ public function addDockerImagePreview()
+ {
+ $this->authorize('deploy', $this->application);
+ $this->validateOnly('manualPullRequestId');
+ $this->validateOnly('manualDockerTag');
+
+ if ($this->application->build_pack !== 'dockerimage') {
+ $this->dispatch('error', 'Manual Docker Image previews are only available for Docker Image applications.');
+
+ return;
+ }
+
+ if ($this->manualPullRequestId === null || str($this->manualDockerTag)->isEmpty()) {
+ $this->dispatch('error', 'Both pull request id and docker tag are required.');
+
+ return;
+ }
+
+ $dockerTag = str($this->manualDockerTag)->trim()->value();
+
+ $this->add_and_deploy($this->manualPullRequestId, null, $dockerTag);
+
+ $this->manualPullRequestId = null;
+ $this->manualDockerTag = null;
+ }
+
private function stopContainers(array $containers, $server)
{
$containersToStop = collect($containers)->pluck('Names')->toArray();
diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php
index 3b3e42619..93eb2a78c 100644
--- a/app/Livewire/Project/CloneMe.php
+++ b/app/Livewire/Project/CloneMe.php
@@ -54,7 +54,7 @@ protected function messages(): array
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
- $this->project = Project::where('uuid', $project_uuid)->firstOrFail();
+ $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()
@@ -100,7 +100,7 @@ public function clone(string $type)
if ($foundProject) {
throw new \Exception('Project with the same name already exists.');
}
- $project = Project::create([
+ $project = Project::forceCreate([
'name' => $this->newName,
'team_id' => currentTeam()->id,
'description' => $this->project->description.' (clone)',
@@ -139,7 +139,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@@ -187,7 +187,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@@ -216,7 +217,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@@ -229,7 +230,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@@ -247,7 +248,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill($payload);
+ ])->forceFill($payload);
$newEnvironmentVariable->save();
}
}
@@ -258,7 +259,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@@ -276,7 +277,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@@ -290,7 +291,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@@ -298,9 +299,9 @@ public function clone(string $type)
}
foreach ($newService->applications() as $application) {
- $application->update([
+ $application->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -315,7 +316,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -344,7 +346,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $application->id,
]);
$newStorage->save();
@@ -352,9 +354,9 @@ public function clone(string $type)
}
foreach ($newService->databases() as $database) {
- $database->update([
+ $database->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -369,7 +371,8 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -398,7 +401,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $database->id,
]);
$newStorage->save();
@@ -411,7 +414,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index 9de75c1c5..ffce8c9bd 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -34,9 +34,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public ?string $customDockerRunOptions = null;
@@ -81,7 +81,7 @@ protected function rules(): array
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
@@ -102,6 +102,8 @@ protected function messages(): array
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
@@ -119,8 +121,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->save();
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index d35e57a9d..2e6c9dca7 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -34,9 +34,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public ?string $customDockerRunOptions = null;
@@ -92,7 +92,7 @@ protected function rules(): array
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
@@ -112,6 +112,8 @@ protected function messages(): array
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
@@ -128,8 +130,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
@@ -276,8 +278,8 @@ public function regenerateSslCertificate()
}
SslHelper::generateSslCertificate(
- commonName: $existingCert->commonName,
- subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ commonName: $existingCert->common_name,
+ subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index adb4ccb5f..235e34e20 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -36,9 +36,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public ?string $customDockerRunOptions = null;
@@ -95,7 +95,7 @@ protected function rules(): array
'image' => 'required|string',
'portsMappings' => 'nullable|string',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'customDockerRunOptions' => 'nullable|string',
'dbUrl' => 'nullable|string',
@@ -117,6 +117,8 @@ protected function messages(): array
'image.required' => 'The Docker Image field is required.',
'image.string' => 'The Docker Image must be a string.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
@@ -134,8 +136,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
@@ -269,9 +271,20 @@ public function regenerateSslCertificate()
->where('is_ca_certificate', true)
->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
- commonName: $existingCert->commonName,
- subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ commonName: $existingCert->common_name,
+ subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 14240c82d..47e0fd091 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -42,9 +42,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
@@ -80,7 +80,7 @@ protected function rules(): array
'image' => 'required',
'portsMappings' => 'nullable',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
@@ -100,6 +100,8 @@ protected function messages(): array
'mariadbDatabase.required' => 'The MariaDB Database field is required.',
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
]
@@ -159,8 +161,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
@@ -289,6 +291,17 @@ public function regenerateSslCertificate()
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 11419ec71..6a3726371 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -40,9 +40,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
@@ -79,7 +79,7 @@ protected function rules(): array
'image' => 'required',
'portsMappings' => 'nullable',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
@@ -99,6 +99,8 @@ protected function messages(): array
'mongoInitdbDatabase.required' => 'The MongoDB Database field is required.',
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
@@ -158,8 +160,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
@@ -297,6 +299,17 @@ public function regenerateSslCertificate()
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 4f0f5eb19..750be4ce7 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -42,9 +42,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
@@ -82,7 +82,7 @@ protected function rules(): array
'image' => 'required',
'portsMappings' => 'nullable',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
@@ -103,6 +103,8 @@ protected function messages(): array
'mysqlDatabase.required' => 'The MySQL Database field is required.',
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.',
@@ -164,8 +166,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
@@ -301,6 +303,17 @@ public function regenerateSslCertificate()
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 4e044672b..8feb9bd22 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -46,9 +46,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
@@ -94,7 +94,7 @@ protected function rules(): array
'image' => 'required',
'portsMappings' => 'nullable',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
@@ -114,6 +114,8 @@ protected function messages(): array
'postgresDb.required' => 'The Postgres Database field is required.',
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.',
@@ -179,8 +181,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
@@ -264,6 +266,17 @@ public function regenerateSslCertificate()
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index ebe2f3ba0..e131bc598 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -34,9 +34,9 @@ class General extends Component
public ?bool $isPublic = null;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isLogDrainEnabled = false;
@@ -75,7 +75,7 @@ protected function rules(): array
'image' => 'required',
'portsMappings' => 'nullable',
'isPublic' => 'nullable|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isLogDrainEnabled' => 'nullable|boolean',
'customDockerRunOptions' => 'nullable',
@@ -93,6 +93,8 @@ protected function messages(): array
'name.required' => 'The Name field is required.',
'image.required' => 'The Docker Image field is required.',
'publicPort.integer' => 'The Public Port must be an integer.',
+ 'publicPort.min' => 'The Public Port must be at least 1.',
+ 'publicPort.max' => 'The Public Port must not exceed 65535.',
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
'redisUsername.required' => 'The Redis Username field is required.',
@@ -148,8 +150,8 @@ public function syncData(bool $toModel = false)
$this->database->image = $this->image;
$this->database->ports_mappings = $this->portsMappings;
$this->database->is_public = $this->isPublic;
- $this->database->public_port = $this->publicPort;
- $this->database->public_port_timeout = $this->publicPortTimeout;
+ $this->database->public_port = $this->publicPort ?: null;
+ $this->database->public_port_timeout = $this->publicPortTimeout ?: null;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->enable_ssl = $this->enableSsl;
@@ -282,9 +284,20 @@ public function regenerateSslCertificate()
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ if (! $caCert) {
+ $this->server->generateCaCertificate();
+ $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
+ }
+
+ if (! $caCert) {
+ $this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
+
+ return;
+ }
+
SslHelper::generateSslCertificate(
- commonName: $existingCert->commonName,
- subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
+ commonName: $existingCert->common_name,
+ subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index a018046fd..d95041c2d 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -21,7 +21,7 @@ class DeleteProject extends Component
public function mount()
{
$this->parameters = get_route_parameters();
- $this->projectName = Project::findOrFail($this->project_id)->name;
+ $this->projectName = Project::ownedByCurrentTeam()->findOrFail($this->project_id)->name;
}
public function delete()
@@ -29,7 +29,7 @@ public function delete()
$this->validate([
'project_id' => 'required|int',
]);
- $project = Project::findOrFail($this->project_id);
+ $project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
$this->authorize('delete', $project);
if ($project->isEmpty()) {
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 634a012c0..99fb2efc4 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -41,8 +41,8 @@ public function submit()
// Validate for command injection BEFORE saving to database
validateDockerComposeForInjection($this->dockerComposeRaw);
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
@@ -54,7 +54,7 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $service = Service::create([
+ $service = Service::forceCreate([
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 8aff83153..8becdf585 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -121,8 +121,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
// Append @sha256 to image name if using digest and not already present
$imageName = $parser->getFullImageNameWithoutTag();
@@ -133,7 +133,7 @@ public function submit()
// Determine the image tag based on whether it's a hash or regular tag
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
- $application = Application::create([
+ $application = Application::forceCreate([
'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php
index 0360365a9..1cdc7e098 100644
--- a/app/Livewire/Project/New/EmptyProject.php
+++ b/app/Livewire/Project/New/EmptyProject.php
@@ -10,7 +10,7 @@ class EmptyProject extends Component
{
public function createEmptyProject()
{
- $project = Project::create([
+ $project = Project::forceCreate([
'name' => generate_random_name(),
'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 61ae0e151..6aa8db085 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -8,6 +8,7 @@
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
+use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
@@ -168,7 +169,7 @@ public function submit()
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
- 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
+ 'docker_compose_location' => ValidationPatterns::filePathRules(),
]);
if ($validator->fails()) {
@@ -185,10 +186,10 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
- $application = Application::create([
+ $application = Application::forceCreate([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
'repository_project_id' => $this->selected_repository_id,
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index e46ad7d78..ba058c6ff 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -11,6 +11,7 @@
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
+use App\Support\ValidationPatterns;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
@@ -66,7 +67,7 @@ protected function rules()
'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
- 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
+ 'docker_compose_location' => ValidationPatterns::filePathRules(),
];
}
@@ -144,8 +145,8 @@ public function submit()
// Note: git_repository has already been validated and transformed in get_git_source()
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
@@ -182,7 +183,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
- $application = Application::create($application_init);
+ $application = Application::forceCreate($application_init);
$application->settings->is_static = $this->is_static;
$application->settings->save();
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 3df31a6a3..6bd71d246 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -11,6 +11,7 @@
use App\Models\SwarmDocker;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
+use App\Support\ValidationPatterns;
use Carbon\Carbon;
use Livewire\Component;
use Spatie\Url\Url;
@@ -72,7 +73,7 @@ protected function rules()
'publish_directory' => 'nullable|string',
'build_pack' => 'required|string',
'base_directory' => 'nullable|string',
- 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
+ 'docker_compose_location' => ValidationPatterns::filePathRules(),
'git_branch' => ['required', 'string', new ValidGitBranch],
];
}
@@ -233,7 +234,7 @@ private function getBranch()
return;
}
- if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) {
+ if ($this->git_source->getMorphClass() === GithubApp::class) {
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s');
$this->branchFound = true;
@@ -278,8 +279,8 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $project_uuid)->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
+ $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;
@@ -298,7 +299,7 @@ public function submit()
$new_service['source_id'] = $this->git_source->id;
$new_service['source_type'] = $this->git_source->getMorphClass();
}
- $service = Service::create($new_service);
+ $service = Service::forceCreate($new_service);
return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid,
@@ -345,7 +346,7 @@ public function submit()
$application_init['docker_compose_location'] = $this->docker_compose_location;
$application_init['base_directory'] = $this->base_directory;
}
- $application = Application::create($application_init);
+ $application = Application::forceCreate($application_init);
$application->settings->is_static = $this->isStatic;
$application->settings->save();
diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php
index c5dc13987..165e4b59e 100644
--- a/app/Livewire/Project/New/Select.php
+++ b/app/Livewire/Project/New/Select.php
@@ -65,7 +65,7 @@ public function mount()
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
- $project = Project::whereUuid($projectUuid)->firstOrFail();
+ $project = Project::ownedByCurrentTeam()->whereUuid($projectUuid)->firstOrFail();
$this->environments = $project->environments;
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
@@ -79,7 +79,7 @@ public function mount()
$this->type = $queryType;
$this->server_id = $queryServerId;
$this->destination_uuid = $queryDestination;
- $this->server = Server::find($queryServerId);
+ $this->server = Server::ownedByCurrentTeam()->find($queryServerId);
$this->current_step = 'select-postgresql-type';
}
} catch (\Exception $e) {
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index 9cc4fbbe2..400b58fea 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -45,14 +45,14 @@ public function submit()
}
$destination_class = $destination->getMorphClass();
- $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
- $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
+ $project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
+ $environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$port = get_port_from_dockerfile($this->dockerfile);
if (! $port) {
$port = 80;
}
- $application = Application::create([
+ $application = Application::forceCreate([
'name' => 'dockerfile-'.new Cuid2,
'repository_project_id' => 0,
'git_repository' => 'coollabsio/coolify',
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 966c66a14..dbe56b079 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -91,7 +91,7 @@ public function mount()
if (in_array($oneClickServiceName, NEEDS_TO_CONNECT_TO_PREDEFINED_NETWORK)) {
data_set($service_payload, 'connect_to_docker_network', true);
}
- $service = Service::create($service_payload);
+ $service = Service::forceCreate($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php
index c77a3a516..cb2d977bc 100644
--- a/app/Livewire/Project/Service/Index.php
+++ b/app/Livewire/Project/Service/Index.php
@@ -51,9 +51,9 @@ class Index extends Component
public bool $excludeFromStatus = false;
- public ?int $publicPort = null;
+ public mixed $publicPort = null;
- public ?int $publicPortTimeout = 3600;
+ public mixed $publicPortTimeout = 3600;
public bool $isPublic = false;
@@ -91,7 +91,7 @@ class Index extends Component
'description' => 'nullable',
'image' => 'required',
'excludeFromStatus' => 'required|boolean',
- 'publicPort' => 'nullable|integer',
+ 'publicPort' => 'nullable|integer|min:1|max:65535',
'publicPortTimeout' => 'nullable|integer|min:1',
'isPublic' => 'required|boolean',
'isLogDrainEnabled' => 'required|boolean',
@@ -160,8 +160,8 @@ private function syncDatabaseData(bool $toModel = false): void
$this->serviceDatabase->description = $this->description;
$this->serviceDatabase->image = $this->image;
$this->serviceDatabase->exclude_from_status = $this->excludeFromStatus;
- $this->serviceDatabase->public_port = $this->publicPort;
- $this->serviceDatabase->public_port_timeout = $this->publicPortTimeout;
+ $this->serviceDatabase->public_port = $this->publicPort ?: null;
+ $this->serviceDatabase->public_port_timeout = $this->publicPortTimeout ?: null;
$this->serviceDatabase->is_public = $this->isPublic;
$this->serviceDatabase->is_log_drain_enabled = $this->isLogDrainEnabled;
} else {
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index 22605e1bb..d0121bdc5 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -16,7 +16,9 @@
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
+use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Process;
+use Livewire\Attributes\Locked;
use Livewire\Component;
class GetLogs extends Component
@@ -29,12 +31,16 @@ class GetLogs extends Component
public string $errors = '';
+ #[Locked]
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
+ #[Locked]
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
+ #[Locked]
public Server $server;
+ #[Locked]
public ?string $container = null;
public ?string $displayName = null;
@@ -54,7 +60,7 @@ class GetLogs extends Component
public function mount()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
@@ -63,7 +69,7 @@ public function mount()
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
- if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource?->getMorphClass() === Application::class) {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
}
@@ -74,11 +80,11 @@ public function mount()
public function instantSave()
{
if (! is_null($this->resource)) {
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
}
- if ($this->resource->getMorphClass() === \App\Models\Service::class) {
+ if ($this->resource->getMorphClass() === Service::class) {
$serviceName = str($this->container)->beforeLast('-')->value();
$subType = $this->resource->applications()->where('name', $serviceName)->first();
if ($subType) {
@@ -118,10 +124,20 @@ public function toggleStreamLogs()
public function getLogs($refresh = false)
{
+ if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
+ $this->outputs = 'Unauthorized.';
+
+ return;
+ }
if (! $this->server->isFunctional()) {
return;
}
- if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
+ if ($this->container && ! ValidationPatterns::isValidContainerName($this->container)) {
+ $this->outputs = 'Invalid container name.';
+
+ return;
+ }
+ if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
return;
}
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
@@ -194,9 +210,15 @@ public function copyLogs(): string
public function downloadAllLogs(): string
{
+ if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
+ return '';
+ }
if (! $this->server->isFunctional() || ! $this->container) {
return '';
}
+ if (! ValidationPatterns::isValidContainerName($this->container)) {
+ return '';
+ }
if ($this->showTimeStamps) {
if ($this->server->isSwarm()) {
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index e769e4bcb..301c51be9 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -7,9 +7,18 @@
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
+use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
+use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
+use App\Models\StandaloneDragonfly;
+use App\Models\StandaloneKeydb;
+use App\Models\StandaloneMariadb;
+use App\Models\StandaloneMongodb;
+use App\Models\StandaloneMysql;
+use App\Models\StandalonePostgresql;
+use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@@ -60,7 +69,7 @@ public function cloneTo($destination_id)
$uuid = (string) new Cuid2;
$server = $new_destination->server;
- if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
$route = route('project.application.configuration', [
@@ -71,21 +80,21 @@ public function cloneTo($destination_id)
return redirect()->to($route);
} elseif (
- $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
+ $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([
+ ])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@@ -133,7 +142,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@@ -162,7 +172,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@@ -175,7 +185,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@@ -194,7 +204,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill($payload);
+ ])->forceFill($payload);
$newEnvironmentVariable->save();
}
@@ -211,7 +221,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@@ -232,7 +242,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@@ -246,7 +256,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ ])->forceFill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@@ -254,9 +264,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->applications() as $application) {
- $application->update([
+ $application->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -271,7 +281,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -296,9 +307,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->databases() as $database) {
- $database->update([
+ $database->forceFill([
'status' => 'exited',
- ]);
+ ])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -313,7 +324,8 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
- ])->fill([
+ 'uuid',
+ ])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -354,9 +366,9 @@ public function moveTo($environment_id)
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
- $this->resource->update([
+ $this->resource->forceFill([
'environment_id' => $environment_id,
- ]);
+ ])->save();
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 02c13a66c..882737f09 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -52,9 +52,15 @@ class Show extends Component
#[Locked]
public string $task_uuid;
- public function mount(string $task_uuid, string $project_uuid, string $environment_uuid, ?string $application_uuid = null, ?string $service_uuid = null)
+ public function mount()
{
try {
+ $task_uuid = request()->route('task_uuid');
+ $project_uuid = request()->route('project_uuid');
+ $environment_uuid = request()->route('environment_uuid');
+ $application_uuid = request()->route('application_uuid');
+ $service_uuid = request()->route('service_uuid');
+
$this->task_uuid = $task_uuid;
if ($application_uuid) {
$this->type = 'application';
@@ -105,6 +111,19 @@ public function syncData(bool $toModel = false)
}
}
+ public function toggleEnabled()
+ {
+ try {
+ $this->authorize('update', $this->resource);
+ $this->isEnabled = ! $this->isEnabled;
+ $this->task->enabled = $this->isEnabled;
+ $this->task->save();
+ $this->dispatch('success', $this->isEnabled ? 'Scheduled task enabled.' : 'Scheduled task disabled.');
+ } catch (\Exception $e) {
+ return handleError($e);
+ }
+ }
+
public function instantSave()
{
try {
diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php
index e884abb4e..b9628dd0d 100644
--- a/app/Livewire/Project/Show.php
+++ b/app/Livewire/Project/Show.php
@@ -42,7 +42,7 @@ public function submit()
{
try {
$this->validate();
- $environment = Environment::create([
+ $environment = Environment::forceCreate([
'name' => $this->name,
'project_id' => $this->project->id,
'uuid' => (string) new Cuid2,
diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php
index 117b43ad6..f41ca00f3 100644
--- a/app/Livewire/Server/Destinations.php
+++ b/app/Livewire/Server/Destinations.php
@@ -43,7 +43,7 @@ public function add($name)
return;
} else {
- SwarmDocker::create([
+ SwarmDocker::forceCreate([
'name' => $this->server->name.'-'.$name,
'network' => $this->name,
'server_id' => $this->server->id,
@@ -57,7 +57,7 @@ public function add($name)
return;
} else {
- StandaloneDocker::create([
+ StandaloneDocker::forceCreate([
'name' => $this->server->name.'-'.$name,
'network' => $name,
'server_id' => $this->server->id,
diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php
index fd55717fa..810b95ed4 100644
--- a/app/Livewire/Server/PrivateKey/Show.php
+++ b/app/Livewire/Server/PrivateKey/Show.php
@@ -63,7 +63,8 @@ public function checkConnection()
$this->dispatch('success', 'Server is reachable.');
$this->dispatch('refreshServerShow');
} else {
- $this->dispatch('error', 'Server is not reachable.
Error: '.$sanitizedError);
return;
}
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index 198d823b9..59ca4cd36 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -89,7 +89,8 @@ public function validateConnection()
$this->authorize('update', $this->server);
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) {
- $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Error: '.$error.'
';
+ $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
+ $this->error = 'Server is not reachable. Please validate your configuration and connection. Check this documentation for further help.
Check if you used the right extension (.yaml or .yml) in the compose file name.");
+ throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
if ($this->deploymentType() === 'deploy_key') {
- throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
+ throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
}
- throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
+ throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.');
}
- throw new \RuntimeException($e->getMessage());
+ throw new RuntimeException('Failed to read the Docker Compose file from the repository.');
} finally {
// Cleanup only - restoration happens in catch block
$commands = collect([
@@ -1793,7 +1878,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$this->base_directory = $initialBaseDirectory;
$this->save();
- throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
+ throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})
Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 34257e7a7..21cb58abe 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -16,6 +16,7 @@
'application_id' => ['type' => 'string'],
'deployment_uuid' => ['type' => 'string'],
'pull_request_id' => ['type' => 'integer'],
+ 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true],
'force_rebuild' => ['type' => 'boolean'],
'commit' => ['type' => 'string'],
'status' => ['type' => 'string'],
@@ -39,9 +40,35 @@
)]
class ApplicationDeploymentQueue extends Model
{
- protected $guarded = [];
+ protected $fillable = [
+ 'application_id',
+ 'deployment_uuid',
+ 'pull_request_id',
+ 'force_rebuild',
+ 'commit',
+ 'status',
+ 'is_webhook',
+ 'logs',
+ 'current_process_id',
+ 'restart_only',
+ 'git_type',
+ 'server_id',
+ 'application_name',
+ 'server_name',
+ 'deployment_url',
+ 'destination_id',
+ 'only_this_server',
+ 'rollback',
+ 'commit_message',
+ 'is_api',
+ 'build_server_id',
+ 'horizon_job_id',
+ 'horizon_job_worker',
+ 'finished_at',
+ ];
protected $casts = [
+ 'pull_request_id' => 'integer',
'finished_at' => 'datetime',
];
diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php
index b8a8a5a85..818f96d8e 100644
--- a/app/Models/ApplicationPreview.php
+++ b/app/Models/ApplicationPreview.php
@@ -10,7 +10,22 @@ class ApplicationPreview extends BaseModel
{
use SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'application_id',
+ 'pull_request_id',
+ 'pull_request_html_url',
+ 'pull_request_issue_comment_id',
+ 'fqdn',
+ 'status',
+ 'git_type',
+ 'docker_compose_domains',
+ 'docker_registry_image_tag',
+ 'last_online_at',
+ ];
+
+ protected $casts = [
+ 'pull_request_id' => 'integer',
+ ];
protected static function booted()
{
@@ -69,7 +84,7 @@ public function application()
public function persistentStorages()
{
- return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource');
+ return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function generate_preview_fqdn()
diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php
index f40977b3e..24b35df7f 100644
--- a/app/Models/ApplicationSetting.php
+++ b/app/Models/ApplicationSetting.php
@@ -28,7 +28,42 @@ class ApplicationSetting extends Model
'docker_images_to_keep' => 'integer',
];
- protected $guarded = [];
+ protected $fillable = [
+ 'is_static',
+ 'is_git_submodules_enabled',
+ 'is_git_lfs_enabled',
+ 'is_auto_deploy_enabled',
+ 'is_force_https_enabled',
+ 'is_debug_enabled',
+ 'is_preview_deployments_enabled',
+ 'is_log_drain_enabled',
+ 'is_gpu_enabled',
+ 'gpu_driver',
+ 'gpu_count',
+ 'gpu_device_ids',
+ 'gpu_options',
+ 'is_include_timestamps',
+ 'is_swarm_only_worker_nodes',
+ 'is_raw_compose_deployment_enabled',
+ 'is_build_server_enabled',
+ 'is_consistent_container_name_enabled',
+ 'is_gzip_enabled',
+ 'is_stripprefix_enabled',
+ 'connect_to_docker_network',
+ 'custom_internal_name',
+ 'is_container_label_escape_enabled',
+ 'is_env_sorting_enabled',
+ 'is_container_label_readonly_enabled',
+ 'is_preserve_repository_enabled',
+ 'disable_build_cache',
+ 'is_spa',
+ 'is_git_shallow_clone_enabled',
+ 'is_pr_deployments_public_enabled',
+ 'use_build_secrets',
+ 'inject_build_args_to_dockerfile',
+ 'include_source_commit_in_build',
+ 'docker_images_to_keep',
+ ];
public function isStatic(): Attribute
{
diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php
index 700ab0992..123376c9b 100644
--- a/app/Models/CloudProviderToken.php
+++ b/app/Models/CloudProviderToken.php
@@ -4,7 +4,11 @@
class CloudProviderToken extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'provider',
+ 'token',
+ 'name',
+ ];
protected $casts = [
'token' => 'encrypted',
diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php
index 23e1f0f12..e86598126 100644
--- a/app/Models/DiscordNotificationSettings.php
+++ b/app/Models/DiscordNotificationSettings.php
@@ -24,7 +24,8 @@ class DiscordNotificationSettings extends Model
'backup_failure_discord_notifications',
'scheduled_task_success_discord_notifications',
'scheduled_task_failure_discord_notifications',
- 'docker_cleanup_discord_notifications',
+ 'docker_cleanup_success_discord_notifications',
+ 'docker_cleanup_failure_discord_notifications',
'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications',
'server_unreachable_discord_notifications',
diff --git a/app/Models/DockerCleanupExecution.php b/app/Models/DockerCleanupExecution.php
index 405037e30..280277951 100644
--- a/app/Models/DockerCleanupExecution.php
+++ b/app/Models/DockerCleanupExecution.php
@@ -6,7 +6,13 @@
class DockerCleanupExecution extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'server_id',
+ 'status',
+ 'message',
+ 'cleanup_log',
+ 'finished_at',
+ ];
public function server(): BelongsTo
{
diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php
index ee31a49b6..1277e45d9 100644
--- a/app/Models/EmailNotificationSettings.php
+++ b/app/Models/EmailNotificationSettings.php
@@ -34,7 +34,11 @@ class EmailNotificationSettings extends Model
'backup_failure_email_notifications',
'scheduled_task_success_email_notifications',
'scheduled_task_failure_email_notifications',
+ 'docker_cleanup_success_email_notifications',
+ 'docker_cleanup_failure_email_notifications',
'server_disk_usage_email_notifications',
+ 'server_reachable_email_notifications',
+ 'server_unreachable_email_notifications',
'server_patch_email_notifications',
'traefik_outdated_email_notifications',
];
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index d4e614e6e..55ce93265 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -25,7 +25,10 @@ class Environment extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ ];
protected static function booted()
{
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index ab82c9a9c..3cffeb8f8 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -6,7 +6,25 @@
class GithubApp extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'organization',
+ 'api_url',
+ 'html_url',
+ 'custom_user',
+ 'custom_port',
+ 'app_id',
+ 'installation_id',
+ 'client_id',
+ 'client_secret',
+ 'webhook_secret',
+ 'is_system_wide',
+ 'is_public',
+ 'contents',
+ 'metadata',
+ 'pull_requests',
+ 'administration',
+ ];
protected $appends = ['type'];
@@ -92,7 +110,7 @@ public function type(): Attribute
{
return Attribute::make(
get: function () {
- if ($this->getMorphClass() === \App\Models\GithubApp::class) {
+ if ($this->getMorphClass() === GithubApp::class) {
return 'github';
}
},
diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php
index 2112a4a66..06df8fd8d 100644
--- a/app/Models/GitlabApp.php
+++ b/app/Models/GitlabApp.php
@@ -4,6 +4,24 @@
class GitlabApp extends BaseModel
{
+ protected $fillable = [
+ 'name',
+ 'organization',
+ 'api_url',
+ 'html_url',
+ 'custom_port',
+ 'custom_user',
+ 'is_system_wide',
+ 'is_public',
+ 'app_id',
+ 'app_secret',
+ 'oauth_id',
+ 'group_name',
+ 'public_key',
+ 'webhook_token',
+ 'deploy_key_id',
+ ];
+
protected $hidden = [
'webhook_token',
'app_secret',
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index ccc361d67..6061bc863 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -9,7 +9,43 @@
class InstanceSettings extends Model
{
- protected $guarded = [];
+ protected $fillable = [
+ 'public_ipv4',
+ 'public_ipv6',
+ 'fqdn',
+ 'public_port_min',
+ 'public_port_max',
+ 'do_not_track',
+ 'is_auto_update_enabled',
+ 'is_registration_enabled',
+ 'next_channel',
+ 'smtp_enabled',
+ 'smtp_from_address',
+ 'smtp_from_name',
+ 'smtp_recipients',
+ 'smtp_host',
+ 'smtp_port',
+ 'smtp_encryption',
+ 'smtp_username',
+ 'smtp_password',
+ 'smtp_timeout',
+ 'resend_enabled',
+ 'resend_api_key',
+ 'is_dns_validation_enabled',
+ 'custom_dns_servers',
+ 'instance_name',
+ 'is_api_enabled',
+ 'allowed_ips',
+ 'auto_update_frequency',
+ 'update_check_frequency',
+ 'new_version_available',
+ 'instance_timezone',
+ 'helper_version',
+ 'disable_two_step_confirmation',
+ 'is_sponsorship_popup_enabled',
+ 'dev_helper_version',
+ 'is_wire_navigate_enabled',
+ ];
protected $casts = [
'smtp_enabled' => 'boolean',
diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php
index b954a1dd5..4b5c602c2 100644
--- a/app/Models/LocalFileVolume.php
+++ b/app/Models/LocalFileVolume.php
@@ -20,7 +20,18 @@ class LocalFileVolume extends BaseModel
use HasFactory;
- protected $guarded = [];
+ protected $fillable = [
+ 'fs_path',
+ 'mount_path',
+ 'content',
+ 'resource_type',
+ 'resource_id',
+ 'is_directory',
+ 'chown',
+ 'chmod',
+ 'is_based_on_git',
+ 'is_preview_suffix_enabled',
+ ];
public $appends = ['is_binary'];
diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php
index 9d539f8ec..2f0f482b0 100644
--- a/app/Models/LocalPersistentVolume.php
+++ b/app/Models/LocalPersistentVolume.php
@@ -7,7 +7,15 @@
class LocalPersistentVolume extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'mount_path',
+ 'host_path',
+ 'container_id',
+ 'resource_type',
+ 'resource_id',
+ 'is_preview_suffix_enabled',
+ ];
protected $casts = [
'is_preview_suffix_enabled' => 'boolean',
diff --git a/app/Models/Project.php b/app/Models/Project.php
index ed1b415c1..ff2cae041 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -24,7 +24,10 @@ class Project extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ ];
/**
* Get query builder for projects owned by current team.
@@ -48,10 +51,10 @@ public static function ownedByCurrentTeamCached()
protected static function booted()
{
static::created(function ($project) {
- ProjectSetting::create([
+ ProjectSetting::forceCreate([
'project_id' => $project->id,
]);
- Environment::create([
+ Environment::forceCreate([
'name' => 'production',
'project_id' => $project->id,
'uuid' => (string) new Cuid2,
diff --git a/app/Models/ProjectSetting.php b/app/Models/ProjectSetting.php
index d93bea05b..7ea17ba7a 100644
--- a/app/Models/ProjectSetting.php
+++ b/app/Models/ProjectSetting.php
@@ -6,7 +6,7 @@
class ProjectSetting extends Model
{
- protected $guarded = [];
+ protected $fillable = [];
public function project()
{
diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php
index 189d05dd4..5ad617ad6 100644
--- a/app/Models/PushoverNotificationSettings.php
+++ b/app/Models/PushoverNotificationSettings.php
@@ -25,7 +25,8 @@ class PushoverNotificationSettings extends Model
'backup_failure_pushover_notifications',
'scheduled_task_success_pushover_notifications',
'scheduled_task_failure_pushover_notifications',
- 'docker_cleanup_pushover_notifications',
+ 'docker_cleanup_success_pushover_notifications',
+ 'docker_cleanup_failure_pushover_notifications',
'server_disk_usage_pushover_notifications',
'server_reachable_pushover_notifications',
'server_unreachable_pushover_notifications',
diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php
index f395a065c..d6feccc7e 100644
--- a/app/Models/S3Storage.php
+++ b/app/Models/S3Storage.php
@@ -12,7 +12,17 @@ class S3Storage extends BaseModel
{
use HasFactory, HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'region',
+ 'key',
+ 'secret',
+ 'bucket',
+ 'endpoint',
+ 'is_usable',
+ 'unusable_email_sent',
+ ];
protected $casts = [
'is_usable' => 'boolean',
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index 3ade21df8..c6aed863d 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -8,7 +8,25 @@
class ScheduledDatabaseBackup extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'description',
+ 'enabled',
+ 'save_s3',
+ 'frequency',
+ 'database_backup_retention_amount_locally',
+ 'database_type',
+ 'database_id',
+ 's3_storage_id',
+ 'databases_to_backup',
+ 'dump_all',
+ 'database_backup_retention_days_locally',
+ 'database_backup_retention_max_storage_locally',
+ 'database_backup_retention_amount_s3',
+ 'database_backup_retention_days_s3',
+ 'database_backup_retention_max_storage_s3',
+ 'timeout',
+ 'disable_local_backup',
+ ];
public static function ownedByCurrentTeam()
{
diff --git a/app/Models/ScheduledDatabaseBackupExecution.php b/app/Models/ScheduledDatabaseBackupExecution.php
index c0298ecc8..f1f6e88b5 100644
--- a/app/Models/ScheduledDatabaseBackupExecution.php
+++ b/app/Models/ScheduledDatabaseBackupExecution.php
@@ -6,7 +6,17 @@
class ScheduledDatabaseBackupExecution extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'status',
+ 'message',
+ 'size',
+ 'filename',
+ 'database_name',
+ 'finished_at',
+ 'local_storage_deleted',
+ 's3_storage_deleted',
+ 's3_uploaded',
+ ];
protected function casts(): array
{
diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php
index e771ce31e..e76f1b7b9 100644
--- a/app/Models/ScheduledTask.php
+++ b/app/Models/ScheduledTask.php
@@ -29,7 +29,14 @@ class ScheduledTask extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'enabled',
+ 'name',
+ 'command',
+ 'frequency',
+ 'container',
+ 'timeout',
+ ];
public static function ownedByCurrentTeamAPI(int $teamId)
{
diff --git a/app/Models/ScheduledTaskExecution.php b/app/Models/ScheduledTaskExecution.php
index c0601a4c9..dd74ba2e0 100644
--- a/app/Models/ScheduledTaskExecution.php
+++ b/app/Models/ScheduledTaskExecution.php
@@ -22,7 +22,15 @@
)]
class ScheduledTaskExecution extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'status',
+ 'message',
+ 'finished_at',
+ 'started_at',
+ 'retry_count',
+ 'duration',
+ 'error_details',
+ ];
protected function casts(): array
{
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 9237763c8..427896a19 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -34,6 +34,7 @@
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
+use Stevebauman\Purify\Facades\Purify;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
@@ -142,19 +143,19 @@ protected static function booted()
}
});
static::created(function ($server) {
- ServerSetting::create([
+ ServerSetting::forceCreate([
'server_id' => $server->id,
]);
if ($server->id === 0) {
if ($server->isSwarm()) {
- SwarmDocker::create([
+ SwarmDocker::forceCreate([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify-overlay',
'server_id' => $server->id,
]);
} else {
- StandaloneDocker::create([
+ StandaloneDocker::forceCreate([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
@@ -163,13 +164,14 @@ protected static function booted()
}
} else {
if ($server->isSwarm()) {
- SwarmDocker::create([
+ SwarmDocker::forceCreate([
'name' => 'coolify-overlay',
'network' => 'coolify-overlay',
'server_id' => $server->id,
]);
} else {
- $standaloneDocker = new StandaloneDocker([
+ $standaloneDocker = new StandaloneDocker;
+ $standaloneDocker->forceFill([
'name' => 'coolify',
'uuid' => (string) new Cuid2,
'network' => 'coolify',
@@ -265,10 +267,15 @@ public static function flushIdentityMap(): void
'server_metadata',
];
- protected $guarded = [];
-
use HasSafeStringAttribute;
+ public function setValidationLogsAttribute($value): void
+ {
+ $this->attributes['validation_logs'] = $value !== null
+ ? Purify::config('validation_logs')->clean($value)
+ : null;
+ }
+
public function type()
{
return 'server';
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index 504cfa60a..d34f2c86b 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -53,9 +53,53 @@
)]
class ServerSetting extends Model
{
- protected $guarded = [];
+ protected $fillable = [
+ 'is_swarm_manager',
+ 'is_jump_server',
+ 'is_build_server',
+ 'is_reachable',
+ 'is_usable',
+ 'wildcard_domain',
+ 'is_cloudflare_tunnel',
+ 'is_logdrain_newrelic_enabled',
+ 'logdrain_newrelic_license_key',
+ 'logdrain_newrelic_base_uri',
+ 'is_logdrain_highlight_enabled',
+ 'logdrain_highlight_project_id',
+ 'is_logdrain_axiom_enabled',
+ 'logdrain_axiom_dataset_name',
+ 'logdrain_axiom_api_key',
+ 'is_swarm_worker',
+ 'is_logdrain_custom_enabled',
+ 'logdrain_custom_config',
+ 'logdrain_custom_config_parser',
+ 'concurrent_builds',
+ 'dynamic_timeout',
+ 'force_disabled',
+ 'is_metrics_enabled',
+ 'generate_exact_labels',
+ 'force_docker_cleanup',
+ 'docker_cleanup_frequency',
+ 'docker_cleanup_threshold',
+ 'server_timezone',
+ 'delete_unused_volumes',
+ 'delete_unused_networks',
+ 'is_sentinel_enabled',
+ 'sentinel_token',
+ 'sentinel_metrics_refresh_rate_seconds',
+ 'sentinel_metrics_history_days',
+ 'sentinel_push_interval_seconds',
+ 'sentinel_custom_url',
+ 'server_disk_usage_notification_threshold',
+ 'is_sentinel_debug_enabled',
+ 'server_disk_usage_check_frequency',
+ 'is_terminal_enabled',
+ 'deployment_queue_limit',
+ 'disable_application_image_retention',
+ ];
protected $casts = [
+ 'force_disabled' => 'boolean',
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 84c047bb7..491924c49 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -15,6 +15,7 @@
use OpenApi\Attributes as OA;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
+use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@@ -47,7 +48,17 @@ class Service extends BaseModel
private static $parserVersion = '5';
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'docker_compose_raw',
+ 'docker_compose',
+ 'connect_to_docker_network',
+ 'service_type',
+ 'config_hash',
+ 'compose_parsing_version',
+ 'is_container_label_escape_enabled',
+ ];
protected $appends = ['server_status', 'status'];
@@ -1552,7 +1563,7 @@ public function saveComposeConfigs()
// Generate SERVICE_NAME_* environment variables from docker-compose services
if ($this->docker_compose) {
try {
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose);
+ $dockerCompose = Yaml::parse($this->docker_compose);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $_) {
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index 4bf78085e..e608c202d 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -5,12 +5,30 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Symfony\Component\Yaml\Yaml;
class ServiceApplication extends BaseModel
{
use HasFactory, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'human_name',
+ 'description',
+ 'fqdn',
+ 'ports',
+ 'exposes',
+ 'status',
+ 'exclude_from_status',
+ 'required_fqdn',
+ 'image',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'is_gzip_enabled',
+ 'is_stripprefix_enabled',
+ 'last_online_at',
+ 'is_migrated',
+ ];
protected static function booted()
{
@@ -211,7 +229,7 @@ public function getRequiredPort(): ?int
return $this->service->getRequiredPort();
}
- $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+ $dockerCompose = Yaml::parse($dockerComposeRaw);
$serviceConfig = data_get($dockerCompose, "services.{$this->name}");
if (! $serviceConfig) {
return $this->service->getRequiredPort();
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index c6a0143a8..e5b28d929 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -9,7 +9,27 @@ class ServiceDatabase extends BaseModel
{
use HasFactory, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'human_name',
+ 'description',
+ 'fqdn',
+ 'ports',
+ 'exposes',
+ 'status',
+ 'exclude_from_status',
+ 'image',
+ 'public_port',
+ 'is_public',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'is_gzip_enabled',
+ 'is_stripprefix_enabled',
+ 'last_online_at',
+ 'is_migrated',
+ 'custom_type',
+ 'public_port_timeout',
+ ];
protected $casts = [
'public_port_timeout' => 'integer',
diff --git a/app/Models/SharedEnvironmentVariable.php b/app/Models/SharedEnvironmentVariable.php
index 9bd42c328..158140b12 100644
--- a/app/Models/SharedEnvironmentVariable.php
+++ b/app/Models/SharedEnvironmentVariable.php
@@ -22,6 +22,9 @@ class SharedEnvironmentVariable extends Model
'is_multiline',
'is_literal',
'is_shown_once',
+
+ // Metadata
+ 'version',
];
protected $casts = [
diff --git a/app/Models/SlackNotificationSettings.php b/app/Models/SlackNotificationSettings.php
index 128b25221..d4f125fb5 100644
--- a/app/Models/SlackNotificationSettings.php
+++ b/app/Models/SlackNotificationSettings.php
@@ -24,7 +24,8 @@ class SlackNotificationSettings extends Model
'backup_failure_slack_notifications',
'scheduled_task_success_slack_notifications',
'scheduled_task_failure_slack_notifications',
- 'docker_cleanup_slack_notifications',
+ 'docker_cleanup_success_slack_notifications',
+ 'docker_cleanup_failure_slack_notifications',
'server_disk_usage_slack_notifications',
'server_reachable_slack_notifications',
'server_unreachable_slack_notifications',
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 143aadb6a..c6d91dd55 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -13,12 +13,39 @@ class StandaloneClickhouse extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'clickhouse_admin_user',
+ 'clickhouse_admin_password',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'custom_docker_run_options',
+ 'clickhouse_db',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
- 'clickhouse_password' => 'encrypted',
+ 'clickhouse_admin_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php
index 0407c2255..09dae022b 100644
--- a/app/Models/StandaloneDocker.php
+++ b/app/Models/StandaloneDocker.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
+use App\Support\ValidationPatterns;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -11,20 +12,33 @@ class StandaloneDocker extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'network',
+ ];
protected static function boot()
{
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
+ $safeNetwork = escapeshellarg($newStandaloneDocker->network);
instant_remote_process([
- "docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
+ "docker network inspect {$safeNetwork} >/dev/null 2>&1 || docker network create --driver overlay --attachable {$safeNetwork} >/dev/null",
], $server, false);
ConnectProxyToNetworksJob::dispatchSync($server);
});
}
+ public function setNetworkAttribute(string $value): void
+ {
+ if (! ValidationPatterns::isValidDockerNetwork($value)) {
+ throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
+ }
+
+ $this->attributes['network'] = $value;
+ }
+
public function applications()
{
return $this->morphMany(Application::class, 'destination');
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index c823c305b..af309f980 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -13,7 +13,33 @@ class StandaloneDragonfly extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'dragonfly_password',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index f286e8538..ee07b4783 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -13,7 +13,34 @@ class StandaloneKeydb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'keydb_password',
+ 'keydb_conf',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index efa62353c..ad5220496 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -14,7 +14,36 @@ class StandaloneMariadb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mariadb_root_password',
+ 'mariadb_user',
+ 'mariadb_password',
+ 'mariadb_database',
+ 'mariadb_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'is_log_drain_enabled',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 9418ebc21..590c173e1 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -13,7 +13,37 @@ class StandaloneMongodb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mongo_conf',
+ 'mongo_initdb_root_username',
+ 'mongo_initdb_root_password',
+ 'mongo_initdb_database',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'ssl_mode',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 2b7e9f2b6..d991617b7 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -13,7 +13,38 @@ class StandaloneMysql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'mysql_root_password',
+ 'mysql_user',
+ 'mysql_password',
+ 'mysql_database',
+ 'mysql_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'ssl_mode',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index cea600236..71034427f 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -13,7 +13,40 @@ class StandalonePostgresql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'postgres_user',
+ 'postgres_password',
+ 'postgres_db',
+ 'postgres_initdb_args',
+ 'postgres_host_auth_method',
+ 'postgres_conf',
+ 'init_scripts',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'ssl_mode',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 0e904ab31..4eb28e038 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -13,7 +13,33 @@ class StandaloneRedis extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'redis_conf',
+ 'status',
+ 'image',
+ 'is_public',
+ 'public_port',
+ 'ports_mappings',
+ 'limits_memory',
+ 'limits_memory_swap',
+ 'limits_memory_swappiness',
+ 'limits_memory_reservation',
+ 'limits_cpus',
+ 'limits_cpuset',
+ 'limits_cpu_shares',
+ 'started_at',
+ 'restart_count',
+ 'last_restart_at',
+ 'last_restart_type',
+ 'last_online_at',
+ 'public_port_timeout',
+ 'enable_ssl',
+ 'is_log_drain_enabled',
+ 'is_include_timestamps',
+ 'custom_docker_run_options',
+ ];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php
index 69d7cbf0d..fa135b29f 100644
--- a/app/Models/Subscription.php
+++ b/app/Models/Subscription.php
@@ -6,7 +6,18 @@
class Subscription extends Model
{
- protected $guarded = [];
+ protected $fillable = [
+ 'stripe_invoice_paid',
+ 'stripe_subscription_id',
+ 'stripe_customer_id',
+ 'stripe_cancel_at_period_end',
+ 'stripe_plan_id',
+ 'stripe_feedback',
+ 'stripe_comment',
+ 'stripe_trial_already_ended',
+ 'stripe_past_due',
+ 'stripe_refunded_at',
+ ];
protected function casts(): array
{
diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php
index 08be81970..656749119 100644
--- a/app/Models/SwarmDocker.php
+++ b/app/Models/SwarmDocker.php
@@ -2,9 +2,23 @@
namespace App\Models;
+use App\Support\ValidationPatterns;
+
class SwarmDocker extends BaseModel
{
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'network',
+ ];
+
+ public function setNetworkAttribute(string $value): void
+ {
+ if (! ValidationPatterns::isValidDockerNetwork($value)) {
+ throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
+ }
+
+ $this->attributes['network'] = $value;
+ }
public function applications()
{
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 3594d1072..9ee58cf7d 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -8,7 +8,9 @@ class Tag extends BaseModel
{
use HasSafeStringAttribute;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ ];
protected function customizeName($value)
{
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 5a7b377b6..8eb8fa050 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -40,7 +40,13 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
{
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'description',
+ 'personal_team',
+ 'show_boarding',
+ 'custom_server_limit',
+ ];
protected $casts = [
'personal_team' => 'boolean',
diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php
index 73889910e..4930f45d4 100644
--- a/app/Models/TelegramNotificationSettings.php
+++ b/app/Models/TelegramNotificationSettings.php
@@ -25,7 +25,8 @@ class TelegramNotificationSettings extends Model
'backup_failure_telegram_notifications',
'scheduled_task_success_telegram_notifications',
'scheduled_task_failure_telegram_notifications',
- 'docker_cleanup_telegram_notifications',
+ 'docker_cleanup_success_telegram_notifications',
+ 'docker_cleanup_failure_telegram_notifications',
'server_disk_usage_telegram_notifications',
'server_reachable_telegram_notifications',
'server_unreachable_telegram_notifications',
@@ -39,7 +40,8 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_backup_failure_thread_id',
'telegram_notifications_scheduled_task_success_thread_id',
'telegram_notifications_scheduled_task_failure_thread_id',
- 'telegram_notifications_docker_cleanup_thread_id',
+ 'telegram_notifications_docker_cleanup_success_thread_id',
+ 'telegram_notifications_docker_cleanup_failure_thread_id',
'telegram_notifications_server_disk_usage_thread_id',
'telegram_notifications_server_reachable_thread_id',
'telegram_notifications_server_unreachable_thread_id',
diff --git a/app/Models/User.php b/app/Models/User.php
index 4561cddb2..ad9a7af31 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,7 +4,9 @@
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail;
+use App\Notifications\TransactionalEmails\EmailChangeVerification;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
+use App\Services\ChangelogService;
use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +43,13 @@ class User extends Authenticatable implements SendsEmail
{
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
- protected $guarded = [];
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ 'force_password_reset',
+ 'marketing_emails',
+ ];
protected $hidden = [
'password',
@@ -87,7 +95,7 @@ protected static function boot()
$team['id'] = 0;
$team['name'] = 'Root Team';
}
- $new_team = Team::create($team);
+ $new_team = Team::forceCreate($team);
$user->teams()->attach($new_team, ['role' => 'owner']);
});
@@ -190,7 +198,7 @@ public function recreate_personal_team()
$team['id'] = 0;
$team['name'] = 'Root Team';
}
- $new_team = Team::create($team);
+ $new_team = Team::forceCreate($team);
$this->teams()->attach($new_team, ['role' => 'owner']);
return $new_team;
@@ -228,7 +236,7 @@ public function changelogReads()
public function getUnreadChangelogCount(): int
{
- return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
+ return app(ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
@@ -239,7 +247,7 @@ public function getRecipients(): array
public function sendVerificationEmail()
{
$mail = new MailMessage;
- $url = Url::temporarySignedRoute(
+ $url = URL::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
@@ -395,20 +403,20 @@ public function canAccessSystemResources(): bool
public function requestEmailChange(string $newEmail): void
{
// Generate 6-digit code
- $code = sprintf('%06d', mt_rand(0, 999999));
+ $code = sprintf('%06d', random_int(0, 999999));
// Set expiration using config value
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
- $this->update([
+ $this->forceFill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
- ]);
+ ])->save();
// Send verification email to new address
- $this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt));
+ $this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt));
}
public function isEmailChangeCodeValid(string $code): bool
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index 7a1f7bcbf..4b56f5860 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -40,7 +40,7 @@ public function toDiscord(): DiscordMessage
color: DiscordMessage::errorColor(),
);
- $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)');
+ $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscription)');
return $message;
}
@@ -48,7 +48,7 @@ public function toDiscord(): DiscordMessage
public function toTelegram(): array
{
return [
- 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
+ 'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscription).",
];
}
@@ -57,7 +57,7 @@ public function toPushover(): PushoverMessage
return new PushoverMessage(
title: 'Server disabled',
level: 'error',
- message: "Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped. Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
+ message: "Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped. Please update your subscription to enable the server again [here](https://app.coolify.io/subscription).",
);
}
@@ -66,7 +66,7 @@ public function toSlack(): SlackMessage
$title = 'Server disabled';
$description = "Server ({$this->server->name}) disabled because it is not paid!\n";
$description .= "All automations and integrations are stopped.\n\n";
- $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscriptions';
+ $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscription';
return new SlackMessage(
title: $title,
diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php
index 179c8d948..511818e21 100644
--- a/app/Notifications/TransactionalEmails/ResetPassword.php
+++ b/app/Notifications/TransactionalEmails/ResetPassword.php
@@ -67,9 +67,12 @@ protected function resetUrl($notifiable)
return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
}
- return url(route('password.reset', [
+ $path = route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
- ], false));
+ ], false);
+
+ // Use server-side config (FQDN / public IP) instead of request host
+ return rtrim(base_url(), '/').$path;
}
}
diff --git a/app/Rules/SafeWebhookUrl.php b/app/Rules/SafeWebhookUrl.php
new file mode 100644
index 000000000..fbeb406af
--- /dev/null
+++ b/app/Rules/SafeWebhookUrl.php
@@ -0,0 +1,95 @@
+ $attribute,
+ 'host' => $host,
+ 'ip' => request()->ip(),
+ 'user_id' => auth()->id(),
+ ]);
+ $fail('The :attribute must not point to localhost or internal hosts.');
+
+ return;
+ }
+
+ // Block loopback (127.0.0.0/8) and link-local/metadata (169.254.0.0/16) when IP is provided directly
+ if (filter_var($host, FILTER_VALIDATE_IP) && ($this->isLoopback($host) || $this->isLinkLocal($host))) {
+ Log::warning('Webhook URL points to blocked IP range', [
+ 'attribute' => $attribute,
+ 'host' => $host,
+ 'ip' => request()->ip(),
+ 'user_id' => auth()->id(),
+ ]);
+ $fail('The :attribute must not point to loopback or link-local addresses.');
+
+ return;
+ }
+ }
+
+ private function isLoopback(string $ip): bool
+ {
+ // 127.0.0.0/8, 0.0.0.0
+ if ($ip === '0.0.0.0' || str_starts_with($ip, '127.')) {
+ return true;
+ }
+
+ // IPv6 loopback
+ $normalized = @inet_pton($ip);
+
+ return $normalized !== false && $normalized === inet_pton('::1');
+ }
+
+ private function isLinkLocal(string $ip): bool
+ {
+ // 169.254.0.0/16 โ covers cloud metadata at 169.254.169.254
+ if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+ return false;
+ }
+
+ $long = ip2long($ip);
+
+ return $long !== false && ($long >> 16) === (ip2long('169.254.0.0') >> 16);
+ }
+}
diff --git a/app/Support/ValidationPatterns.php b/app/Support/ValidationPatterns.php
index 7084b4cc2..cec607f4e 100644
--- a/app/Support/ValidationPatterns.php
+++ b/app/Support/ValidationPatterns.php
@@ -58,6 +58,13 @@ class ValidationPatterns
*/
public const CONTAINER_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
+ /**
+ * Pattern for Docker network names
+ * Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores
+ * Matches Docker's network naming rules and prevents shell injection
+ */
+ public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
+
/**
* Get validation rules for name fields
*/
@@ -210,6 +217,44 @@ public static function isValidContainerName(string $name): bool
return preg_match(self::CONTAINER_NAME_PATTERN, $name) === 1;
}
+ /**
+ * Get validation rules for Docker network name fields
+ */
+ public static function dockerNetworkRules(bool $required = true, int $maxLength = 255): array
+ {
+ $rules = [];
+
+ if ($required) {
+ $rules[] = 'required';
+ } else {
+ $rules[] = 'nullable';
+ }
+
+ $rules[] = 'string';
+ $rules[] = "max:$maxLength";
+ $rules[] = 'regex:'.self::DOCKER_NETWORK_PATTERN;
+
+ return $rules;
+ }
+
+ /**
+ * Get validation messages for Docker network name fields
+ */
+ public static function dockerNetworkMessages(string $field = 'network'): array
+ {
+ return [
+ "{$field}.regex" => 'The network name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.',
+ ];
+ }
+
+ /**
+ * Check if a string is a valid Docker network name.
+ */
+ public static function isValidDockerNetwork(string $name): bool
+ {
+ return preg_match(self::DOCKER_NETWORK_PATTERN, $name) === 1;
+ }
+
/**
* Get combined validation messages for both name and description fields
*/
diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php
index ec42761f7..c10ed6158 100644
--- a/bootstrap/helpers/api.php
+++ b/bootstrap/helpers/api.php
@@ -95,9 +95,9 @@ function sharedDataApplications()
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
'docker_registry_image_name' => 'string|nullable',
'docker_registry_image_tag' => 'string|nullable',
- 'install_command' => 'string|nullable',
- 'build_command' => 'string|nullable',
- 'start_command' => 'string|nullable',
+ 'install_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
+ 'build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
+ 'start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
'custom_network_aliases' => 'string|nullable',
@@ -144,6 +144,7 @@ function sharedDataApplications()
'docker_compose_custom_start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
'docker_compose_custom_build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
'is_container_label_escape_enabled' => 'boolean',
+ 'is_preserve_repository_enabled' => 'boolean'
];
}
@@ -193,5 +194,6 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('force_domain_override');
$request->offsetUnset('autogenerate_domain');
$request->offsetUnset('is_container_label_escape_enabled');
+ $request->offsetUnset('is_preserve_repository_enabled');
$request->offsetUnset('docker_compose_raw');
}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index c522cd0ca..ceae64d84 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -6,12 +6,13 @@
use App\Jobs\VolumeCloneJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
+use App\Models\EnvironmentVariable;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
-function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)
+function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false, ?string $docker_registry_image_tag = null)
{
$application_id = $application->id;
$deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}");
@@ -46,6 +47,7 @@ function queue_application_deployment(Application $application, string $deployme
$existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
->where('commit', $commit)
->where('pull_request_id', $pull_request_id)
+ ->where('docker_registry_image_tag', $docker_registry_image_tag)
->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])
->first();
@@ -71,6 +73,7 @@ function queue_application_deployment(Application $application, string $deployme
'deployment_uuid' => $deployment_uuid,
'deployment_url' => $deployment_url,
'pull_request_id' => $pull_request_id,
+ 'docker_registry_image_tag' => $docker_registry_image_tag,
'force_rebuild' => $force_rebuild,
'is_webhook' => $is_webhook,
'is_api' => $is_api,
@@ -192,7 +195,7 @@ function clone_application(Application $source, $destination, array $overrides =
$server = $destination->server;
if ($server->team_id !== currentTeam()->id) {
- throw new \RuntimeException('Destination does not belong to the current team.');
+ throw new RuntimeException('Destination does not belong to the current team.');
}
// Prepare name and URL
@@ -211,7 +214,7 @@ function clone_application(Application $source, $destination, array $overrides =
'updated_at',
'additional_servers_count',
'additional_networks_count',
- ])->fill(array_merge([
+ ])->forceFill(array_merge([
'uuid' => $uuid,
'name' => $name,
'fqdn' => $url,
@@ -299,6 +302,7 @@ function clone_application(Application $source, $destination, array $overrides =
'id',
'created_at',
'updated_at',
+ 'uuid',
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id,
@@ -322,8 +326,8 @@ function clone_application(Application $source, $destination, array $overrides =
destination: $source->destination,
no_questions_asked: true
);
- } catch (\Exception $e) {
- \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
+ } catch (Exception $e) {
+ Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
@@ -344,7 +348,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone production environment variables without triggering the created hook
$environmentVariables = $source->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
- \App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
+ EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
@@ -361,7 +365,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone preview environment variables
$previewEnvironmentVariables = $source->environment_variables_preview()->get();
foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) {
- \App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
+ EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
$newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([
'id',
'created_at',
diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php
index 4ca693fcb..751851283 100644
--- a/bootstrap/helpers/parsers.php
+++ b/bootstrap/helpers/parsers.php
@@ -22,25 +22,25 @@
*
* @param string $composeYaml The raw Docker Compose YAML content
*
- * @throws \Exception If the compose file contains command injection attempts
+ * @throws Exception If the compose file contains command injection attempts
*/
function validateDockerComposeForInjection(string $composeYaml): void
{
try {
$parsed = Yaml::parse($composeYaml);
- } catch (\Exception $e) {
- throw new \Exception('Invalid YAML format: '.$e->getMessage(), 0, $e);
+ } catch (Exception $e) {
+ throw new Exception('Invalid YAML format: '.$e->getMessage(), 0, $e);
}
if (! is_array($parsed) || ! isset($parsed['services']) || ! is_array($parsed['services'])) {
- throw new \Exception('Docker Compose file must contain a "services" section');
+ throw new Exception('Docker Compose file must contain a "services" section');
}
// Validate service names
foreach ($parsed['services'] as $serviceName => $serviceConfig) {
try {
validateShellSafePath($serviceName, 'service name');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.',
0,
@@ -68,8 +68,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($source, 'volume source');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.',
0,
@@ -84,8 +84,8 @@ function validateDockerComposeForInjection(string $composeYaml): void
if (is_string($target)) {
try {
validateShellSafePath($target, 'volume target');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.',
0,
@@ -105,7 +105,7 @@ function validateDockerComposeForInjection(string $composeYaml): void
*
* @param string $volumeString The volume string to validate
*
- * @throws \Exception If the volume string contains command injection attempts
+ * @throws Exception If the volume string contains command injection attempts
*/
function validateVolumeStringForInjection(string $volumeString): void
{
@@ -325,9 +325,9 @@ function parseDockerVolumeString(string $volumeString): array
if (! $isSimpleEnvVar && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceStr, 'volume source');
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// Re-throw with more context about the volume string
- throw new \Exception(
+ throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -343,8 +343,8 @@ function parseDockerVolumeString(string $volumeString): array
// Still, defense in depth is important
try {
validateShellSafePath($targetStr, 'volume target');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition: '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -375,7 +375,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
try {
$yaml = Yaml::parse($compose);
- } catch (\Exception) {
+ } catch (Exception) {
return collect([]);
}
$services = data_get($yaml, 'services', collect([]));
@@ -409,8 +409,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
// Validate service name for command injection
try {
validateShellSafePath($serviceName, 'service name');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.'
);
@@ -465,7 +465,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$fqdn = generateFqdn(server: $server, random: "$uuid", parserVersion: $resource->compose_parsing_version);
}
- if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
+ if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
$fqdn = "$fqdn$path";
@@ -738,8 +738,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceValue, 'volume source');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -749,8 +749,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
if ($target !== null && ! empty($target->value())) {
try {
validateShellSafePath($target->value(), 'volume target');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -1489,7 +1489,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
}
}
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
}
@@ -1519,7 +1519,7 @@ function serviceParser(Service $resource): Collection
try {
$yaml = Yaml::parse($compose);
- } catch (\Exception) {
+ } catch (Exception) {
return collect([]);
}
$services = data_get($yaml, 'services', collect([]));
@@ -1566,8 +1566,8 @@ function serviceParser(Service $resource): Collection
// Validate service name for command injection
try {
validateShellSafePath($serviceName, 'service name');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker Compose service name: '.$e->getMessage().
' Service names must not contain shell metacharacters.'
);
@@ -1593,20 +1593,25 @@ function serviceParser(Service $resource): Collection
// Use image detection for non-migrated services
$isDatabase = isDatabaseImage($image, $service);
if ($isDatabase) {
- $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
- if ($applicationFound) {
- $savedService = $applicationFound;
+ $databaseFound = ServiceDatabase::where('name', $serviceName)->where('service_id', $resource->id)->first();
+ if ($databaseFound) {
+ $savedService = $databaseFound;
} else {
- $savedService = ServiceDatabase::firstOrCreate([
+ $savedService = ServiceDatabase::forceCreate([
'name' => $serviceName,
'service_id' => $resource->id,
]);
}
} else {
- $savedService = ServiceApplication::firstOrCreate([
- 'name' => $serviceName,
- 'service_id' => $resource->id,
- ]);
+ $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
+ if ($applicationFound) {
+ $savedService = $applicationFound;
+ } else {
+ $savedService = ServiceApplication::forceCreate([
+ 'name' => $serviceName,
+ 'service_id' => $resource->id,
+ ]);
+ }
}
}
// Update image if it changed
@@ -1772,7 +1777,7 @@ function serviceParser(Service $resource): Collection
// Strip scheme for environment variable values
$fqdnValueForEnv = str($fqdn)->after('://')->value();
- if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) {
+ if ($value && get_class($value) === Illuminate\Support\Stringable::class && $value->startsWith('/')) {
$path = $value->value();
if ($path !== '/') {
// Only add path if it's not already present (prevents duplication on subsequent parse() calls)
@@ -2120,8 +2125,8 @@ function serviceParser(Service $resource): Collection
if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) {
try {
validateShellSafePath($sourceValue, 'volume source');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -2131,8 +2136,8 @@ function serviceParser(Service $resource): Collection
if ($target !== null && ! empty($target->value())) {
try {
validateShellSafePath($target->value(), 'volume target');
- } catch (\Exception $e) {
- throw new \Exception(
+ } catch (Exception $e) {
+ throw new Exception(
'Invalid Docker volume definition (array syntax): '.$e->getMessage().
' Please use safe path names without shell metacharacters.'
);
@@ -2741,7 +2746,7 @@ function serviceParser(Service $resource): Collection
}
}
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// If parsing fails, keep the original docker_compose_raw unchanged
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
}
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index cf9f648bb..ed18dfe76 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -109,18 +109,20 @@ function connectProxyToNetworks(Server $server)
['networks' => $networks] = collectDockerNetworksByServer($server);
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null",
- "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
- "echo 'Successfully connected coolify-proxy to $network network.'",
+ "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --driver overlay --attachable {$safe} >/dev/null",
+ "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to {$safe} network.'",
];
});
} else {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null",
- "docker network connect $network coolify-proxy >/dev/null 2>&1 || true",
- "echo 'Successfully connected coolify-proxy to $network network.'",
+ "docker network ls --format '{{.Name}}' | grep '^{$network}$' >/dev/null || docker network create --attachable {$safe} >/dev/null",
+ "docker network connect {$safe} coolify-proxy >/dev/null 2>&1 || true",
+ "echo 'Successfully connected coolify-proxy to {$safe} network.'",
];
});
}
@@ -141,16 +143,18 @@ function ensureProxyNetworksExist(Server $server)
if ($server->isSwarm()) {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "echo 'Ensuring network $network exists...'",
- "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable $network",
+ "echo 'Ensuring network {$safe} exists...'",
+ "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --driver overlay --attachable {$safe}",
];
});
} else {
$commands = $networks->map(function ($network) {
+ $safe = escapeshellarg($network);
return [
- "echo 'Ensuring network $network exists...'",
- "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable $network",
+ "echo 'Ensuring network {$safe} exists...'",
+ "docker network ls --format '{{.Name}}' | grep -q '^{$network}$' || docker network create --attachable {$safe}",
];
});
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 84472a07e..cd773f6a9 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -16,6 +16,7 @@
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
+use App\Models\SharedEnvironmentVariable;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -28,8 +29,10 @@
use App\Models\User;
use Carbon\CarbonImmutable;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Process\Pool;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
@@ -49,10 +52,14 @@
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\Builder;
+use Livewire\Component;
+use Nubs\RandomNameGenerator\All;
+use Nubs\RandomNameGenerator\Alliteration;
use phpseclib3\Crypt\EC;
use phpseclib3\Crypt\RSA;
use Poliander\Cron\CronExpression;
use PurplePixie\PhpDns\DNSQuery;
+use PurplePixie\PhpDns\DNSTypes;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
@@ -116,7 +123,7 @@ function sanitize_string(?string $input = null): ?string
* @param string $context Descriptive name for error messages (e.g., 'volume source', 'service name')
* @return string The validated input (unchanged if valid)
*
- * @throws \Exception If dangerous characters are detected
+ * @throws Exception If dangerous characters are detected
*/
function validateShellSafePath(string $input, string $context = 'path'): string
{
@@ -138,7 +145,7 @@ function validateShellSafePath(string $input, string $context = 'path'): string
// Check for dangerous characters
foreach ($dangerousChars as $char => $description) {
if (str_contains($input, $char)) {
- throw new \Exception(
+ throw new Exception(
"Invalid {$context}: contains forbidden character '{$char}' ({$description}). ".
'Shell metacharacters are not allowed for security reasons.'
);
@@ -160,7 +167,7 @@ function validateShellSafePath(string $input, string $context = 'path'): string
* @param string $input The databases_to_backup string
* @return string The validated input
*
- * @throws \Exception If any component contains dangerous characters
+ * @throws Exception If any component contains dangerous characters
*/
function validateDatabasesBackupInput(string $input): string
{
@@ -211,7 +218,7 @@ function validateDatabasesBackupInput(string $input): string
* @param string $context Descriptive name for error messages
* @return string The validated input (trimmed)
*
- * @throws \Exception If the input contains disallowed characters
+ * @throws Exception If the input contains disallowed characters
*/
function validateGitRef(string $input, string $context = 'git ref'): string
{
@@ -223,12 +230,12 @@ function validateGitRef(string $input, string $context = 'git ref'): string
// Must not start with a hyphen (git flag injection)
if (str_starts_with($input, '-')) {
- throw new \Exception("Invalid {$context}: must not start with a hyphen.");
+ throw new Exception("Invalid {$context}: must not start with a hyphen.");
}
// Allow only alphanumeric characters, dots, hyphens, underscores, and slashes
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/', $input)) {
- throw new \Exception("Invalid {$context}: contains disallowed characters. Only alphanumeric characters, dots, hyphens, underscores, and slashes are allowed.");
+ throw new Exception("Invalid {$context}: contains disallowed characters. Only alphanumeric characters, dots, hyphens, underscores, and slashes are allowed.");
}
return $input;
@@ -282,7 +289,7 @@ function refreshSession(?Team $team = null): void
});
session(['currentTeam' => $team]);
}
-function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
+function handleError(?Throwable $error = null, ?Component $livewire = null, ?string $customErrorMessage = null)
{
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
@@ -299,7 +306,7 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.';
}
- if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ if ($error instanceof ModelNotFoundException) {
abort(404);
}
@@ -329,7 +336,7 @@ function get_latest_sentinel_version(): string
$versions = $response->json();
return data_get($versions, 'coolify.sentinel.version');
- } catch (\Throwable) {
+ } catch (Throwable) {
return '0.0.0';
}
}
@@ -339,7 +346,7 @@ function get_latest_version_of_coolify(): string
$versions = get_versions_data();
return data_get($versions, 'coolify.v4.version', '0.0.0');
- } catch (\Throwable $e) {
+ } catch (Throwable $e) {
return '0.0.0';
}
@@ -347,9 +354,9 @@ function get_latest_version_of_coolify(): string
function generate_random_name(?string $cuid = null): string
{
- $generator = new \Nubs\RandomNameGenerator\All(
+ $generator = new All(
[
- new \Nubs\RandomNameGenerator\Alliteration,
+ new Alliteration,
]
);
if (is_null($cuid)) {
@@ -448,7 +455,7 @@ function getFqdnWithoutPort(string $fqdn)
$path = $url->getPath();
return "$scheme://$host$path";
- } catch (\Throwable) {
+ } catch (Throwable) {
return $fqdn;
}
}
@@ -478,13 +485,13 @@ function base_url(bool $withPort = true): string
}
if ($settings->public_ipv6) {
if ($withPort) {
- return "http://$settings->public_ipv6:$port";
+ return "http://[$settings->public_ipv6]:$port";
}
- return "http://$settings->public_ipv6";
+ return "http://[$settings->public_ipv6]";
}
- return url('/');
+ return config('app.url');
}
function isSubscribed()
@@ -537,21 +544,21 @@ function validate_cron_expression($expression_to_validate): bool
* Even if the job runs minutes late, it still catches the missed cron window.
* Without a dedupKey, falls back to a simple isDue() check.
*/
-function shouldRunCronNow(string $frequency, string $timezone, ?string $dedupKey = null, ?\Illuminate\Support\Carbon $executionTime = null): bool
+function shouldRunCronNow(string $frequency, string $timezone, ?string $dedupKey = null, ?Carbon $executionTime = null): bool
{
- $cron = new \Cron\CronExpression($frequency);
- $executionTime = ($executionTime ?? \Illuminate\Support\Carbon::now())->copy()->setTimezone($timezone);
+ $cron = new Cron\CronExpression($frequency);
+ $executionTime = ($executionTime ?? Carbon::now())->copy()->setTimezone($timezone);
if ($dedupKey === null) {
return $cron->isDue($executionTime);
}
- $previousDue = \Illuminate\Support\Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true));
+ $previousDue = Carbon::instance($cron->getPreviousRunDate($executionTime, allowCurrentDate: true));
$lastDispatched = Cache::get($dedupKey);
$shouldFire = $lastDispatched === null
? $cron->isDue($executionTime)
- : $previousDue->gt(\Illuminate\Support\Carbon::parse($lastDispatched));
+ : $previousDue->gt(Carbon::parse($lastDispatched));
// Always write: seeds on first miss, refreshes on dispatch.
// 30-day static TTL covers all intervals; orphan keys self-clean.
@@ -932,7 +939,7 @@ function get_service_templates(bool $force = false): Collection
$services = $response->json();
return collect($services);
- } catch (\Throwable) {
+ } catch (Throwable) {
$services = File::get(base_path('templates/'.config('constants.services.file_name')));
return collect(json_decode($services))->sortKeys();
@@ -955,7 +962,7 @@ function getResourceByUuid(string $uuid, ?int $teamId = null)
}
// ServiceDatabase has a different relationship path: service->environment->project->team_id
- if ($resource instanceof \App\Models\ServiceDatabase) {
+ if ($resource instanceof ServiceDatabase) {
if ($resource->service?->environment?->project?->team_id === $teamId) {
return $resource;
}
@@ -1081,7 +1088,7 @@ function generateGitManualWebhook($resource, $type)
if ($resource->source_id !== 0 && ! is_null($resource->source_id)) {
return null;
}
- if ($resource->getMorphClass() === \App\Models\Application::class) {
+ if ($resource->getMorphClass() === Application::class) {
$baseUrl = base_url();
return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual";
@@ -1102,11 +1109,11 @@ function sanitizeLogsForExport(string $text): string
function getTopLevelNetworks(Service|Application $resource)
{
- if ($resource->getMorphClass() === \App\Models\Service::class) {
+ if ($resource->getMorphClass() === Service::class) {
if ($resource->docker_compose_raw) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// If the docker-compose.yml file is not valid, we will return the network name as the key
$topLevelNetworks = collect([
$resource->uuid => [
@@ -1169,10 +1176,10 @@ function getTopLevelNetworks(Service|Application $resource)
return $topLevelNetworks->keys();
}
- } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
+ } elseif ($resource->getMorphClass() === Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
// If the docker-compose.yml file is not valid, we will return the network name as the key
$topLevelNetworks = collect([
$resource->uuid => [
@@ -1479,7 +1486,7 @@ function validateDNSEntry(string $fqdn, Server $server)
$ip = $server->ip;
}
$found_matching_ip = false;
- $type = \PurplePixie\PhpDns\DNSTypes::NAME_A;
+ $type = DNSTypes::NAME_A;
foreach ($dns_servers as $dns_server) {
try {
$query = new DNSQuery($dns_server);
@@ -1500,7 +1507,7 @@ function validateDNSEntry(string $fqdn, Server $server)
}
}
}
- } catch (\Exception) {
+ } catch (Exception) {
}
}
@@ -1682,7 +1689,7 @@ function get_public_ips()
}
InstanceSettings::get()->update(['public_ipv4' => $ipv4]);
}
- } catch (\Exception $e) {
+ } catch (Exception $e) {
echo "Error: {$e->getMessage()}\n";
}
try {
@@ -1697,7 +1704,7 @@ function get_public_ips()
}
InstanceSettings::get()->update(['public_ipv6' => $ipv6]);
}
- } catch (\Throwable $e) {
+ } catch (Throwable $e) {
echo "Error: {$e->getMessage()}\n";
}
}
@@ -1795,15 +1802,15 @@ function customApiValidator(Collection|array $item, array $rules)
}
function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null)
{
- if ($resource->getMorphClass() === \App\Models\Service::class) {
+ if ($resource->getMorphClass() === Service::class) {
if ($resource->docker_compose_raw) {
// Extract inline comments from raw YAML before Symfony parser discards them
$envComments = extractYamlEnvironmentComments($resource->docker_compose_raw);
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception $e) {
- throw new \RuntimeException($e->getMessage());
+ } catch (Exception $e) {
+ throw new RuntimeException($e->getMessage());
}
$allServices = get_service_templates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
@@ -2567,10 +2574,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
} else {
return collect([]);
}
- } elseif ($resource->getMorphClass() === \App\Models\Application::class) {
+ } elseif ($resource->getMorphClass() === Application::class) {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
- } catch (\Exception) {
+ } catch (Exception) {
return;
}
$server = $resource->destination->server;
@@ -3332,7 +3339,7 @@ function isAssociativeArray($array)
}
if (! is_array($array)) {
- throw new \InvalidArgumentException('Input must be an array or a Collection.');
+ throw new InvalidArgumentException('Input must be an array or a Collection.');
}
if ($array === []) {
@@ -3448,7 +3455,7 @@ function wireNavigate(): string
// Return wire:navigate.hover for SPA navigation with prefetching, or empty string if disabled
return ($settings->is_wire_navigate_enabled ?? true) ? 'wire:navigate.hover' : '';
- } catch (\Exception $e) {
+ } catch (Exception $e) {
return 'wire:navigate.hover';
}
}
@@ -3457,13 +3464,13 @@ function wireNavigate(): string
* Redirect to a named route with SPA navigation support.
* Automatically uses wire:navigate when is_wire_navigate_enabled is true.
*/
-function redirectRoute(Livewire\Component $component, string $name, array $parameters = []): mixed
+function redirectRoute(Component $component, string $name, array $parameters = []): mixed
{
$navigate = true;
try {
$navigate = instanceSettings()->is_wire_navigate_enabled ?? true;
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$navigate = true;
}
@@ -3505,7 +3512,7 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
]);
try {
return instant_remote_process($commands, $server);
- } catch (\Exception) {
+ } catch (Exception) {
// continue
}
}
@@ -3636,8 +3643,8 @@ function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp|
// If this happens, the user may have provided an HTTP URL when they needed an SSH one
// Let's try and fix that for known Git providers
switch ($source->getMorphClass()) {
- case \App\Models\GithubApp::class:
- case \App\Models\GitlabApp::class:
+ case GithubApp::class:
+ case GitlabApp::class:
$providerInfo['host'] = Url::fromString($source->html_url)->getHost();
$providerInfo['port'] = $source->custom_port;
$providerInfo['user'] = $source->custom_user;
@@ -3915,10 +3922,10 @@ function shouldSkipPasswordConfirmation(): bool
* - User has no password (OAuth users)
*
* @param mixed $password The password to verify (may be array if skipped by frontend)
- * @param \Livewire\Component|null $component Optional Livewire component to add errors to
+ * @param Component|null $component Optional Livewire component to add errors to
* @return bool True if verification passed (or skipped), false if password is incorrect
*/
-function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $component = null): bool
+function verifyPasswordConfirmation(mixed $password, ?Component $component = null): bool
{
// Skip if password confirmation should be skipped
if (shouldSkipPasswordConfirmation()) {
@@ -3941,17 +3948,17 @@ function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $compon
* Extract hard-coded environment variables from docker-compose YAML.
*
* @param string $dockerComposeRaw Raw YAML content
- * @return \Illuminate\Support\Collection Collection of arrays with: key, value, comment, service_name
+ * @return Collection Collection of arrays with: key, value, comment, service_name
*/
-function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): \Illuminate\Support\Collection
+function extractHardcodedEnvironmentVariables(string $dockerComposeRaw): Collection
{
if (blank($dockerComposeRaw)) {
return collect([]);
}
try {
- $yaml = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
- } catch (\Exception $e) {
+ $yaml = Yaml::parse($dockerComposeRaw);
+ } catch (Exception $e) {
// Malformed YAML - return empty collection
return collect([]);
}
@@ -4100,7 +4107,7 @@ function resolveSharedEnvironmentVariables(?string $value, $resource): ?string
if (is_null($id)) {
continue;
}
- $found = \App\Models\SharedEnvironmentVariable::where('type', $type)
+ $found = SharedEnvironmentVariable::where('type', $type)
->where('key', $variable)
->where('team_id', $resource->team()->id)
->where("{$type}_id", $id)
diff --git a/config/purify.php b/config/purify.php
index 66dbbb568..a5dcabb92 100644
--- a/config/purify.php
+++ b/config/purify.php
@@ -49,6 +49,17 @@
'AutoFormat.RemoveEmpty' => false,
],
+ 'validation_logs' => [
+ 'Core.Encoding' => 'utf-8',
+ 'HTML.Doctype' => 'HTML 4.01 Transitional',
+ 'HTML.Allowed' => 'a[href|title|target|class],br,div[class],pre[class],span[class],p[class]',
+ 'HTML.ForbiddenElements' => '',
+ 'CSS.AllowedProperties' => '',
+ 'AutoFormat.AutoParagraph' => false,
+ 'AutoFormat.RemoveEmpty' => false,
+ 'Attr.AllowedFrameTargets' => ['_blank'],
+ ],
+
],
/*
diff --git a/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php
new file mode 100644
index 000000000..a4a6988f2
--- /dev/null
+++ b/database/migrations/2026_03_29_000000_encrypt_existing_clickhouse_admin_passwords.php
@@ -0,0 +1,39 @@
+chunkById(100, function ($clickhouses) {
+ foreach ($clickhouses as $clickhouse) {
+ $password = $clickhouse->clickhouse_admin_password;
+
+ if (empty($password)) {
+ continue;
+ }
+
+ // Skip if already encrypted (idempotent)
+ try {
+ Crypt::decryptString($password);
+
+ continue;
+ } catch (Exception) {
+ // Not encrypted yet โ encrypt it
+ }
+
+ DB::table('standalone_clickhouses')
+ ->where('id', $clickhouse->id)
+ ->update(['clickhouse_admin_password' => Crypt::encryptString($password)]);
+ }
+ });
+ } catch (Exception $e) {
+ echo 'Encrypting ClickHouse admin passwords failed.';
+ echo $e->getMessage();
+ }
+ }
+}
diff --git a/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php b/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php
new file mode 100644
index 000000000..2dafa2737
--- /dev/null
+++ b/database/migrations/2026_03_30_120000_add_docker_registry_image_tag_to_application_previews_and_deployment_queues.php
@@ -0,0 +1,30 @@
+string('docker_registry_image_tag')->nullable()->after('docker_compose_domains');
+ });
+
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->string('docker_registry_image_tag')->nullable()->after('pull_request_id');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('application_previews', function (Blueprint $table) {
+ $table->dropColumn('docker_registry_image_tag');
+ });
+
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->dropColumn('docker_registry_image_tag');
+ });
+ }
+};
diff --git a/database/seeders/RootUserSeeder.php b/database/seeders/RootUserSeeder.php
index e3968a1c9..c4e93af63 100644
--- a/database/seeders/RootUserSeeder.php
+++ b/database/seeders/RootUserSeeder.php
@@ -45,12 +45,13 @@ public function run(): void
}
try {
- User::create([
+ $user = (new User)->forceFill([
'id' => 0,
'name' => env('ROOT_USERNAME', 'Root User'),
'email' => env('ROOT_USER_EMAIL'),
'password' => Hash::make(env('ROOT_USER_PASSWORD')),
]);
+ $user->save();
echo "\n SUCCESS Root user created successfully.\n\n";
} catch (\Exception $e) {
echo "\n ERROR Failed to create root user: {$e->getMessage()}\n\n";
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
index ada19b3a3..dbc472d06 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run
@@ -1,12 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:horizon
-}
+if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
+ echo " INFO Horizon is disabled, sleeping."
+ exec sleep infinity
+fi
+echo " INFO Horizon is enabled, starting..."
+exec php artisan horizon
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index 1166ccd08..ee46dba7e 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -1,12 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:nightwatch
-}
+if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
+ echo " INFO Nightwatch is enabled, starting..."
+ exec php artisan nightwatch:agent
+fi
+echo " INFO Nightwatch is disabled, sleeping."
+exec sleep infinity
diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index b81a44833..bfa44c7e3 100644
--- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -1,13 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:scheduler
-}
-
+if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
+ echo " INFO Scheduler is disabled, sleeping."
+ exec sleep infinity
+fi
+echo " INFO Scheduler is enabled, starting..."
+exec php artisan schedule:work
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
index be6647607..dbc472d06 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run
@@ -1,11 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:horizon
-}
+if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then
+ echo " INFO Horizon is disabled, sleeping."
+ exec sleep infinity
+fi
+
+echo " INFO Horizon is enabled, starting..."
+exec php artisan horizon
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
index 80d73eadb..ee46dba7e 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run
@@ -1,11 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:nightwatch
-}
+if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then
+ echo " INFO Nightwatch is enabled, starting..."
+ exec php artisan nightwatch:agent
+fi
+
+echo " INFO Nightwatch is disabled, sleeping."
+exec sleep infinity
diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
index a2ecb0a73..bfa44c7e3 100644
--- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
+++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run
@@ -1,10 +1,11 @@
-#!/command/execlineb -P
+#!/bin/sh
-# Use with-contenv to ensure environment variables are available
-with-contenv
cd /var/www/html
-foreground {
- php
- artisan
- start:scheduler
-}
+
+if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then
+ echo " INFO Scheduler is disabled, sleeping."
+ exec sleep infinity
+fi
+
+echo " INFO Scheduler is enabled, starting..."
+exec php artisan schedule:work
diff --git a/openapi.json b/openapi.json
index aec5a2843..ed8decb48 100644
--- a/openapi.json
+++ b/openapi.json
@@ -407,6 +407,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -852,6 +857,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -1297,6 +1307,11 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Preserve repository during deployment."
}
},
"type": "object"
@@ -2704,6 +2719,10 @@
"type": "boolean",
"default": true,
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
+ },
+ "is_preserve_repository_enabled": {
+ "type": "boolean",
+ "description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
}
},
"type": "object"
@@ -4544,6 +4563,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -4989,6 +5012,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5117,6 +5144,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5241,6 +5272,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5369,6 +5404,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5497,6 +5536,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5637,6 +5680,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5777,6 +5824,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -5905,6 +5956,10 @@
"type": "integer",
"description": "Public port of the database"
},
+ "public_port_timeout": {
+ "type": "integer",
+ "description": "Public port timeout in seconds (default: 3600)"
+ },
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
@@ -7219,6 +7274,22 @@
"schema": {
"type": "integer"
}
+ },
+ {
+ "name": "pull_request_id",
+ "in": "query",
+ "description": "Preview deployment identifier. Alias of pr.",
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "docker_tag",
+ "in": "query",
+ "description": "Docker image tag for Docker Image preview deployments. Requires pull_request_id.",
+ "schema": {
+ "type": "string"
+ }
}
],
"responses": {
@@ -12679,6 +12750,10 @@
"pull_request_id": {
"type": "integer"
},
+ "docker_registry_image_tag": {
+ "type": "string",
+ "nullable": true
+ },
"force_rebuild": {
"type": "boolean"
},
diff --git a/openapi.yaml b/openapi.yaml
index 93038ce80..157cd9f69 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -291,6 +291,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -575,6 +579,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -859,6 +867,10 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ default: false
+ description: 'Preserve repository during deployment.'
type: object
responses:
'201':
@@ -1741,6 +1753,9 @@ paths:
type: boolean
default: true
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
+ is_preserve_repository_enabled:
+ type: boolean
+ description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
type: object
responses:
'200':
@@ -2873,6 +2888,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3189,6 +3207,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3281,6 +3302,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3370,6 +3394,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3462,6 +3489,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3554,6 +3584,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3655,6 +3688,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3756,6 +3792,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -3848,6 +3887,9 @@ paths:
public_port:
type: integer
description: 'Public port of the database'
+ public_port_timeout:
+ type: integer
+ description: 'Public port timeout in seconds (default: 3600)'
limits_memory:
type: string
description: 'Memory limit of the database'
@@ -4668,6 +4710,18 @@ paths:
description: 'Pull Request Id for deploying specific PR builds. Cannot be used with tag parameter.'
schema:
type: integer
+ -
+ name: pull_request_id
+ in: query
+ description: 'Preview deployment identifier. Alias of pr.'
+ schema:
+ type: integer
+ -
+ name: docker_tag
+ in: query
+ description: 'Docker image tag for Docker Image preview deployments. Requires pull_request_id.'
+ schema:
+ type: string
responses:
'200':
description: "Get deployment(s) UUID's"
@@ -8063,6 +8117,9 @@ components:
type: string
pull_request_id:
type: integer
+ docker_registry_image_tag:
+ type: string
+ nullable: true
force_rebuild:
type: boolean
commit:
diff --git a/public/svgs/electricsql.svg b/public/svgs/electricsql.svg
new file mode 100644
index 000000000..bbffe200a
--- /dev/null
+++ b/public/svgs/electricsql.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/views/components/forms/env-var-input.blade.php b/resources/views/components/forms/env-var-input.blade.php
index 2466a57f9..d26e248c1 100644
--- a/resources/views/components/forms/env-var-input.blade.php
+++ b/resources/views/components/forms/env-var-input.blade.php
@@ -10,7 +10,7 @@
@endif
-