diff --git a/.cursor/rules/database-patterns.mdc b/.cursor/rules/database-patterns.mdc index a4f65f5fb..ec60a43b3 100644 --- a/.cursor/rules/database-patterns.mdc +++ b/.cursor/rules/database-patterns.mdc @@ -142,6 +142,29 @@ Schema::create('applications', function (Blueprint $table) { - **Soft deletes** for audit trails - **Activity logging** with Spatie package +### **CRITICAL: Mass Assignment Protection** +**When adding new database columns, you MUST update the model's `$fillable` array.** Without this, Laravel will silently ignore mass assignment operations like `Model::create()` or `$model->update()`. + +**Checklist for new columns:** +1. ✅ Create migration file +2. ✅ Run migration +3. ✅ **Add column to model's `$fillable` array** +4. ✅ Update any Livewire components that sync this property +5. ✅ Test that the column can be read and written + +**Example:** +```php +class Server extends BaseModel +{ + protected $fillable = [ + 'name', + 'ip', + 'port', + 'is_validating', // ← MUST add new columns here + ]; +} +``` + ### Relationship Patterns ```php // Typical relationship structure in Application model diff --git a/.github/workflows/cleanup-ghcr-untagged.yml b/.github/workflows/cleanup-ghcr-untagged.yml new file mode 100644 index 000000000..1ad41ce16 --- /dev/null +++ b/.github/workflows/cleanup-ghcr-untagged.yml @@ -0,0 +1,24 @@ +name: Cleanup Untagged GHCR Images + +on: + workflow_dispatch: # Allow manual trigger + schedule: + - cron: '0 */6 * * *' # Run every 6 hours to handle large volume (16k+ images) + +env: + GITHUB_REGISTRY: ghcr.io + +jobs: + cleanup-testing-host: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Delete untagged coolify-testing-host images + uses: actions/delete-package-versions@v5 + with: + package-name: 'coolify-testing-host' + package-type: 'container' + min-versions-to-keep: 0 + delete-only-untagged-versions: 'true' diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index 09b1e9421..c6aa2dd90 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -28,6 +28,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: @@ -50,8 +57,8 @@ jobs: platforms: linux/amd64 push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} aarch64: runs-on: [self-hosted, arm64] @@ -61,6 +68,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: @@ -83,8 +97,8 @@ jobs: platforms: linux/aarch64 push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 merge-manifest: runs-on: ubuntu-latest @@ -95,6 +109,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Sanitize branch name for Docker tag + id: sanitize + run: | + # Replace slashes and other invalid characters with dashes + SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g') + echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT + - uses: docker/setup-buildx-action@v3 - name: Login to ${{ env.GITHUB_REGISTRY }} @@ -114,14 +135,14 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ - --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ - --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }} - uses: sarisia/actions-status-discord@v1 if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index aefabfd29..ea4510776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -390,1254 +390,636 @@ ### ⚙️ Miscellaneous Tasks ## [4.0.0-beta.426] - 2025-08-28 -### 🚜 Refactor - -- *(policy)* Simplify ServiceDatabasePolicy methods to always return true and add manageBackups method - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Update coolify version to 4.0.0-beta.426 and nightly version to 4.0.0-beta.427 - -## [4.0.0-beta.425] - 2025-08-28 - -### 🚀 Features - -- *(domains)* Implement domain conflict detection and user confirmation modal across application components -- *(domains)* Add force_domain_override option and enhance domain conflict detection responses - -### 🐛 Bug Fixes - -- *(previews)* Simplify FQDN generation logic by removing unnecessary empty check -- *(templates)* Update Matrix service compose configuration for improved compatibility and clarity - -### 🚜 Refactor - -- *(urls)* Replace generateFqdn with generateUrl for consistent URL generation across applications -- *(domains)* Rename check_domain_usage to checkDomainUsage and update references across the application -- *(auth)* Simplify access control logic in CanAccessTerminal and ServerPolicy by allowing all users to perform actions - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Update coolify version to 4.0.0-beta.425 and nightly version to 4.0.0-beta.426 - -## [4.0.0-beta.424] - 2025-08-27 - -### 🐛 Bug Fixes - -- *(parsers)* Do not modify service names, only for getting fqdns and related envs -- *(compose)* Temporary allow to edit volumes in apps (compose based) and services - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Update coolify version to 4.0.0-beta.424 and nightly version to 4.0.0-beta.425 - -## [4.0.0-beta.423] - 2025-08-27 - -### 🚜 Refactor - -- *(parsers)* Remove unnecessary hyphen-to-underscore replacement for service names in serviceParser function - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Update coolify version to 4.0.0-beta.423 and nightly version to 4.0.0-beta.424 - -## [4.0.0-beta.422] - 2025-08-27 - -### 🐛 Bug Fixes - -- *(parsers)* Replace hyphens with underscores in service names for consistency. this allows to properly parse custom domains in docker compose based applications -- *(parsers)* Implement parseDockerVolumeString function to handle various Docker volume formats and modes, including environment variables and Windows paths. Add unit tests for comprehensive coverage. -- *(git)* Submodule update command uses an unsupported option (#6454) -- *(service)* Swap URL for FQDN on matrix template (#6466) -- *(parsers)* Enhance volume string handling by preserving mode in application and service parsers. Update related unit tests for validation. -- *(docker)* Update parser version in FQDN generation for service-specific URLs - -### 🚜 Refactor - -- *(git)* Improve submodule cloning - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Update version -- Update development node version - -## [4.0.0-beta.421] - 2025-08-26 - -### 🚀 Features - -- *(policies)* Add EnvironmentVariablePolicy for managing environment variables ( it was missing ) - -### 🐛 Bug Fixes - -- *(backups)* Rollback helper update for now - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(core)* Update version -- *(versions)* Update coolify version to 4.0.0-beta.421 and nightly version to 4.0.0-beta.422 - -## [4.0.0-beta.420.9] - 2025-08-26 - -### 🐛 Bug Fixes - -- *(backups)* S3 backup upload is failing - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(core)* Update version - -## [4.0.0-beta.420.8] - 2025-08-26 - -### 🚜 Refactor - -- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.420.7] - 2025-08-26 - -### 🚀 Features - -- *(service)* Add TriliumNext service (#5970) -- *(service)* Add Matrix service (#6029) -- *(service)* Add GitHub Action runner service (#6209) -- *(terminal)* Dispatch focus event for terminal after connection and enhance focus handling in JavaScript -- *(lang)* Add Polish language & improve forgot_password translation (#6306) -- *(service)* Update Authentik template (#6264) -- *(service)* Add sequin template (#6105) -- *(service)* Add pi-hole template (#6020) -- *(services)* Add Chroma service (#6201) -- *(service)* Add OpenPanel template (#5310) -- *(service)* Add librechat template (#5654) -- *(service)* Add Homebox service (#6116) -- *(service)* Add pterodactyl & wings services (#5537) -- *(service)* Add Bluesky PDS template (#6302) -- *(input)* Add autofocus attribute to input component for improved accessibility -- *(core)* Finally fqdn is fqdn and url is url. haha -- *(user)* Add changelog read tracking and unread count method -- *(templates)* Add new service templates and update existing compose files for various applications -- *(changelog)* Implement automated changelog fetching from GitHub and enhance changelog read tracking -- *(drizzle-gateway)* Add new drizzle-gateway service with configuration and logo -- *(drizzle-gateway)* Enhance service configuration by adding Master Password field and updating compose file path -- *(templates)* Add new service templates for Homebox, LibreChat, Pterodactyl, and Wings with corresponding configurations and logos -- *(templates)* Add Bluesky PDS service template and update compose file with new environment variable -- *(readme)* Add CubePath as a big sponsor and include new small sponsors with logos -- *(api)* Add create_environment endpoint to ProjectController for environment creation in projects -- *(api)* Add endpoints for managing environments in projects, including listing, creating, and deleting environments -- *(backup)* Add disable local backup option and related logic for S3 uploads -- *(dev patches)* Add functionality to send test email with patch data in development mode -- *(templates)* Added category per service -- *(email)* Implement email change request and verification process -- Generate category for services -- *(service)* Add elasticsearch template (#6300) -- *(sanitization)* Integrate DOMPurify for HTML sanitization across components -- *(cleanup)* Add command for sanitizing name fields across models -- *(sanitization)* Enhance HTML sanitization with improved DOMPurify configuration -- *(validation)* Centralize validation patterns for names and descriptions -- *(git-settings)* Add support for shallow cloning in application settings -- *(auth)* Implement authorization checks for server updates across multiple components -- *(auth)* Implement authorization for PrivateKey management -- *(auth)* Implement authorization for Docker and server management -- *(validation)* Add custom validation rules for Git repository URLs and branches -- *(security)* Add authorization checks for package updates in Livewire components -- *(auth)* Implement authorization checks for application management -- *(auth)* Enhance API error handling for authorization exceptions -- *(auth)* Add comprehensive authorization checks for all kind of resource creations -- *(auth)* Implement authorization checks for database management -- *(auth)* Refine authorization checks for S3 storage and service management -- *(auth)* Implement comprehensive authorization checks across API controllers -- *(auth)* Introduce resource creation authorization middleware and policies for enhanced access control -- *(auth)* Add middleware for resource creation authorization -- *(auth)* Enhance authorization checks in Livewire components for resource management -- *(validation)* Add ValidIpOrCidr rule for validating IP addresses and CIDR notations; update API access settings UI and add comprehensive tests -- *(docs)* Update architecture and development guidelines; enhance form components with built-in authorization system and improve routing documentation -- *(docs)* Expand authorization documentation for custom Alpine.js components; include manual protection patterns and implementation guidelines -- *(sentinel)* Implement SentinelRestarted event and update Livewire components to handle server restart notifications -- *(api)* Enhance IP access control in middleware and settings; support CIDR notation and special case for 0.0.0.0 to allow all IPs -- *(acl)* Change views/backend code to able to use proper ACL's later on. Currently it is not enabled. -- *(docs)* Add Backlog.md guidelines and project manager backlog agent; enhance CLAUDE.md with new links for task management -- *(docs)* Add tasks for implementing Docker build caching and optimizing staging builds; include detailed acceptance criteria and implementation plans -- *(docker)* Implement Docker cleanup processing in ScheduledJobManager; refactor server task scheduling to streamline cleanup job dispatching -- *(docs)* Expand Backlog.md guidelines with comprehensive usage instructions, CLI commands, and best practices for task management to enhance project organization and collaboration - -### 🐛 Bug Fixes - -- *(service)* Triliumnext platform and link -- *(application)* Update service environment variables when generating domain for Docker Compose -- *(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) -- *(docker)* Volumes get delete when stopping a service if `Delete Unused Volumes` is activated (#6317) -- *(docker)* Cleanup always running on deletion -- *(proxy)* Remove hardcoded port 80/443 checks (#6275) -- *(service)* Update healthcheck of penpot backend container (#6272) -- *(api)* Duplicated logs in application endpoint (#6292) -- *(service)* Documenso signees always pending (#6334) -- *(api)* Update service upsert to retain name and description values if not set -- *(database)* Custom postgres configs with SSL (#6352) -- *(policy)* Update delete method to check for admin status in S3StoragePolicy -- *(container)* Sort containers alphabetically by name in ExecuteContainerCommand and update filtering in Terminal Index -- *(application)* Streamline environment variable updates for Docker Compose services and enhance FQDN generation logic -- *(constants)* Update 'Change Log' to 'Changelog' in settings dropdown -- *(constants)* Update coolify version to 4.0.0-beta.420.7 -- *(parsers)* Clarify comments and update variable checks for FQDN and URL handling -- *(terminal)* Update text color for terminal availability message and improve readability -- *(drizzle-gateway)* Remove healthcheck from drizzle-gateway compose file and update service template -- *(templates)* Should generate old SERVICE_FQDN service templates as well -- *(constants)* Update official service template URL to point to the v4.x branch for accuracy -- *(git)* Use exact refspec in ls-remote to avoid matching similarly named branches (e.g., changeset-release/main). Use refs/heads/ or provider-specific PR refs. -- *(ApplicationPreview)* Change null check to empty check for fqdn in generate_preview_fqdn method -- *(email notifications)* Enhance EmailChannel to validate team membership for recipients and handle errors gracefully -- *(service api)* Separate create and update service functionalities -- *(templates)* Added a category tag for the docs service filter -- *(application)* Clear Docker Compose specific data when switching away from dockercompose -- *(database)* Conditionally set started_at only if the database is running -- *(ui)* Handle null values in postgres metrics (#6388) -- Disable env sorting by default -- *(proxy)* Filter host network from default proxy (#6383) -- *(modal)* Enhance confirmation text handling -- *(notification)* Update unread count display and improve HTML rendering -- *(select)* Remove unnecessary sanitization for logo rendering -- *(tags)* Update tag display to limit name length and adjust styling -- *(init)* Improve error handling for deployment and template pulling processes -- *(settings-dropdown)* Adjust unread count badge size and display logic for better consistency -- *(sanitization)* Enhance DOMPurify hook to remove Alpine.js directives for improved XSS protection -- *(servercheck)* Properly check server statuses with and without Sentinel -- *(errors)* Update error pages to provide navigation options -- *(github-deploy-key)* Update background color for selected private keys in deployment key selection UI -- *(auth)* Enhance authorization checks in application management - -### 💼 Other - -- *(settings-dropdown)* Add icons to buttons for improved UI in settings dropdown -- *(ui)* Introduce task for simplifying resource operations UI by replacing boxes with dropdown selections to enhance user experience and streamline interactions - -### 🚜 Refactor - -- *(jobs)* Remove logging for ScheduledJobManager and ServerResourceManager start and completion -- *(services)* Update validation rules to be optional -- *(service)* Improve langfuse -- *(service)* Improve openpanel template -- *(service)* Improve librechat -- *(public-git-repository)* Enhance form structure and add autofocus to repository URL input -- *(public-git-repository)* Remove commented-out code for cleaner template -- *(templates)* Update service template file handling to use dynamic file name from constants -- *(parsers)* Streamline domain handling in applicationParser and improve DNS validation logic -- *(templates)* Replace SERVICE_FQDN variables with SERVICE_URL in compose files for consistency -- *(links)* Replace inline SVGs with reusable external link component for consistency and improved maintainability -- *(previews)* Improve layout and add deployment/application logs links for previews -- *(docker compose)* Remove deprecated newParser function and associated test file to streamline codebase -- *(shared helpers)* Remove unused parseServiceVolumes function to clean up codebase -- *(parsers)* Update volume parsing logic to use beforeLast and afterLast for improved accuracy -- *(validation)* Implement centralized validation patterns across components -- *(jobs)* Rename job classes to indicate deprecation status -- Update check frequency logic for cloud and self-hosted environments; streamline server task scheduling and timezone handling -- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility - -### 📚 Documentation - -- *(claude)* Clarify that artisan commands should only be run inside the "coolify" container during development -- Add AGENTS.md for project guidance and development instructions -- Update changelog -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(service)* Improve matrix service -- *(service)* Format runner service -- *(service)* Improve sequin -- *(service)* Add `NOT_SECURED` env to Postiz (#6243) -- *(service)* Improve evolution-api environment variables (#6283) -- *(service)* Update Langfuse template to v3 (#6301) -- *(core)* Remove unused argument -- *(deletion)* Rename isDeleteOperation to deleteConnectedNetworks -- *(docker)* Remove unused arguments on StopService -- *(service)* Homebox formatting -- Clarify usage of custom redis configuration (#6321) -- *(changelogs)* Add .gitignore for changelogs directory and remove outdated changelog files for May, June, and July 2025 -- *(service)* Change affine images (#6366) -- Elasticsearch URL, fromatting and add category -- Update service-templates json files -- *(docs)* Remove AGENTS.md file; enhance CLAUDE.md with detailed form authorization patterns and service configuration examples -- *(cleanup)* Remove unused GitLab view files for change, new, and show pages -- *(workflows)* Add backlog directory to build triggers for production and staging workflows -- *(config)* Disable auto_commit in backlog configuration to prevent automatic commits -- *(versions)* Update coolify version to 4.0.0-beta.420.8 and nightly version to 4.0.0-beta.420.9 in versions.json and constants.php -- *(docker)* Update soketi image version to 1.0.10 in production and Windows configurations - -### ◀️ Revert - -- *(parser)* Enhance FQDN generation logic for services and applications - -## [4.0.0-beta.420.6] - 2025-07-18 - -### 🚀 Features - -- *(service)* Enable password protection for the Wireguard Ul -- *(queues)* Improve Horizon config to reduce CPU and RAM usage (#6212) -- *(service)* Add Gowa service (#6164) -- *(container)* Add updatedSelectedContainer method to connect to non-default containers and update wire:model for improved reactivity -- *(application)* Implement environment variable updates for Docker Compose applications, including creation, updating, and deletion of SERVICE_FQDN and SERVICE_URL variables - -### 🐛 Bug Fixes - -- *(installer)* Public IPv4 link does not work -- *(composer)* Version constraint of prompts -- *(service)* Budibase secret keys (#6205) -- *(service)* Wg-easy host should be just the FQDN -- *(ui)* Search box overlaps the sidebar navigation (#6176) -- *(webhooks)* Exclude webhook routes from CSRF protection (#6200) -- *(services)* Update environment variable naming convention to use underscores instead of dashes for SERVICE_FQDN and SERVICE_URL - -### 🚜 Refactor - -- *(service)* Improve gowa -- *(previews)* Streamline preview domain generation logic in ApplicationDeploymentJob for improved clarity and maintainability -- *(services)* Simplify environment variable updates by using updateOrCreate and add cleanup for removed FQDNs - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(service)* Update Nitropage template (#6181) -- *(versions)* Update all version -- *(bump)* Update composer deps -- *(version)* Bump Coolify version to 4.0.0-beta.420.6 - -## [4.0.0-beta.420.4] - 2025-07-08 - -### 🚀 Features - -- *(scheduling)* Add command to manually run scheduled database backups and tasks with options for chunking, delays, and dry runs -- *(scheduling)* Add frequency filter option for manual execution of scheduled jobs -- *(logging)* Implement scheduled logs command and enhance backup/task scheduling with cron checks -- *(logging)* Add frequency filters for scheduled logs command to support hourly, daily, weekly, and monthly job views -- *(scheduling)* Introduce ScheduledJobManager and ServerResourceManager for enhanced job scheduling and resource management -- *(previews)* Implement soft delete and cleanup for ApplicationPreview, enhancing resource management in DeleteResourceJob - -### 🐛 Bug Fixes - -- *(service)* Update Postiz compose configuration for improved server availability -- *(install.sh)* Use IPV4_PUBLIC_IP variable in output instead of repeated curl -- *(env)* Generate literal env variables better -- *(deployment)* Update x-data initialization in deployment view for improved functionality -- *(deployment)* Enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility -- *(deployment)* Improve docker-compose domain handling and environment variable generation -- *(deployment)* Refactor domain parsing and environment variable generation using Spatie URL library -- *(deployment)* Update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy -- *(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 -- *(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 -- *(horizon)* Add silenced jobs -- *(application)* Sanitize service names for HTML form binding and ensure original names are stored in docker compose domains -- *(previews)* Adjust padding for rate limit message in application previews -- *(previews)* Order application previews by pull request ID in descending order -- *(previews)* Add unique wire keys for preview containers and services based on pull request ID -- *(previews)* Enhance domain generation logic for application previews, ensuring unique domains are created when none are set -- *(previews)* Refine preview domain generation for Docker Compose applications, ensuring correct method usage based on build pack type -- *(ui)* Typo on proxy request handler tooltip (#6192) -- *(backups)* Large database backups are not working (#6217) -- *(backups)* Error message if there is no exception - -### 🚜 Refactor - -- *(previews)* Streamline preview URL generation by utilizing application method -- *(application)* Adjust layout and spacing in general application view for improved UI -- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency -- *(scheduling)* Replace deprecated job checks with ScheduledJobManager and ServerResourceManager for improved scheduling efficiency -- *(previews)* Move preview domain generation logic to ApplicationPreview model for better encapsulation and consistency across webhook handlers - -### 📚 Documentation - -- Update changelog -- Update changelog - -## [4.0.0-beta.420.3] - 2025-07-03 - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.420.2] - 2025-07-03 - -### 🚀 Features - -- *(template)* Added excalidraw (#6095) -- *(template)* Add excalidraw service configuration with documentation and tags - -### 🐛 Bug Fixes - -- *(terminal)* Ensure shell execution only uses valid shell if available in terminal command -- *(ui)* Improve destination selection description for clarity in resource segregation -- *(jobs)* Update middleware to use expireAfter for WithoutOverlapping in multiple job classes -- Removing eager loading (#6071) -- *(template)* Adjust health check interval and retries for excalidraw service -- *(ui)* Env variable settings wrong order -- *(service)* Ensure configuration changes are properly tracked and dispatched - -### 🚜 Refactor - -- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection -- *(terminal)* Simplify command construction for SSH execution -- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components -- *(policy)* Optimize team membership checks in S3StoragePolicy -- *(popup)* Improve styling and structure of the small popup component -- *(shared)* Enhance FQDN generation logic for services in newParser function -- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic -- *(init)* Standardize method naming conventions and improve command structure in Init.php -- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml -- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(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 - -## [4.0.0-beta.420.1] - 2025-06-26 - -### 🐛 Bug Fixes - -- *(server)* Prepend 'mux_' to UUID in muxFilename method for consistent naming -- *(ui)* Enhance terminal access messaging to clarify server functionality and terminal status -- *(database)* Proxy ssl port if ssl is enabled - -### 🚜 Refactor - -- *(ui)* Separate views for instance settings to separate paths to make it cleaner -- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files - -## [4.0.0-beta.420] - 2025-06-26 - -### 🚀 Features - -- *(service)* Add Miniflux service (#5843) -- *(service)* Add Pingvin Share service (#5969) -- *(auth)* Add Discord OAuth Provider (#5552) -- *(auth)* Add Clerk OAuth Provider (#5553) -- *(auth)* Add Zitadel OAuth Provider (#5490) -- *(core)* Set custom API rate limit (#5984) -- *(service)* Enhance service status handling and UI updates -- *(cleanup)* Add functionality to delete teams with no members or servers in CleanupStuckedResources command -- *(ui)* Add heart icon and enhance popup messaging for sponsorship support -- *(settings)* Add sponsorship popup toggle and corresponding database migration -- *(migrations)* Add optimized indexes to activity_log for improved query performance - -### 🐛 Bug Fixes - -- *(service)* Audiobookshelf healthcheck command (#5993) -- *(service)* Downgrade Evolution API phone version (#5977) -- *(service)* Pingvinshare-with-clamav -- *(ssh)* Scp requires square brackets for ipv6 (#6001) -- *(github)* Changing github app breaks the webhook. it does not anymore -- *(parser)* Improve FQDN generation and update environment variable handling -- *(ui)* Enhance status refresh buttons with loading indicators -- *(ui)* Update confirmation button text for stopping database and service -- *(routes)* Update middleware for deploy route to use 'api.ability:deploy' -- *(ui)* Refine API token creation form and update helper text for clarity -- *(ui)* Adjust layout of deployments section for improved alignment -- *(ui)* Adjust project grid layout and refine server border styling for better visibility -- *(ui)* Update border styling for consistency across components and enhance loading indicators -- *(ui)* Add padding to section headers in settings views for improved spacing -- *(ui)* Reduce gap between input fields in email settings for better alignment -- *(docker)* Conditionally enable gzip compression in Traefik labels based on configuration -- *(parser)* Enable gzip compression conditionally for Pocketbase images and streamline service creation logic -- *(ui)* Update padding for trademarks policy and enhance spacing in advanced settings section -- *(ui)* Correct closing tag for sponsorship link in layout popups -- *(ui)* Refine wording in sponsorship donation prompt in layout popups -- *(ui)* Update navbar icon color and enhance popup layout for sponsorship support -- *(ui)* Add target="_blank" to sponsorship links in layout popups for improved user experience -- *(models)* Refine comment wording in User model for clarity on user deletion criteria -- *(models)* Improve user deletion logic in User model to handle team member roles and prevent deletion if user is alone in root team -- *(ui)* Update wording in sponsorship prompt for clarity and engagement -- *(shared)* Refactor gzip handling for Pocketbase in newParser function for improved clarity - -### 🚜 Refactor - -- *(service)* Update Hoarder to their new name karakeep (#5964) -- *(service)* Karakeep naming and formatting -- *(service)* Improve miniflux -- *(core)* Rename API rate limit ENV -- *(ui)* Simplify container selection form in execute-container-command view -- *(email)* Streamline SMTP and resend settings logic for improved clarity -- *(invitation)* Rename methods for consistency and enhance invitation deletion logic -- *(user)* Streamline user deletion process and enhance team management logic - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(service)* Update Evolution API image to the official one (#6031) -- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421 -- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0 -- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates - -## [4.0.0-beta.419] - 2025-06-17 - -### 🚀 Features - -- *(core)* Add 'postmarketos' to supported OS list -- *(service)* Add memos service template (#5032) -- *(ui)* Upgrade to Tailwind v4 (#5710) -- *(service)* Add Navidrome service template (#5022) -- *(service)* Add Passbolt service (#5769) -- *(service)* Add Vert service (#5663) -- *(service)* Add Ryot service (#5232) -- *(service)* Add Marimo service (#5559) -- *(service)* Add Diun service (#5113) -- *(service)* Add Observium service (#5613) -- *(service)* Add Leantime service (#5792) -- *(service)* Add Limesurvey service (#5751) -- *(service)* Add Paymenter service (#5809) -- *(service)* Add CodiMD service (#4867) -- *(modal)* Add dispatchAction property to confirmation modal -- *(security)* Implement server patching functionality -- *(service)* Add Typesense service (#5643) -- *(service)* Add Yamtrack service (#5845) -- *(service)* Add PG Back Web service (#5079) -- *(service)* Update Maybe service and adjust it for the new release (#5795) -- *(oauth)* Set redirect uri as optional and add default value (#5760) -- *(service)* Add apache superset service (#4891) -- *(service)* Add One Time Secret service (#5650) -- *(service)* Add Seafile service (#5817) -- *(service)* Add Netbird-Client service (#5873) -- *(service)* Add OrangeHRM and Grist services (#5212) -- *(rules)* Add comprehensive documentation for Coolify architecture and development practices for AI tools, especially for cursor -- *(server)* Implement server patch check notifications -- *(api)* Add latest query param to Service restart API (#5881) -- *(api)* Add connect_to_docker_network setting to App creation API (#5691) -- *(routes)* Restrict backup download access to team admins and owners -- *(destination)* Update confirmation modal text and add persistent storage warning for server deployment -- *(terminal-access)* Implement terminal access control for servers and containers, including UI updates and backend logic -- *(ca-certificate)* Add CA certificate management functionality with UI integration and routing -- *(security-patches)* Add update check initialization and enhance notification messaging in UI -- *(previews)* Add force deploy without cache functionality and update deploy method to accept force rebuild parameter -- *(security-patterns)* Expand sensitive patterns list to include additional security-related variables -- *(database-backup)* Add MongoDB credential extraction and backup handling to DatabaseBackupJob -- *(activity-monitor)* Implement auto-scrolling functionality and dynamic content observation for improved user experience -- *(utf8-handling)* Implement UTF-8 sanitization for command outputs and enhance error handling in logs processing -- *(navbar)* Add Traefik dashboard availability check and server IP handling; refactor dynamic configurations loading -- *(proxy-dashboard)* Implement ProxyDashboardCacheService to manage Traefik dashboard cache; clear cache on configuration changes and proxy actions -- *(terminal-connection)* Enhance terminal connection handling with auto-connect feature and improved status messaging -- *(terminal)* Implement resize handling with ResizeObserver for improved terminal responsiveness -- *(migration)* Add is_sentinel_enabled column to server_settings with default true -- *(seeder)* Dispatch StartProxy action for each server in ProductionSeeder -- *(seeder)* Add CheckAndStartSentinelJob dispatch for each server in ProductionSeeder -- *(seeder)* Conditionally dispatch StartProxy action based on proxy check result -- *(service)* Update Changedetection template (#5937) - -### 🐛 Bug Fixes - -- *(constants)* Adding 'fedora-asahi-remix' as a supported OS (#5646) -- *(authentik)* Update docker-compose configuration for authentik service -- *(api)* Allow nullable destination_uuid (#5683) -- *(service)* Fix documenso startup and mail (#5737) -- *(docker)* Fix production dockerfile -- *(service)* Navidrome service -- *(service)* Passbolt -- *(service)* Add missing ENVs to NTFY service (#5629) -- *(service)* NTFY is behind a proxy -- *(service)* Vert logo and ENVs -- *(service)* Add platform to Observium service -- *(ActivityMonitor)* Prevent multiple event dispatches during polling -- *(service)* Convex ENVs and update image versions (#5827) -- *(service)* Paymenter -- *(ApplicationDeploymentJob)* Ensure correct COOLIFY_FQDN/COOLIFY_URL values (#4719) -- *(service)* Snapdrop no matching manifest error (#5849) -- *(service)* Use the same volume between chatwoot and sidekiq (#5851) -- *(api)* Validate docker_compose_raw input in ApplicationsController -- *(api)* Enhance validation for docker_compose_raw in ApplicationsController -- *(select)* Update PostgreSQL versions and titles in resource selection -- *(database)* Include DatabaseStatusChanged event in activityMonitor dispatch -- *(css)* Tailwind v5 things -- *(service)* Diun ENV for consistency -- *(service)* Memos service name -- *(css)* 8+ issue with new tailwind v4 -- *(css)* `bg-coollabs-gradient` not working anymore -- *(ui)* Add back missing service navbar components -- *(deploy)* Update resource timestamp handling in deploy_resource method -- *(patches)* DNF reboot logic is flipped -- *(deployment)* Correct syntax for else statement in docker compose build command -- *(shared)* Remove unused relation from queryDatabaseByUuidWithinTeam function -- *(deployment)* Correct COOLIFY_URL and COOLIFY_FQDN assignments based on parsing version in preview deployments -- *(docker)* Ensure correct parsing of environment variables by limiting explode to 2 parts -- *(project)* Update selected environment handling to use environment name instead of UUID -- *(ui)* Update server status display and improve server addition layout -- *(service)* Neon WS Proxy service not working on ARM64 (#5887) -- *(server)* Enhance error handling in server patch check notifications -- *(PushServerUpdateJob)* Add null checks before updating application and database statuses -- *(environment-variables)* Update label text for build variable checkboxes to improve clarity -- *(service-management)* Update service stop and restart messages for improved clarity and formatting -- *(preview-form)* Update helper text formatting in preview URL template input for better readability -- *(application-management)* Improve stop messages for application, database, and service to enhance clarity and formatting -- *(application-configuration)* Prevent access to preview deployments for deploy_key applications and update menu visibility accordingly -- *(select-component)* Handle exceptions during parameter retrieval and environment selection in the mount method -- *(previews)* Escape container names in stopContainers method to prevent shell injection vulnerabilities -- *(docker)* Add protection against empty container queries in GetContainersStatus to prevent unnecessary updates -- *(modal-confirmation)* Decode HTML entities in confirmation text to ensure proper display -- *(select-component)* Enhance user interaction by adding cursor styles and disabling selection during processing -- *(deployment-show)* Remove unnecessary fixed positioning for button container to improve layout responsiveness -- *(email-notifications)* Change notify method to notifyNow for immediate test email delivery -- *(service-templates)* Update Convex service configuration to use FQDN variables -- *(database-heading)* Simplify stop database message for clarity -- *(navbar)* Remove unnecessary x-init directive for loading proxy configuration -- *(patches)* Add padding to loading message for better visibility during update checks -- *(terminal-connection)* Improve error handling and stability for auto-connection; enhance component readiness checks and retry logic -- *(terminal)* Add unique wire:key to terminal component for improved reactivity and state management -- *(css)* Adjust utility classes in utilities.css for consistent application of Tailwind directives -- *(css)* Refine utility classes in utilities.css for proper Tailwind directive application -- *(install)* Update Docker installation script to use dynamic OS_TYPE and correct installation URL -- *(cloudflare)* Add error handling to automated Cloudflare configuration script -- *(navbar)* Add error handling for proxy status check to improve user feedback -- *(web)* Update user team retrieval method for consistent authentication handling -- *(cloudflare)* Update refresh method to correctly set Cloudflare tunnel status and improve user notification on IP address update -- *(service)* Update service template for affine and add migration service for improved deployment process -- *(supabase)* Update Supabase service images and healthcheck methods for improved reliability -- *(terminal)* Now it should work -- *(degraded-status)* Remove unnecessary whitespace in badge element for cleaner HTML -- *(routes)* Add name to security route for improved route management -- *(migration)* Update default value handling for is_sentinel_enabled column in server_settings -- *(seeder)* Conditionally dispatch CheckAndStartSentinelJob based on server's sentinel status -- *(service)* Disable healthcheck logging for Gotenberg (#6005) -- *(service)* Joplin volume name (#5930) -- *(server)* Update sentinelUpdatedAt assignment to use server's sentinel_updated_at property - -### 💼 Other - -- Add support for postmarketOS (#5608) -- *(core)* Simplify events for app/db/service status changes - -### 🚜 Refactor - -- *(service)* Observium -- *(service)* Improve leantime -- *(service)* Imporve limesurvey -- *(service)* Improve CodiMD -- *(service)* Typsense -- *(services)* Improve yamtrack -- *(service)* Improve paymenter -- *(service)* Consolidate configuration change dispatch logic and remove unused navbar component -- *(sidebar)* Simplify server patching link by removing button element -- *(slide-over)* Streamline button element and improve code readability -- *(service)* Enhance modal confirmation component with event dispatching for service stop actions -- *(slide-over)* Enhance class merging for improved component styling -- *(core)* Use property promotion -- *(service)* Improve maybe -- *(applications)* Remove unused docker compose raw decoding -- *(service)* Make TYPESENSE_API_KEY required -- *(ui)* Show toast when server does not work and on stop -- *(service)* Improve superset -- *(service)* Improve Onetimesecret -- *(service)* Improve Seafile -- *(service)* Improve orangehrm -- *(service)* Improve grist -- *(application)* Enhance application stopping logic to support multiple servers -- *(pricing-plans)* Improve label class binding for payment frequency selection -- *(error-handling)* Replace generic Exception with RuntimeException for improved error specificity -- *(error-handling)* Change Exception to RuntimeException for clearer error reporting -- *(service)* Remove informational dispatch during service stop for cleaner execution -- *(server-ui)* Improve layout and messaging in advanced settings and charts views -- *(terminal-access)* Streamline resource retrieval and enhance terminal access messaging in UI -- *(terminal)* Enhance terminal connection management and error handling, including improved reconnection logic and cleanup procedures -- *(application-deployment)* Separate handling of FAILED and CANCELLED_BY_USER statuses for clearer logic and notification -- *(jobs)* Update middleware to include job-specific identifiers for WithoutOverlapping -- *(jobs)* Modify middleware to use job-specific identifier for WithoutOverlapping -- *(environment-variables)* Remove debug logging from bulk submit handling for cleaner code -- *(environment-variables)* Simplify application build pack check in environment variable handling -- *(logs)* Adjust padding in logs view for improved layout consistency -- *(application-deployment)* Streamline post-deployment process by always dispatching container status check -- *(service-management)* Enhance container stopping logic by implementing parallel processing and removing deprecated methods -- *(activity-monitor)* Change activity property visibility and update view references for consistency -- *(activity-monitor)* Enhance layout responsiveness by adjusting class bindings and structure for better display -- *(service-management)* Update stopContainersInParallel method to enforce Server type hint for improved type safety -- *(service-management)* Rearrange docker cleanup logic in StopService to improve readability -- *(database-management)* Simplify docker cleanup logic in StopDatabase to enhance readability -- *(activity-monitor)* Consolidate activity monitoring logic and remove deprecated NewActivityMonitor component -- *(activity-monitor)* Update dispatch method to use activityMonitor instead of deprecated newActivityMonitor -- *(push-server-update)* Enhance application preview handling by incorporating pull request IDs and adding status update protections -- *(docker-compose)* Replace hardcoded Docker Compose configuration with external YAML template for improved database detection testing -- *(test-database-detection)* Rename services for clarity, add new database configurations, and update application service dependencies -- *(database-detection)* Enhance isDatabaseImage function to utilize service configuration for improved detection accuracy -- *(install-scripts)* Update Docker installation process to include manual installation fallback and improve error handling -- *(logs-view)* Update logs display for service containers with improved headings and dynamic key binding -- *(logs)* Enhance container loading logic and improve UI for logs display across various resource types -- *(cloudflare-tunnel)* Enhance layout and structure of Cloudflare Tunnel documentation and confirmation modal -- *(terminal-connection)* Streamline auto-connection logic and improve component readiness checks -- *(logs)* Remove unused methods and debug functionality from Logs.php for cleaner code -- *(remoteProcess)* Update sanitize_utf8_text function to accept nullable string parameter for improved type safety -- *(events)* Remove ProxyStarted event and associated ProxyStartedNotification listener for code cleanup -- *(navbar)* Remove unnecessary parameters from server navbar component for cleaner implementation -- *(proxy)* Remove commented-out listener and method for cleaner code structure -- *(events)* Update ProxyStatusChangedUI constructor to accept nullable teamId for improved flexibility -- *(cloudflare)* Update server retrieval method for improved query efficiency -- *(navbar)* Remove unused PHP use statement for cleaner code -- *(proxy)* Streamline proxy status handling and improve dashboard availability checks -- *(navbar)* Simplify proxy status handling and enhance loading indicators for better user experience -- *(resource-operations)* Filter out build servers from the server list and clean up commented-out code in the resource operations view -- *(execute-container-command)* Simplify connection logic and improve terminal availability checks -- *(navigation)* Remove wire:navigate directive from configuration links for cleaner HTML structure -- *(proxy)* Update StartProxy calls to use named parameter for async option -- *(clone-project)* Enhance server retrieval by including destinations and filtering out build servers -- *(ui)* Terminal -- *(ui)* Remove terminal header from execute-container-command view -- *(ui)* Remove unnecessary padding from deployment, backup, and logs sections - -### 📚 Documentation - -- Update changelog -- *(service)* Add new docs link for zipline (#5912) -- Update changelog -- Update changelog -- Update changelog - -### 🎨 Styling - -- *(css)* Update padding utility for password input and add newline in app.css -- *(css)* Refine badge utility styles in utilities.css -- *(css)* Enhance badge utility styles in utilities.css - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update coolify version to 4.0.0-beta.419 and nightly version to 4.0.0-beta.420 in configuration files -- *(service)* Rename hoarder server to karakeep (#5607) -- *(service)* Update Supabase services (#5708) -- *(service)* Remove unused documenso env -- *(service)* Formatting and cleanup of ryot -- *(docs)* Remove changelog and add it to gitignore -- *(versions)* Update version to 4.0.0-beta.419 -- *(service)* Diun formatting -- *(docs)* Update CHANGELOG.md -- *(service)* Switch convex vars -- *(service)* Pgbackweb formatting and naming update -- *(service)* Remove typesense default API key -- *(service)* Format yamtrack healthcheck -- *(core)* Remove unused function -- *(ui)* Remove unused stopEvent code -- *(service)* Remove unused env -- *(tests)* Update test environment database name and add new feature test for converting container environment variables to array -- *(service)* Update Immich service (#5886) -- *(service)* Remove unused logo -- *(api)* Update API docs -- *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance -- *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features -- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files -- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421 -- *(service)* Changedetection remove unused code - -## [4.0.0-beta.417] - 2025-05-07 - -### 🐛 Bug Fixes - -- *(select)* Update fallback logo path to use absolute URL for improved reliability - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update coolify version to 4.0.0-beta.418 - -## [4.0.0-beta.416] - 2025-05-05 - -### 🚀 Features - -- *(migration)* Add 'is_migrated' and 'custom_type' columns to service_applications and service_databases tables -- *(backup)* Implement custom database type selection and enhance scheduled backups management -- *(README)* Add Gozunga and Macarne to sponsors list -- *(redis)* Add scheduled cleanup command for Redis keys and enhance cleanup logic - -### 🐛 Bug Fixes - -- *(service)* Graceful shutdown of old container (#5731) -- *(ServerCheck)* Enhance proxy container check to ensure it is running before proceeding -- *(applications)* Include pull_request_id in deployment queue check to prevent duplicate deployments -- *(database)* Update label for image input field to improve clarity -- *(ServerCheck)* Set default proxy status to 'exited' to handle missing container state -- *(database)* Reduce container stop timeout from 300 to 30 seconds for improved responsiveness -- *(ui)* System theming for charts (#5740) -- *(dev)* Mount points?! -- *(dev)* Proxy mount point -- *(ui)* Allow adding scheduled backups for non-migrated databases -- *(DatabaseBackupJob)* Escape PostgreSQL password in backup command (#5759) -- *(ui)* Correct closing div tag in service index view - -### 🚜 Refactor - -- *(Database)* Streamline container shutdown process and reduce timeout duration -- *(core)* Streamline container stopping process and reduce timeout duration; update related methods for consistency -- *(database)* Update DB facade usage for consistency across service files -- *(database)* Enhance application conversion logic and add existence checks for databases and applications -- *(actions)* Standardize method naming for network and configuration deletion across application and service classes -- *(logdrain)* Consolidate log drain stopping logic to reduce redundancy -- *(StandaloneMariadb)* Add type hint for destination method to improve code clarity -- *(DeleteResourceJob)* Streamline resource deletion logic and improve conditional checks for database types -- *(jobs)* Update middleware to prevent job release after expiration for CleanupInstanceStuffsJob, RestartProxyJob, and ServerCheckJob -- *(jobs)* Unify middleware configuration to prevent job release after expiration for DockerCleanupJob and PushServerUpdateJob - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(seeder)* Update git branch from 'main' to 'v4.x' for multiple examples in ApplicationSeeder -- *(versions)* Update coolify version to 4.0.0-beta.417 and nightly version to 4.0.0-beta.418 - -## [4.0.0-beta.415] - 2025-04-29 - -### 🐛 Bug Fixes - -- *(ui)* Remove required attribute from image input in service application view -- *(ui)* Change application image validation to be nullable in service application view -- *(Server)* Correct proxy path formatting for Traefik proxy type - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update coolify version to 4.0.0-beta.416 and nightly version to 4.0.0-beta.417 in configuration files; fix links in deployment view - -## [4.0.0-beta.414] - 2025-04-28 - -### 🐛 Bug Fixes - -- *(ui)* Disable livewire navigate feature (causing spam of setInterval()) - -## [4.0.0-beta.413] - 2025-04-28 - -### 💼 Other - -- Adjust Workflows for v5 (#5689) - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(workflows)* Adjust workflow for announcement - -## [4.0.0-beta.411] - 2025-04-23 - -### 🚀 Features - -- *(deployment)* Add repository_project_id handling for private GitHub apps and clean up unused Caddy label logic -- *(api)* Enhance OpenAPI specifications with token variable and additional key attributes -- *(docker)* Add HTTP Basic Authentication support and enhance hostname parsing in Docker run conversion -- *(api)* Add HTTP Basic Authentication fields to OpenAPI specifications and enhance PrivateKey model descriptions -- *(README)* Add InterviewPal sponsorship link and corresponding SVG icon - -### 🐛 Bug Fixes - -- *(backup-edit)* Conditionally enable S3 checkbox based on available validated S3 storage -- *(source)* Update no sources found message for clarity -- *(api)* Correct middleware for service update route to ensure proper permissions -- *(api)* Handle JSON response in service creation and update methods for improved error handling -- Add 201 json code to servers validate api response -- *(docker)* Ensure password hashing only occurs when HTTP Basic Authentication is enabled -- *(docker)* Enhance hostname and GPU option validation in Docker run to compose conversion -- *(terminal)* Enhance WebSocket client verification with authorized IPs in terminal server -- *(ApplicationDeploymentJob)* Ensure source is an object before checking GitHub app properties - -### 🚜 Refactor - -- *(jobs)* Comment out unused Caddy label handling in ApplicationDeploymentJob and simplify proxy path logic in Server model -- *(database)* Simplify database type checks in ServiceDatabase and enhance image validation in Docker helper -- *(shared)* Remove unused ray debugging statement from newParser function -- *(applications)* Remove redundant error response in create_env method -- *(api)* Restructure routes to include versioning and maintain existing feedback endpoint -- *(api)* Remove token variable from OpenAPI specifications for clarity -- *(environment-variables)* Remove protected variable checks from delete methods for cleaner logic -- *(http-basic-auth)* Rename 'http_basic_auth_enable' to 'http_basic_auth_enabled' across application files for consistency -- *(docker)* Remove debug statement and enhance hostname handling in Docker run conversion -- *(server)* Simplify proxy path logic and remove unnecessary conditions - -### 📚 Documentation - -- Update changelog -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files -- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files -- *(versions)* Update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files -- *(versions)* Update realtime version to 1.0.8 in versions.json -- *(versions)* Update realtime version to 1.0.8 in versions.json -- *(docker)* Update soketi image version to 1.0.8 in production configuration files -- *(versions)* Update coolify version to 4.0.0-beta.414 and nightly version to 4.0.0-beta.415 in configuration files - -## [4.0.0-beta.410] - 2025-04-18 - -### 🚀 Features - -- Add HTTP Basic Authentication -- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README -- *(core)* Enable magic env variables for compose based applications - -### 🐛 Bug Fixes - -- *(application)* Append base directory to git branch URLs for improved path handling -- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON -- *(navbar)* Update error message link to use route for environment variables navigation -- Unsend template -- Replace ports with expose -- *(templates)* Update Unsend compose configuration for improved service integration - -### 🚜 Refactor - -- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management - -### 📚 Documentation - -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files -- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service - -## [4.0.0-beta.409] - 2025-04-16 - -### 🐛 Bug Fixes - -- *(parser)* Transform associative array labels into key=value format for better compatibility -- *(redis)* Update username and password input handling to clarify database sync requirements -- *(source)* Update connected source display to handle cases with no source connected - -### 🚜 Refactor - -- *(source)* Conditionally display connected source and change source options based on private key presence - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files - -## [4.0.0-beta.408] - 2025-04-14 - -### 🚀 Features - -- *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation -- *(subscription)* Enhance subscription management with loading states and Stripe status checks - -### 🐛 Bug Fixes - -- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command -- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans -- *(migrations)* Make stripe_comment field nullable in subscriptions table -- *(mongodb)* Also apply custom config when SSL is enabled -- *(templates)* Correct casing of denoKV references in service templates and YAML files -- *(deployment)* Handle missing destination in deployment process to prevent errors - -### 💼 Other - -- Add missing openapi items to PrivateKey - -### 🚜 Refactor - -- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files -- *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency -- *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience -- *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance - -### 📚 Documentation - -- Update changelog -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update nightly version to 4.0.0-beta.410 -- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook -- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json - -## [4.0.0-beta.407] - 2025-04-09 - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.406] - 2025-04-05 - -### 🚀 Features - -- *(Deploy)* Add info dispatch for proxy check initiation -- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component -- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic -- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values -- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling -- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process -- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages -- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings - -### 🐛 Bug Fixes - -- *(CheckProxy)* Update port conflict check to ensure accurate grep matching -- *(CheckProxy)* Refine port conflict detection with improved grep patterns -- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output -- *(api)* Add back validateDataApplications (#5539) -- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General -- *(Status)* Conditionally check proxy status and refresh button based on force_stop state -- *(General)* Change redis_password property to nullable string -- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint - -### 💼 Other - -- Add missing UUID to openapi spec - -### 🚜 Refactor - -- *(Server)* Use data_get for safer access to settings properties in isFunctional method -- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency -- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios -- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support -- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling -- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method -- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function -- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null -- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration -- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations -- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Bump version to 406 -- *(versions)* Bump version to 407 and 408 for coolify and nightly -- *(versions)* Bump version to 408 for coolify and 409 for nightly - -## [4.0.0-beta.405] - 2025-04-04 - -### 🚀 Features - -- *(api)* Update OpenAPI spec for services (#5448) -- *(proxy)* Enhance proxy handling and port conflict detection - -### 🐛 Bug Fixes - -- *(api)* Used ssh keys can be deleted -- *(email)* Transactional emails not sending - -### 🚜 Refactor - -- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks - -### 📚 Documentation - -- Update changelog -- Update changelog -- Update changelog -- Update changelog -- Update changelog -- Update changelog -- Update changelog -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Bump version to 406 -- *(versions)* Bump version to 407 - -## [4.0.0-beta.404] - 2025-04-03 - -### 🚀 Features - -- *(lang)* Added Azerbaijani language updated turkish language. (#5497) -- *(lang)* Added Portuguese from Brazil language (#5500) -- *(lang)* Add Indonesian language translations (#5513) - -### 🐛 Bug Fixes - -- *(docs)* Comment out execute for now -- *(installation)* Mount the docker config -- *(installation)* Path to config file for docker login -- *(service)* Add health check to Bugsink service (#5512) -- *(email)* Emails are not sent in multiple cases -- *(deployments)* Use graceful shutdown instead of `rm` -- *(docs)* Contribute service url (#5517) -- *(proxy)* Proxy restart does not work on domain -- *(ui)* Only show copy button on https -- *(database)* Custom config for MongoDB (#5471) - -### 📚 Documentation - -- Update changelog -- Update changelog -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(service)* Remove unused code in Bugsink service -- *(versions)* Update version to 404 -- *(versions)* Bump version to 403 (#5520) -- *(versions)* Bump version to 404 - -## [4.0.0-beta.402] - 2025-04-01 - -### 🚀 Features - -- *(deployments)* Add list application deployments api route -- *(deploy)* Add pull request ID parameter to deploy endpoint -- *(api)* Add pull request ID parameter to applications endpoint -- *(api)* Add endpoints for retrieving application logs and deployments -- *(lang)* Added Norwegian language (#5280) -- *(dep)* Bump all dependencies - -### 🐛 Bug Fixes - -- Only get apps for the current team -- *(DeployController)* Cast 'pr' query parameter to integer -- *(deploy)* Validate team ID before deployment -- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424) -- *(ui)* Instance Backup settings - -### 🚜 Refactor - -- *(dev)* Remove OpenAPI generation functionality -- *(migration)* Enhance local file volumes migration with logging - -### ⚙️ Miscellaneous Tasks - -- *(service)* Update minecraft service ENVs -- *(service)* Add more vars to infisical.yaml (#5418) -- *(service)* Add google variables to plausible.yaml (#5429) -- *(service)* Update authentik.yaml versions (#5373) -- *(core)* Remove redocs -- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404 - -## [4.0.0-beta.401] - 2025-03-28 - -### 📚 Documentation - -- Update changelog -- Update changelog - -## [4.0.0-beta.400] - 2025-03-27 - -### 🚀 Features - -- *(database)* Disable MongoDB SSL by default in migration -- *(database)* Add CA certificate generation for database servers -- *(application)* Add SPA configuration and update Nginx generation logic - -### 🐛 Bug Fixes - -- *(file-storage)* Double save on compose volumes -- *(parser)* Add logging support for applications in services - -### 🚜 Refactor - -- *(proxy)* Improve port availability checks with multiple methods -- *(database)* Update MongoDB SSL configuration for improved security -- *(database)* Enhance SSL configuration handling for various databases -- *(notifications)* Update Telegram button URL for staging environment -- *(models)* Remove unnecessary cloud check in isEnabled method -- *(database)* Streamline event listeners in Redis General component -- *(database)* Remove redundant database status display in MongoDB view -- *(database)* Update import statements for Auth in database components -- *(database)* Require PEM key file for SSL certificate regeneration -- *(database)* Change MySQL daemon command to MariaDB daemon -- *(nightly)* Update version numbers and enhance upgrade script -- *(versions)* Update version numbers for coolify and nightly -- *(email)* Validate team membership for email recipients -- *(shared)* Simplify deployment status check logic -- *(shared)* Add logging for running deployment jobs -- *(shared)* Enhance job status check to include 'reserved' -- *(email)* Improve error handling by passing context to handleError -- *(email)* Streamline email sending logic and improve configuration handling -- *(email)* Remove unnecessary whitespace in email sending logic -- *(email)* Allow custom email recipients in email sending logic -- *(email)* Enhance sender information formatting in email logic -- *(proxy)* Remove redundant stop call in restart method -- *(file-storage)* Add loadStorageOnServer method for improved error handling -- *(docker)* Parse and sanitize YAML compose file before encoding -- *(file-storage)* Improve layout and structure of input fields -- *(email)* Update label for test email recipient input -- *(database-backup)* Remove existing Docker container before backup upload -- *(database)* Improve decryption and deduplication of local file volumes -- *(database)* Remove debug output from volume update process - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update version numbers for coolify and nightly - -### ◀️ Revert - -- Encrypting mount and fs_path - -## [4.0.0-beta.399] - 2025-03-25 - ### 🚀 Features +- Use tags in update +- New update process (#115) +- VaultWarden service +- Www <-> non-www redirection for apps +- Www <-> non-www redirection +- Follow logs +- Generate www & non-www SSL certs +- Basic password reset form +- Scan for lock files and set right commands +- Public port range (WIP) +- Ports range +- Random subdomain for demo +- Random domain for services +- Astro buildpack +- 11ty buildpack +- Registration page +- Languagetool service +- Send version with update request +- Service secrets +- Webhooks inititate all applications with the correct branch +- Check ssl for new apps/services first +- Autodeploy pause +- Install pnpm into docker image if pnpm lock file is used +- Add PHP modules +- Use compose instead of normal docker cmd +- Be able to redeploy PRs +- Add n8n.io service +- Add update kuma service +- Ghost service +- Initial python support +- Add loading on register button +- *(dev)* Allow windows users to use pnpm dev +- MeiliSearch service +- Add abilitry to paste env files +- Wordpress on-demand SFTP +- Finalize on-demand sftp for wp +- PHP Composer support +- Working on-demand sftp to wp data +- Admin team sees everything +- Able to change service version/tag +- Basic white labeled version +- Able to modify database passwords +- Add persistent storage for services +- Multiply dockerfile locations for docker buildpack +- Testing fluentd logging driver +- Fluentbit investigation +- Initial deno support +- Deno DB migration +- Show exited containers on UI & better UX +- Query container state periodically +- Install svelte-18n and init setup +- Umami service +- Coolify auto-updater +- Autoupdater +- Select base image for buildpacks +- Hasura as a service +- Gzip compression +- Laravel buildpack is working! +- Laravel +- Fider service +- Database and services logs +- DNS check settings for SSL generation +- Cancel builds! +- Basic server usage on dashboard +- Show usage trends +- Usage on dashboard +- Custom script path for Plausible +- WP could have custom db +- Python image selection +- PageLoader +- Database + service usage +- Ability to change deployment type for nextjs +- Ability to change deployment type for nuxtjs +- Gitpod ready code(almost) +- Add Docker buildpack exposed port setting +- Custom port for git instances +- Gitpod integration +- Init moodle and separate stuffs to shared package +- Moodle init +- Remote docker engine init +- Working on remote docker engine +- Rde +- Remote docker engine +- Ipv4 and ipv6 +- Contributors +- Add arch to database +- Stop preview deployment +- Persistent storage for all services +- Cleanup clickhouse db +- Init heroku buildpacks +- Databases on ARM +- Mongodb arm support +- New dashboard +- Appwrite service +- Heroku deployments +- Deploy bots (no domains) +- Custom dns servers +- Import public repos (wip) +- Public repo deployment +- Force rebuild + env.PORT for port + public repo build +- Add GlitchTip service +- Searxng service +- *(ui)* Rework home UI and with responsive design +- New service - weblate +- Restart application +- Show elapsed time on running builds +- Github allow fual branches +- Gitlab dual branch +- Taiga +- *(routes)* Rework ui from login and register page +- Add traefik acme json to coolify container +- Database secrets +- New servers view +- Add queue reset button +- Previewapplications init +- PreviewApplications finalized +- Fluentbit +- Show remote servers +- *(layout)* Added drawer when user is in mobile +- Re-apply ui improves +- *(ui)* Improve header of pages +- *(styles)* Make header css component +- *(routes)* Improve ui for apps, databases and services logs +- Add migration button to appwrite +- Custom certificate +- Ssl cert on traefik config +- Refresh resource status on dashboard +- Ssl certificate sets custom ssl for applications +- System-wide github apps +- Cleanup unconfigured applications +- Cleanup unconfigured services and databases +- Docker compose support +- Docker compose +- Docker compose +- Monitoring by container +- Initial support for specific git commit +- Add default to latest commit and support for gitlab +- Redirect catch-all rule +- Rollback coolify +- Only show expose if no proxy conf defined in template +- Custom/private docker registries +- Use registry for building +- Docker registries working +- Custom docker compose file location in repo +- Save doNotTrackData to db +- Add default sentry +- Do not track in settings +- System wide git out of beta +- Custom previewseparator +- Sentry frontend +- Able to host static/php sites on arm +- Save application data before deploying +- SimpleDockerfile deployment +- Able to push image to docker registry +- Revert to remote image +- *(api)* Name label +- Add Openblocks icon +- Adding icon for whoogle +- *(ui)* Add libretranslate service icon +- Handle invite_only plausible analytics +- Init h2c (http2/grpc) support +- Http + h2c paralel +- Github raw icon url +- Remove svg support +- Add host path to any container +- Able to control multiplexing +- Add runRemoteCommandSync +- Github repo with deployment key +- Add persistent volumes +- Debuggable executeNow commands +- Add private gh repos +- Delete gh app +- Installation/update github apps +- Auto-deploy +- Deploy key based deployments +- Resource limits +- Long running queue with 1 hour of timeout +- Add arm build to dev +- Disk cleanup threshold by server +- Notify user of disk cleanup init +- Pricing plans ans subs +- Add s3 storages +- Init postgresql database +- Add backup notifications +- Dockerfile build pack +- Cloud +- Force password reset + waitlist +- Send internal notification to discord +- Monitor server connection +- Invite by email from waitlist +- Rolling update +- Add resend as transactional emails +- Send request in cloud +- Add discord notifications +- Public database +- Telegram topics separation +- Developer view for env variables +- Cache team settings +- Generate public key from private keys +- Able to invite more people at once +- Trial +- Dynamic trial period +- Ssh-agent instead of filesystem based ssh keys +- New container status checks +- Generate ssh key +- Sentry add email for better support +- Healthcheck for apps +- Add cloudflare tunnel support +- Services +- Image tag for services +- Container logs +- Reset root password +- Attach Coolify defined networks to services +- Delete resource command +- Multiselect removable resources +- Disable service, required version +- Basedir / monorepo initial support +- Init version of any git deployment +- Deploy private repo with ssh key +- Add email verification for cloud +- Able to deploy docker images +- Add dockerfile location +- Proxy logs on the ui +- Add custom redis conf +- Use docker login credentials from server +- Able to customize docker labels on applications +- Show if config is not applied +- Standalone mongodb +- Cloning project +- Api tokens + deploy webhook +- Start all kinds of things +- Simple search functionality +- Mysql, mariadb +- Lock environment variables +- Download local backups +- Improve deployment time by a lot +- Deployment logs fullscreen +- Service database backups +- Make service databases public +- Log drain (wip) +- Enable/disable log drain by service +- Log drainer container check +- Add docker engine support install script to rhel based systems +- Save timestamp configuration for logs +- Custom log drain endpoints +- Auto-restart tcp proxies for databases +- Execute command in container +- Autoupdate env during seed +- Disable autoupdate +- Randomly sleep between executions +- Pull latest images for services +- Custom docker compose commands +- Add environment description + able to change name +- Raw docker compose deployments +- Add www-non-www redirects to traefik +- Import backups +- Search between resources +- Move resources between projects / environments +- Clone any resource +- Shared environments +- Concurrent builds / server +- Able to deploy multiple resources with webhook +- Add PR comments +- Dashboard live deployment view +- Added manual webhook support for bitbucket +- Add initial support for custom docker run commands +- Cleanup unreachable servers +- Tags and tag deploy webhooks +- Clone to env +- Multi deployments +- Cleanup queue +- Magic for traefik redirectregex in services +- Revalidate server +- Disable gzip compression on service applications +- Save github app permission locally +- Minversion for services +- Able to add dynamic configurations from proxy dashboard +- Custom server limit +- Delay container/server jobs +- Add static ipv4 ipv6 support +- Server disabled by overflow +- Preview deployment logs +- Collect webhooks during maintenance +- Logs and execute commands with several servers +- Domains api endpoint +- Resources api endpoint +- Team api endpoint +- Add deployment details to deploy endpoint +- Add deployments api +- Experimental caddy support +- Dynamic configuration for caddy +- Reset password +- Show resources on source page +- Able to run scheduler/horizon programatically +- Change page width +- Watch paths +- Able to make rsa/ed ssh keys +- *(application)* Update submodules after git checkout +- Add amazon linux 2023 +- Upload large backups +- Edit domains easier for compose +- Able to delete configuration from server +- Configuration checker for all resources +- Allow tab in textarea +- Dynamic mux time +- Literal env variables +- Lazy load stuffs + tell user if compose based deployments have missing envs +- Can edit file/dir volumes from ui in compose based apps +- Upgrade Appwrite service template to 1.5 +- Upgrade Appwrite service template to 1.5 +- Add db name to backup notifications +- Initial datalist +- Update service contribution docs URL +- The final pricing plan, pay-as-you-go +- Add container name to network aliases in ApplicationDeploymentJob +- Add lazy loading for images in General.php and improve Docker Compose file handling in Application.php +- Experimental sentinel +- Start Sentinel on servers. +- Pull new sentinel image and restart container +- Init metrics +- Add AdminRemoveUser command to remove users from the database +- Adding new COOLIFY_ variables +- Save commit message and better view on deployments +- Toggle label escaping mechanism +- Shows the latest deployment commit + message on status +- New manual update process + remove next_channel +- Add lastDeploymentInfo and lastDeploymentLink props to breadcrumbs and status components +- Sort envs alphabetically and creation date +- Improve sorting of environment variables in the All component +- Update healthcheck test in StartMongodb action +- Add pull_request_id filter to get_last_successful_deployment method in Application model +- Add hc logs to healthchecks +- Add SerpAPI as a Github Sponsor +- Admin view for deleting users +- Scheduled task failed notification +- If the time seems too long it remains at 0s +- Improve Docker Engine start logic in ServerStatusJob +- If proxy stopped manually, it won't start back again +- Exclude_from_hc magic +- Gitea manual webhooks +- Add container logs in case the container does not start healthy +- Handle incomplete expired subscriptions in Stripe webhook +- Add more persistent storage types +- Add PHP memory limit environment variable to docker-compose.prod.yml +- Add manual update option to UpdateCoolify handle method +- Add port configuration for Vaultwarden service +- Able to change database passwords on the UI. It won't sync to the database. +- Able to add several domains to compose based previews +- Add bounty program link to bug report template +- Add titles +- Db proxy logs +- Easily redirect between www-and-non-www domains +- Add logos for new sponsors +- Add homepage template +- Update homepage.yaml with environment variables and volumes +- Spanish translation +- Cancelling a deployment will check if new could be started. +- Add supaguide logo to donations section +- Nixpacks now could reach local dbs internally +- Add Tigris logo to other/logos directory +- COOLIFY_CONTAINER_NAME predefined variable +- Charts +- Sentinel + charts +- Container metrics +- Add high priority queue +- Add metrics warning for servers without Sentinel enabled +- Add blacksmith logo to donations section +- Preselect server and destination if only one found +- More api endpoints +- Add API endpoint to update application by UUID +- Update statusnook logo filename in compose template +- Local fonts +- More API endpoints +- Bulk env update api endpoint +- Update server settings metrics history days to 7 +- New app API endpoint +- Private gh deployments through api +- Lots of api endpoints +- Api api api api api api +- Rename CloudCleanupSubs to CloudCleanupSubscriptions +- Early fraud warning webhook +- Improve internal notification message for early fraud warning webhook +- Add schema for uuid property in app update response +- Cleanup unused docker networks from proxy +- Compose parser v2 +- Display time interval for rollback images +- Add security and storage access key env to twenty template +- Add new logo for Latitude +- Enable legacy model binding in Livewire configuration +- Improve error handling in loadComposeFile method +- Add readonly labels +- Preserve git repository +- Force cleanup server +- Create/delete project endpoints +- Add patch request to projects +- Add server api endpoints +- Add branddev logo to README.md +- Update API endpoint summaries +- Update Caddy button label in proxy.blade.php +- Check custom internal name through server's applications. +- New server check job +- Delete team in cloud without subscription +- Coolify init should cleanup stuck networks in proxy +- Add manual update check functionality to settings page +- Update auto update and update check frequencies in settings +- Update Upgrade component to check for latest version of Coolify +- Improve homepage service template +- Support map fields in Directus +- Labels by proxy type +- Able to generate only the required labels for resources +- Preserve git repository with advanced file storages +- Added Windmill template +- Added Budibase template +- Add shm-size for custom docker commands +- Add custom docker container options to all databases +- Able to select different postgres database +- Add new logos for jobscollider and hostinger +- Order scheduled task executions +- Add Code Server environment variables to Service model +- Add coolify build env variables to building phase +- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid +- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid +- Update server_settings table to force docker cleanup +- Update Docker Compose file with DB_URL environment variable +- Refactor shared.php to improve environment variable handling +- Expose project description in API response +- Add elixir finetunes to the deployment job +- Make coolify full width by default +- Fully functional terminal for command center +- Custom terminal host +- Add buddy logo +- Add nullable constraint to 'fingerprint' column in private_keys table +- *(api)* Add an endpoint to execute a command +- *(api)* Add endpoint to execute a command +- Add ContainerStatusTypes enum for managing container status +- Allow specify use_build_server when creating/updating an application +- Add support for `use_build_server` in API endpoints for creating/updating applications +- Add Mixpost template +- Update resource deletion job to allow configurable options through API +- Add query parameters for deleting configurations, volumes, docker cleanup, and connected networks +- Add command to check application deployment queue +- Support Hetzner S3 +- Handle HTTPS domain in ConfigureCloudflareTunnels +- Backup all databases for mysql,mariadb,postgresql +- Restart service without pulling the latest image +- Add strapi template +- Add it-tools service template and logo +- Add homarr service tamplate and logo +- Add Argilla service configuration to Service model +- Add Invoice Ninja service configuration to Service model +- Project search on frontend +- Add ollama service with open webui and logo +- Update setType method to use slug value for type +- Refactor setType method to use slug value for type +- Refactor setType method to use slug value for type +- Add Supertokens template +- Add easyappointments service template +- Add dozzle template +- Adds forgejo service with runners +- Add Mautic 4 and 5 to service templates +- Add keycloak template +- Add onedev template +- Improve search functionality in project selection +- Add customHelper to stack-form +- Add cloudbeaver template +- Add ntfy template +- Add qbittorrent template +- Add Homebox template +- Add owncloud service and logo +- Add immich service +- Auto generate url +- Refactored to work with coolify auto env vars +- Affine service template and logo +- Add LibreTranslate template +- Open version in a new tab +- Add Transmission template +- Add transmission healhcheck +- Add zipline template +- Dify template +- Required envs +- Add EdgeDB +- Show warning if people would like to use sslip with https +- Add is shared to env variables +- Variabel sync and support shared vars +- Add notification settings to server_disk_usage +- Add coder service tamplate and logo +- Debug mode for sentinel +- Add jitsi template +- Add --gpu support for custom docker command +- Add Firefox template +- Add template for Wiki.js +- Add upgrade logs to /data/coolify/source +- Custom nginx configuration for static deployments + fix 404 redirects in nginx conf +- Check local horizon scheduler deployments +- Add internal api docs to /docs/api with auth +- Add proxy type change to create/update apis +- Add MacOS template +- Add Windows template +- *(service)* :sparkles: add mealie +- Add hex magic env var +- Add deploy-only token permission +- Able to deploy without cache on every commit +- Update private key nam with new slug as well +- Allow disabling default redirect, set status to 503 +- Add TLS configuration for default redirect in Server model +- Slack notifications +- Introduce root permission +- Able to download schedule task logs +- Migrate old email notification settings from the teams table +- Migrate old discord notification settings from the teams table +- Migrate old telegram notification settings from the teams table +- Add slack notifications to a new table +- Enable success messages again +- Use new notification stuff inside team model +- Some more notification settings and better defaults +- New email notification settings +- New shared function name `is_transactional_emails_enabled()` +- New shared notifications functions +- Email Notification Settings Model +- Telegram notification settings Model +- Discord notification settings Model +- Slack notification settings Model +- New Discord notification UI +- New Slack notification UI +- New telegram UI +- Use new notification event names +- Always sent notifications +- Scheduled task success notification +- Notification trait +- Get discord Webhook form new table +- Get Slack Webhook form new table +- Use new table or instance settings for email +- Use new place for settings and topic IDs for telegram +- Encrypt instance email settings +- Use encryption in instance settings model +- Scheduled task success and failure notifications +- Add docker cleanup success and failure notification settings columns +- UI for docker cleanup success and failure notification +- Docker cleanup email views +- Docker cleanup success and failure notification files +- Scheduled task success email +- Send new docker cleanup notifications +- :passport_control: integrate Authentik authentication with Coolify +- *(notification)* Add Pushover +- Add seeder command and configuration for database seeding +- Add new password magic env with symbols +- Add documenso service +- New ServerReachabilityChanged event +- Use new ServerReachabilityChanged event instead of isDirty +- Add infomaniak oauth +- Add server disk usage check frequency +- Add environment_uuid support and update API documentation +- Add service/resource/project labels +- Add coolify.environment label +- Add database subtype +- Migrate to new encryption options +- New encryption options +- Able to import full db backups for pg/mysql/mariadb +- Restore backup from server file +- Docker volume data cloning +- Move volume data cloning to a Job +- Volume cloning for ResourceOperations +- Remote server volume cloning +- Add horizon server details to queue +- Enhance horizon:manage command with worker restart check +- Add is_coolify_host to the server api responses +- DB migration for Backup retention +- UI for backup retention settings +- New global s3 and local backup deletion function +- Use new backup deletion functions +- Add calibre-web service +- Add actual-budget service +- Add rallly service +- Template for Gotenberg, a Docker-powered stateless API for PDF files +- Enhance import command options with additional guidance and improved checkbox label +- Purify for better sanitization +- Move docker cleanup to its own tab +- DB and Model for docker cleanup executions +- DockerCleanupExecutions relationship +- DockerCleanupDone event +- Get command and output for logs from CleanupDocker +- New sidebar menu and order +- Docker cleanup executions UI +- Add execution log to dockerCleanupJob +- Improve deployment UI +- Root user envs and seeding +- Email, username and password validation when they are set via envs +- Improved error handling and log output +- Add root user configuration variables to production environment +- Add log file check message in upgrade script for better troubleshooting +- Add root user details to install script +- *(core)* Wip version of coolify.json +- *(core)* Add SOURCE_COMMIT variable to build environment in ApplicationDeploymentJob +- *(service)* Update affine.yaml with AI environment variables (#4918) +- *(service)* Add new service Flipt (#4875) +- *(docs)* Update tech stack +- *(terminal)* Show terminal unavailable if the container does not have a shell on the global terminal UI +- *(ui)* Improve deployment UI +- *(template)* Add Open Web UI +- *(templates)* Add Open Web UI service template +- *(ui)* Update GitHub source creation advanced section label +- *(core)* Add dynamic label reset for application settings +- *(ui)* Conditionally enable advanced application settings based on label readonly status +- *(env)* Added COOLIFY_RESOURCE_UUID environment variable +- *(vite)* Add Cloudflare async script and style tag attributes +- *(meta)* Add comprehensive SEO and social media meta tags +- *(core)* Add name to default proxy configuration +- Add application api route +- Container logs +- Remove ansi color from log +- Add lines query parameter +- *(changelog)* Add git cliff for automatic changelog generation +- *(workflows)* Improve changelog generation and workflows +- *(ui)* Add periodic status checking for services +- *(deployment)* Ensure private key is stored in filesystem before deployment +- *(slack)* Show message title in notification previews (#5063) +- *(i18n)* Add Arabic translations (#4991) +- *(i18n)* Add French translations (#4992) +- *(services)* Update `service-templates.json` +- *(ui)* Add top padding to pricing plans view +- *(core)* Add error logging and cron parsing to docker/server schedules +- *(core)* Prevent using servers with existing resources as build servers +- *(ui)* Add textarea switching option in service compose editor +- *(ui)* Add wire:key to two-step confirmation settings +- *(database)* Add index to scheduled task executions for improved query performance +- *(database)* Add index to scheduled database backup executions +- *(billing)* Add Stripe past due subscription status tracking +- *(ui)* Add past due subscription warning banner - *(service)* Neon - *(migration)* Add `ssl_certificates` table and model - *(migration)* Add ssl setting to `standalone_postgresqls` table @@ -1699,4202 +1081,1087 @@ ### 🚀 Features - *(notifications)* Add discord ping functionality and settings - *(user)* Implement session deletion on password reset - *(github)* Enhance repository loading and validation in applications +- *(database)* Disable MongoDB SSL by default in migration +- *(database)* Add CA certificate generation for database servers +- *(application)* Add SPA configuration and update Nginx generation logic +- *(deployments)* Add list application deployments api route +- *(deploy)* Add pull request ID parameter to deploy endpoint +- *(api)* Add pull request ID parameter to applications endpoint +- *(api)* Add endpoints for retrieving application logs and deployments +- *(lang)* Added Norwegian language (#5280) +- *(dep)* Bump all dependencies +- *(lang)* Added Azerbaijani language updated turkish language. (#5497) +- *(lang)* Added Portuguese from Brazil language (#5500) +- *(lang)* Add Indonesian language translations (#5513) +- *(api)* Update OpenAPI spec for services (#5448) +- *(proxy)* Enhance proxy handling and port conflict detection +- *(Deploy)* Add info dispatch for proxy check initiation +- *(EnvironmentVariable)* Add handling for Redis credentials in the environment variable component +- *(EnvironmentVariable)* Implement protection for critical environment variables and enhance deletion logic +- *(Application)* Add networkAliases attribute for handling network aliases as JSON or comma-separated values +- *(GithubApp)* Update default events to include 'pull_request' and streamline event handling +- *(CleanupDocker)* Add support for realtime image management in Docker cleanup process +- *(Deployment)* Enhance queue_application_deployment to handle existing deployments and return appropriate status messages +- *(SourceManagement)* Add functionality to change Git source and display current source in the application settings +- *(OpenApi)* Enhance OpenAPI specifications by adding UUID parameters for application, project, and service updates; improve deployment listing with pagination parameters; update command signature for OpenApi generation +- *(subscription)* Enhance subscription management with loading states and Stripe status checks +- Add HTTP Basic Authentication +- *(readme)* Add new sponsors Supadata AI and WZ-IT to the README +- *(core)* Enable magic env variables for compose based applications +- *(deployment)* Add repository_project_id handling for private GitHub apps and clean up unused Caddy label logic +- *(api)* Enhance OpenAPI specifications with token variable and additional key attributes +- *(docker)* Add HTTP Basic Authentication support and enhance hostname parsing in Docker run conversion +- *(api)* Add HTTP Basic Authentication fields to OpenAPI specifications and enhance PrivateKey model descriptions +- *(README)* Add InterviewPal sponsorship link and corresponding SVG icon +- *(migration)* Add 'is_migrated' and 'custom_type' columns to service_applications and service_databases tables +- *(backup)* Implement custom database type selection and enhance scheduled backups management +- *(README)* Add Gozunga and Macarne to sponsors list +- *(redis)* Add scheduled cleanup command for Redis keys and enhance cleanup logic +- *(core)* Add 'postmarketos' to supported OS list +- *(service)* Add memos service template (#5032) +- *(ui)* Upgrade to Tailwind v4 (#5710) +- *(service)* Add Navidrome service template (#5022) +- *(service)* Add Passbolt service (#5769) +- *(service)* Add Vert service (#5663) +- *(service)* Add Ryot service (#5232) +- *(service)* Add Marimo service (#5559) +- *(service)* Add Diun service (#5113) +- *(service)* Add Observium service (#5613) +- *(service)* Add Leantime service (#5792) +- *(service)* Add Limesurvey service (#5751) +- *(service)* Add Paymenter service (#5809) +- *(service)* Add CodiMD service (#4867) +- *(modal)* Add dispatchAction property to confirmation modal +- *(security)* Implement server patching functionality +- *(service)* Add Typesense service (#5643) +- *(service)* Add Yamtrack service (#5845) +- *(service)* Add PG Back Web service (#5079) +- *(service)* Update Maybe service and adjust it for the new release (#5795) +- *(oauth)* Set redirect uri as optional and add default value (#5760) +- *(service)* Add apache superset service (#4891) +- *(service)* Add One Time Secret service (#5650) +- *(service)* Add Seafile service (#5817) +- *(service)* Add Netbird-Client service (#5873) +- *(service)* Add OrangeHRM and Grist services (#5212) +- *(rules)* Add comprehensive documentation for Coolify architecture and development practices for AI tools, especially for cursor +- *(server)* Implement server patch check notifications +- *(api)* Add latest query param to Service restart API (#5881) +- *(api)* Add connect_to_docker_network setting to App creation API (#5691) +- *(routes)* Restrict backup download access to team admins and owners +- *(destination)* Update confirmation modal text and add persistent storage warning for server deployment +- *(terminal-access)* Implement terminal access control for servers and containers, including UI updates and backend logic +- *(ca-certificate)* Add CA certificate management functionality with UI integration and routing +- *(security-patches)* Add update check initialization and enhance notification messaging in UI +- *(previews)* Add force deploy without cache functionality and update deploy method to accept force rebuild parameter +- *(security-patterns)* Expand sensitive patterns list to include additional security-related variables +- *(database-backup)* Add MongoDB credential extraction and backup handling to DatabaseBackupJob +- *(activity-monitor)* Implement auto-scrolling functionality and dynamic content observation for improved user experience +- *(utf8-handling)* Implement UTF-8 sanitization for command outputs and enhance error handling in logs processing +- *(navbar)* Add Traefik dashboard availability check and server IP handling; refactor dynamic configurations loading +- *(proxy-dashboard)* Implement ProxyDashboardCacheService to manage Traefik dashboard cache; clear cache on configuration changes and proxy actions +- *(terminal-connection)* Enhance terminal connection handling with auto-connect feature and improved status messaging +- *(terminal)* Implement resize handling with ResizeObserver for improved terminal responsiveness +- *(migration)* Add is_sentinel_enabled column to server_settings with default true +- *(seeder)* Dispatch StartProxy action for each server in ProductionSeeder +- *(seeder)* Add CheckAndStartSentinelJob dispatch for each server in ProductionSeeder +- *(seeder)* Conditionally dispatch StartProxy action based on proxy check result +- *(service)* Update Changedetection template (#5937) +- *(service)* Add Miniflux service (#5843) +- *(service)* Add Pingvin Share service (#5969) +- *(auth)* Add Discord OAuth Provider (#5552) +- *(auth)* Add Clerk OAuth Provider (#5553) +- *(auth)* Add Zitadel OAuth Provider (#5490) +- *(core)* Set custom API rate limit (#5984) +- *(service)* Enhance service status handling and UI updates +- *(cleanup)* Add functionality to delete teams with no members or servers in CleanupStuckedResources command +- *(ui)* Add heart icon and enhance popup messaging for sponsorship support +- *(settings)* Add sponsorship popup toggle and corresponding database migration +- *(migrations)* Add optimized indexes to activity_log for improved query performance +- *(template)* Added excalidraw (#6095) +- *(template)* Add excalidraw service configuration with documentation and tags +- *(scheduling)* Add command to manually run scheduled database backups and tasks with options for chunking, delays, and dry runs +- *(scheduling)* Add frequency filter option for manual execution of scheduled jobs +- *(logging)* Implement scheduled logs command and enhance backup/task scheduling with cron checks +- *(logging)* Add frequency filters for scheduled logs command to support hourly, daily, weekly, and monthly job views +- *(scheduling)* Introduce ScheduledJobManager and ServerResourceManager for enhanced job scheduling and resource management +- *(previews)* Implement soft delete and cleanup for ApplicationPreview, enhancing resource management in DeleteResourceJob +- *(service)* Enable password protection for the Wireguard Ul +- *(queues)* Improve Horizon config to reduce CPU and RAM usage (#6212) +- *(service)* Add Gowa service (#6164) +- *(container)* Add updatedSelectedContainer method to connect to non-default containers and update wire:model for improved reactivity +- *(application)* Implement environment variable updates for Docker Compose applications, including creation, updating, and deletion of SERVICE_FQDN and SERVICE_URL variables +- *(service)* Add TriliumNext service (#5970) +- *(service)* Add Matrix service (#6029) +- *(service)* Add GitHub Action runner service (#6209) +- *(terminal)* Dispatch focus event for terminal after connection and enhance focus handling in JavaScript +- *(lang)* Add Polish language & improve forgot_password translation (#6306) +- *(service)* Update Authentik template (#6264) +- *(service)* Add sequin template (#6105) +- *(service)* Add pi-hole template (#6020) +- *(services)* Add Chroma service (#6201) +- *(service)* Add OpenPanel template (#5310) +- *(service)* Add librechat template (#5654) +- *(service)* Add Homebox service (#6116) +- *(service)* Add pterodactyl & wings services (#5537) +- *(service)* Add Bluesky PDS template (#6302) +- *(input)* Add autofocus attribute to input component for improved accessibility +- *(core)* Finally fqdn is fqdn and url is url. haha +- *(user)* Add changelog read tracking and unread count method +- *(templates)* Add new service templates and update existing compose files for various applications +- *(changelog)* Implement automated changelog fetching from GitHub and enhance changelog read tracking +- *(drizzle-gateway)* Add new drizzle-gateway service with configuration and logo +- *(drizzle-gateway)* Enhance service configuration by adding Master Password field and updating compose file path +- *(templates)* Add new service templates for Homebox, LibreChat, Pterodactyl, and Wings with corresponding configurations and logos +- *(templates)* Add Bluesky PDS service template and update compose file with new environment variable +- *(readme)* Add CubePath as a big sponsor and include new small sponsors with logos +- *(api)* Add create_environment endpoint to ProjectController for environment creation in projects +- *(api)* Add endpoints for managing environments in projects, including listing, creating, and deleting environments +- *(backup)* Add disable local backup option and related logic for S3 uploads +- *(dev patches)* Add functionality to send test email with patch data in development mode +- *(templates)* Added category per service +- *(email)* Implement email change request and verification process +- Generate category for services +- *(service)* Add elasticsearch template (#6300) +- *(sanitization)* Integrate DOMPurify for HTML sanitization across components +- *(cleanup)* Add command for sanitizing name fields across models +- *(sanitization)* Enhance HTML sanitization with improved DOMPurify configuration +- *(validation)* Centralize validation patterns for names and descriptions +- *(git-settings)* Add support for shallow cloning in application settings +- *(auth)* Implement authorization checks for server updates across multiple components +- *(auth)* Implement authorization for PrivateKey management +- *(auth)* Implement authorization for Docker and server management +- *(validation)* Add custom validation rules for Git repository URLs and branches +- *(security)* Add authorization checks for package updates in Livewire components +- *(auth)* Implement authorization checks for application management +- *(auth)* Enhance API error handling for authorization exceptions +- *(auth)* Add comprehensive authorization checks for all kind of resource creations +- *(auth)* Implement authorization checks for database management +- *(auth)* Refine authorization checks for S3 storage and service management +- *(auth)* Implement comprehensive authorization checks across API controllers +- *(auth)* Introduce resource creation authorization middleware and policies for enhanced access control +- *(auth)* Add middleware for resource creation authorization +- *(auth)* Enhance authorization checks in Livewire components for resource management +- *(validation)* Add ValidIpOrCidr rule for validating IP addresses and CIDR notations; update API access settings UI and add comprehensive tests +- *(docs)* Update architecture and development guidelines; enhance form components with built-in authorization system and improve routing documentation +- *(docs)* Expand authorization documentation for custom Alpine.js components; include manual protection patterns and implementation guidelines +- *(sentinel)* Implement SentinelRestarted event and update Livewire components to handle server restart notifications +- *(api)* Enhance IP access control in middleware and settings; support CIDR notation and special case for 0.0.0.0 to allow all IPs +- *(acl)* Change views/backend code to able to use proper ACL's later on. Currently it is not enabled. +- *(docs)* Add Backlog.md guidelines and project manager backlog agent; enhance CLAUDE.md with new links for task management +- *(docs)* Add tasks for implementing Docker build caching and optimizing staging builds; include detailed acceptance criteria and implementation plans +- *(docker)* Implement Docker cleanup processing in ScheduledJobManager; refactor server task scheduling to streamline cleanup job dispatching +- *(docs)* Expand Backlog.md guidelines with comprehensive usage instructions, CLI commands, and best practices for task management to enhance project organization and collaboration +- *(policies)* Add EnvironmentVariablePolicy for managing environment variables ( it was missing ) +- *(domains)* Implement domain conflict detection and user confirmation modal across application components +- *(domains)* Add force_domain_override option and enhance domain conflict detection responses ### 🐛 Bug Fixes -- *(api)* Docker compose based apps creationg through api -- *(database)* Improve database type detection for Supabase Postgres images -- *(ssl)* Permission of ssl crt and key inside the container -- *(ui)* Make sure file mounts do not showing the encrypted values -- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert -- *(ui)* Select component should not always uses title case -- *(db)* SSL certificates table and model -- *(migration)* Ssl certificates table -- *(databases)* Fix database name users new `uuid` instead of DB one -- *(database)* Fix volume and file mounts and naming -- *(migration)* Store subjectAlternativeNames as a json array in the db -- *(ssl)* Make sure the subjectAlternativeNames are unique and stored correctly -- *(ui)* Certificate expiration data is null before starting the DB -- *(deletion)* Fix DB deletion -- *(ssl)* Improve SSL cert file mounts -- *(ssl)* Always create ca crt on disk even if it is already there -- *(ssl)* Use mountPath parameter not a hardcoded path -- *(ssl)* Use 1 instead of on for mysql -- *(ssl)* Do not remove SSL directory -- *(ssl)* Wrong ssl cert is loaded to the server and UI error when regenerating SSL -- *(ssl)* Make sure when regenerating the CA cert it is not overwritten with a server cert -- *(ssl)* Regenerating certs for a specific DB -- *(ssl)* Fix MariaDB and MySQL need CA cert -- *(ssl)* Add mount path to DB to fix regeneration of certs -- *(ssl)* Fix SSL regeneration to sign with CA cert and use mount path -- *(ssl)* Get caCert correctly -- *(ssl)* Remove caCert even if it is a folder by accident -- *(ssl)* Ger caCert and `mountPath` correctly -- *(ui)* Only show Regenerate SSL Certificates button when there is a cert -- *(ssl)* Server id -- *(ssl)* When regenerating SSL certs the cert is not singed with the new CN -- *(ssl)* Adjust ca paths for MySQL -- *(ssl)* Remove mode selection for MariaDB as it is not supported -- *(ssl)* Permission issue with MariDB cert and key and paths -- *(ssl)* Rename Redis mode to verify-ca as it is not verify-full -- *(ui)* Remove unused mode for MongoDB -- *(ssl)* KeyDB port and caCert args are missing -- *(ui)* Enable SSL is not working correctly for KeyDB -- *(ssl)* Add `--tls` arg to DrangflyDB -- *(notification)* Always send SSL notifications -- *(database)* Change default value of enable_ssl to false for multiple tables -- *(ui)* Correct grammatical error in 404 page -- *(seeder)* Update GitHub app name in GithubAppSeeder -- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration -- *(domain)* Dispatch refreshStatus event after successful domain update -- *(database)* Correct container name generation for service databases -- *(database)* Limit container name length for database proxy -- *(database)* Handle unsupported database types in StartDatabaseProxy -- *(database)* Simplify container name generation in StartDatabaseProxy -- *(install)* Handle potential errors in Docker address pool configuration -- *(backups)* Retention settings -- *(redis)* Set default redis_username for new instances -- *(core)* Improve instantSave logic and error handling -- *(general)* Correct link to framework specific documentation -- *(core)* Redirect healthcheck route for dockercompose applications -- *(api)* Use name from request payload -- *(issue#4746)* Do not use setGitImportSettings inside of generateGitLsRemoteCommands -- Correct some spellings -- *(service)* Replace deprecated credentials env variables on keycloak service -- *(keycloak)* Update keycloak image version to 26.1 -- *(console)* Handle missing root user in password reset command -- *(ssl)* Handle missing CA certificate in SSL regeneration job -- *(copy-button)* Ensure text is safely passed to clipboard - -### 💼 Other - -- Bump Coolify to 4.0.0-beta.400 -- *(migration)* Add SSL fields to database tables -- SSL Support for KeyDB - -### 🚜 Refactor - -- *(ui)* Unhide log toggle in application settings -- *(nginx)* Streamline default Nginx configuration and improve error handling -- *(install)* Clean up install script and enhance Docker installation logic -- *(ScheduledTask)* Clean up code formatting and remove unused import -- *(app)* Remove unused MagicBar component and related code -- *(database)* Streamline SSL configuration handling across database types -- *(application)* Streamline healthcheck parsing from Dockerfile -- *(notifications)* Standardize getRecipients method signatures -- *(configuration)* Centralize configuration management in ConfigurationRepository -- *(docker)* Update image references to use centralized registry URL -- *(env)* Add centralized registry URL to environment configuration -- *(storage)* Simplify file storage iteration in Blade template -- *(models)* Add is_directory attribute to LocalFileVolume model -- *(modal)* Add ignoreWire attribute to modal-confirmation component -- *(invite-link)* Adjust layout for better responsiveness in form -- *(invite-link)* Enhance form layout for improved responsiveness -- *(network)* Enhance docker network creation with ipv6 fallback -- *(network)* Check for existing coolify network before creation -- *(database)* Enhance encryption process for local file volumes - -### 📚 Documentation - -- Update changelog -- Update changelog -- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(migration)* Remove unused columns -- *(ssl)* Improve code in ssl helper -- *(migration)* Ssl cert and key should not be nullable -- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts -- Rename ca crt folder to ssl -- *(ui)* Improve valid until handling -- Improve code quality suggested by code rabbit -- *(supabase)* Update Supabase service template and Postgres image version -- *(versions)* Update version numbers for coolify and nightly - -## [4.0.0-beta.398] - 2025-03-01 - -### 🚀 Features - -- *(billing)* Add Stripe past due subscription status tracking -- *(ui)* Add past due subscription warning banner - -### 🐛 Bug Fixes - -- *(billing)* Restrict Stripe subscription status update to 'active' only - -### 💼 Other - -- Bump Coolify to 4.0.0-beta.398 - -### 🚜 Refactor - -- *(billing)* Enhance Stripe subscription status handling and notifications - -## [4.0.0-beta.397] - 2025-02-28 - -### 🐛 Bug Fixes - -- *(billing)* Handle 'past_due' subscription status in Stripe processing -- *(revert)* Label parsing -- *(helpers)* Initialize command variable in parseCommandFromMagicEnvVariable - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.396] - 2025-02-28 - -### 🚀 Features - -- *(ui)* Add wire:key to two-step confirmation settings -- *(database)* Add index to scheduled task executions for improved query performance -- *(database)* Add index to scheduled database backup executions - -### 🐛 Bug Fixes - -- *(core)* Production dockerfile -- *(ui)* Update storage configuration guidance link -- *(ui)* Set default SMTP encryption to starttls -- *(notifications)* Correct environment URL path in application notifications -- *(config)* Update default PostgreSQL host to coolify-db instead of postgres -- *(docker)* Improve Docker compose file validation process -- *(ui)* Restrict service retrieval to current team -- *(core)* Only validate custom compose files -- *(mail)* Set default mailer to array when not specified -- *(ui)* Correct redirect routes after task deletion -- *(core)* Adding a new server should not try to make the default docker network -- *(core)* Clean up unnecessary files during application image build -- *(core)* Improve label generation and merging for applications and services - -### 💼 Other - -- Bump all dependencies (#5216) - -### 🚜 Refactor - -- *(ui)* Simplify file storage modal confirmations -- *(notifications)* Improve transactional email settings handling -- *(scheduled-tasks)* Improve scheduled task creation and management - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Bump helper and realtime version - -## [4.0.0-beta.395] - 2025-02-22 - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.394] - 2025-02-17 - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.393] - 2025-02-15 - -### 📚 Documentation - -- Update changelog - -## [4.0.0-beta.392] - 2025-02-13 - -### 🚀 Features - -- *(ui)* Add top padding to pricing plans view -- *(core)* Add error logging and cron parsing to docker/server schedules -- *(core)* Prevent using servers with existing resources as build servers -- *(ui)* Add textarea switching option in service compose editor - -### 🐛 Bug Fixes - -- Pull latest image from registry when using build server -- *(deployment)* Improve server selection for deployment cancellation -- *(deployment)* Improve log line rendering and formatting -- *(s3-storage)* Optimize team admin notification query -- *(core)* Improve connection testing with dynamic disk configuration for s3 backups -- *(core)* Update service status refresh event handling -- *(ui)* Adjust polling intervals for database and service status checks -- *(service)* Update Fider service template healthcheck command -- *(core)* Improve server selection error handling in Docker component -- *(core)* Add server functionality check before dispatching container status -- *(ui)* Disable sticky scroll in Monaco editor -- *(ui)* Add literal and multiline env support to services. -- *(services)* Owncloud docs link -- *(template)* Remove db-migration step from `infisical.yaml` (#5209) -- *(service)* Penpot (#5047) - -### 🚜 Refactor - -- Use pull flag on docker compose up - -### 📚 Documentation - -- Update changelog -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- Rollback Coolify version to 4.0.0-beta.392 -- Bump Coolify version to 4.0.0-beta.393 -- Bump Coolify version to 4.0.0-beta.394 -- Bump Coolify version to 4.0.0-beta.395 -- Bump Coolify version to 4.0.0-beta.396 -- *(services)* Update zipline to use new Database env var. (#5210) -- *(service)* Upgrade authentik service -- *(service)* Remove unused env from zipline - -## [4.0.0-beta.391] - 2025-02-04 - -### 🚀 Features - -- Add application api route -- Container logs -- Remove ansi color from log -- Add lines query parameter -- *(changelog)* Add git cliff for automatic changelog generation -- *(workflows)* Improve changelog generation and workflows -- *(ui)* Add periodic status checking for services -- *(deployment)* Ensure private key is stored in filesystem before deployment -- *(slack)* Show message title in notification previews (#5063) -- *(i18n)* Add Arabic translations (#4991) -- *(i18n)* Add French translations (#4992) -- *(services)* Update `service-templates.json` - -### 🐛 Bug Fixes - -- *(core)* Improve deployment failure Slack notification formatting -- *(core)* Update Slack notification formatting to use bold correctly -- *(core)* Enhance Slack deployment success notification formatting -- *(ui)* Simplify service templates loading logic -- *(ui)* Align title and add button vertically in various views -- Handle pullrequest:updated for reliable preview deployments -- *(ui)* Fix typo on team page (#5105) -- Cal.com documentation link give 404 (#5070) -- *(slack)* Notification settings URL in `HighDiskUsage` message (#5071) -- *(ui)* Correct typo in Storage delete dialog (#5061) -- *(lang)* Add missing italian translations (#5057) -- *(service)* Improve duplicati.yaml (#4971) -- *(service)* Links in homepage service (#5002) -- *(service)* Added SMTP credentials to getoutline yaml template file (#5011) -- *(service)* Added `KEY` Variable to Beszel Template (#5021) -- *(cloudflare-tunnels)* Dead links to docs (#5104) -- System-wide GitHub apps (#5114) - -### 🚜 Refactor - -- Simplify service start and restart workflows - -### 📚 Documentation - -- *(services)* Reword nitropage url and slogan -- *(readme)* Add Convex to special sponsors section -- Update changelog - -### ⚙️ Miscellaneous Tasks - -- *(config)* Increase default PHP memory limit to 256M -- Add openapi response -- *(workflows)* Make naming more clear and remove unused code -- Bump Coolify version to 4.0.0-beta.392/393 -- *(ci)* Update changelog generation workflow to target 'next' branch -- *(ci)* Update changelog generation workflow to target main branch - -## [4.0.0-beta.390] - 2025-01-28 - -### 🚀 Features - -- *(template)* Add Open Web UI -- *(templates)* Add Open Web UI service template -- *(ui)* Update GitHub source creation advanced section label -- *(core)* Add dynamic label reset for application settings -- *(ui)* Conditionally enable advanced application settings based on label readonly status -- *(env)* Added COOLIFY_RESOURCE_UUID environment variable -- *(vite)* Add Cloudflare async script and style tag attributes -- *(meta)* Add comprehensive SEO and social media meta tags -- *(core)* Add name to default proxy configuration - -### 🐛 Bug Fixes - -- *(ui)* Update database control UI to check server functionality before displaying actions -- *(ui)* Typo in upgrade message -- *(ui)* Cloudflare tunnel configuration should be an info, not a warning -- *(s3)* DigitalOcean storage buckets do not work -- *(ui)* Correct typo in container label helper text -- Disable certain parts if readonly label is turned off -- Cleanup old scheduled_task_executions -- Validate cron expression in Scheduled Task update -- *(core)* Check cron expression on save -- *(database)* Detect more postgres database image types -- *(templates)* Update service templates -- Remove quotes in COOLIFY_CONTAINER_NAME -- *(templates)* Update Trigger.dev service templates with v3 configuration -- *(database)* Adjust MongoDB restore command and import view styling -- *(core)* Improve public repository URL parsing for branch and base directory -- *(core)* Increase HTTP/2 max concurrent streams to 250 (default) -- *(ui)* Update docker compose file helper text to clarify repository modification -- *(ui)* Skip SERVICE_FQDN and SERVICE_URL variables during update -- *(core)* Stopping database is not disabling db proxy -- *(core)* Remove --remove-orphans flag from proxy startup command to prevent other proxy deletions (db) -- *(api)* Domain check when updating domain -- *(ui)* Always redirect to dashboard after team switch -- *(backup)* Escape special characters in database backup commands - -### 💼 Other - -- Trigger.dev templates - wrong key length issue -- Trigger.dev template - missing ports and wrong env usage -- Trigger.dev template - fixed otel config -- Trigger.dev template - fixed otel config -- Trigger.dev template - fixed port config - -### 🚜 Refactor - -- *(s3)* Improve S3 bucket endpoint formatting -- *(vite)* Improve environment variable handling in Vite configuration -- *(ui)* Simplify GitHub App registration UI and layout - -### ⚙️ Miscellaneous Tasks - -- *(version)* Bump Coolify version to 4.0.0-beta.391 - -### ◀️ Revert - -- Remove Cloudflare async tag attributes - -## [4.0.0-beta.389] - 2025-01-23 - -### 🚀 Features - -- *(docs)* Update tech stack -- *(terminal)* Show terminal unavailable if the container does not have a shell on the global terminal UI -- *(ui)* Improve deployment UI - -### 🐛 Bug Fixes - -- *(service)* Infinite loading and lag with invoiceninja service (#4876) -- *(service)* Invoiceninja service -- *(workflows)* `Waiting for changes` label should also be considered and improved messages -- *(workflows)* Remove tags only if the PR has been merged into the main branch -- *(terminal)* Terminal shows that it is not available, even though it is -- *(labels)* Docker labels do not generated correctly -- *(helper)* Downgrade Nixpacks to v1.29.0 -- *(labels)* Generate labels when they are empty not when they are already generated -- *(storage)* Hetzner storage buckets not working - -### 📚 Documentation - -- Add TECH_STACK.md (#4883) - -### ⚙️ Miscellaneous Tasks - -- *(versions)* Update coolify versions to v4.0.0-beta.389 -- *(core)* EnvironmentVariable Model now extends BaseModel to remove duplicated code -- *(versions)* Update coolify versions to v4.0.0-beta.3909 - -## [4.0.0-beta.388] - 2025-01-22 - -### 🚀 Features - -- *(core)* Add SOURCE_COMMIT variable to build environment in ApplicationDeploymentJob -- *(service)* Update affine.yaml with AI environment variables (#4918) -- *(service)* Add new service Flipt (#4875) - -### 🐛 Bug Fixes - -- *(core)* Update environment variable generation logic in ApplicationDeploymentJob to handle different build packs -- *(env)* Shared variables can not be updated -- *(ui)* Metrics stuck in loading state -- *(ui)* Use `wire:navigate` to navigate to the server settings page -- *(service)* Plunk API & health check endpoint (#4925) - -## [4.0.0-beta.386] - 2025-01-22 - -### 🐛 Bug Fixes - -- *(redis)* Update environment variable keys from standalone_redis_id to resourceable_id -- *(routes)* Local API docs not available on domain or IP -- *(routes)* Local API docs not available on domain or IP -- *(core)* Update application_id references to resourable_id and resourable_type for Nixpacks configuration -- *(core)* Correct spelling of 'resourable' to 'resourceable' in Nixpacks configuration for ApplicationDeploymentJob -- *(ui)* Traefik dashboard url not working -- *(ui)* Proxy status badge flashing during navigation - -### 🚜 Refactor - -- *(workflows)* Replace jq with PHP script for version retrieval in workflows - -### ⚙️ Miscellaneous Tasks - -- *(dep)* Bump helper version to 1.0.5 -- *(docker)* Add blank line for readability in Dockerfile -- *(versions)* Update coolify versions to v4.0.0-beta.388 -- *(versions)* Update coolify versions to v4.0.0-beta.389 and add helper version retrieval script - -## [4.0.0-beta.385] - 2025-01-21 - -### 🚀 Features - -- *(core)* Wip version of coolify.json - -### 🐛 Bug Fixes - -- *(email)* Transactional email sending -- *(ui)* Add missing save button for new Docker Cleanup page -- *(ui)* Show preview deployment environment variables -- *(ui)* Show error on terminal if container has no shell (bash/sh) -- *(parser)* Resource URL should only be parsed if there is one -- *(core)* Compose parsing for apps - -### ⚙️ Miscellaneous Tasks - -- *(dep)* Bump nixpacks version -- *(dep)* Version++ - -## [4.0.0-beta.384] - 2025-01-21 - -### 🐛 Bug Fixes - -- *(ui)* Backups link should not redirected to general -- Envs with special chars during build -- *(db)* `finished_at` timestamps are not set for existing deployments -- Load service templates on cloud - -## [4.0.0-beta.383] - 2025-01-20 - -### 🐛 Bug Fixes - -- *(service)* Add healthcheck to Cloudflared service (#4859) -- Remove wire:navigate from import backups - -## [4.0.0-beta.382] - 2025-01-17 - -### 🚀 Features - -- Add log file check message in upgrade script for better troubleshooting -- Add root user details to install script - -### 🐛 Bug Fixes - -- Create the private key before the server in the prod seeder -- Update ProductionSeeder to check for private key instead of server's private key -- *(ui)* Missing underline for docs link in the Swarm section (#4860) -- *(service)* Change chatwoot service postgres image from `postgres:12` to `pgvector/pgvector:pg12` -- Docker image parser -- Add public key attribute to privatekey model -- Correct service update logic in Docker Compose parser -- Update CDN URL in install script to point to nightly version - -### 🚜 Refactor - -- Comment out RootUserSeeder call in ProductionSeeder for clarity -- Streamline ProductionSeeder by removing debug logs and unnecessary checks, while ensuring essential seeding operations remain intact -- Remove debug echo statements from Init command to clean up output and improve readability - -## [4.0.0-beta.381] - 2025-01-17 - -### 🚀 Features - -- Able to import full db backups for pg/mysql/mariadb -- Restore backup from server file -- Docker volume data cloning -- Move volume data cloning to a Job -- Volume cloning for ResourceOperations -- Remote server volume cloning -- Add horizon server details to queue -- Enhance horizon:manage command with worker restart check -- Add is_coolify_host to the server api responses -- DB migration for Backup retention -- UI for backup retention settings -- New global s3 and local backup deletion function -- Use new backup deletion functions -- Add calibre-web service -- Add actual-budget service -- Add rallly service -- Template for Gotenberg, a Docker-powered stateless API for PDF files -- Enhance import command options with additional guidance and improved checkbox label -- Purify for better sanitization -- Move docker cleanup to its own tab -- DB and Model for docker cleanup executions -- DockerCleanupExecutions relationship -- DockerCleanupDone event -- Get command and output for logs from CleanupDocker -- New sidebar menu and order -- Docker cleanup executions UI -- Add execution log to dockerCleanupJob -- Improve deployment UI -- Root user envs and seeding -- Email, username and password validation when they are set via envs -- Improved error handling and log output -- Add root user configuration variables to production environment - -### 🐛 Bug Fixes - -- Compose envs -- Scheduled tasks and backups are executed by server timezone. -- Show backup timezone on the UI -- Disappearing UI after livewire event received -- Add default vector db for anythingllm -- We need XSRF-TOKEN for terminal -- Prevent default link behavior for resource and settings actions in dashboard -- Increase default php memory limit -- Show if only build servers are added to your team -- Update Livewire button click method to use camelCase -- Local dropzonejs -- Import backups due to js stuff should not be navigated -- Install inetutils on Arch Linux -- Use ip in place of hostname from inetutils in arch -- Update import command to append file redirection for database restoration -- Ui bug on pw confirmation -- Exclude system and computed fields from model replication -- Service cloning on a separate server -- Application cloning -- `Undefined variable $fs_path` for databases -- Service and database cloning and label generation -- Labels and URL generation when cloning -- Clone naming for different database data volumes -- Implement all the cloneMe changes for ResourceOperations as well -- Volume and fileStorages cloning -- View text and helpers -- Teable -- Trigger with external db -- Set `EXPERIMENTAL_FEATURES` to false for labelstudio -- Monaco editor disabled state -- Edge case where executions could be null -- Create destination properly -- Getcontainer status should timeout after 30s -- Enable response for temporary unavailability in sentinel push endpoint -- Use timeout in cleanup resources -- Add timeout to sentinel process checks for improved reliability -- Horizon job checker -- Update response message for sentinel push route -- Add own servers on cloud -- Application deployment -- Service update statsu -- If $SERVICE found in the service specific configuration, then search for it in the db -- Instance wide GitHub apps are not available on other teams then the source team -- Function calls -- UI -- Deletion of single backup -- Backup job deletion - delete all backups from s3 and local -- Use new removeOldBackups function -- Retention functions and folder deletion for local backups -- Storage retention setting -- Db without s3 should still backup -- Wording -- `Undefined variable $service` when creating a new service -- Nodebb service -- Calibre-web service -- Rallly and actualbudget service -- Removed container_name -- Added healthcheck for gotenberg template -- Gotenberg -- *(template)* Gotenberg healthcheck, use /health instead of /version -- Use wire:navigate on sidebar -- Use wire:navigate on dashboard -- Use wire:navigate on projects page -- More wire:navigate -- Even more wire:navigate -- Service navigation -- Logs icons everywhere + terminal -- Redis DB should use the new resourceable columns -- Joomla service -- Add back letters to prod password requirement -- Check System and GitHub time and throw and error if it is over 50s out of sync -- Error message and server time getting -- Error rendering -- Render html correctly now -- Indent -- Potential fix for permissions update -- Expiration time claim ('exp') must be a numeric value -- Sanitize html error messages -- Production password rule and cleanup code -- Use json as it is just better than string for huge amount of logs -- Use `wire:navigate` on server sidebar -- Use finished_at for the end time instead of created_at -- Cancelled deployments should not show end and duration time -- Redirect to server index instead of show on error in Advanced and DockerCleanup components -- Disable registration after creating the root user -- RootUserSeeder -- Regex username validation -- Add spacing around echo outputs -- Success message -- Silent return if envs are empty or not set. - -### 💼 Other - -- Arrrrr -- Dep -- Docker dep - -### 🚜 Refactor - -- Rename parameter in DatabaseBackupJob for clarity -- Improve checkbox component accessibility and styling -- Remove unused tags method from ApplicationDeploymentJob -- Improve deployment status check in isAnyDeploymentInprogress function -- Extend HorizonServiceProvider from HorizonApplicationServiceProvider -- Streamline job status retrieval and clean up repository interface -- Enhance ApplicationDeploymentJob and HorizonServiceProvider for improved job handling -- Remove commented-out unsubscribe route from API -- Update redirect calls to use a consistent navigation method in deployment functions -- AppServiceProvider -- Github.php -- Improve data formatting and UI - -### ⚙️ Miscellaneous Tasks - -- Improve Penpot healthchecks -- Switch up readonly lables to make more sense -- Remove unused computed fields -- Use the new job dispatch -- Disable volume data cloning for now -- Improve code -- Lowcoder service naming -- Use new functions -- Improve error styling -- Css -- More css as it still looks like shit -- Final css touches -- Ajust time to 50s (tests done) -- Remove debug log, finally found it -- Remove more logging -- Remove limit on commit message -- Remove dayjs -- Remove unused code and fix import - -## [4.0.0-beta.380] - 2024-12-27 - -### 🚀 Features - -- New ServerReachabilityChanged event -- Use new ServerReachabilityChanged event instead of isDirty -- Add infomaniak oauth -- Add server disk usage check frequency -- Add environment_uuid support and update API documentation -- Add service/resource/project labels -- Add coolify.environment label -- Add database subtype -- Migrate to new encryption options -- New encryption options - -### 🐛 Bug Fixes - -- Render html on error page correctly -- Invalid API response on missing project -- Applications API response code + schema -- Applications API writing to unavailable models -- If an init script is renamed the old version is still on the server -- Oauthseeder -- Compose loading seq -- Resource clone name + volume name generation -- Update Dockerfile entrypoint path to /etc/entrypoint.d -- Debug mode -- Unreachable notifications -- Remove duplicated ServerCheckJob call -- Few fixes and use new ServerReachabilityChanged event -- Use serverStatus not just status -- Oauth seeder -- Service ui structure -- Check port 8080 and fallback to 80 -- Refactor database view -- Always use docker cleanup frequency -- Advanced server UI -- Html css -- Fix domain being override when update application -- Use nixpacks predefined build variables, but still could update the default values from Coolify -- Use local monaco-editor instead of Cloudflare -- N8n timezone -- Smtp encryption -- Bind() to 0.0.0.0:80 failed -- Oauth seeder -- Unreachable notifications -- Instance settings migration -- Only encrypt instance email settings if there are any -- Error message -- Update healthcheck and port configurations to use port 8080 - -### 🚜 Refactor - -- Rename `coolify.environment` to `coolify.environmentName` - -### ⚙️ Miscellaneous Tasks - -- Regenerate API spec, removing notification fields -- Remove ray debugging -- Version ++ - -## [4.0.0-beta.378] - 2024-12-13 - -### 🐛 Bug Fixes - -- Monaco editor light and dark mode switching -- Service status indicator + oauth saving -- Socialite for azure and authentik -- Saving oauth -- Fallback for copy button -- Copy the right text -- Maybe fallback is now working -- Only show copy button on secure context - -## [4.0.0-beta.377] - 2024-12-13 - -### 🚀 Features - -- Add deploy-only token permission -- Able to deploy without cache on every commit -- Update private key nam with new slug as well -- Allow disabling default redirect, set status to 503 -- Add TLS configuration for default redirect in Server model -- Slack notifications -- Introduce root permission -- Able to download schedule task logs -- Migrate old email notification settings from the teams table -- Migrate old discord notification settings from the teams table -- Migrate old telegram notification settings from the teams table -- Add slack notifications to a new table -- Enable success messages again -- Use new notification stuff inside team model -- Some more notification settings and better defaults -- New email notification settings -- New shared function name `is_transactional_emails_enabled()` -- New shared notifications functions -- Email Notification Settings Model -- Telegram notification settings Model -- Discord notification settings Model -- Slack notification settings Model -- New Discord notification UI -- New Slack notification UI -- New telegram UI -- Use new notification event names -- Always sent notifications -- Scheduled task success notification -- Notification trait -- Get discord Webhook form new table -- Get Slack Webhook form new table -- Use new table or instance settings for email -- Use new place for settings and topic IDs for telegram -- Encrypt instance email settings -- Use encryption in instance settings model -- Scheduled task success and failure notifications -- Add docker cleanup success and failure notification settings columns -- UI for docker cleanup success and failure notification -- Docker cleanup email views -- Docker cleanup success and failure notification files -- Scheduled task success email -- Send new docker cleanup notifications -- :passport_control: integrate Authentik authentication with Coolify -- *(notification)* Add Pushover -- Add seeder command and configuration for database seeding -- Add new password magic env with symbols -- Add documenso service - -### 🐛 Bug Fixes - -- Resolve undefined searchInput reference in Alpine.js component -- URL and sync new app name -- Typos and naming -- Client and webhook secret disappear after sync -- Missing `mysql_password` API property -- Incorrect MongoDB init API property -- Old git versions does not have --cone implemented properly -- Don't allow editing traefik config -- Restart proxy -- Dev mode -- Ui -- Display actual values for disk space checks in installer script -- Proxy change behaviour -- Add warning color -- Import NotificationSlack correctly -- Add middleware to new abilities, better ux for selecting permissions, etc. -- Root + read:sensive could read senstive data with a middlewarew -- Always have download logs button on scheduled tasks -- Missing css -- Development image -- Dockerignore -- DB migration error -- Drop all unused smtp columns -- Backward compatibility -- Email notification channel enabled function -- Instance email settins -- Make sure resend is false if SMTP is true and vice versa -- Email Notification saving -- Slack and discord url now uses text filed because encryption makes the url very long -- Notification trait -- Encryption fixes -- Docker cleanup email template -- Add missing deployment notifications to telegram -- New docker cleanup settings are now saved to the DB correctly -- Ui + migrations -- Docker cleanup email notifications -- General notifications does not go through email channel -- Test notifications to only send it to the right channel -- Remove resale_license from db as well -- Nexus service -- Fileflows volume names -- --cone -- Provider error -- Database migration -- Seeder -- Migration call -- Slack helper -- Telegram helper -- Discord helper -- Telegram topic IDs -- Make pushover settings more clear -- Typo in pushover user key -- Use Livewire refresh method and lock properties -- Create pushover settings for existing teams -- Update token permission check from 'write' to 'root' -- Pushover -- Oauth seeder -- Correct heading display for OAuth settings in settings-oauth.blade.php -- Adjust spacing in login form for improved layout -- Services env values should be sensitive -- Documenso -- Dolibarr +- Secrets join +- ENV variables set differently +- Capture non-error as error +- Only delete id.rsa in case of it exists +- Status is not available yet +- Docker Engine bug related to live-restore and IPs +- Version +- PreventDefault on a button, thats all +- Haproxy check should not throw error +- Delete all build files +- Cleanup images +- More error handling in proxy configuration + cleanups +- Local static assets +- Check sentry - Typo -- Update OauthSettingSeeder to handle new provider definitions and ensure authentik is recreated if missing -- Improve OauthSettingSeeder to correctly delete non-existent providers and ensure proper handling of provider definitions -- Encrypt resend API key in instance settings -- Resend api key is already a text column - -### 💼 Other - -- Test rename GitHub app -- Checkmate service and fix prowlar slogan (too long) - -### 🚜 Refactor - -- Update Traefik configuration for improved security and logging -- Improve proxy configuration and code consistency in Server model -- Rename name method to sanitizedName in BaseModel for clarity -- Improve migration command and enhance application model with global scope and status checks -- Unify notification icon -- Remove unused Azure and Authentik service configurations from services.php -- Change email column types in instance_settings migration from string to text -- Change OauthSetting creation to updateOrCreate for better handling of existing records - -### ⚙️ Miscellaneous Tasks - -- Regenerate openapi spec -- Composer dep bump -- Dep bump -- Upgrade cloudflared and minio -- Remove comments and improve DB column naming -- Remove unused seeder -- Remove unused waitlist stuff -- Remove wired.php (not used anymore) -- Remove unused resale license job -- Remove commented out internal notification -- Remove more waitlist stuff -- Remove commented out notification -- Remove more waitlist stuff -- Remove unused code -- Fix typo -- Remove comment out code -- Some reordering -- Remove resale license reference -- Remove functions from shared.php -- Public settings for email notification -- Remove waitlist redirect -- Remove log -- Use new notification trait -- Remove unused route -- Remove unused email component -- Comment status changes as it is disabled for now -- Bump dep -- Reorder navbar -- Rename topicID to threadId like in the telegram API response -- Update PHP configuration to set memory limit using environment variable - -## [4.0.0-beta.376] - 2024-12-07 - -### 🐛 Bug Fixes - -- Api endpoint - -## [4.0.0-beta.374] - 2024-12-03 - -### 🐛 Bug Fixes - -- Application view loading -- Postiz service -- Only able to select the right keys -- Test email should not be required -- A few inputs - -### 🧪 Testing - -- Setup database for upcoming tests - -## [4.0.0-beta.372] - 2024-11-26 - -### 🚀 Features - -- Add MacOS template -- Add Windows template -- *(service)* :sparkles: add mealie -- Add hex magic env var - -### 🐛 Bug Fixes - -- Service generate includes yml files as well (haha) -- ServercheckJob should run every 5 minutes on cloud -- New resource icons -- Search should be more visible on scroll on new resource -- Logdrain settings -- Ui -- Email should be retried with backoff -- Alpine in body layout - -### 💼 Other - -- Caddy docker labels do not honor "strip prefix" option - -## [4.0.0-beta.371] - 2024-11-22 - -### 🐛 Bug Fixes - -- Improve helper text for metrics input fields -- Refine helper text for metrics input fields -- If mux conn fails, still use it without mux + save priv key with better logic -- Migration -- Always validate ssh key -- Make sure important jobs/actions are running on high prio queue -- Do not send internal notification for backups and status jobs -- Validateconnection -- View issue -- Heading -- Remove mux cleanup -- Db backup for services -- Version should come from constants + fix stripe webhook error reporting -- Undefined variable -- Remove version.php as everything is coming from constants.php -- Sentry error -- Websocket connections autoreconnect -- Sentry error -- Sentry -- Empty server API response -- Incorrect server API patch response -- Missing `uuid` parameter on server API patch -- Missing `settings` property on servers API -- Move servers API `delete_unused_*` properties -- Servers API returning `port` as a string -> integer -- Only return server uuid on server update - -## [4.0.0-beta.370] - 2024-11-15 - -### 🐛 Bug Fixes - -- Modal (+ add) on dynamic config was not opening, removed x-cloak -- AUTOUPDATE + checkbox opacity - -## [4.0.0-beta.369] - 2024-11-15 - -### 🐛 Bug Fixes - -- Modal-input - -## [4.0.0-beta.368] - 2024-11-15 - -### 🚀 Features - -- Check local horizon scheduler deployments -- Add internal api docs to /docs/api with auth -- Add proxy type change to create/update apis - -### 🐛 Bug Fixes - -- Show proper error message on invalid Git source -- Convert HTTP to SSH source when using deploy key on GitHub -- Cloud + stripe related -- Terminal view loading in async -- Cool 500 error (thanks hugodos) -- Update schema in code decorator -- Openapi docs -- Add tests for git url converts -- Minio / logto url generation -- Admin view -- Min docker version 26 -- Pull latest service-templates.json on init -- Workflow files for coolify build -- Autocompletes -- Timezone settings validation -- Invalid tz should not prevent other jobs to be executed -- Testing-host should be built locally -- Poll with modal issue -- Terminal opening issue -- If service img not found, use github as a source -- Fallback to local coolify.png -- Gather private ips -- Cf tunnel menu should be visible when server is not validated -- Deployment optimizations -- Init script + optimize laravel -- Default docker engine version + fix install script -- Pull helper image on init -- SPA static site default nginx conf - -### 💼 Other - -- Https://github.com/coollabsio/coolify/issues/4186 -- Separate resources by type in projects view -- Improve s3 add view - -### ⚙️ Miscellaneous Tasks - -- Update dep - -## [4.0.0-beta.365] - 2024-11-11 - -### 🚀 Features - -- Custom nginx configuration for static deployments + fix 404 redirects in nginx conf - -### 🐛 Bug Fixes - -- Trigger.dev db host & sslmode=disable -- Manual update should be executed only once + better UX -- Upgrade.sh -- Missing privateKey - -## [4.0.0-beta.364] - 2024-11-08 - -### 🐛 Bug Fixes - -- Define separate volumes for mattermost service template -- Github app name is too long -- ServerTimezone update - -### ⚙️ Miscellaneous Tasks - -- Edit www helper - -## [4.0.0-beta.363] - 2024-11-08 - -### 🚀 Features - -- Add Firefox template -- Add template for Wiki.js -- Add upgrade logs to /data/coolify/source - -### 🐛 Bug Fixes - -- Saving resend api key -- Wildcard domain save -- Disable cloudflare tunnel on "localhost" - -## [4.0.0-beta.362] - 2024-11-08 - -### 🐛 Bug Fixes - -- Notifications ui -- Disable wire:navigate -- Confirmation Settings css for light mode -- Server wildcard - -## [4.0.0-beta.361] - 2024-11-08 - -### 🚀 Features - -- Add Transmission template -- Add transmission healhcheck -- Add zipline template -- Dify template -- Required envs -- Add EdgeDB -- Show warning if people would like to use sslip with https -- Add is shared to env variables -- Variabel sync and support shared vars -- Add notification settings to server_disk_usage -- Add coder service tamplate and logo -- Debug mode for sentinel -- Add jitsi template -- Add --gpu support for custom docker command - -### 🐛 Bug Fixes - -- Make sure caddy is not removed by cleanup -- Libretranslate -- Do not allow to change number of lines when streaming logs -- Plunk -- No manual timezones -- Helper push -- Format -- Add port metadata and Coolify magic to generate the domain -- Sentinel -- Metrics -- Generate sentinel url -- Only enable Sentinel for new servers -- Is_static through API -- Allow setting standalone redis variables via ENVs (team variables...) -- Check for username separately form password -- Encrypt all existing redis passwords -- Pull helper image on helper_version change -- Redis database user and password -- Able to update ipv4 / ipv6 instance settings -- Metrics for dbs -- Sentinel start fixed -- Validate sentinel custom URL when enabling sentinel -- Should be able to reset labels in read-only mode with manual click -- No sentinel for swarm yet -- Charts ui -- Volume -- Sentinel config changes restarts sentinel -- Disable sentinel for now -- Disable Sentinel temporarily -- Disable Sentinel temporarily for non-dev environments -- Access team's github apps only -- Admins should now invite owner -- Add experimental flag -- GenerateSentinelUrl method -- NumberOfLines could be null -- Login / register view -- Restart sentinel once a day -- Changing private key manually won't trigger a notification -- Grammar for helper -- Fix my own grammar -- Add telescope only in dev mode -- New way to update container statuses -- Only run server storage every 10 mins if sentinel is not active -- Cloud admin view -- Queries in kernel.php -- Lower case emails only -- Change emails to lowercase on init -- Do not error on update email -- Always authenticate with lowercase emails -- Dashboard refactor -- Add min/max length to input/texarea -- Remove livewire legacy from help view -- Remove unnecessary endpoints (magic) -- Transactional email livewire -- Destinations livewire refactor -- Refactor destination/docker view -- Logdrains validation -- Reworded -- Use Auth(), add new db proxy stop event refactor clickhouse view -- Add user/pw to db view -- Sort servers by name -- Keydb view -- Refactor tags view / remove obsolete one -- Send discord/telegram notifications on high job queue -- Server view refresh on validation -- ShowBoarding -- Show docker installation logs & ubuntu 24.10 notification -- Do not overlap servercheckjob -- Server limit check -- Server validation -- Clear route / view -- Only skip docker installation on 24.10 if its not installed -- For --gpus device support -- Db/service start should be on high queue -- Do not stop sentinel on Coolify restart -- Run resourceCheck after new serviceCheckJob -- Mongodb in dev -- Better invitation errors -- Loading indicator for db proxies -- Do not execute gh workflow on template changes -- Only use sentry in cloud -- Update packagejson of coolify-realtime + add lock file -- Update last online with old function -- Seeder should not start sentinel -- Start sentinel on seeder - -### 💼 Other - -- Add peppermint -- Loggy -- Add UI for redis password and username -- Wireguard-easy template - -### 📚 Documentation - -- Update link to deploy api docs - -### ⚙️ Miscellaneous Tasks - -- Add transmission template desc -- Update transmission docs link -- Update version numbers to 4.0.0-beta.360 in configuration files -- Update AWS environment variable names in unsend.yaml -- Update AWS environment variable names in unsend.yaml -- Update livewire/livewire dependency to version 3.4.9 -- Update version to 4.0.0-beta.361 -- Update Docker build and push actions to v6 -- Update Docker build and push actions to v6 -- Update Docker build and push actions to v6 -- Sync coolify-helper to dockerhub as well -- Push realtime to dockerhub -- Sync coolify-realtime to dockerhub -- Rename workflows -- Rename development to staging build -- Sync coolify-testing-host to dockerhbu -- Sync coolify prod image to dockerhub as well -- Update Docker version to 26.0 -- Update project resource index page -- Update project service configuration view - -## [4.0.0-beta.360] - 2024-10-11 - -### ⚙️ Miscellaneous Tasks - -- Update livewire/livewire dependency to version 3.4.9 - -## [4.0.0-beta.359] - 2024-10-11 - -### 🐛 Bug Fixes - -- Use correct env variable for invoice ninja password - -### ⚙️ Miscellaneous Tasks - -- Update laravel/horizon dependency to version 5.29.1 -- Update service extra fields to use dynamic keys - -## [4.0.0-beta.358] - 2024-10-10 - -### 🚀 Features - -- Add customHelper to stack-form -- Add cloudbeaver template -- Add ntfy template -- Add qbittorrent template -- Add Homebox template -- Add owncloud service and logo -- Add immich service -- Auto generate url -- Refactored to work with coolify auto env vars -- Affine service template and logo -- Add LibreTranslate template -- Open version in a new tab - -### 🐛 Bug Fixes - -- Signup -- Application domains should be http and https only -- Validate and sanitize application domains -- Sanitize and validate application domains - -### 💼 Other - -- Other DB options for freshrss -- Nextcloud MariaDB and MySQL versions - -### ⚙️ Miscellaneous Tasks - -- Fix form submission and keydown event handling in modal-confirmation.blade.php -- Update version numbers to 4.0.0-beta.359 in configuration files -- Disable adding default environment variables in shared.php - -## [4.0.0-beta.357] - 2024-10-08 - -### 🚀 Features - -- Add Mautic 4 and 5 to service templates -- Add keycloak template -- Add onedev template -- Improve search functionality in project selection - -### 🐛 Bug Fixes - -- Update mattermost image tag and add default port -- Remove env, change timezone -- Postgres healthcheck -- Azimutt template - still not working haha -- New parser with SERVICE_URL_ envs -- Improve service template readability -- Update password variables in Service model -- Scheduled database server -- Select server view - -### 💼 Other - -- Keycloak - -### ⚙️ Miscellaneous Tasks - -- Add mattermost logo as svg -- Add mattermost svg to compose -- Update version to 4.0.0-beta.357 - -## [4.0.0-beta.356] - 2024-10-07 - -### 🚀 Features - -- Add Argilla service configuration to Service model -- Add Invoice Ninja service configuration to Service model -- Project search on frontend -- Add ollama service with open webui and logo -- Update setType method to use slug value for type -- Refactor setType method to use slug value for type -- Refactor setType method to use slug value for type -- Add Supertokens template -- Add easyappointments service template -- Add dozzle template -- Adds forgejo service with runners - -### 🐛 Bug Fixes - -- Reset description and subject fields after submitting feedback -- Tag mass redeployments -- Service env orders, application env orders -- Proxy conf in dev -- One-click services -- Use local service-templates in dev -- New services -- Remove not used extra host -- Chatwoot service -- Directus -- Database descriptions -- Update services -- Soketi -- Select server view - -### 💼 Other - -- Update helper version -- Outline -- Directus -- Supertokens -- Supertokens json -- Rabbitmq -- Easyappointments -- Soketi -- Dozzle -- Windmill -- Coolify.json - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.356 -- Remove commented code for shared variable type validation -- Update MariaDB image to version 11 and fix service environment variable orders -- Update anythingllm.yaml volumes configuration -- Update proxy configuration paths for Caddy and Nginx in dev -- Update password form submission in modal-confirmation component -- Update project query to order by name in uppercase -- Update project query to order by name in lowercase -- Update select.blade.php with improved search functionality -- Add Nitropage service template and logo -- Bump coolify-helper version to 1.0.2 -- Refactor loadServices2 method and remove unused code -- Update version to 4.0.0-beta.357 -- Update service names and volumes in windmill.yaml -- Update version to 4.0.0-beta.358 -- Ignore .ignition.json files in Docker and Git - -## [4.0.0-beta.355] - 2024-10-03 - -### 🐛 Bug Fixes - -- Scheduled backup for services view -- Parser, espacing container labels - -### ⚙️ Miscellaneous Tasks - -- Update homarr service template and remove unnecessary code -- Update version to 4.0.0-beta.355 - -## [4.0.0-beta.354] - 2024-10-03 - -### 🚀 Features - -- Add it-tools service template and logo -- Add homarr service tamplate and logo - -### 🐛 Bug Fixes - -- Parse proxy config and check the set ports usage -- Update FQDN - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.354 -- Remove debug statement in Service model -- Remove commented code in Server model -- Fix application deployment queue filter logic -- Refactor modal-confirmation component -- Update it-tools service template and port configuration -- Update homarr service template and remove unnecessary code - -## [4.0.0-beta.353] - 2024-10-03 - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.353 -- Update service application view - -## [4.0.0-beta.352] - 2024-10-03 - -### 🐛 Bug Fixes - -- Service application view -- Add new supported database images - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.352 -- Refactor DatabaseBackupJob to handle missing team - -## [4.0.0-beta.351] - 2024-10-03 - -### 🚀 Features - -- Add strapi template - -### 🐛 Bug Fixes - -- Able to support more database dynamically from Coolify's UI -- Strapi template -- Bitcoin core template -- Api useBuildServer - -## [4.0.0-beta.349] - 2024-10-01 - -### 🚀 Features - -- Add command to check application deployment queue -- Support Hetzner S3 -- Handle HTTPS domain in ConfigureCloudflareTunnels -- Backup all databases for mysql,mariadb,postgresql -- Restart service without pulling the latest image - -### 🐛 Bug Fixes - -- Remove autofocuses -- Ipv6 scp should use -6 flag -- Cleanup stucked applicationdeploymentqueue -- Realtime watch in development mode -- Able to select root permission easier - -### 💼 Other - -- Show backup button on supported db service stacks - -### 🚜 Refactor - -- Remove deployment queue when deleting an application -- Improve SSH command generation in Terminal.php and terminal-server.js -- Fix indentation in modal-confirmation.blade.php -- Improve parsing of commands for sudo in parseCommandsByLineForSudo -- Improve popup component styling and button behavior -- Encode delimiter in SshMultiplexingHelper -- Remove inactivity timer in terminal-server.js -- Improve socket reconnection interval in terminal.js -- Remove unnecessary watch command from soketi service entrypoint - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.350 in configuration files -- Update command signature and description for cleanup application deployment queue -- Add missing import for Attribute class in ApplicationDeploymentQueue model -- Update modal input in server form to prevent closing on outside click -- Remove unnecessary command from SshMultiplexingHelper -- Remove commented out code for uploading to S3 in DatabaseBackupJob -- Update soketi service image to version 1.0.3 - -## [4.0.0-beta.348] - 2024-10-01 - -### 🚀 Features - -- Update resource deletion job to allow configurable options through API -- Add query parameters for deleting configurations, volumes, docker cleanup, and connected networks - -### 🐛 Bug Fixes - -- In dev mode do not ask confirmation on delete -- Mixpost -- Handle deletion of 'hello' in confirmation modal for dev environment - -### 💼 Other - -- Server storage check - -### 🚜 Refactor - -- Update search input placeholder in resource index view - -### ⚙️ Miscellaneous Tasks - -- Fix docs link in running state -- Update Coolify Realtime workflow to only trigger on the main branch -- Refactor instanceSettings() function to improve code readability -- Update Coolify Realtime image to version 1.0.2 -- Remove unnecessary code in DatabaseBackupJob.php -- Add "Not Usable" indicator for storage items -- Refactor instanceSettings() function and improve code readability -- Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350 - -## [4.0.0-beta.347] - 2024-09-28 - -### 🚀 Features - -- Allow specify use_build_server when creating/updating an application -- Add support for `use_build_server` in API endpoints for creating/updating applications -- Add Mixpost template - -### 🐛 Bug Fixes - -- Filebrowser template -- Edit is_build_server_enabled upon creating application on other application type -- Save settings after assigning value - -### 💼 Other - -- Remove memlock as it caused problems for some users - -### ⚙️ Miscellaneous Tasks - -- Update Mailpit logo to use SVG format - -## [4.0.0-beta.346] - 2024-09-27 - -### 🚀 Features - -- Add ContainerStatusTypes enum for managing container status - -### 🐛 Bug Fixes - -- Proxy fixes -- Proxy -- *(templates)* Filebrowser FQDN env variable -- Handle edge case when build variables and env variables are in different format -- Compose based terminal - -### 💼 Other - -- Manual cleanup button and unused volumes and network deletion -- Force helper image removal -- Use the new confirmation flow +- Package.json +- Build secrets should be visible in runtime +- New secret should have default values +- Validate secrets +- Truncate git clone errors +- Branch used does not throw error - Typo -- Typo in install script -- If API is disabeled do not show API token creation stuff -- Disable API by default -- Add debug bar - -### 🚜 Refactor - -- Update environment variable name for uptime-kuma service -- Improve start proxy script to handle existing containers gracefully -- Update delete server confirmation modal buttons -- Remove unnecessary code - -### ⚙️ Miscellaneous Tasks - -- Add autocomplete attribute to input fields -- Refactor API Tokens component to use isApiEnabled flag -- Update versions.json file -- Remove unused .env.development.example file -- Update API Tokens view to include link to Settings menu -- Update web.php to cast server port as integer -- Update backup deletion labels to use language files -- Update database startup heading title -- Update database startup heading title -- Custom vite envs -- Update version numbers to 4.0.0-beta.348 -- Refactor code to improve SSH key handling and storage - -## [4.0.0-beta.343] - 2024-09-25 - -### 🐛 Bug Fixes - -- Parser -- Exited services statuses -- Make sure to reload window if app status changes -- Deploy key based deployments - -### 🚜 Refactor - -- Remove commented out code and improve environment variable handling in newParser function -- Improve label positioning in input and checkbox components -- Group and sort fields in StackForm by service name and password status -- Improve layout and add checkbox for task enablement in scheduled task form -- Update checkbox component to support full width option -- Update confirmation label in danger.blade.php template -- Fix typo in execute-container-command.blade.php -- Update OS_TYPE for Asahi Linux in install.sh script -- Add localhost as Server if it doesn't exist and not in cloud environment -- Add localhost as Server if it doesn't exist and not in cloud environment -- Update ProductionSeeder to fix issue with coolify_key assignment -- Improve modal confirmation titles and button labels -- Update install.sh script to remove redirection of upgrade output to /dev/null -- Fix modal input closeOutside prop in configuration.blade.php -- Add support for IPv6 addresses in sslip function - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.343 -- Update version numbers to 4.0.0-beta.344 -- Update version numbers to 4.0.0-beta.345 -- Update version numbers to 4.0.0-beta.346 - -## [4.0.0-beta.342] - 2024-09-24 - -### 🚀 Features - -- Add nullable constraint to 'fingerprint' column in private_keys table -- *(api)* Add an endpoint to execute a command -- *(api)* Add endpoint to execute a command - -### 🐛 Bug Fixes - -- Proxy status -- Coolify-db should not be in the managed resources -- Store original root key in the original location -- Logto service -- Cloudflared service -- Migrations -- Cloudflare tunnel configuration, ui, etc - -### 💼 Other - -- Volumes on development environment -- Clean new volume name for dev volumes -- Persist DBs, services and so on stored in data/coolify -- Add SSH Key fingerprint to DB -- Add a fingerprint to every private key on save, create... -- Make sure invalid private keys can not be added -- Encrypt private SSH keys in the DB -- Add is_sftp and is_server_ssh_key coloums -- New ssh key file name on disk -- Store all keys on disk by default -- Populate SSH key folder -- Populate SSH keys in dev -- Use new function names and logic everywhere -- Create a Multiplexing Helper -- SSH multiplexing -- Remove unused code form multiplexing -- SSH Key cleanup job -- Private key with ID 2 on dev -- Move more functions to the PrivateKey Model -- Add ssh key fingerprint and generate one for existing keys -- ID issues on dev seeders -- Server ID 0 -- Make sure in use private keys are not deleted -- Do not delete SSH Key from disk during server validation error -- UI bug, do not write ssh key to disk in server dialog -- SSH Multiplexing for Jobs -- SSH algorhytm text -- Few multiplexing things -- Clear mux directory -- Multiplexing do not write file manually -- Integrate tow step process in the modal component WIP -- Ability to hide labels -- DB start, stop confirm -- Del init script -- General confirm -- Preview deployments and typos -- Service confirmation -- Confirm file storage -- Stop service confirm -- DB image cleanup -- Confirm ressource operation -- Environment variabel deletion -- Confirm scheduled tasks -- Confirm API token -- Confirm private key -- Confirm server deletion -- Confirm server settings -- Proxy stop and restart confirmation -- GH app deletion confirmation -- Redeploy all confirmation -- User deletion confirmation -- Team deletion confirmation -- Backup job confirmation -- Delete volume confirmation -- More conformations and fixes -- Delete unused private keys button -- Ray error because port is not uncommented -- #3322 deploy DB alterations before updating -- Css issue with advanced settings and remove cf tunnel in onboarding -- New cf tunnel install flow -- Made help text more clear -- Cloudflare tunnel -- Make helper text more clean to use a FQDN and not an URL - -### 🚜 Refactor - -- Update Docker cleanup label in Heading.php and Navbar.php -- Remove commented out code in Navbar.php -- Remove CleanupSshKeysJob from schedule in Kernel.php -- Update getAJoke function to exclude offensive jokes -- Update getAJoke function to use HTTPS for API request -- Update CleanupHelperContainersJob to use more efficient Docker command -- Update PrivateKey model to improve code readability and maintainability -- Remove unnecessary code in PrivateKey model -- Update PrivateKey model to use ownedByCurrentTeam() scope for cleanupUnusedKeys() -- Update install.sh script to check if coolify-db volume exists before generating SSH key -- Update ServerSeeder and PopulateSshKeysDirectorySeeder -- Improve attribute sanitization in Server model -- Update confirmation button text for deletion actions -- Remove unnecessary code in shared.php file -- Update environment variables for services in compose files -- Update select.blade.php to improve trademarks policy display -- Update select.blade.php to improve trademarks policy display -- Fix typo in subscription URLs -- Add Postiz service to compose file (disabled for now) -- Update shared.php to include predefined ports for services -- Simplify SSH key synchronization logic -- Remove unused code in DatabaseBackupStatusJob and PopulateSshKeysDirectorySeeder - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.342 -- Update remove-labels-and-assignees-on-close.yml -- Add SSH key for localhost in ProductionSeeder -- Update SSH key generation in install.sh script -- Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder -- Update install.sh to support Asahi Linux -- Update install.sh version to 1.6 -- Remove unused middleware and uniqueId method in DockerCleanupJob -- Refactor DockerCleanupJob to remove unused middleware and uniqueId method -- Remove unused migration file for populating SSH keys and clearing mux directory -- Add modified files to the commit -- Refactor pre-commit hook to improve performance and readability -- Update CONTRIBUTING.md with troubleshooting note about database migrations -- Refactor pre-commit hook to improve performance and readability -- Update cleanup command to use Redis instead of queue -- Update Docker commands to start proxy - -## [4.0.0-beta.341] - 2024-09-18 - -### 🚀 Features - -- Add buddy logo - -## [4.0.0-beta.336] - 2024-09-16 - -### 🚀 Features - -- Make coolify full width by default -- Fully functional terminal for command center -- Custom terminal host - -### 🐛 Bug Fixes - -- Keep-alive ws connections -- Add build.sh to debug logs -- Update Coolify installer -- Terminal -- Generate https for minio -- Install script -- Handle WebSocket connection close in terminal.blade.php -- Able to open terminal to any containers -- Refactor run-command -- If you exit a container manually, it should close the underlying tty as well -- Move terminal to separate view on services -- Only update helper image in DB -- Generated fqdn for SERVICE_FQDN_APP_3000 magic envs - -### 💼 Other - -- Remove labels and assignees on issue close -- Make sure this action is also triggered on PR issue close - -### 🚜 Refactor - -- Remove unnecessary code in ExecuteContainerCommand.php -- Improve Docker network connection command in StartService.php -- Terminal / run command -- Add authorization check in ExecuteContainerCommand mount method -- Remove unnecessary code in Terminal.php -- Remove unnecessary code in Terminal.blade.php -- Update WebSocket connection initialization in terminal.blade.php -- Remove unnecessary console.log statements in terminal.blade.php - -### ⚙️ Miscellaneous Tasks - -- Update release version to 4.0.0-beta.336 -- Update coolify environment variable assignment with double quotes -- Update shared.php to fix issues with source and network variables -- Update terminal styling for better readability -- Update button text for container connection form -- Update Dockerfile and workflow for Coolify Realtime (v4) -- Remove unused entrypoint script and update volume mapping -- Update .env file and docker-compose configuration -- Update APP_NAME environment variable in docker-compose.prod.yml -- Update WebSocket URL in terminal.blade.php -- Update Dockerfile and workflow for Coolify Realtime (v4) -- Update Dockerfile and workflow for Coolify Realtime (v4) -- Update Dockerfile and workflow for Coolify Realtime (v4) -- Rename Command Center to Terminal in code and views -- Update branch restriction for push event in coolify-helper.yml -- Update terminal button text and layout in application heading view -- Refactor terminal component and select form layout -- Update coolify nightly version to 4.0.0-beta.335 -- Update helper version to 1.0.1 -- Fix syntax error in versions.json -- Update version numbers to 4.0.0-beta.337 -- Update Coolify installer and scripts to include a function for fetching programming jokes -- Update docker network connection command in ApplicationDeploymentJob.php -- Add validation to prevent selecting 'default' server or container in RunCommand.php -- Update versions.json to reflect latest version of realtime container -- Update soketi image to version 1.0.1 -- Nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container -- Update version numbers to 4.0.0-beta.339 -- Update version numbers to 4.0.0-beta.340 -- Update version numbers to 4.0.0-beta.341 - -### ◀️ Revert - -- Databasebackup - -## [4.0.0-beta.335] - 2024-09-12 - -### 🐛 Bug Fixes - -- Cloudflare tunnel with new multiplexing feature - -### 💼 Other - -- SSH Multiplexing on docker desktop on Windows - -### ⚙️ Miscellaneous Tasks - -- Update release version to 4.0.0-beta.335 -- Update constants.ssh.mux_enabled in remoteProcess.php -- Update listeners and proxy settings in server form and new server components -- Remove unnecessary null check for proxy_type in generate_default_proxy_configuration -- Remove unnecessary SSH command execution time logging - -## [4.0.0-beta.334] - 2024-09-12 - -### ⚙️ Miscellaneous Tasks - -- Remove itsgoingd/clockwork from require-dev in composer.json -- Update 'key' value of gitlab in Service.php to use environment variable - -## [4.0.0-beta.333] - 2024-09-11 - -### 🐛 Bug Fixes - -- Disable mux_enabled during server validation -- Move mc command to coolify image from helper -- Keydb. add `:` delimiter for connection string - -### 💼 Other - -- Remote servers with port and user -- Do not change localhost server name on revalidation -- Release.md file - -### 🚜 Refactor - -- Improve handling of environment variable merging in upgrade script - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.333 -- Copy .env file to .env-{DATE} if it exists -- Update .env file with new values -- Update server check job middleware to use server ID instead of UUID -- Add reminder to backup .env file before running install script again -- Copy .env file to backup location during installation script -- Add reminder to backup .env file during installation script -- Update permissions in pr-build.yml and version numbers -- Add minio/mc command to Dockerfile - -## [4.0.0-beta.332] - 2024-09-10 - -### 🚀 Features - -- Expose project description in API response -- Add elixir finetunes to the deployment job - -### 🐛 Bug Fixes - -- Reenable overlapping servercheckjob -- Appwrite template + parser -- Don't add `networks` key if `network_mode` is used -- Remove debug statement in shared.php -- Scp through cloudflare -- Delete older versions of the helper image other than the latest one -- Update remoteProcess.php to handle null values in logItem properties - -### 💼 Other - -- Set a default server timezone -- Implement SSH Multiplexing -- Enabel mux -- Cleanup stale multiplexing connections - -### 🚜 Refactor - -- Improve environment variable handling in shared.php - -### ⚙️ Miscellaneous Tasks - -- Set timeout for ServerCheckJob to 60 seconds -- Update appwrite.yaml to include OpenSSL key variable assignment - -## [4.0.0-beta.330] - 2024-09-06 - -### 🐛 Bug Fixes - -- Parser -- Plunk NEXT_PUBLIC_API_URI - -### 💼 Other - -- Pull helper image if not available otherwise s3 backup upload fails - -### 🚜 Refactor - -- Improve handling of server timezones in scheduled backups and tasks -- Improve handling of server timezones in scheduled backups and tasks -- Improve handling of server timezones in scheduled backups and tasks -- Update cleanup schedule to run daily at midnight -- Skip returning volume if driver type is cifs or nfs - -### ⚙️ Miscellaneous Tasks - -- Update coolify-helper.yml to get version from versions.json -- Disable Ray by default -- Enable Ray by default and update Dockerfile with latest versions of PACK and NIXPACKS -- Update Ray configuration and Dockerfile -- Add middleware for updating environment variables by UUID in `api.php` routes -- Expose port 3000 in browserless.yaml template -- Update Ray configuration and Dockerfile -- Update coolify version to 4.0.0-beta.331 -- Update versions.json and sentry.php to 4.0.0-beta.332 -- Update version to 4.0.0-beta.332 -- Update DATABASE_URL in plunk.yaml to use plunk database -- Add coolify.managed=true label to Docker image builds -- Update docker image pruning command to exclude managed images -- Update docker cleanup schedule to run daily at midnight -- Update versions.json to version 1.0.1 -- Update coolify-helper.yml to include "next" branch in push trigger - -## [4.0.0-beta.326] - 2024-09-03 - -### 🚀 Features - -- Update server_settings table to force docker cleanup -- Update Docker Compose file with DB_URL environment variable -- Refactor shared.php to improve environment variable handling - -### 🐛 Bug Fixes - -- Wrong executions order -- Handle project not found error in environment_details API endpoint -- Deployment running for - without "ago" -- Update helper image pulling logic to only pull if the version is newer - -### 💼 Other - -- Plunk svg - -### 📚 Documentation - -- Update Plunk documentation link in compose/plunk.yaml - -### ⚙️ Miscellaneous Tasks - -- Update UI for displaying no executions found in scheduled task list -- Update UI for displaying deployment status in deployment list -- Update UI for displaying deployment status in deployment list -- Ignore unnecessary files in production build workflow -- Update server form layout and settings -- Update Dockerfile with latest versions of PACK and NIXPACKS - -## [4.0.0-beta.324] - 2024-09-02 - -### 🚀 Features - -- Preserve git repository with advanced file storages -- Added Windmill template -- Added Budibase template -- Add shm-size for custom docker commands -- Add custom docker container options to all databases -- Able to select different postgres database -- Add new logos for jobscollider and hostinger -- Order scheduled task executions -- Add Code Server environment variables to Service model -- Add coolify build env variables to building phase -- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid -- Add new logos for GlueOps, Ubicloud, Juxtdigital, Saasykit, and Massivegrid - -### 🐛 Bug Fixes - -- Timezone not updated when systemd is missing -- If volumes + file mounts are defined, should merge them together in the compose file -- All mongo v4 backups should use the different backup command -- Database custom environment variables -- Connect compose apps to the right predefined network -- Docker compose destination network -- Server status when there are multiple servers -- Sync fqdn change on the UI -- Pr build names in case custom name is used -- Application patch request instant_deploy -- Canceling deployment on build server -- Backup of password protected postgresql database -- Docker cleanup job -- Storages with preserved git repository -- Parser parser parser -- New parser only in dev -- Parser parser -- Numberoflines should be number -- Docker cleanup job -- Fix directory and file mount headings in file-storage.blade.php -- Preview fqdn generation -- Revert a few lines -- Service ui sync bug -- Setup script doesn't work on rhel based images with some curl variant already installed -- Let's wait for healthy container during installation and wait an extra 20 seconds (for migrations) -- Infra files -- Log drain only for Applications -- Copy large compose files through scp (not ssh) -- Check if array is associative or not -- Openapi endpoint urls -- Convert environment variables to one format in shared.php -- Logical volumes could be overwritten with new path -- Env variable in value parsed -- Pull coolify image only when the app needs to be updated - -### 💼 Other - -- Actually update timezone on the server -- Cron jobs are executed based on the server timezone -- Server timezone seeder -- Recent backups UI -- Use apt-get instead of apt +- Error handling +- Stopping service without proxy +- Coolify proxy start +- Window error in SSR +- GitHub sync PR's +- Load more button +- Small fixes - Typo -- Only pull helper image if the version is newer than the one - -### 🚜 Refactor - -- Update event listeners in Show components -- Refresh application to get latest database changes -- Update RabbitMQ configuration to use environment variable for port -- Remove debug statement in parseDockerComposeFile function -- ParseServiceVolumes -- Update OpenApi command to generate documentation -- Remove unnecessary server status check in destination view -- Remove unnecessary admin user email and password in budibase.yaml -- Improve saving of custom internal name in Advanced.php -- Add conditional check for volumes in generate_compose_file() -- Improve storage mount forms in add.blade.php -- Load environment variables based on resource type in sortEnvironmentVariables() -- Remove unnecessary network cleanup in Init.php -- Remove unnecessary environment variable checks in parseDockerComposeFile() -- Add null check for docker_compose_raw in parseCompose() -- Update dockerComposeParser to use YAML data from $yaml instead of $compose -- Convert service variables to key-value pairs in parseDockerComposeFile function -- Update database service name from mariadb to mysql -- Remove unnecessary code in DatabaseBackupJob and BackupExecutions -- Update Docker Compose parsing function to convert service variables to key-value pairs -- Update Docker Compose parsing function to convert service variables to key-value pairs -- Remove unused server timezone seeder and related code -- Remove unused server timezone seeder and related code -- Remove unused PullCoolifyImageJob from schedule -- Update parse method in Advanced, All, ApplicationPreview, General, and ApplicationDeploymentJob classes -- Remove commented out code for getIptables() in Dashboard.php -- Update .env file path in install.sh script -- Update SELF_HOSTED environment variable in docker-compose.prod.yml -- Remove unnecessary code for creating coolify network in upgrade.sh -- Update environment variable handling in StartClickhouse.php and ApplicationDeploymentJob.php -- Improve handling of COOLIFY_URL in shared.php -- Update build_args property type in ApplicationDeploymentJob -- Update background color of sponsor section in README.md -- Update Docker Compose location handling in PublicGitRepository -- Upgrade process of Coolify - -### 🧪 Testing - -- More tests - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.324 -- New compose parser with tests -- Update version to 1.3.4 in install.sh and 1.0.6 in upgrade.sh -- Update memory limit to 64MB in horizon configuration -- Update php packages -- Update axios npm dependency to version 1.7.5 -- Update Coolify version to 4.0.0-beta.324 and fix file paths in upgrade script -- Update Coolify version to 4.0.0-beta.324 -- Update Coolify version to 4.0.0-beta.325 -- Update Coolify version to 4.0.0-beta.326 -- Add cd command to change directory before removing .env file -- Update Coolify version to 4.0.0-beta.327 -- Update Coolify version to 4.0.0-beta.328 -- Update sponsor links in README.md -- Update version.json to versions.json in GitHub workflow -- Cleanup stucked resources and scheduled backups -- Update GitHub workflow to use versions.json instead of version.json -- Update GitHub workflow to use versions.json instead of version.json -- Update GitHub workflow to use versions.json instead of version.json -- Update GitHub workflow to use jq container for version extraction -- Update GitHub workflow to use jq container for version extraction - -## [4.0.0-beta.323] - 2024-08-08 - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.323 - -## [4.0.0-beta.322] - 2024-08-08 - -### 🐛 Bug Fixes - -- Manual update process - -### 🚜 Refactor - -- Update Server model getContainers method to use collect() for containers and containerReplicates -- Import ProxyTypes enum and use TRAEFIK instead of TRAEFIK_V2 - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.322 - -## [4.0.0-beta.321] - 2024-08-08 - -### 🐛 Bug Fixes - -- Scheduledbackup not found - -### 🚜 Refactor - -- Update StandalonePostgresql database initialization and backup handling -- Update cron expressions and add helper text for scheduled tasks - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.321 - -## [4.0.0-beta.320] - 2024-08-08 - -### 🚀 Features - -- Delete team in cloud without subscription -- Coolify init should cleanup stuck networks in proxy -- Add manual update check functionality to settings page -- Update auto update and update check frequencies in settings -- Update Upgrade component to check for latest version of Coolify -- Improve homepage service template -- Support map fields in Directus -- Labels by proxy type -- Able to generate only the required labels for resources - -### 🐛 Bug Fixes - -- Only append docker network if service/app is running -- Remove lazy load from scheduled tasks -- Plausible template -- Service_url should not have a trailing slash -- If usagebefore cannot be determined, cleanup docker with force -- Async remote command -- Only run logdrain if necessary -- Remove network if it is only connected to coolify proxy itself -- Dir mounts should have proper dirs -- File storages (dir/file mount) handled properly -- Do not use port exposes on docker compose buildpacks -- Minecraft server template fixed -- Graceful shutdown -- Stop resources gracefully -- Handle null and empty disk usage in DockerCleanupJob -- Show latest version on manual update view -- Empty string content should be saved as a file -- Update Traefik labels on init -- Add missing middleware for server check job - -### 🚜 Refactor - -- Update CleanupDatabase.php to adjust keep_days based on environment -- Adjust keep_days in CleanupDatabase.php based on environment -- Remove commented out code for cleaning up networks in CleanupDocker.php -- Update livewire polling interval in heading.blade.php -- Remove unused code for checking server status in Heading.php -- Simplify log drain installation in ServerCheckJob -- Remove unnecessary debug statement in ServerCheckJob -- Simplify log drain installation and stop log drain if necessary -- Cleanup unnecessary dynamic proxy configuration in Init command -- Remove unnecessary debug statement in ApplicationDeploymentJob -- Update timeout for graceful_shutdown_container in ApplicationDeploymentJob -- Remove unused code and optimize CheckForUpdatesJob -- Update ProxyTypes enum values to use TRAEFIK instead of TRAEFIK_V2 -- Update Traefik labels on init and cleanup unnecessary dynamic proxy configuration - -### 🎨 Styling - -- Linting - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.320 -- Add pull_request image builds to GH actions -- Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy() -- Update formbricks template -- Update registration view to display a notice for first user that it will be an admin -- Update server form to use password input for IP Address/Domain field -- Update navbar to include service status check -- Update navbar and configuration to improve service status check functionality -- Update workflows to include PR build and merge manifest steps -- Update UpdateCoolifyJob timeout to 10 minutes -- Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously - -## [4.0.0-beta.319] - 2024-07-26 - -### 🐛 Bug Fixes - -- Parse docker composer -- Service env parsing -- Service env variables -- Activity type invalid -- Update env on ui - -### 💼 Other - -- Service env parsing - -### ⚙️ Miscellaneous Tasks - -- Collect/create/update volumes in parseDockerComposeFile function - -## [4.0.0-beta.318] - 2024-07-24 - -### 🚀 Features - -- Create/delete project endpoints -- Add patch request to projects -- Add server api endpoints -- Add branddev logo to README.md -- Update API endpoint summaries -- Update Caddy button label in proxy.blade.php -- Check custom internal name through server's applications. -- New server check job - -### 🐛 Bug Fixes - -- Preview deployments should be stopped properly via gh webhook -- Deleting application should delete preview deployments -- Plane service images -- Fix issue with deployment start command in ApplicationDeploymentJob -- Directory will be created by default for compose host mounts -- Restart proxy does not work + status indicator on the UI -- Uuid in api docs type -- Raw compose deployment .env not found -- Api -> application patch endpoint -- Remove pull always when uploading backup to s3 -- Handle array env vars -- Link in task failed job notifications -- Random generated uuid will be full length (not 7 characters) -- Gitlab service -- Gitlab logo -- Bitbucket repository url -- By default volumes that we cannot determine if they are directories or files are treated as directories -- Domain update on services on the UI -- Update SERVICE_FQDN/URL env variables when you change the domain -- Several shared environment variables in one value, parsed correctly -- Members of root team should not see instance admin stuff - -### 💼 Other - -- Formbricks template add required CRON_SECRET -- Add required CRON_SECRET to Formbricks template - -### ⚙️ Miscellaneous Tasks - -- Update APP_BASE_URL to use SERVICE_FQDN_PLANE -- Update resource-limits.blade.php with improved input field helpers -- Update version numbers to 4.0.0-beta.319 -- Remove commented out code for docker image pruning - -## [4.0.0-beta.314] - 2024-07-15 - -### 🚀 Features - -- Improve error handling in loadComposeFile method -- Add readonly labels -- Preserve git repository -- Force cleanup server - -### 🐛 Bug Fixes - -- Typo in is_literal helper -- Env is_literal helper text typo -- Update docker compose pull command with --policy always -- Plane service template -- Vikunja -- Docmost template -- Drupal -- Improve github source creation -- Tag deployments -- New docker compose parsing -- Handle / in preselecting branches -- Handle custom_internal_name check in ApplicationDeploymentJob.php -- If git limit reached, ignore it and continue with a default selection -- Backup downloads -- Missing input for api endpoint -- Volume detection (dir or file) is fixed -- Supabase -- Create file storage even if content is empty - -### 💼 Other - -- Add basedir + compose file in new compose based apps - -### 🚜 Refactor - -- Remove unused code and fix storage form layout -- Update Docker Compose build command to include --pull flag -- Update DockerCleanupJob to handle nullable usageBefore property -- Server status job and docker cleanup job -- Update DockerCleanupJob to use server settings for force cleanup -- Update DockerCleanupJob to use server settings for force cleanup -- Disable health check for Rust applications during deployment - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.315 -- Update version to 4.0.0-beta.316 -- Update bug report template -- Update repository form with simplified URL input field -- Update width of container in general.blade.php -- Update checkbox labels in general.blade.php -- Update general page of apps -- Handle JSON parsing errors in format_docker_command_output_to_json -- Update Traefik image version to v2.11 -- Update version to 4.0.0-beta.317 -- Update version to 4.0.0-beta.318 -- Update helper message with link to documentation -- Disable health check by default -- Remove commented out code for sending internal notification - -### ◀️ Revert - -- Pull policy -- Advanced dropdown - -## [4.0.0-beta.308] - 2024-07-11 - -### 🚀 Features - -- Cleanup unused docker networks from proxy -- Compose parser v2 -- Display time interval for rollback images -- Add security and storage access key env to twenty template -- Add new logo for Latitude -- Enable legacy model binding in Livewire configuration - -### 🐛 Bug Fixes - -- Do not overwrite hardcoded variables if they rely on another variable -- Remove networks when deleting a docker compose based app -- Api -- Always set project name during app deployments -- Remove volumes as well -- Gitea pr previews -- Prevent instance fqdn persisting to other servers dynamic proxy configs -- Better volume cleanups -- Cleanup parameter -- Update redirect URL in unauthenticated exception handler -- Respect top-level configs and secrets -- Service status changed event -- Disable sentinel until a few bugs are fixed -- Service domains and envs are properly updated -- *(reactive-resume)* New healthcheck command for MinIO -- *(MinIO)* New command healthcheck -- Update minio hc in services -- Add validation for missing docker compose file - -### 🚜 Refactor - -- Add force parameter to StartProxy handle method -- Comment out unused code for network cleanup -- Reset default labels when docker_compose_domains is modified -- Webhooks view -- Tags view -- Only get instanceSettings once from db -- Update Dockerfile to set CI environment variable to true -- Remove unnecessary code in AppServiceProvider.php -- Update Livewire configuration views -- Update Webhooks.php to use nullable type for webhook URLs -- Add lazy loading to tags in Livewire configuration view -- Update metrics.blade.php to improve alert message clarity -- Update version numbers to 4.0.0-beta.312 -- Update version numbers to 4.0.0-beta.314 - -### ⚙️ Miscellaneous Tasks - -- Update Plausible docker compose template to Plausible 2.1.0 -- Update Plausible docker compose template to Plausible 2.1.0 -- Update livewire/livewire dependency to version 3.4.9 -- Refactor checkIfDomainIsAlreadyUsed function -- Update storage.blade.php view for livewire project service -- Update version to 4.0.0-beta.310 -- Update composer dependencies -- Add new logo for Latitude -- Bump version to 4.0.0-beta.311 - -### ◀️ Revert - -- Instancesettings - -## [4.0.0-beta.301] - 2024-06-24 - -### 🚀 Features - -- Local fonts -- More API endpoints -- Bulk env update api endpoint -- Update server settings metrics history days to 7 -- New app API endpoint -- Private gh deployments through api -- Lots of api endpoints -- Api api api api api api -- Rename CloudCleanupSubs to CloudCleanupSubscriptions -- Early fraud warning webhook -- Improve internal notification message for early fraud warning webhook -- Add schema for uuid property in app update response - -### 🐛 Bug Fixes - -- Run user commands on high prio queue -- Load js locally -- Remove lemon + paddle things -- Run container commands on high priority -- Image logo -- Remove both option for api endpoints. it just makes things complicated -- Cleanup subs in cloud -- Show keydbs/dragonflies/clickhouses -- Only run cloud clean on cloud + remove root team -- Force cleanup on busy servers -- Check domain on new app via api -- Custom container name will be the container name, not just internal network name -- Api updates -- Yaml everywhere -- Add newline character to private key before saving -- Add validation for webhook endpoint selection -- Database input validators -- Remove own app from domain checks -- Return data of app update - -### 💼 Other - +- Error with follow logs +- IsDomainConfigured +- TransactionIds +- Coolify image cleanup +- Cleanup every 10 mins +- Cleanup images +- Add no user redis to uri +- Secure cookie disabled by default +- Buggy svelte-kit-cookie-session +- Login issues +- SSL app off +- Local docker host +- Typo +- Lets encrypt +- Remove SSL with stop +- SSL off for services +- Grr +- Running state css +- Minor fixes +- Remove force SSL when doing let's encrypt request +- GhToken in session now +- Random port for certbot +- Follow icon +- Plausible volume fixed +- Database connection strings +- Gitlab webhooks fixed +- If DNS not found, do not redirect +- Github token +- Move tokens from session to cookie/store +- Email is lowercased in login +- Lowercase email everywhere +- Use normal docker-compose in dev +- Random network name for demo +- Settings fqdn grr +- Revert default network +- Http for demo, oops +- Docker scanner +- Improvement on image pulls +- Coolify image pulls +- Remove wrong/stuck proxy configurations +- Always use a buildpack +- Add icons for eleventy + astro +- Fix proxy every 10 secs +- Do not remove coolify proxy +- Update version +- Be sure .env exists +- Missing fqdn for services +- Default npm command +- Add coolify-image label for build images +- Cleanup old images, > 3 days +- Better proxy check +- Ssl + sslrenew +- Null proxyhash on restart +- Reconfigure proxy on restart - Update process -- Glances service -- Glances -- Able to update application - -### 🚜 Refactor - -- Update Service model's saveComposeConfigs method -- Add default environment to Service model's saveComposeConfigs method -- Improve handling of default environment in Service model's saveComposeConfigs method -- Remove commented out code in Service model's saveComposeConfigs method -- Update stack-form.blade.php to include wire:target attribute for submit button -- Update code to use str() instead of Str::of() for string manipulation -- Improve formatting and readability of source.blade.php -- Add is_build_time property to nixpacks_php_fallback_path and nixpacks_php_root_dir -- Simplify code for retrieving subscription in Stripe webhook - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.302 -- Update version to 4.0.0-beta.303 -- Update version to 4.0.0-beta.305 -- Update version to 4.0.0-beta.306 -- Add log1x/laravel-webfonts package -- Update version to 4.0.0-beta.307 -- Refactor ServerStatusJob constructor formatting -- Update Monaco Editor for Docker Compose and Proxy Configuration -- More details -- Refactor shared.php helper functions - -## [4.0.0-beta.298] - 2024-06-24 - -### 🚀 Features - -- Spanish translation -- Cancelling a deployment will check if new could be started. -- Add supaguide logo to donations section -- Nixpacks now could reach local dbs internally -- Add Tigris logo to other/logos directory -- COOLIFY_CONTAINER_NAME predefined variable -- Charts -- Sentinel + charts -- Container metrics -- Add high priority queue -- Add metrics warning for servers without Sentinel enabled -- Add blacksmith logo to donations section -- Preselect server and destination if only one found -- More api endpoints -- Add API endpoint to update application by UUID -- Update statusnook logo filename in compose template - -### 🐛 Bug Fixes - -- Stripprefix middleware correctly labeled to http -- Bitbucket link -- Compose generator -- Do no truncate repositories wtih domain (git) in it -- In services should edit compose file for volumes and envs -- Handle laravel deployment better -- Db proxy status shown better in the UI -- Show commit message on webhooks + prs -- Metrics parsing -- Charts -- Application custom labels reset after saving -- Static build with new nixpacks build process -- Make server charts one livewire component with one interval selector -- You can now add env variable from ui to services -- Update compose environment with UI defined variables -- Refresh deployable compose without reload -- Remove cloud stripe notifications -- App deployment should be in high queue -- Remove zoom from modals -- Get envs before sortby -- MB is % lol -- Projects with 0 envs - -### 💼 Other - -- Unnecessary notification - -### 🚜 Refactor - -- Update text color for stderr output in deployment show view -- Update text color for stderr output in deployment show view -- Remove debug code for saving environment variables -- Update Docker build commands for better performance and flexibility -- Update image sizes and add new logos to README.md -- Update README.md with new logos and fix styling -- Update shared.php to use correct key for retrieving sentinel version -- Update container name assignment in Application model -- Remove commented code for docker container removal -- Update Application model to include getDomainsByUuid method -- Update Project/Show component to sort environments by created_at -- Update profile index view to display 2FA QR code in a centered container -- Update dashboard.blade.php to use project's default environment for redirection -- Update gitCommitLink method to handle null values in source.html_url -- Update docker-compose generation to use multi-line literal block - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.298 -- Switch to database sessions from redis -- Update dependencies and remove unused code -- Update tailwindcss and vue versions in package.json -- Update service template URL in constants.php -- Update sentinel version to 0.0.8 -- Update chart styling and loading text -- Update sentinel version to 0.0.9 -- Update Spanish translation for failed authentication messages -- Add portuguese traslation -- Add Turkish translations -- Add Vietnamese translate -- Add Treive logo to donations section -- Update README.md with latest release version badge -- Update latest release version badge in README.md -- Update version to 4.0.0-beta.299 -- Move server delete component to the bottom of the page -- Update version to 4.0.0-beta.301 - -## [4.0.0-beta.297] - 2024-06-11 - -### 🚀 Features - -- Easily redirect between www-and-non-www domains -- Add logos for new sponsors -- Add homepage template -- Update homepage.yaml with environment variables and volumes - -### 🐛 Bug Fixes - -- Multiline build args -- Setup script doesnt link to the correct source code file -- Install.sh do not reinstall packages on arch -- Just restart - -### 🚜 Refactor - -- Replaces duplications in code with a single function - -### ⚙️ Miscellaneous Tasks - -- Update page title in resource index view -- Update logo file path in logto.yaml -- Update logo file path in logto.yaml -- Remove commented out code for docker container removal -- Add isAnyDeploymentInprogress function to check if any deployments are in progress -- Add ApplicationDeploymentJob and pint.json - -## [4.0.0-beta.295] - 2024-06-10 - -### 🚀 Features - -- Able to change database passwords on the UI. It won't sync to the database. -- Able to add several domains to compose based previews -- Add bounty program link to bug report template -- Add titles -- Db proxy logs - -### 🐛 Bug Fixes - -- Custom docker compose commands, add project dir if needed -- Autoupdate process -- Backup executions view -- Handle previously defined compose previews -- Sort backup executions -- Supabase service, newest versions -- Set default name for Docker volumes if it is null -- Multiline variable should be literal + should be multiline in bash with \ -- Gitlab merge request should close PR - -### 💼 Other - -- Rocketchat -- New services based git apps - -### 🚜 Refactor - -- Append utm_source parameter to documentation URL -- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview -- Update deployment previews heading to "Deployments" -- Remove unused variables and improve code readability -- Initialize null properties in Github Change component -- Improve pre and post deployment command inputs -- Improve handling of Docker volumes in parseDockerComposeFile function - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.295 -- Update supported OS list with almalinux -- Update install.sh to support PopOS -- Update install.sh script to version 1.3.2 and handle Linux Mint as Ubuntu - -## [4.0.0-beta.294] - 2024-06-04 - -### ⚙️ Miscellaneous Tasks - -- Update Dockerfile with latest versions of Docker, Docker Compose, Docker Buildx, Pack, and Nixpacks - -## [4.0.0-beta.289] - 2024-05-29 - -### 🚀 Features - -- Add PHP memory limit environment variable to docker-compose.prod.yml -- Add manual update option to UpdateCoolify handle method -- Add port configuration for Vaultwarden service - -### 🐛 Bug Fixes - -- Sync upgrade process -- Publish horizon -- Add missing team model -- Test new upgrade process? -- Throw exception -- Build server dirs not created on main server -- Compose load with non-root user -- Able to redeploy dockerfile based apps without cache -- Compose previews does have env variables -- Fine-tune cdn pulls -- Spamming :D -- Parse docker version better -- Compose issues -- SERVICE_FQDN has source port in it -- Logto service -- Allow invitations via email -- Sort by defined order + fixed typo -- Only ignore volumes with driver_opts -- Check env in args for compose based apps - -### 🚜 Refactor - -- Update destination.blade.php to add group class for better styling -- Applicationdeploymentjob -- Improve code structure in ApplicationDeploymentJob.php -- Remove unnecessary debug statement in ApplicationDeploymentJob.php -- Remove unnecessary debug statements and improve code structure in RunRemoteProcess.php and ApplicationDeploymentJob.php -- Remove unnecessary logging statements from UpdateCoolify -- Update storage form inputs in show.blade.php -- Improve Docker Compose parsing for services -- Remove unnecessary port appending in updateCompose function -- Remove unnecessary form class in profile index.blade.php -- Update form layout in invite-link.blade.php -- Add log entry when starting new application deployment -- Improve Docker Compose parsing for services -- Update Docker Compose parsing for services -- Update slogan in shlink.yaml -- Improve display of deployment time in index.blade.php -- Remove commented out code for clearing Ray logs -- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview - -### ⚙️ Miscellaneous Tasks - -- Update for version 289 -- Fix formatting issue in deployment index.blade.php file -- Remove unnecessary wire:navigate attribute in breadcrumbs.blade.php -- Rename docker dirs -- Update laravel/socialite to version v5.14.0 and livewire/livewire to version 3.4.9 -- Update modal styles for better user experience -- Update deployment index.blade.php script for better performance -- Update version numbers to 4.0.0-beta.290 -- Update version numbers to 4.0.0-beta.291 -- Update version numbers to 4.0.0-beta.292 -- Update version numbers to 4.0.0-beta.293 -- Add upgrade guide link to upgrade.blade.php -- Improve upgrade.blade.php with clearer instructions and formatting -- Update version numbers to 4.0.0-beta.294 -- Add Lightspeed.run as a sponsor -- Update Dockerfile to install vim - -## [4.0.0-beta.288] - 2024-05-28 - -### 🐛 Bug Fixes - -- Do not allow service storage mount point modifications -- Volume adding - -### ⚙️ Miscellaneous Tasks - -- Update Sentry release version to 4.0.0-beta.288 - -## [4.0.0-beta.287] - 2024-05-27 - -### 🚀 Features - -- Handle incomplete expired subscriptions in Stripe webhook -- Add more persistent storage types - -### 🐛 Bug Fixes - -- Force load services from cdn on reload list - -### ⚙️ Miscellaneous Tasks - -- Update Sentry release version to 4.0.0-beta.287 -- Add Thompson Edolo as a sponsor -- Add null checks for team in Stripe webhook - -## [4.0.0-beta.286] - 2024-05-27 - -### 🚀 Features - -- If the time seems too long it remains at 0s -- Improve Docker Engine start logic in ServerStatusJob -- If proxy stopped manually, it won't start back again -- Exclude_from_hc magic -- Gitea manual webhooks -- Add container logs in case the container does not start healthy - -### 🐛 Bug Fixes - -- Wrong time during a failed deployment -- Removal of the failed deployment condition, addition of since started instead of finished time -- Use local versions + service templates and query them every 10 minutes -- Check proxy functionality before removing unnecessary coolify.yaml file and checking Docker Engine -- Show first 20 users only in admin view -- Add subpath for services -- Ghost subdir -- Do not pull templates in dev -- Templates -- Update error message for invalid token to mention invalid signature -- Disable containerStopped job for now -- Disable unreachable/revived notifications for now -- JSON_UNESCAPED_UNICODE -- Add wget to nixpacks builds -- Pre and post deployment commands -- Bitbucket commits link -- Better way to add curl/wget to nixpacks -- Root team able to download backups -- Build server should not have a proxy -- Improve build server functionalities -- Sentry issue -- Sentry -- Sentry error + livewire downgrade -- Sentry -- Sentry -- Sentry error -- Sentry - -### 🚜 Refactor - -- Update edit-domain form in project service view -- Add Huly services to compose file -- Remove redundant heading in backup settings page -- Add isBuildServer method to Server model -- Update docker network creation in ApplicationDeploymentJob - -### ⚙️ Miscellaneous Tasks - -- Change pre and post deployment command length in applications table -- Refactor container name logic in GetContainersStatus.php and ForcePasswordReset.php -- Remove unnecessary content from Docker Compose file - -## [4.0.0-beta.285] - 2024-05-21 - -### 🚀 Features - -- Add SerpAPI as a Github Sponsor -- Admin view for deleting users -- Scheduled task failed notification - -### 🐛 Bug Fixes - -- Optimize new resource creation -- Show it docker compose has syntax errors - -### 💼 Other - -- Responsive here and there - -## [4.0.0-beta.284] - 2024-05-19 - -### 🚀 Features - -- Add hc logs to healthchecks - -### ◀️ Revert - -- Hc return code check - -## [4.0.0-beta.283] - 2024-05-17 - -### 🚀 Features - -- Update healthcheck test in StartMongodb action -- Add pull_request_id filter to get_last_successful_deployment method in Application model - -### 🐛 Bug Fixes - -- PR deployments have good predefined envs - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.283 - -## [4.0.0-beta.281] - 2024-05-17 - -### 🚀 Features - -- Shows the latest deployment commit + message on status -- New manual update process + remove next_channel -- Add lastDeploymentInfo and lastDeploymentLink props to breadcrumbs and status components -- Sort envs alphabetically and creation date -- Improve sorting of environment variables in the All component - -### 🐛 Bug Fixes - -- Hc from localhost to 127.0.0.1 -- Use rc in hc -- Telegram group chat notifications - -## [4.0.0-beta.280] - 2024-05-16 - -### 🐛 Bug Fixes - -- Commit message length - -## [4.0.0-beta.279] - 2024-05-16 - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.279 -- Limit commit message length to 50 characters in ApplicationDeploymentJob - -## [4.0.0-beta.278] - 2024-05-16 - -### 🚀 Features - -- Adding new COOLIFY_ variables -- Save commit message and better view on deployments -- Toggle label escaping mechanism - -### 🐛 Bug Fixes - -- Use commit hash on webhooks - -### ⚙️ Miscellaneous Tasks - -- Refactor Service.php to handle missing admin user in extraFields() method -- Update twenty CRM template with environment variables and dependencies -- Refactor applications.php to remove unused imports and improve code readability -- Refactor deployment index.blade.php for improved readability and rollback handling -- Refactor GitHub app selection UI in project creation form -- Update ServerLimitCheckJob.php to handle missing serverLimit value -- Remove unnecessary code for saving commit message -- Update DOCKER_VERSION to 26.0 in install.sh script -- Update Docker and Docker Compose versions in Dockerfiles - -## [4.0.0-beta.277] - 2024-05-10 - -### 🚀 Features - -- Add AdminRemoveUser command to remove users from the database - -### 🐛 Bug Fixes - -- Color for resource operation server and project name -- Only show realtime error on non-cloud instances -- Only allow push and mr gitlab events -- Improve scheduled task adding/removing -- Docker compose dependencies for pr previews -- Properly populating dependencies - -### 💼 Other - -- Fix a few boxes here and there - -### ⚙️ Miscellaneous Tasks - -- Update version numbers to 4.0.0-beta.278 -- Update hover behavior and cursor style in scheduled task executions view -- Refactor scheduled task view to improve code readability and maintainability -- Skip scheduled tasks if application or service is not running -- Remove debug logging statements in Kernel.php -- Handle invalid cron strings in Kernel.php - -## [4.0.0-beta.275] - 2024-05-06 - -### 🚀 Features - -- Add container name to network aliases in ApplicationDeploymentJob -- Add lazy loading for images in General.php and improve Docker Compose file handling in Application.php -- Experimental sentinel -- Start Sentinel on servers. -- Pull new sentinel image and restart container -- Init metrics - -### 🐛 Bug Fixes - -- Typo in tags.blade.php -- Install.sh error -- Env file -- Comment out internal notification in email_verify method -- Confirmation for custom labels -- Change permissions on newly created dirs - -### 💼 Other - -- Fix tag view - -### 🚜 Refactor - -- Add SCHEDULER environment variable to StartSentinel.php - -### ⚙️ Miscellaneous Tasks - -- Dark mode should be the default -- Improve menu item styling and spacing in service configuration and index views -- Improve menu item styling and spacing in service configuration and index views -- Improve menu item styling and spacing in project index and show views -- Remove docker compose versions -- Add Listmonk service template and logo -- Refactor GetContainersStatus.php for improved readability and maintainability -- Refactor ApplicationDeploymentJob.php for improved readability and maintainability -- Add metrics and logs directories to installation script -- Update sentinel version to 0.0.2 in versions.json -- Update permissions on metrics and logs directories -- Comment out server sentinel check in ServerStatusJob - -## [4.0.0-beta.273] - 2024-05-03 - -### 🐛 Bug Fixes - -- Formbricks image origin -- Add port even if traefik is used - -### ⚙️ Miscellaneous Tasks - -- Update version to 4.0.0-beta.275 -- Update DNS server validation helper text - -## [4.0.0-beta.267] - 2024-04-26 - -### 🚀 Features - -- Initial datalist -- Update service contribution docs URL -- The final pricing plan, pay-as-you-go - -### 🐛 Bug Fixes - -- Move s3 storages to separate view -- Mongo db backup -- Backups -- Autoupdate -- Respect start period and chekc interval for hc -- Parse HEALTHCHECK from dockerfile -- Make s3 name and endpoint required -- Able to update source path for predefined volumes -- Get logs with non-root user -- Mongo 4.0 db backup - -### 💼 Other - -- Update resource operations view - -### ◀️ Revert - -- Variable parsing - -## [4.0.0-beta.266] - 2024-04-24 - -### 🐛 Bug Fixes - -- Refresh public ips on start - -## [4.0.0-beta.259] - 2024-04-17 - -### 🚀 Features - -- Literal env variables -- Lazy load stuffs + tell user if compose based deployments have missing envs -- Can edit file/dir volumes from ui in compose based apps -- Upgrade Appwrite service template to 1.5 -- Upgrade Appwrite service template to 1.5 -- Add db name to backup notifications - -### 🐛 Bug Fixes - -- Helper image only pulled if required, not every 10 mins -- Make sure that confs when checking if it is changed sorted -- Respect .env file (for default values) -- Remove temporary cloudflared config -- Remove lazy loading until bug figured out -- Rollback feature -- Base64 encode .env -- $ in labels escaped -- .env saved to deployment server, not to build server -- Do no able to delete gh app without deleting resources -- 500 error on edge case -- Able to select server when creating new destination -- N8n template - -### 💼 Other - -- Non-root user for remote servers -- Non-root - -## [4.0.0-beta.258] - 2024-04-12 - -### 🚀 Features - -- Dynamic mux time - -### 🐛 Bug Fixes - -- Check each required binaries one-by-one - -## [4.0.0-beta.256] - 2024-04-12 - -### 🚀 Features - -- Upload large backups -- Edit domains easier for compose -- Able to delete configuration from server -- Configuration checker for all resources -- Allow tab in textarea - -### 🐛 Bug Fixes - -- Service config hash update -- Redeploy if image not found in restart only mode - -### 💼 Other - -- New pricing -- Fix allowTab logic -- Use 2 space instead of tab - -## [4.0.0-beta.252] - 2024-04-09 - -### 🚀 Features - -- Add amazon linux 2023 - -### 🐛 Bug Fixes - -- Git submodule update -- Unintended left padding on sidebar -- Hashed random delimeter in ssh commands + make sure to remove the delimeter from the command - -## [4.0.0-beta.250] - 2024-04-05 - -### 🚀 Features - -- *(application)* Update submodules after git checkout - -## [4.0.0-beta.249] - 2024-04-03 - -### 🚀 Features - -- Able to make rsa/ed ssh keys - -### 🐛 Bug Fixes - -- Warning if you use multiple domains for a service -- New github app creation -- Always rebuild Dockerfile / dockerimage buildpacks -- Do not rebuild dockerfile based apps twice -- Make sure if envs are changed, rebuild is needed -- Members cannot manage subscriptions -- IsMember -- Storage layout -- How to update docker-compose, environment variables and fqdns - -### 💼 Other - -- Light buttons -- Multiple server view - -## [4.0.0-beta.242] - 2024-03-25 - -### 🚀 Features - -- Change page width -- Watch paths - -### 🐛 Bug Fixes - -- Compose env has SERVICE, but not defined for Coolify -- Public service database -- Make sure service db proxy restarted -- Restart service db proxies -- Two factor -- Ui for tags -- Update resources view -- Realtime connection check -- Multline env in dev mode -- Scheduled backup for other service databases (supabase) -- PR deployments should not be distributed to 2 servers -- Name/from address required for resend -- Autoupdater -- Async service loads -- Disabled inputs are not trucated -- Duplicated generated fqdns are now working -- Uis -- Ui for cftunnels -- Search services -- Trial users subscription page -- Async public key loading -- Unfunctional server should see resources - -### 💼 Other - -- Run cleanup every day -- Fix -- Fix log outputs -- Automatic cloudflare tunnels -- Backup executions - -## [4.0.0-beta.241] - 2024-03-20 - -### 🚀 Features - -- Able to run scheduler/horizon programatically - -### 🐛 Bug Fixes - -- Volumes for prs -- Shared env variable parsing - -### 💼 Other - -- Redesign -- Redesign - -## [4.0.0-beta.240] - 2024-03-18 - -### 🐛 Bug Fixes - -- Empty get logs number of lines -- Only escape envs after v239+ -- 0 in env value -- Consistent container name -- Custom ip address should turn off rolling update -- Multiline input -- Raw compose deployment -- Dashboard view if no project found - -## [4.0.0-beta.239] - 2024-03-14 - -### 🐛 Bug Fixes - -- Duplicate dockerfile -- Multiline env variables -- Server stopped, service page not reachable - -## [4.0.0-beta.237] - 2024-03-14 - -### 🚀 Features - -- Domains api endpoint -- Resources api endpoint -- Team api endpoint -- Add deployment details to deploy endpoint -- Add deployments api -- Experimental caddy support -- Dynamic configuration for caddy -- Reset password -- Show resources on source page - -### 🐛 Bug Fixes - -- Deploy api messages -- Fqdn null in case docker compose bp -- Reload caddy issue -- /realtime endpoint -- Proxy switch -- Service ports for services + caddy -- Failed deployments should send failed email/notification -- Consider custom healthchecks in dockerfile -- Create initial files async -- Docker compose validation - -## [4.0.0-beta.235] - 2024-03-05 - -### 🐛 Bug Fixes - -- Should note delete personal teams -- Make sure to show some buttons -- Sort repositories by name - -## [4.0.0-beta.224] - 2024-02-23 - -### 🚀 Features - -- Custom server limit -- Delay container/server jobs -- Add static ipv4 ipv6 support -- Server disabled by overflow -- Preview deployment logs -- Collect webhooks during maintenance -- Logs and execute commands with several servers - -### 🐛 Bug Fixes - -- Subscription / plan switch, etc -- Firefly service -- Force enable/disable server in case ultimate package quantity decreases -- Server disabled -- Custom dockerfile location always checked -- Import to mysql and mariadb -- Resource tab not loading if server is not reachable -- Load unmanaged async -- Do not show n/a networsk -- Service container status updates -- Public prs should not be commented -- Pull request deployments + build servers -- Env value generation -- Sentry error -- Service status updated - -### 💼 Other - -- Change + icon to hamburger. - -## [4.0.0-beta.222] - 2024-02-22 - -### 🚀 Features - -- Able to add dynamic configurations from proxy dashboard - -### 🐛 Bug Fixes - -- Connections being stuck and not processed until proxy restarts -- Use latest image if nothing is specified -- No coolify.yaml found -- Server validation -- Statuses -- Unknown image of service until it is uploaded - -## [4.0.0-beta.220] - 2024-02-19 - -### 🚀 Features - -- Save github app permission locally -- Minversion for services - -### 🐛 Bug Fixes - -- Add openbsd ssh server check -- Resources -- Empty build variables -- *(server)* Revalidate server button not showing in server's page -- Fluent bit ident level -- Submodule cloning -- Database status -- Permission change updates from webhook -- Server validation - -### 💼 Other - -- Updates - -## [4.0.0-beta.213] - 2024-02-12 - -### 🚀 Features - -- Magic for traefik redirectregex in services -- Revalidate server -- Disable gzip compression on service applications - -### 🐛 Bug Fixes - -- Cleanup scheduled tasks -- Padding left on input boxes -- Use ls / command instead ls -- Do not add the same server twice -- Only show redeployment required if status is not exited - -## [4.0.0-beta.212] - 2024-02-08 - -### 🚀 Features - -- Cleanup queue - -### 🐛 Bug Fixes - -- New menu on navbar -- Make sure resources are deleted in async mode -- Go to prod env from dashboard if there is no other envs defined -- User proper image_tag, if set -- New menu ui -- Lock logdrain configuration when one of them are enabled -- Add docker compose check during server validation -- Get service stack as uuid, not name -- Menu -- Flex wrap deployment previews -- Boolean docker options -- Only add 'networks' key if 'network_mode' is absent - -## [4.0.0-beta.206] - 2024-02-05 - -### 🚀 Features - -- Clone to env -- Multi deployments - -### 🐛 Bug Fixes - -- Wrap tags and avoid horizontal overflow -- Stripe webhooks -- Feedback from self-hosted envs to discord - -### 💼 Other - -- Specific about newrelic logdrains - -## [4.0.0-beta.201] - 2024-01-29 - -### 🚀 Features - -- Added manual webhook support for bitbucket -- Add initial support for custom docker run commands -- Cleanup unreachable servers -- Tags and tag deploy webhooks - -### 🐛 Bug Fixes - -- Bitbucket manual deployments -- Webhooks for multiple apps -- Unhealthy deployments should be failed -- Add env variables for wordpress template without database -- Service deletion function -- Service deletion fix -- Dns validation + duplicated fqdns -- Validate server navbar upated -- Regenerate labels on application clone -- Service deletion -- Not able to use other shared envs -- Sentry fix -- Sentry -- Sentry error -- Sentry -- Sentry error -- Create dynamic directory -- Migrate to new modal -- Duplicate domain check -- Tags - -### 💼 Other - -- New modal component - -## [4.0.0-beta.188] - 2024-01-11 - -### 🚀 Features - -- Search between resources -- Move resources between projects / environments -- Clone any resource -- Shared environments -- Concurrent builds / server -- Able to deploy multiple resources with webhook -- Add PR comments -- Dashboard live deployment view - -### 🐛 Bug Fixes - -- Preview deployments with nixpacks -- Cleanup docker stuffs before upgrading -- Service deletion command -- Cpuset limits was determined in a way that apps only used 1 CPU max, ehh, sorry. -- Service stack view -- Change proxy view -- Checkbox click -- Git pull command for deploy key based previews -- Server status job -- Service deletion bug! -- Links -- Redis custom conf -- Sentry error -- Restrict concurrent deployments per server -- Queue -- Change env variable length - -### 💼 Other - -- Send notification email if payment - -### 🚜 Refactor - -- Compose file and install script - -## [4.0.0-beta.186] - 2024-01-11 - -### 🚀 Features - -- Import backups - -### 🐛 Bug Fixes - -- Do not include thegameplan.json into build image -- Submit error on postgresql -- Email verification / forgot password -- Escape build envs properly for nixpacks + docker build -- Undead endpoint -- Upload limit on ui -- Save cmd output propely (merge) -- Load profile on remote commands -- Load profile and set envs on remote cmd -- Restart should not update config hash - -## [4.0.0-beta.184] - 2024-01-09 - -### 🐛 Bug Fixes - -- Healthy status -- Show framework based notification in build logs -- Traefik labels -- Use ip for sslip in dev if remote server is used -- Service labels without ports (unknown ports) -- Sort and rename (unique part) of labels -- Settings menu -- Remove traefik debug in dev mode -- Php pgsql to 8.2 -- Static buildpack should set port 80 -- Update navbar on build_pack change - -## [4.0.0-beta.183] - 2024-01-06 - -### 🚀 Features - -- Add www-non-www redirects to traefik - -### 🐛 Bug Fixes - -- Database env variables - -## [4.0.0-beta.182] - 2024-01-04 - -### 🐛 Bug Fixes - -- File storage save - -## [4.0.0-beta.181] - 2024-01-03 - -### 🐛 Bug Fixes - -- Nixpacks buildpack - -## [4.0.0-beta.180] - 2024-01-03 - -### 🐛 Bug Fixes - -- Nixpacks cache -- Only add restart policy if its empty (compose) - -## [4.0.0-beta.179] - 2024-01-02 - -### 🐛 Bug Fixes - -- Set deployment failed if new container is not healthy - -## [4.0.0-beta.177] - 2024-01-02 - -### 🚀 Features - -- Raw docker compose deployments - -### 🐛 Bug Fixes - -- Duplicate compose variable - -## [4.0.0-beta.176] - 2023-12-31 - -### 🐛 Bug Fixes - -- Horizon - -## [4.0.0-beta.175] - 2023-12-30 - -### 🚀 Features - -- Add environment description + able to change name - -### 🐛 Bug Fixes - -- Sub -- Wrong env variable parsing -- Deploy key + docker compose - -## [4.0.0-beta.174] - 2023-12-27 - -### 🐛 Bug Fixes - -- Restore falsely deleted coolify-db-backup - -## [4.0.0-beta.173] - 2023-12-27 - -### 🐛 Bug Fixes - -- Cpu limit to float from int -- Add source commit to final envs -- Routing, switch back to old one -- Deploy instead of restart in case swarm is used -- Button title - -## [4.0.0-beta.163] - 2023-12-15 - -### 🚀 Features - -- Custom docker compose commands - -### 🐛 Bug Fixes - -- Domains for compose bp -- No action in webhooks -- Add debug output to gitlab webhooks -- Do not push dockerimage -- Add alpha to swarm -- Server not found -- Do not autovalidate server on mount -- Server update schedule -- Swarm support ui -- Server ready -- Get swarm service logs -- Docker compose apps env rewritten -- Storage error on dbs -- Why?! -- Stay tuned - -### 💼 Other - -- Swarm -- Swarm - -## [4.0.0-beta.155] - 2023-12-11 - -### 🚀 Features - -- Autoupdate env during seed -- Disable autoupdate -- Randomly sleep between executions -- Pull latest images for services - -### 🐛 Bug Fixes - -- Do not send telegram noti on intent payment failed -- Database ui is realtime based -- Live mode for github webhooks -- Ui -- Realtime connection popup could be disabled -- Realtime check -- Add new destination -- Proxy logs -- Db status check -- Pusher host -- Add ipv6 -- Realtime connection?! -- Websocket -- Better handling of errors with install script -- Install script parse version -- Only allow to modify in .env file if AUTOUPDATE is set -- Is autoupdate not null -- Run init command after production seeder -- Init -- Comma in traefik custom labels -- Ignore if dynamic config could not be set -- Service env variable ovewritten if it has a default value -- Labelling -- Non-ascii chars in labels -- Labels -- Init script echos -- Update Coolify script -- Null notify -- Check queued deployments as well -- Copy invitation -- Password reset / invitation link requests -- Add catch all route -- Revert random container job delay -- Backup executions view -- Only check server status in container status job -- Improve server status check times -- Handle other types of generated values -- Server checking status -- Ui for adding new destination -- Reset domains on compose file change - -### 💼 Other - -- Fix for comma in labels -- Add image name to service stack + better options visibility - -### 🚜 Refactor - -- Service logs are now on one page -- Application status changed realtime -- Custom labels -- Clone project - -## [4.0.0-beta.154] - 2023-12-07 - -### 🚀 Features - -- Execute command in container - -### 🐛 Bug Fixes - -- Container selection -- Service navbar using new realtime events -- Do not create duplicated networks -- Live event -- Service start + event -- Service deletion job -- Double ws connection -- Boarding view - -### 💼 Other - -- Env vars -- Migrate to livewire 3 - -## [4.0.0-beta.124] - 2023-11-13 - -### 🚀 Features - -- Log drain (wip) -- Enable/disable log drain by service -- Log drainer container check -- Add docker engine support install script to rhel based systems -- Save timestamp configuration for logs -- Custom log drain endpoints -- Auto-restart tcp proxies for databases - -### 🐛 Bug Fixes - -- *(fider template)* Use the correct docs url -- Fqdn for minio -- Generate service fields -- Mariadb backups -- When to pull image -- Do not allow to enter local ip addresses -- Reset password -- Only report nonruntime errors -- Handle different label formats in services -- Server adding process -- Show defined resources in server tab, so you will know what you need to delete before you can delete the server. -- Lots of regarding git + docker compose deployments -- Pull request build variables -- Double default password length -- Do not remove deployment in case compose based failed -- No container servers -- Sentry issue -- Dockercompose save ./ volumes under /data/coolify -- Server view for link() -- Default value do not overwrite existing env value -- Use official install script with rancher (one will work for sure) -- Add cf tunnel to boarding server view -- Prevent autorefresh of proxy status -- Missing docker image thing -- Add hc for soketi -- Deploy the right compose file -- Bind volumes for compose bp -- Use hc port 80 in case of static build -- Switching to static build - -### 💼 Other - -- New deployment jobs -- Compose based apps -- Swarm -- Swarm -- Swarm -- Swarm -- Disable trial -- Meilisearch -- Broadcast -- 🌮 - -### 🚜 Refactor - -- Env variable generator - -### ◀️ Revert - -- Wip - -## [4.0.0-beta.109] - 2023-11-06 - -### 🚀 Features - -- Deployment logs fullscreen -- Service database backups -- Make service databases public - -### 🐛 Bug Fixes - -- Missing environment variables prevewi on service -- Invoice.paid should sleep for 5 seconds -- Local dev repo -- Deployments ui -- Dockerfile build pack fix -- Set labels on generate domain -- Network service parse -- Notification url in containerstatusjob -- Gh webhook response 200 to installation_repositories -- Delete destination -- No id found -- Missing $mailMessage -- Set default from/sender names -- No environments -- Telegram text -- Private key not found error +- Reload proxy on ssl cert +- Volume name +- Update process +- Check when a container is running +- Reload haproxy if new cert is added +- Cleanup coolify images +- Application state in UI +- Do not error if proxy is not running +- Personal Gitlab repos +- Autodeploy true by default for GH repos +- No cookie found +- Missing session data +- No error if GitSource is missing +- No webhook secret found? +- Basedir for dockerfiles +- Better queue system + more support on monorepos +- Remove build logs in case of app removed +- Cleanup old builds +- Only cleanup same app +- Add nginx + htaccess files +- Skip ssl cert in case of error +- Volumes +- Cleanup only 2 hours+ old images +- Ghost logo size +- Ghost icon, remove console.log +- List ghost services +- Reload window on settings saved +- Persistent storage on webhooks +- Add license +- Space in repo names +- Gitlab repo url +- No need to dashify anymore +- Registration enabled/disabled +- Add PROTO headers +- Haproxy errors +- Build variables +- Use NodeJS for sveltekit for now +- Ignore coolify proxy error for now +- Python no wsgi +- If user not found +- Rename envs to secrets +- Infinite loop on www domains +- No need to paste clear text env for previews +- Build log fix attempt #1 +- Small UI fix on logs +- Lets await! +- Async progress +- Remove console.log +- Build log - UI -- Resourcesdelete command -- Port number should be int -- Separate delete with validation of server -- Add nixpacks info -- Remove filter -- Container logs are now followable in full-screen and sorted by timestamp -- Ui for labels -- Ui -- Deletions -- Build_image not found -- Github source view -- Github source view -- Dockercleanupjob should be released back -- Ui -- Local ip address -- Revert workdir to basedir -- Container status jobs for old pr deployments -- Service updates - -## [4.0.0-beta.99] - 2023-10-24 - -### 🚀 Features - -- Improve deployment time by a lot - -### 🐛 Bug Fixes - -- Space in build args -- Lock SERVICE_FQDN envs -- If user is invited, that means its email is verified -- Force password reset on invited accounts -- Add ssh options to git ls-remote -- Git ls-remote -- Remove coolify labels from ui - -### 💼 Other - -- Fix subs - -## [4.0.0-beta.97] - 2023-10-20 - -### 🚀 Features - -- Standalone mongodb -- Cloning project -- Api tokens + deploy webhook -- Start all kinds of things -- Simple search functionality -- Mysql, mariadb -- Lock environment variables -- Download local backups - -### 🐛 Bug Fixes - -- Service docs links -- Add PGUSER to prevent HC warning -- Preselect s3 storage if available -- Port exposes change, shoud regenerate label -- Boarding -- Clone to with the same environment name -- Cleanup stucked resources on start -- Do not allow to delete env if a resource is defined -- Service template generator + appwrite -- Mongodb backup -- Make sure coolfiy network exists on install -- Syncbunny command -- Encrypt mongodb password -- Mongodb healtcheck command -- Rate limit for api + add mariadb + mysql -- Server settings guarded - -### 💼 Other - -- Generate services -- Mongodb backup -- Mongodb backup -- Updates - -## [4.0.0-beta.93] - 2023-10-18 - -### 🚀 Features - -- Able to customize docker labels on applications -- Show if config is not applied - -### 🐛 Bug Fixes - -- Setup:dev script & contribution guide -- Do not show configuration changed if config_hash is null -- Add config_hash if its null (old deployments) -- Label generation -- Labels -- Email channel no recepients -- Limit horizon processes to 2 by default -- Add custom port as ssh option to deploy_key based commands -- Remove custom port from git repo url -- ContainerStatus job - -### 💼 Other - -- PAT by team - -## [4.0.0-beta.92] - 2023-10-17 - -### 🐛 Bug Fixes - -- Proxy start process - -## [4.0.0-beta.91] - 2023-10-17 - -### 🐛 Bug Fixes - -- Always start proxy if not NONE is selected - -### 💼 Other - -- Add helper to service domains - -## [4.0.0-beta.90] - 2023-10-17 - -### 🐛 Bug Fixes - -- Only include config.json if its exists and a file - -### 💼 Other - -- Wordpress - -## [4.0.0-beta.89] - 2023-10-17 - -### 🐛 Bug Fixes - -- Noindex meta tag -- Show docker build logs - -## [4.0.0-beta.88] - 2023-10-17 - -### 🚀 Features - -- Use docker login credentials from server - -## [4.0.0-beta.87] - 2023-10-17 - -### 🐛 Bug Fixes - -- Service status check is a bit better -- Generate fqdn if you deleted a service app, but it requires fqdn -- Cancel any deployments + queue next -- Add internal domain names during build process - -## [4.0.0-beta.86] - 2023-10-15 - -### 🐛 Bug Fixes - -- Build image before starting dockerfile buildpacks - -## [4.0.0-beta.85] - 2023-10-14 - -### 🐛 Bug Fixes - -- Redis URL generated - -## [4.0.0-beta.83] - 2023-10-13 - -### 🐛 Bug Fixes - -- Docker hub URL - -## [4.0.0-beta.70] - 2023-10-09 - -### 🚀 Features - -- Add email verification for cloud -- Able to deploy docker images -- Add dockerfile location -- Proxy logs on the ui -- Add custom redis conf - -### 🐛 Bug Fixes - -- Server validation process -- Fqdn could be null -- Small -- Server unreachable count -- Do not reset unreachable count -- Contact docs -- Check connection -- Server saving -- No env goto envs from dashboard -- Goto -- Tcp proxy for dbs -- Database backups -- Only send email if transactional email set -- Backupfailed notification is forced -- Use port exposed for reverse proxy -- Contact link -- Use only ip addresses for servers -- Deleted team and it is the current one -- Add new team button -- Transactional email link -- Dashboard goto link -- Only require registry image in case of dockerimage bp -- Instant save build pack change -- Public git -- Cannot remove localhost -- Check localhost connection -- Send unreachable/revived notifications -- Boarding + verification -- Make sure proxy wont start in NONE mode -- Service check status 10 sec -- IsCloud in production seeder -- Make sure to use IP address -- Dockerfile location feature -- Server ip could be hostname in self-hosted -- Urls should be password fields -- No backup for redis -- Show database logs in case of its not healthy and running -- Proxy check for ports, do not kill anything listening on port 80/443 -- Traefik dashboard ip -- Db labels -- Docker cleanup jobs -- Timeout for instant remote processes -- Dev containerjobs -- Backup database one-by-one. -- Turn off static deployment if you switch buildpacks - -### 💼 Other - -- Dockerimage -- Updated dashboard -- Fix -- Fix -- Coolify proxy access logs exposed in dev -- Able to select environment on new resource -- Delete server -- Redis - -## [4.0.0-beta.58] - 2023-10-02 - -### 🚀 Features - -- Reset root password -- Attach Coolify defined networks to services -- Delete resource command -- Multiselect removable resources -- Disable service, required version -- Basedir / monorepo initial support -- Init version of any git deployment -- Deploy private repo with ssh key - -### 🐛 Bug Fixes - -- If waitlist is disabled, redirect to register -- Add destination to new services -- Predefined content for files -- Move /data to ./_data in dev -- UI -- Show all storages in one place for services -- Ui -- Add _data to vite ignore -- Only use _ in volume names for services -- Volume names in services -- Volume names -- Service logs visible if the whole service stack is not running -- Ui -- Compose magic -- Compose parser updated -- Dev compose files -- Traefik labels for multiport deployments -- Visible version number -- Remove SERVICE_ from deployable compose -- Delete event to deleting -- Move dev data to volumes to prevent permission issues -- Traefik labelling in case of several http and https domain added -- PR deployments use the first fqdn as base -- Email notifications subscription fixed -- Services - do not remove unnecessary things for now -- Decrease max horizon processes to get lower memory usage -- Test emails only available for user owned smtp/resend -- Ui for self-hosted email settings -- Set smtp notifications on by default -- Select branch on other git -- Private repository +- Gitlab & Github urls +- Secrets build/runtime coudl be changed after save +- Default configuration +- *(php)* If .htaccess file found use apache +- Add default webhook domain for n8n +- Add git lfs while deploying +- Try to update build status several times +- Update stucked builds +- Update stucked builds on startup +- Revert seed +- Lame fixing +- Remove asyncUntil +- Add openssl to image +- Permission issues +- On-demand sFTP for wp +- Fix for fix haha +- Do not pull latest image +- Updated db versions +- Only show proxy for admin team +- Team view for root team +- Do not trigger >1 webhooks on GitLab +- Possible fix for spikes in CPU usage +- Last commit +- Www or not-www, that's the question +- Fix for the fix that fixes the fix +- Ton of updates for users/teams +- Small typo +- Unique storage paths +- Self-hosted GitLab URL +- No line during buildLog +- Html/apiUrls cannot end with / +- Typo +- Missing buildpack +- Enable https for Ghost +- Postgres root passwor shown and set +- Able to change postgres user password from ui +- DB Connecting string generator +- Missing install repositories GitHub +- Return own and other sources better +- Show config missing on sources +- Remove unnecessary save button haha +- Update dockerfile +- Haproxy build stuffs +- Proxy +- Types +- Invitations +- Timeout values +- Cleanup images older than a day +- Meilisearch service +- Load all branches, not just the first 30 +- ProjectID for Github +- DNS check before creating SSL cert +- Try catch me +- Restart policy for resources +- No permission on first registration +- Reverting postgres password for now +- Destinations to HAProxy +- Register should happen if coolify proxy cannot be started +- GitLab typo +- Remove system wide pw reset +- Postgres root pw is pw field +- Teams view +- Improved tcp proxy monitoring for databases/ftp +- Add HTTP proxy checks +- Loading of new destinations +- Better performance for cleanup images +- Remove proxy container in case of dependent container is down +- Restart local docker coolify proxy in case of something happens to it +- Id of service container +- Switch from bitnami/redis to normal redis +- Use redis-alpine +- Wordpress extra config +- Stop sFTP connection on wp stop +- Change user's id in sftp wp instance +- Use arm based certbot on arm +- Buildlog line number is not string +- Application logs paginated +- Switch to stream on applications logs +- Scroll to top for logs +- Pull new images for services all the time it's started. +- White-labeled custom logo +- Application logs +- Deno configurations +- Text on deno buildpack +- Correct branch shown in build logs +- Vscode permission fix +- I18n +- Locales +- Application logs is not reversed and queried better +- Do not activate i18n for now +- GitHub token cleanup on team switch +- No logs found +- Code cleanups +- Reactivate posgtres password - Contribution guide -- Public repository names -- *(create)* Flex wrap on server & network selection -- Better unreachable/revived server statuses -- Able to set base dir for Dockerfile build pack - -### 💼 Other - -- Uptime kume hc updated -- Switch back to /data (volume errors) +- Simplify list services +- Contribution +- Contribution guide +- Contribution guide +- Packagemanager finder +- Unami svg size +- Team switching moved to IAM menu +- Always use IP address for webhooks +- Remove unnecessary test endpoint +- UI +- Migration +- Fider envs +- Checking low disk space +- Build image +- Update autoupdate env variable +- Renew certificates +- Webhook build images +- Missing node versions +- ExposedPorts +- Logos for dbs +- Do not run SSL renew in development +- Check domain for coolify before saving +- Remove debug info +- Cancel jobs +- Cancel old builds in database +- Better DNS check to prevent errors +- Check DNS in prod only +- DNS check +- Disable sentry for now +- Cancel +- Sentry +- No image for Docker buildpack +- Default packagemanager +- Server usage only shown for root team +- Expose ports for services +- UI +- Navbar UI +- UI +- UI +- Remove RC python +- UI +- UI +- UI +- Default Python package +- WP custom db +- UI +- Gastby buildpack +- Service checks +- Remove console.log +- Traefik +- Remove debug things +- WIP Traefik +- Proxy for http +- PR deployments view +- Minio urls + domain checks +- Remove gh token on git source changes +- Do not fetch app state in case of missconfiguration +- Demo instance save domain instantly +- Instant save on demo instance +- New source canceled view +- Lint errors in database services +- Otherfqdns +- Host key verification +- Ftp connection +- GitHub fixes +- TrustProxy +- Force restart proxy +- Only restart coolify proxy in case of version prior to 2.9.2 +- Force restart proxy on seeding +- Add GIT ENV variable for submodules +- Recurisve clone instead of submodule +- Versions +- Only reconfigure coolify proxy if its missconfigured +- Demo version forms +- Typo +- Revert gh and gl cloning +- Proxy stop missing argument +- Fider changed an env variable name +- Pnpm command +- Plausible custom script +- Plausible script and middlewares +- Remove console log +- Remove comments +- Traefik middleware +- Persistent nocodb +- Nocodb persistency +- Host and reload for uvicorn +- Remove package-lock +- Be able to change database + service versions +- Lock file +- Seeding +- Forgot that the version bump changed 😅 +- New destination can be created +- Include post +- New destinations +- Domain check +- Domain check +- TrustProxy for Fastify +- Hostname issue +- GitLab pagination load data +- Service domain checker +- Wp missing ftp solution +- Ftp WP issues +- Ftp?! +- Gitpod updates +- Gitpod +- Gitpod +- Wordpress FTP permission issues +- GitLab search fields +- GitHub App button +- GitLab loop on misconfigured source +- Gitpod +- Cleanup less often and can do it manually +- Admin password reset should not timeout +- Message for double branches +- Turn off autodeploy if double branch is configured +- More types for API +- More types +- Do not rebuild in case image exists and sha not changed +- Gitpod urls +- Remove new service start process +- Remove shared dir, deployment does not work +- Gitlab custom url +- Location url for services and apps +- Settings from api +- Selectable destinations +- Gitpod hardcodes +- Typo +- Typo +- Expose port checker +- States and exposed ports +- CleanupStorage +- Remote traefik webhook +- Remote engine ip address +- RemoteipAddress +- Explanation for remote engine url +- Tcp proxy +- Lol +- Webhook +- Dns check for rde +- Gitpod +- Revert last commit +- Dns check +- Dns checker +- Webhook +- Df and more debug +- Webhooks +- Load previews async +- Destination icon +- Pr webhook +- Cache image +- No ssh key found +- Prisma migration + update of docker and stuffs +- Ui +- Ui +- Only 1 ssh-agent is needed +- Reuse ssh connection +- Ssh tunnel +- Dns checking +- Fider BASE_URL set correctly +- Rde local ports +- Empty remote destinations could be removed +- Tips +- Lowercase issues fider +- Tooltip colors +- Update clickhouse configuration +- Cleanup command +- Enterprise Github instance endpoint +- Follow/cancel buttons +- Only remove coolify managed containers +- White-labeled env +- Schema +- Coolify-network on verification +- Cleanup stucked prisma-engines +- Toast +- Secrets +- Cleanup prisma engine if there is more than 1 +- !isARM to isARM +- Enterprise GH link +- Empty buildpack icons +- Debounce dashboard status requests +- Decryption errors +- Postgresql on ARM +- Make it public button +- Loading indicator +- Replace docker compose with docker-compose on CSB +- Dashboard ui +- Create coolify-infra, if it does not exists +- Gitpod conf and heroku buildpacks +- Appwrite +- Autoimport + readme +- Services import +- Heroku icon +- Heroku icon +- Dns button ui +- Bot deployments +- Bots +- AutoUpdater & cleanupStorage jobs +- Revert docker compose version to 2.6.1 +- Trim secrets +- Restart containers on-failure instead of always +- Show that Ghost values could be changed +- Bots without exposed ports +- Missing commas +- ExposedPort is just optional +- Port checker +- Cancel build after 5 seconds +- ExposedPort checker +- Batch secret = +- Dashboard for non-root users +- Stream build logs +- Show build log start/end +- Ui buttons +- Clear queue on cancelling jobs +- Cancelling jobs +- Dashboard for admins +- Never stop deplyo queue +- Build queue system +- High cpu usage +- Worker +- Better worker system +- Secrets decryption +- UI thinkgs +- Delete team while it is active +- Team switching +- Queue cleanup +- Decrypt secrets +- Cleanup build cache as well +- Pr deployments + remove public gits +- Copy all files during install process +- Typo +- Process +- White labeled icon on navbar +- Whitelabeled icon +- Next/nuxt deployment type +- Again +- Pr deployment +- CompareVersions +- Include +- Include +- Gitlab apps +- Oh god Prisma +- Glitchtip things +- Loading state on start +- Ui +- Submodule +- Gitlab webhooks +- UI + refactor +- Exposedport on save +- Appwrite letsencrypt +- Traefik appwrite +- Traefik +- Finally works! :) +- Rename components + remove PR/MR deployment from public repos +- Settings missing id +- Explainer component +- Database name on logs view +- Taiga +- Ssh pid agent name +- Repository link trim +- Fqdn or expose port required +- Service deploymentEnabled +- Expose port is not required +- Remote verification +- Dockerfile +- Debug api logging + gh actions +- Workdir +- Move restart button to settings +- Gitlab webhook +- Use ip address instead of window location +- Use ip instead of window location host +- Service state update +- Add initial DNS servers +- Revert last change with domain check +- Service volume generation +- Minio default env variables +- Add php 8.1/8.2 +- Edgedb ui +- Edgedb stuff +- Edgedb +- Pr previews +- DnsServer formatting +- Settings for service +- Change to execa from utils +- Save search input +- Ispublic status on databases +- Port checkers +- Ui variables +- Glitchtip env to pyhton boolean +- Autoupdater +- Show restarting apps +- Show restarting application & logs +- Remove unnecessary gitlab group name +- Secrets for PR +- Volumes for services +- Build secrets for apps +- Delete resource use window location +- Changing umami image URL to get latest version +- Gitlab importer for public repos +- Show error logs +- Umami init sql +- Plausible analytics actions +- Login +- Dev url +- UpdateMany build logs +- Fallback to db logs +- Fluentbit configuration +- Coolify update +- Fluentbit and logs +- Canceling build +- Logging +- Load more +- Build logs +- Versions of appwrite +- Appwrite?! +- Get building status +- Await +- Await #2 +- Update PR building status +- Appwrite default version 1.0 +- Undead endpoint does not require JWT +- *(routes)* Improve design of application page +- *(routes)* Improve design of git sources page +- *(routes)* Ui from destinations page +- *(routes)* Ui from databases page +- *(routes)* Ui from databases page +- *(routes)* Ui from databases page +- *(routes)* Ui from services page +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- *(routes)* Ui from settings page +- *(routes)* Duplicates classes in services page +- *(routes)* Searchbar ui +- Github conflicts +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- *(routes)* More ui tweaks +- Ui with headers +- *(routes)* Header of settings page in databases +- *(routes)* Ui from secrets table +- Ui +- Tooltip +- Dropdown +- Ssl certificate distribution +- Db migration +- Multiplex ssh connections +- Able to search with id +- Not found redirect +- Settings db requests +- Error during saving logs +- Consider base directory in heroku bp +- Basedirectory should be empty if null +- Allow basedirectory for heroku +- Stream logs for heroku bp +- Debug log for bp +- Scp without host verification & cert copy +- Base directory & docker bp +- Laravel php chooser +- Multiplex ssh and ssl copy +- Seed new preview secret types +- Error notification +- Empty preview value +- Error notification +- Seed +- Service logs +- Appwrite function network is not the default +- Logs in docker bp +- Able to delete apps in unconfigured state +- Disable development low disk space +- Only log things to console in dev mode +- Do not get status of more than 10 resources defined by category +- BaseDirectory +- Dashboard statuses +- Default buildImage and baseBuildImage +- Initial deploy status +- Show logs better +- Do not start tcp proxy without main container +- Cleanup stucked tcp proxies +- Default 0 pending invitations +- Handle forked repositories +- Typo +- Pr branches +- Fork pr previews +- Remove unnecessary things +- Meilisearch data dir +- Verify and configure remote docker engines +- Add buildkit features +- Nope if you are not logged in +- Do not use npx +- Pure docker based development +- Do not show nope as ip address for dbs +- Add git sha to build args +- Smart search for new services +- Logs for not running containers +- Update docker binaries +- Gh release +- Dev container +- Gitlab auth and compose reload +- Check compose domains in general +- Port required if fqdn is set +- Appwrite v1 missing containers +- Dockerfile +- Pull does not work remotely on huge compose file +- Single container logs and usage with compose +- Secret errors +- Service logs +- Heroku bp +- Expose port is readonly on the wrong condition +- Toast +- Traefik proxy q 10s +- App logs view +- Tooltip +- Toast, rde, webhooks +- Pathprefix +- Load public repos +- Webhook simplified +- Remote webhooks +- Previews wbh +- Webhooks +- Websecure redirect +- Wb for previews +- Pr stopps main deployment +- Preview wbh +- Wh catchall for all +- Remove old minio proxies +- Template files +- Compose icon +- Templates +- Confirm restart service +- Template +- Templates +- Templates +- Plausible analytics things +- Appwrite webhook +- Coolify instance proxy +- Migrate template +- Preview webhooks +- Simplify webhooks +- Remove ghost-mariadb from the list +- More simplified webhooks +- Umami + ghost issues +- Remove contribution docs +- Umami template +- Compose webhooks fixed +- Variable replacements +- Doc links +- For rollback +- N8n and weblate icon +- Expose ports for services +- Wp + mysql on arm +- Show rollback button loading +- No tags error +- Update on mobile +- Dashboard error +- GetTemplates +- Docker compose persistent volumes +- Application persistent storage things +- Volume names for undefined volume names in compose +- Empty secrets on UI +- Ports for services +- Default icon for new services +- IsBot issue +- Local dev api/ws urls +- Wrong template/type +- Gitea icon is svg +- Gh actions +- Gh actions +- Replace $$generate vars +- Webhook traefik +- Exposed ports +- Wrong icons on dashboard +- Escape % in secrets +- Move debug log settings to build logs +- Storage for compose bp + debug on +- Hasura admin secret +- Logs +- Mounts +- Load logs after build failed +- Accept logged and not logged user in /base +- Remote haproxy password/etc +- Remove hardcoded sentry dsn +- Nope in database strings +- 0 destinations redirect after creation +- Seed +- Sentry dsn update +- Dnt +- Ui +- Only visible with publicrepo +- Migrations +- Prevent webhook errors to be logged +- Login error +- Remove beta from systemwide git +- Git checkout +- Remove sentry before migration +- Webhook previewseparator +- Apache on arm +- Update PR/MRs with new previewSeparator +- Static for arm +- Failed builds should not push images +- Turn off autodeploy for simpledockerfiles +- Security hole +- Rde +- Delete resource on dashboard +- Wrong port in case of docker compose +- Public db icon on dashboard +- Cleanup +- Build commands +- Migration file +- Adding missing appwrite volume +- Appwrite tmp volume +- Do not replace secret +- Root user for dbs on arm +- Escape secrets +- Escape env vars +- Envs +- Docker buildpack env +- Secrets with newline +- Secrets +- Add default node_env variable +- Add default node_env variable +- Secrets +- Secrets +- Gh actions +- Duplicate env variables +- Cleanupstorage +- Remove unused imports +- Parsing secrets +- Read-only permission +- Read-only iam +- $ sign in secrets +- Custom gitlab git user +- Add documentation link again +- Remove prefetches +- Doc link +- Temporary disable dns check with dns servers +- Local images for reverting +- Secrets +- Compose file location +- Docker log sequence +- Delete apps with previews +- Do not cleanup compose applications as unconfigured +- Build env variables with docker compose +- Public gh repo reload compose +- Build args docker compose +- Grpc +- Secrets +- Www redirect +- Cleanup function +- Cleanup stucked containers +- Deletion + cleanupStuckedContainers +- Stucked containers +- CleanupStuckedContainers +- CleanupStuckedContainers +- Typos in docs +- Url +- Network in compose files +- Escape new line chars in wp custom configs +- Applications cannot be deleted +- Arm servics +- Base directory not found +- Cannot delete resource when you are not on root team +- Empty port in docker compose +- Set PACK_VERSION to 0.27.0 +- PublishDirectory +- Host volumes +- Replace . & .. & $PWD with ~ +- Handle log format volumes +- Nestjs buildpack +- Show ip address as host in public dbs +- Revert from dockerhub if ghcr.io does not exists +- Logo of CCCareers +- Typo +- Ssh +- Nullable name on deploy_keys +- Enviroments +- Remove dd - oops +- Add inprogress activity +- Application view +- Only set status in case the last command block is finished +- Poll activity +- Small typo +- Show activity on load +- Deployment should fail on error +- Tests +- Version +- Status not needed +- No project redirect +- Gh actions +- Set status +- Seeders +- Do not modify localhost +- Deployment_uuid -> type_uuid +- Read env from config, bc of cache +- Private key change view +- New destination +- Do not update next channel all the time +- Cancel deployment button +- Public repo limit shown + branch should be preselected. +- Better status on ui for apps +- Arm coolify version +- Formatting +- Gh actions +- Show github app secrets +- Do not force next version updates +- Debug log button +- Deployment key based works +- Deployment cancel/debug buttons +- Upgrade button +- Changing static build changes port +- Overwrite default nginx configuration +- Do not overlap docker image names +- Oops +- Found image name +- Name length +- Semicolons encoding by traefik +- Base_dir wip & outputs +- Cleanup docker images +- Nginx try_files +- Master is the default, not main +- No ms in rate limit resets +- Loading after button text +- Default value +- Localhost is usable +- Update docker-compose prod +- Cloud/checkoutid/lms +- Type of license code +- More verbose error +- Version lol +- Update prod compose +- Version +- Remove buggregator from dev +- Able to change localhost's private key +- Readonly input box - Notifications -- Add shared email option to everyone - -## [4.0.0-beta.57] - 2023-10-02 - -### 🚀 Features - -- Container logs - -### 🐛 Bug Fixes - -- Always pull helper image in dev -- Only show last 1000 lines -- Service status - -## [4.0.0-beta.47] - 2023-09-28 - -### 🐛 Bug Fixes - -- Next helper image -- Service templates -- Sync:bunny -- Update process if server has been renamed -- Reporting handler -- Localhost privatekey update -- Remove private key in case you removed a github app -- Only show manually added private keys on server view -- Show source on all type of applications -- Docker cleanup should be a job by server -- File/dir based volumes are now read from the server -- Respect server fqdn -- If public repository does not have a main branch -- Preselect branc on private repos -- Deploykey branch -- Backups are now working again -- Not found base_branch in git webhooks -- Coolify db backup -- Preview deployments name, status etc -- Services should have destination as well -- Dockerfile expose is not overwritten -- If app settings is not saved to db -- Do not show subscription cancelled noti -- Show real volume names -- Only parse expose in dockerfiles if ports_exposes is empty -- Add uuid to volume names -- New volumes for services should have - instead of _ - -### 💼 Other - -- Fix previews to preview - -## [4.0.0-beta.46] - 2023-09-28 - -### 🐛 Bug Fixes - -- Containerstatusjob -- Aaaaaaaaaaaaaaaaa -- Services view -- Services -- Manually create network for services -- Disable early updates -- Sslip for localhost -- ContainerStatusJob -- Cannot delete env with available services -- Sync command -- Install script drops an error -- Prevent sync version (it needs an option) -- Instance fqdn setting -- Sentry 4510197209 -- Sentry 4504136641 -- Sentry 4502634789 - -## [4.0.0-beta.45] - 2023-09-24 - -### 🚀 Features - -- Services -- Image tag for services - -### 🐛 Bug Fixes - -- Applications with port mappins do a normal update (not rolling update) -- Put back build pack chooser -- Proxy configuration + starter -- Show real storage name on services -- New service template layout - -### 💼 Other - -- Fixed z-index for version link. -- Add source button -- Fixed z-index for magicbar -- A bit better error -- More visible feedback button -- Update help modal -- Help -- Marketing emails - -## [4.0.0-beta.28] - 2023-09-08 - -### 🚀 Features - -- Telegram topics separation -- Developer view for env variables -- Cache team settings -- Generate public key from private keys -- Able to invite more people at once -- Trial -- Dynamic trial period -- Ssh-agent instead of filesystem based ssh keys -- New container status checks -- Generate ssh key -- Sentry add email for better support -- Healthcheck for apps -- Add cloudflare tunnel support - -### 🐛 Bug Fixes - +- Licensing +- Subscription link +- Migrate db schema for smtp + discord +- Text field +- Null fqdn notifications +- Remove old modal +- Proxy stop/start ui +- Proxy UI +- Empty description +- Input and textarea +- Postgres_username name to not name, lol +- DatabaseBackupJob.php +- No storage +- Backup now button +- Ui + subscription +- Self-hosted +- Make coolify-db backups unique dir +- Limits & server creation page +- Fqdn on apps +- DockerCleanupjob +- Validation +- Webhook endpoint in cloud and no system wide gh app +- Subscriptions +- Password confirmation +- Proxy start job +- Dockerimage jobs are not overlapping +- Sentry bug +- Button loading animation +- Form address +- Show hosted email service, just disable for non pro subs +- Add navbar for source + keys +- Add docker network to build process +- Overlapping apps +- Do not show system wide git on cloud +- Lowercase image names +- Typo +- SaveModel email settings +- Bug - Db backup job - Sentry 4459819517 - Sentry 4451028626 @@ -5985,795 +2252,1754 @@ ### 🐛 Bug Fixes - Add traefik labels no matter if traefik is selected or not - Add expose port for containers - Also check docker socks permission on validation - -### 💼 Other - -- User should know that the public key -- Services are not availble yet -- Show registered users on waitlist page -- Nixpacksarchive -- Add Plausible analytics -- Global env variables -- Fix -- Trial emails -- Server check instead of app check -- Show trial instead of sub -- Server lost connection +- Applications with port mappins do a normal update (not rolling update) +- Put back build pack chooser +- Proxy configuration + starter +- Show real storage name on services +- New service template layout +- Containerstatusjob +- Aaaaaaaaaaaaaaaaa +- Services view - Services -- Services -- Services -- Ui for services -- Services -- Services -- Services -- Fixes -- Fix typo - -## [4.0.0-beta.27] - 2023-09-08 - -### 🐛 Bug Fixes - -- Bug - -## [4.0.0-beta.26] - 2023-09-08 - -### 🚀 Features - -- Public database - -## [4.0.0-beta.25] - 2023-09-07 - -### 🐛 Bug Fixes - -- SaveModel email settings - -## [4.0.0-beta.24] - 2023-09-06 - -### 🚀 Features - -- Send request in cloud -- Add discord notifications - -### 🐛 Bug Fixes - -- Form address -- Show hosted email service, just disable for non pro subs -- Add navbar for source + keys -- Add docker network to build process -- Overlapping apps -- Do not show system wide git on cloud -- Lowercase image names -- Typo - -### 💼 Other - -- Backup existing database - -## [4.0.0-beta.23] - 2023-09-01 - -### 🐛 Bug Fixes - -- Sentry bug -- Button loading animation - -## [4.0.0-beta.22] - 2023-09-01 - -### 🚀 Features - -- Add resend as transactional emails - -### 🐛 Bug Fixes - -- DockerCleanupjob -- Validation -- Webhook endpoint in cloud and no system wide gh app -- Subscriptions -- Password confirmation -- Proxy start job -- Dockerimage jobs are not overlapping - -## [4.0.0-beta.21] - 2023-08-27 - -### 🚀 Features - -- Invite by email from waitlist -- Rolling update - -### 🐛 Bug Fixes - -- Limits & server creation page -- Fqdn on apps - -### 💼 Other - +- Manually create network for services +- Disable early updates +- Sslip for localhost +- ContainerStatusJob +- Cannot delete env with available services +- Sync command +- Install script drops an error +- Prevent sync version (it needs an option) +- Instance fqdn setting +- Sentry 4510197209 +- Sentry 4504136641 +- Sentry 4502634789 +- Next helper image +- Service templates +- Sync:bunny +- Update process if server has been renamed +- Reporting handler +- Localhost privatekey update +- Remove private key in case you removed a github app +- Only show manually added private keys on server view +- Show source on all type of applications +- Docker cleanup should be a job by server +- File/dir based volumes are now read from the server +- Respect server fqdn +- If public repository does not have a main branch +- Preselect branc on private repos +- Deploykey branch +- Backups are now working again +- Not found base_branch in git webhooks +- Coolify db backup +- Preview deployments name, status etc +- Services should have destination as well +- Dockerfile expose is not overwritten +- If app settings is not saved to db +- Do not show subscription cancelled noti +- Show real volume names +- Only parse expose in dockerfiles if ports_exposes is empty +- Add uuid to volume names +- New volumes for services should have - instead of _ +- Always pull helper image in dev +- Only show last 1000 lines +- Service status +- If waitlist is disabled, redirect to register +- Add destination to new services +- Predefined content for files +- Move /data to ./_data in dev +- UI +- Show all storages in one place for services +- Ui +- Add _data to vite ignore +- Only use _ in volume names for services +- Volume names in services +- Volume names +- Service logs visible if the whole service stack is not running +- Ui +- Compose magic +- Compose parser updated +- Dev compose files +- Traefik labels for multiport deployments +- Visible version number +- Remove SERVICE_ from deployable compose +- Delete event to deleting +- Move dev data to volumes to prevent permission issues +- Traefik labelling in case of several http and https domain added +- PR deployments use the first fqdn as base +- Email notifications subscription fixed +- Services - do not remove unnecessary things for now +- Decrease max horizon processes to get lower memory usage +- Test emails only available for user owned smtp/resend +- Ui for self-hosted email settings +- Set smtp notifications on by default +- Select branch on other git +- Private repository +- Contribution guide +- Public repository names +- *(create)* Flex wrap on server & network selection +- Better unreachable/revived server statuses +- Able to set base dir for Dockerfile build pack +- Server validation process +- Fqdn could be null +- Small +- Server unreachable count +- Do not reset unreachable count +- Contact docs +- Check connection +- Server saving +- No env goto envs from dashboard +- Goto +- Tcp proxy for dbs +- Database backups +- Only send email if transactional email set +- Backupfailed notification is forced +- Use port exposed for reverse proxy +- Contact link +- Use only ip addresses for servers +- Deleted team and it is the current one +- Add new team button +- Transactional email link +- Dashboard goto link +- Only require registry image in case of dockerimage bp +- Instant save build pack change +- Public git +- Cannot remove localhost +- Check localhost connection +- Send unreachable/revived notifications +- Boarding + verification +- Make sure proxy wont start in NONE mode +- Service check status 10 sec +- IsCloud in production seeder +- Make sure to use IP address +- Dockerfile location feature +- Server ip could be hostname in self-hosted +- Urls should be password fields +- No backup for redis +- Show database logs in case of its not healthy and running +- Proxy check for ports, do not kill anything listening on port 80/443 +- Traefik dashboard ip +- Db labels +- Docker cleanup jobs +- Timeout for instant remote processes +- Dev containerjobs +- Backup database one-by-one. +- Turn off static deployment if you switch buildpacks +- Docker hub URL +- Redis URL generated +- Build image before starting dockerfile buildpacks +- Service status check is a bit better +- Generate fqdn if you deleted a service app, but it requires fqdn +- Cancel any deployments + queue next +- Add internal domain names during build process +- Noindex meta tag +- Show docker build logs +- Only include config.json if its exists and a file +- Always start proxy if not NONE is selected +- Proxy start process +- Setup:dev script & contribution guide +- Do not show configuration changed if config_hash is null +- Add config_hash if its null (old deployments) +- Label generation +- Labels +- Email channel no recepients +- Limit horizon processes to 2 by default +- Add custom port as ssh option to deploy_key based commands +- Remove custom port from git repo url +- ContainerStatus job +- Service docs links +- Add PGUSER to prevent HC warning +- Preselect s3 storage if available +- Port exposes change, shoud regenerate label - Boarding - -## [4.0.0-beta.20] - 2023-08-17 - -### 🚀 Features - -- Send internal notification to discord -- Monitor server connection - -### 🐛 Bug Fixes - -- Make coolify-db backups unique dir - -## [4.0.0-beta.19] - 2023-08-15 - -### 🚀 Features - -- Pricing plans ans subs -- Add s3 storages -- Init postgresql database -- Add backup notifications -- Dockerfile build pack -- Cloud -- Force password reset + waitlist - -### 🐛 Bug Fixes - -- Remove buggregator from dev -- Able to change localhost's private key -- Readonly input box -- Notifications -- Licensing -- Subscription link -- Migrate db schema for smtp + discord -- Text field -- Null fqdn notifications -- Remove old modal -- Proxy stop/start ui -- Proxy UI -- Empty description -- Input and textarea -- Postgres_username name to not name, lol -- DatabaseBackupJob.php -- No storage -- Backup now button -- Ui + subscription -- Self-hosted - -### 💼 Other - -- Scheduled backups - -## [4.0.0-beta.18] - 2023-07-14 - -### 🚀 Features - -- Able to control multiplexing -- Add runRemoteCommandSync -- Github repo with deployment key -- Add persistent volumes -- Debuggable executeNow commands -- Add private gh repos -- Delete gh app -- Installation/update github apps -- Auto-deploy -- Deploy key based deployments -- Resource limits -- Long running queue with 1 hour of timeout -- Add arm build to dev -- Disk cleanup threshold by server -- Notify user of disk cleanup init - -### 🐛 Bug Fixes - -- Logo of CCCareers -- Typo -- Ssh -- Nullable name on deploy_keys -- Enviroments -- Remove dd - oops -- Add inprogress activity -- Application view -- Only set status in case the last command block is finished -- Poll activity -- Small typo -- Show activity on load -- Deployment should fail on error -- Tests -- Version -- Status not needed -- No project redirect -- Gh actions -- Set status -- Seeders -- Do not modify localhost -- Deployment_uuid -> type_uuid -- Read env from config, bc of cache -- Private key change view -- New destination -- Do not update next channel all the time -- Cancel deployment button -- Public repo limit shown + branch should be preselected. -- Better status on ui for apps -- Arm coolify version -- Formatting -- Gh actions -- Show github app secrets -- Do not force next version updates -- Debug log button -- Deployment key based works -- Deployment cancel/debug buttons -- Upgrade button -- Changing static build changes port -- Overwrite default nginx configuration -- Do not overlap docker image names -- Oops -- Found image name -- Name length -- Semicolons encoding by traefik -- Base_dir wip & outputs -- Cleanup docker images -- Nginx try_files -- Master is the default, not main -- No ms in rate limit resets -- Loading after button text -- Default value -- Localhost is usable -- Update docker-compose prod -- Cloud/checkoutid/lms -- Type of license code -- More verbose error -- Version lol -- Update prod compose -- Version - -### 💼 Other - -- Extract process handling from async job. -- Extract process handling from async job. -- Extract process handling from async job. -- Extract process handling from async job. -- Extract process handling from async job. -- Extract process handling from async job. -- Extract process handling from async job. -- Persisting data - -## [3.12.28] - 2023-03-16 - -### 🐛 Bug Fixes - -- Revert from dockerhub if ghcr.io does not exists - -## [3.12.27] - 2023-03-07 - -### 🐛 Bug Fixes - -- Show ip address as host in public dbs - -## [3.12.24] - 2023-03-04 - -### 🐛 Bug Fixes - -- Nestjs buildpack - -## [3.12.22] - 2023-03-03 - -### 🚀 Features - -- Add host path to any container - -### 🐛 Bug Fixes - -- Set PACK_VERSION to 0.27.0 -- PublishDirectory -- Host volumes -- Replace . & .. & $PWD with ~ -- Handle log format volumes - -## [3.12.19] - 2023-02-20 - -### 🚀 Features - -- Github raw icon url -- Remove svg support - -### 🐛 Bug Fixes - -- Typos in docs -- Url -- Network in compose files -- Escape new line chars in wp custom configs -- Applications cannot be deleted -- Arm servics -- Base directory not found -- Cannot delete resource when you are not on root team -- Empty port in docker compose - -## [3.12.18] - 2023-01-24 - -### 🐛 Bug Fixes - -- CleanupStuckedContainers -- CleanupStuckedContainers - -## [3.12.16] - 2023-01-20 - -### 🐛 Bug Fixes - -- Stucked containers - -## [3.12.15] - 2023-01-20 - -### 🐛 Bug Fixes - -- Cleanup function -- Cleanup stucked containers -- Deletion + cleanupStuckedContainers - -## [3.12.14] - 2023-01-19 - -### 🐛 Bug Fixes - -- Www redirect - -## [3.12.13] - 2023-01-18 - -### 🐛 Bug Fixes - -- Secrets - -## [3.12.12] - 2023-01-17 - -### 🚀 Features - -- Init h2c (http2/grpc) support -- Http + h2c paralel - -### 🐛 Bug Fixes - -- Build args docker compose -- Grpc - -## [3.12.11] - 2023-01-16 - -### 🐛 Bug Fixes - -- Compose file location -- Docker log sequence -- Delete apps with previews -- Do not cleanup compose applications as unconfigured -- Build env variables with docker compose -- Public gh repo reload compose - -### 💼 Other - -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc - -## [3.12.10] - 2023-01-11 - -### 💼 Other - -- Add missing variables - -## [3.12.9] - 2023-01-11 - -### 🚀 Features - -- Add Openblocks icon -- Adding icon for whoogle -- *(ui)* Add libretranslate service icon -- Handle invite_only plausible analytics - -### 🐛 Bug Fixes - -- Custom gitlab git user -- Add documentation link again -- Remove prefetches -- Doc link -- Temporary disable dns check with dns servers -- Local images for reverting -- Secrets - -## [3.12.8] - 2022-12-27 - -### 🐛 Bug Fixes - -- Parsing secrets -- Read-only permission -- Read-only iam -- $ sign in secrets - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.12.5] - 2022-12-26 - -### 🐛 Bug Fixes - -- Remove unused imports - -### 💼 Other - -- Conditional on environment - -## [3.12.2] - 2022-12-19 - -### 🐛 Bug Fixes - -- Appwrite tmp volume -- Do not replace secret -- Root user for dbs on arm -- Escape secrets -- Escape env vars -- Envs -- Docker buildpack env -- Secrets with newline -- Secrets -- Add default node_env variable -- Add default node_env variable -- Secrets -- Secrets -- Gh actions -- Duplicate env variables -- Cleanupstorage - -### 💼 Other - -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc -- Trpc - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.12.1] - 2022-12-13 - -### 🐛 Bug Fixes - -- Build commands -- Migration file -- Adding missing appwrite volume - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.12.0] - 2022-12-09 - -### 🚀 Features - -- Use registry for building -- Docker registries working -- Custom docker compose file location in repo -- Save doNotTrackData to db -- Add default sentry -- Do not track in settings -- System wide git out of beta -- Custom previewseparator -- Sentry frontend -- Able to host static/php sites on arm -- Save application data before deploying -- SimpleDockerfile deployment -- Able to push image to docker registry -- Revert to remote image -- *(api)* Name label - -### 🐛 Bug Fixes - -- 0 destinations redirect after creation -- Seed -- Sentry dsn update -- Dnt +- Clone to with the same environment name +- Cleanup stucked resources on start +- Do not allow to delete env if a resource is defined +- Service template generator + appwrite +- Mongodb backup +- Make sure coolfiy network exists on install +- Syncbunny command +- Encrypt mongodb password +- Mongodb healtcheck command +- Rate limit for api + add mariadb + mysql +- Server settings guarded +- Space in build args +- Lock SERVICE_FQDN envs +- If user is invited, that means its email is verified +- Force password reset on invited accounts +- Add ssh options to git ls-remote +- Git ls-remote +- Remove coolify labels from ui +- Missing environment variables prevewi on service +- Invoice.paid should sleep for 5 seconds +- Local dev repo +- Deployments ui +- Dockerfile build pack fix +- Set labels on generate domain +- Network service parse +- Notification url in containerstatusjob +- Gh webhook response 200 to installation_repositories +- Delete destination +- No id found +- Missing $mailMessage +- Set default from/sender names +- No environments +- Telegram text +- Private key not found error +- UI +- Resourcesdelete command +- Port number should be int +- Separate delete with validation of server +- Add nixpacks info +- Remove filter +- Container logs are now followable in full-screen and sorted by timestamp +- Ui for labels - Ui -- Only visible with publicrepo +- Deletions +- Build_image not found +- Github source view +- Github source view +- Dockercleanupjob should be released back +- Ui +- Local ip address +- Revert workdir to basedir +- Container status jobs for old pr deployments +- Service updates +- *(fider template)* Use the correct docs url +- Fqdn for minio +- Generate service fields +- Mariadb backups +- When to pull image +- Do not allow to enter local ip addresses +- Reset password +- Only report nonruntime errors +- Handle different label formats in services +- Server adding process +- Show defined resources in server tab, so you will know what you need to delete before you can delete the server. +- Lots of regarding git + docker compose deployments +- Pull request build variables +- Double default password length +- Do not remove deployment in case compose based failed +- No container servers +- Sentry issue +- Dockercompose save ./ volumes under /data/coolify +- Server view for link() +- Default value do not overwrite existing env value +- Use official install script with rancher (one will work for sure) +- Add cf tunnel to boarding server view +- Prevent autorefresh of proxy status +- Missing docker image thing +- Add hc for soketi +- Deploy the right compose file +- Bind volumes for compose bp +- Use hc port 80 in case of static build +- Switching to static build +- Container selection +- Service navbar using new realtime events +- Do not create duplicated networks +- Live event +- Service start + event +- Service deletion job +- Double ws connection +- Boarding view +- Do not send telegram noti on intent payment failed +- Database ui is realtime based +- Live mode for github webhooks +- Ui +- Realtime connection popup could be disabled +- Realtime check +- Add new destination +- Proxy logs +- Db status check +- Pusher host +- Add ipv6 +- Realtime connection?! +- Websocket +- Better handling of errors with install script +- Install script parse version +- Only allow to modify in .env file if AUTOUPDATE is set +- Is autoupdate not null +- Run init command after production seeder +- Init +- Comma in traefik custom labels +- Ignore if dynamic config could not be set +- Service env variable ovewritten if it has a default value +- Labelling +- Non-ascii chars in labels +- Labels +- Init script echos +- Update Coolify script +- Null notify +- Check queued deployments as well +- Copy invitation +- Password reset / invitation link requests +- Add catch all route +- Revert random container job delay +- Backup executions view +- Only check server status in container status job +- Improve server status check times +- Handle other types of generated values +- Server checking status +- Ui for adding new destination +- Reset domains on compose file change +- Domains for compose bp +- No action in webhooks +- Add debug output to gitlab webhooks +- Do not push dockerimage +- Add alpha to swarm +- Server not found +- Do not autovalidate server on mount +- Server update schedule +- Swarm support ui +- Server ready +- Get swarm service logs +- Docker compose apps env rewritten +- Storage error on dbs +- Why?! +- Stay tuned +- Cpu limit to float from int +- Add source commit to final envs +- Routing, switch back to old one +- Deploy instead of restart in case swarm is used +- Button title +- Restore falsely deleted coolify-db-backup +- Sub +- Wrong env variable parsing +- Deploy key + docker compose +- Horizon +- Duplicate compose variable +- Set deployment failed if new container is not healthy +- Nixpacks cache +- Only add restart policy if its empty (compose) +- Nixpacks buildpack +- File storage save +- Database env variables +- Healthy status +- Show framework based notification in build logs +- Traefik labels +- Use ip for sslip in dev if remote server is used +- Service labels without ports (unknown ports) +- Sort and rename (unique part) of labels +- Settings menu +- Remove traefik debug in dev mode +- Php pgsql to 8.2 +- Static buildpack should set port 80 +- Update navbar on build_pack change +- Do not include thegameplan.json into build image +- Submit error on postgresql +- Email verification / forgot password +- Escape build envs properly for nixpacks + docker build +- Undead endpoint +- Upload limit on ui +- Save cmd output propely (merge) +- Load profile on remote commands +- Load profile and set envs on remote cmd +- Restart should not update config hash +- Preview deployments with nixpacks +- Cleanup docker stuffs before upgrading +- Service deletion command +- Cpuset limits was determined in a way that apps only used 1 CPU max, ehh, sorry. +- Service stack view +- Change proxy view +- Checkbox click +- Git pull command for deploy key based previews +- Server status job +- Service deletion bug! +- Links +- Redis custom conf +- Sentry error +- Restrict concurrent deployments per server +- Queue +- Change env variable length +- Bitbucket manual deployments +- Webhooks for multiple apps +- Unhealthy deployments should be failed +- Add env variables for wordpress template without database +- Service deletion function +- Service deletion fix +- Dns validation + duplicated fqdns +- Validate server navbar upated +- Regenerate labels on application clone +- Service deletion +- Not able to use other shared envs +- Sentry fix +- Sentry +- Sentry error +- Sentry +- Sentry error +- Create dynamic directory +- Migrate to new modal +- Duplicate domain check +- Tags +- Wrap tags and avoid horizontal overflow +- Stripe webhooks +- Feedback from self-hosted envs to discord +- New menu on navbar +- Make sure resources are deleted in async mode +- Go to prod env from dashboard if there is no other envs defined +- User proper image_tag, if set +- New menu ui +- Lock logdrain configuration when one of them are enabled +- Add docker compose check during server validation +- Get service stack as uuid, not name +- Menu +- Flex wrap deployment previews +- Boolean docker options +- Only add 'networks' key if 'network_mode' is absent +- Cleanup scheduled tasks +- Padding left on input boxes +- Use ls / command instead ls +- Do not add the same server twice +- Only show redeployment required if status is not exited +- Add openbsd ssh server check +- Resources +- Empty build variables +- *(server)* Revalidate server button not showing in server's page +- Fluent bit ident level +- Submodule cloning +- Database status +- Permission change updates from webhook +- Server validation +- Connections being stuck and not processed until proxy restarts +- Use latest image if nothing is specified +- No coolify.yaml found +- Server validation +- Statuses +- Unknown image of service until it is uploaded +- Subscription / plan switch, etc +- Firefly service +- Force enable/disable server in case ultimate package quantity decreases +- Server disabled +- Custom dockerfile location always checked +- Import to mysql and mariadb +- Resource tab not loading if server is not reachable +- Load unmanaged async +- Do not show n/a networsk +- Service container status updates +- Public prs should not be commented +- Pull request deployments + build servers +- Env value generation +- Sentry error +- Service status updated +- Should note delete personal teams +- Make sure to show some buttons +- Sort repositories by name +- Deploy api messages +- Fqdn null in case docker compose bp +- Reload caddy issue +- /realtime endpoint +- Proxy switch +- Service ports for services + caddy +- Failed deployments should send failed email/notification +- Consider custom healthchecks in dockerfile +- Create initial files async +- Docker compose validation +- Duplicate dockerfile +- Multiline env variables +- Server stopped, service page not reachable +- Empty get logs number of lines +- Only escape envs after v239+ +- 0 in env value +- Consistent container name +- Custom ip address should turn off rolling update +- Multiline input +- Raw compose deployment +- Dashboard view if no project found +- Volumes for prs +- Shared env variable parsing +- Compose env has SERVICE, but not defined for Coolify +- Public service database +- Make sure service db proxy restarted +- Restart service db proxies +- Two factor +- Ui for tags +- Update resources view +- Realtime connection check +- Multline env in dev mode +- Scheduled backup for other service databases (supabase) +- PR deployments should not be distributed to 2 servers +- Name/from address required for resend +- Autoupdater +- Async service loads +- Disabled inputs are not trucated +- Duplicated generated fqdns are now working +- Uis +- Ui for cftunnels +- Search services +- Trial users subscription page +- Async public key loading +- Unfunctional server should see resources +- Warning if you use multiple domains for a service +- New github app creation +- Always rebuild Dockerfile / dockerimage buildpacks +- Do not rebuild dockerfile based apps twice +- Make sure if envs are changed, rebuild is needed +- Members cannot manage subscriptions +- IsMember +- Storage layout +- How to update docker-compose, environment variables and fqdns +- Git submodule update +- Unintended left padding on sidebar +- Hashed random delimeter in ssh commands + make sure to remove the delimeter from the command +- Service config hash update +- Redeploy if image not found in restart only mode +- Check each required binaries one-by-one +- Helper image only pulled if required, not every 10 mins +- Make sure that confs when checking if it is changed sorted +- Respect .env file (for default values) +- Remove temporary cloudflared config +- Remove lazy loading until bug figured out +- Rollback feature +- Base64 encode .env +- $ in labels escaped +- .env saved to deployment server, not to build server +- Do no able to delete gh app without deleting resources +- 500 error on edge case +- Able to select server when creating new destination +- N8n template +- Refresh public ips on start +- Move s3 storages to separate view +- Mongo db backup +- Backups +- Autoupdate +- Respect start period and chekc interval for hc +- Parse HEALTHCHECK from dockerfile +- Make s3 name and endpoint required +- Able to update source path for predefined volumes +- Get logs with non-root user +- Mongo 4.0 db backup +- Formbricks image origin +- Add port even if traefik is used +- Typo in tags.blade.php +- Install.sh error +- Env file +- Comment out internal notification in email_verify method +- Confirmation for custom labels +- Change permissions on newly created dirs +- Color for resource operation server and project name +- Only show realtime error on non-cloud instances +- Only allow push and mr gitlab events +- Improve scheduled task adding/removing +- Docker compose dependencies for pr previews +- Properly populating dependencies +- Use commit hash on webhooks +- Commit message length +- Hc from localhost to 127.0.0.1 +- Use rc in hc +- Telegram group chat notifications +- PR deployments have good predefined envs +- Optimize new resource creation +- Show it docker compose has syntax errors +- Wrong time during a failed deployment +- Removal of the failed deployment condition, addition of since started instead of finished time +- Use local versions + service templates and query them every 10 minutes +- Check proxy functionality before removing unnecessary coolify.yaml file and checking Docker Engine +- Show first 20 users only in admin view +- Add subpath for services +- Ghost subdir +- Do not pull templates in dev +- Templates +- Update error message for invalid token to mention invalid signature +- Disable containerStopped job for now +- Disable unreachable/revived notifications for now +- JSON_UNESCAPED_UNICODE +- Add wget to nixpacks builds +- Pre and post deployment commands +- Bitbucket commits link +- Better way to add curl/wget to nixpacks +- Root team able to download backups +- Build server should not have a proxy +- Improve build server functionalities +- Sentry issue +- Sentry +- Sentry error + livewire downgrade +- Sentry +- Sentry +- Sentry error +- Sentry +- Force load services from cdn on reload list +- Do not allow service storage mount point modifications +- Volume adding +- Sync upgrade process +- Publish horizon +- Add missing team model +- Test new upgrade process? +- Throw exception +- Build server dirs not created on main server +- Compose load with non-root user +- Able to redeploy dockerfile based apps without cache +- Compose previews does have env variables +- Fine-tune cdn pulls +- Spamming :D +- Parse docker version better +- Compose issues +- SERVICE_FQDN has source port in it +- Logto service +- Allow invitations via email +- Sort by defined order + fixed typo +- Only ignore volumes with driver_opts +- Check env in args for compose based apps +- Custom docker compose commands, add project dir if needed +- Autoupdate process +- Backup executions view +- Handle previously defined compose previews +- Sort backup executions +- Supabase service, newest versions +- Set default name for Docker volumes if it is null +- Multiline variable should be literal + should be multiline in bash with \ +- Gitlab merge request should close PR +- Multiline build args +- Setup script doesnt link to the correct source code file +- Install.sh do not reinstall packages on arch +- Just restart +- Stripprefix middleware correctly labeled to http +- Bitbucket link +- Compose generator +- Do no truncate repositories wtih domain (git) in it +- In services should edit compose file for volumes and envs +- Handle laravel deployment better +- Db proxy status shown better in the UI +- Show commit message on webhooks + prs +- Metrics parsing +- Charts +- Application custom labels reset after saving +- Static build with new nixpacks build process +- Make server charts one livewire component with one interval selector +- You can now add env variable from ui to services +- Update compose environment with UI defined variables +- Refresh deployable compose without reload +- Remove cloud stripe notifications +- App deployment should be in high queue +- Remove zoom from modals +- Get envs before sortby +- MB is % lol +- Projects with 0 envs +- Run user commands on high prio queue +- Load js locally +- Remove lemon + paddle things +- Run container commands on high priority +- Image logo +- Remove both option for api endpoints. it just makes things complicated +- Cleanup subs in cloud +- Show keydbs/dragonflies/clickhouses +- Only run cloud clean on cloud + remove root team +- Force cleanup on busy servers +- Check domain on new app via api +- Custom container name will be the container name, not just internal network name +- Api updates +- Yaml everywhere +- Add newline character to private key before saving +- Add validation for webhook endpoint selection +- Database input validators +- Remove own app from domain checks +- Return data of app update +- Do not overwrite hardcoded variables if they rely on another variable +- Remove networks when deleting a docker compose based app +- Api +- Always set project name during app deployments +- Remove volumes as well +- Gitea pr previews +- Prevent instance fqdn persisting to other servers dynamic proxy configs +- Better volume cleanups +- Cleanup parameter +- Update redirect URL in unauthenticated exception handler +- Respect top-level configs and secrets +- Service status changed event +- Disable sentinel until a few bugs are fixed +- Service domains and envs are properly updated +- *(reactive-resume)* New healthcheck command for MinIO +- *(MinIO)* New command healthcheck +- Update minio hc in services +- Add validation for missing docker compose file +- Typo in is_literal helper +- Env is_literal helper text typo +- Update docker compose pull command with --policy always +- Plane service template +- Vikunja +- Docmost template +- Drupal +- Improve github source creation +- Tag deployments +- New docker compose parsing +- Handle / in preselecting branches +- Handle custom_internal_name check in ApplicationDeploymentJob.php +- If git limit reached, ignore it and continue with a default selection +- Backup downloads +- Missing input for api endpoint +- Volume detection (dir or file) is fixed +- Supabase +- Create file storage even if content is empty +- Preview deployments should be stopped properly via gh webhook +- Deleting application should delete preview deployments +- Plane service images +- Fix issue with deployment start command in ApplicationDeploymentJob +- Directory will be created by default for compose host mounts +- Restart proxy does not work + status indicator on the UI +- Uuid in api docs type +- Raw compose deployment .env not found +- Api -> application patch endpoint +- Remove pull always when uploading backup to s3 +- Handle array env vars +- Link in task failed job notifications +- Random generated uuid will be full length (not 7 characters) +- Gitlab service +- Gitlab logo +- Bitbucket repository url +- By default volumes that we cannot determine if they are directories or files are treated as directories +- Domain update on services on the UI +- Update SERVICE_FQDN/URL env variables when you change the domain +- Several shared environment variables in one value, parsed correctly +- Members of root team should not see instance admin stuff +- Parse docker composer +- Service env parsing +- Service env variables +- Activity type invalid +- Update env on ui +- Only append docker network if service/app is running +- Remove lazy load from scheduled tasks +- Plausible template +- Service_url should not have a trailing slash +- If usagebefore cannot be determined, cleanup docker with force +- Async remote command +- Only run logdrain if necessary +- Remove network if it is only connected to coolify proxy itself +- Dir mounts should have proper dirs +- File storages (dir/file mount) handled properly +- Do not use port exposes on docker compose buildpacks +- Minecraft server template fixed +- Graceful shutdown +- Stop resources gracefully +- Handle null and empty disk usage in DockerCleanupJob +- Show latest version on manual update view +- Empty string content should be saved as a file +- Update Traefik labels on init +- Add missing middleware for server check job +- Scheduledbackup not found +- Manual update process +- Timezone not updated when systemd is missing +- If volumes + file mounts are defined, should merge them together in the compose file +- All mongo v4 backups should use the different backup command +- Database custom environment variables +- Connect compose apps to the right predefined network +- Docker compose destination network +- Server status when there are multiple servers +- Sync fqdn change on the UI +- Pr build names in case custom name is used +- Application patch request instant_deploy +- Canceling deployment on build server +- Backup of password protected postgresql database +- Docker cleanup job +- Storages with preserved git repository +- Parser parser parser +- New parser only in dev +- Parser parser +- Numberoflines should be number +- Docker cleanup job +- Fix directory and file mount headings in file-storage.blade.php +- Preview fqdn generation +- Revert a few lines +- Service ui sync bug +- Setup script doesn't work on rhel based images with some curl variant already installed +- Let's wait for healthy container during installation and wait an extra 20 seconds (for migrations) +- Infra files +- Log drain only for Applications +- Copy large compose files through scp (not ssh) +- Check if array is associative or not +- Openapi endpoint urls +- Convert environment variables to one format in shared.php +- Logical volumes could be overwritten with new path +- Env variable in value parsed +- Pull coolify image only when the app needs to be updated +- Wrong executions order +- Handle project not found error in environment_details API endpoint +- Deployment running for - without "ago" +- Update helper image pulling logic to only pull if the version is newer +- Parser +- Plunk NEXT_PUBLIC_API_URI +- Reenable overlapping servercheckjob +- Appwrite template + parser +- Don't add `networks` key if `network_mode` is used +- Remove debug statement in shared.php +- Scp through cloudflare +- Delete older versions of the helper image other than the latest one +- Update remoteProcess.php to handle null values in logItem properties +- Disable mux_enabled during server validation +- Move mc command to coolify image from helper +- Keydb. add `:` delimiter for connection string +- Cloudflare tunnel with new multiplexing feature +- Keep-alive ws connections +- Add build.sh to debug logs +- Update Coolify installer +- Terminal +- Generate https for minio +- Install script +- Handle WebSocket connection close in terminal.blade.php +- Able to open terminal to any containers +- Refactor run-command +- If you exit a container manually, it should close the underlying tty as well +- Move terminal to separate view on services +- Only update helper image in DB +- Generated fqdn for SERVICE_FQDN_APP_3000 magic envs +- Proxy status +- Coolify-db should not be in the managed resources +- Store original root key in the original location +- Logto service +- Cloudflared service - Migrations -- Prevent webhook errors to be logged -- Login error -- Remove beta from systemwide git -- Git checkout -- Remove sentry before migration -- Webhook previewseparator -- Apache on arm -- Update PR/MRs with new previewSeparator -- Static for arm -- Failed builds should not push images -- Turn off autodeploy for simpledockerfiles -- Security hole -- Rde -- Delete resource on dashboard -- Wrong port in case of docker compose -- Public db icon on dashboard -- Cleanup +- Cloudflare tunnel configuration, ui, etc +- Parser +- Exited services statuses +- Make sure to reload window if app status changes +- Deploy key based deployments +- Proxy fixes +- Proxy +- *(templates)* Filebrowser FQDN env variable +- Handle edge case when build variables and env variables are in different format +- Compose based terminal +- Filebrowser template +- Edit is_build_server_enabled upon creating application on other application type +- Save settings after assigning value +- In dev mode do not ask confirmation on delete +- Mixpost +- Handle deletion of 'hello' in confirmation modal for dev environment +- Remove autofocuses +- Ipv6 scp should use -6 flag +- Cleanup stucked applicationdeploymentqueue +- Realtime watch in development mode +- Able to select root permission easier +- Able to support more database dynamically from Coolify's UI +- Strapi template +- Bitcoin core template +- Api useBuildServer +- Service application view +- Add new supported database images +- Parse proxy config and check the set ports usage +- Update FQDN +- Scheduled backup for services view +- Parser, espacing container labels +- Reset description and subject fields after submitting feedback +- Tag mass redeployments +- Service env orders, application env orders +- Proxy conf in dev +- One-click services +- Use local service-templates in dev +- New services +- Remove not used extra host +- Chatwoot service +- Directus +- Database descriptions +- Update services +- Soketi +- Select server view +- Update mattermost image tag and add default port +- Remove env, change timezone +- Postgres healthcheck +- Azimutt template - still not working haha +- New parser with SERVICE_URL_ envs +- Improve service template readability +- Update password variables in Service model +- Scheduled database server +- Select server view +- Signup +- Application domains should be http and https only +- Validate and sanitize application domains +- Sanitize and validate application domains +- Use correct env variable for invoice ninja password +- Make sure caddy is not removed by cleanup +- Libretranslate +- Do not allow to change number of lines when streaming logs +- Plunk +- No manual timezones +- Helper push +- Format +- Add port metadata and Coolify magic to generate the domain +- Sentinel +- Metrics +- Generate sentinel url +- Only enable Sentinel for new servers +- Is_static through API +- Allow setting standalone redis variables via ENVs (team variables...) +- Check for username separately form password +- Encrypt all existing redis passwords +- Pull helper image on helper_version change +- Redis database user and password +- Able to update ipv4 / ipv6 instance settings +- Metrics for dbs +- Sentinel start fixed +- Validate sentinel custom URL when enabling sentinel +- Should be able to reset labels in read-only mode with manual click +- No sentinel for swarm yet +- Charts ui +- Volume +- Sentinel config changes restarts sentinel +- Disable sentinel for now +- Disable Sentinel temporarily +- Disable Sentinel temporarily for non-dev environments +- Access team's github apps only +- Admins should now invite owner +- Add experimental flag +- GenerateSentinelUrl method +- NumberOfLines could be null +- Login / register view +- Restart sentinel once a day +- Changing private key manually won't trigger a notification +- Grammar for helper +- Fix my own grammar +- Add telescope only in dev mode +- New way to update container statuses +- Only run server storage every 10 mins if sentinel is not active +- Cloud admin view +- Queries in kernel.php +- Lower case emails only +- Change emails to lowercase on init +- Do not error on update email +- Always authenticate with lowercase emails +- Dashboard refactor +- Add min/max length to input/texarea +- Remove livewire legacy from help view +- Remove unnecessary endpoints (magic) +- Transactional email livewire +- Destinations livewire refactor +- Refactor destination/docker view +- Logdrains validation +- Reworded +- Use Auth(), add new db proxy stop event refactor clickhouse view +- Add user/pw to db view +- Sort servers by name +- Keydb view +- Refactor tags view / remove obsolete one +- Send discord/telegram notifications on high job queue +- Server view refresh on validation +- ShowBoarding +- Show docker installation logs & ubuntu 24.10 notification +- Do not overlap servercheckjob +- Server limit check +- Server validation +- Clear route / view +- Only skip docker installation on 24.10 if its not installed +- For --gpus device support +- Db/service start should be on high queue +- Do not stop sentinel on Coolify restart +- Run resourceCheck after new serviceCheckJob +- Mongodb in dev +- Better invitation errors +- Loading indicator for db proxies +- Do not execute gh workflow on template changes +- Only use sentry in cloud +- Update packagejson of coolify-realtime + add lock file +- Update last online with old function +- Seeder should not start sentinel +- Start sentinel on seeder +- Notifications ui +- Disable wire:navigate +- Confirmation Settings css for light mode +- Server wildcard +- Saving resend api key +- Wildcard domain save +- Disable cloudflare tunnel on "localhost" +- Define separate volumes for mattermost service template +- Github app name is too long +- ServerTimezone update +- Trigger.dev db host & sslmode=disable +- Manual update should be executed only once + better UX +- Upgrade.sh +- Missing privateKey +- Show proper error message on invalid Git source +- Convert HTTP to SSH source when using deploy key on GitHub +- Cloud + stripe related +- Terminal view loading in async +- Cool 500 error (thanks hugodos) +- Update schema in code decorator +- Openapi docs +- Add tests for git url converts +- Minio / logto url generation +- Admin view +- Min docker version 26 +- Pull latest service-templates.json on init +- Workflow files for coolify build +- Autocompletes +- Timezone settings validation +- Invalid tz should not prevent other jobs to be executed +- Testing-host should be built locally +- Poll with modal issue +- Terminal opening issue +- If service img not found, use github as a source +- Fallback to local coolify.png +- Gather private ips +- Cf tunnel menu should be visible when server is not validated +- Deployment optimizations +- Init script + optimize laravel +- Default docker engine version + fix install script +- Pull helper image on init +- SPA static site default nginx conf +- Modal-input +- Modal (+ add) on dynamic config was not opening, removed x-cloak +- AUTOUPDATE + checkbox opacity +- Improve helper text for metrics input fields +- Refine helper text for metrics input fields +- If mux conn fails, still use it without mux + save priv key with better logic +- Migration +- Always validate ssh key +- Make sure important jobs/actions are running on high prio queue +- Do not send internal notification for backups and status jobs +- Validateconnection +- View issue +- Heading +- Remove mux cleanup +- Db backup for services +- Version should come from constants + fix stripe webhook error reporting +- Undefined variable +- Remove version.php as everything is coming from constants.php +- Sentry error +- Websocket connections autoreconnect +- Sentry error +- Sentry +- Empty server API response +- Incorrect server API patch response +- Missing `uuid` parameter on server API patch +- Missing `settings` property on servers API +- Move servers API `delete_unused_*` properties +- Servers API returning `port` as a string -> integer +- Only return server uuid on server update +- Service generate includes yml files as well (haha) +- ServercheckJob should run every 5 minutes on cloud +- New resource icons +- Search should be more visible on scroll on new resource +- Logdrain settings +- Ui +- Email should be retried with backoff +- Alpine in body layout +- Application view loading +- Postiz service +- Only able to select the right keys +- Test email should not be required +- A few inputs +- Api endpoint +- Resolve undefined searchInput reference in Alpine.js component +- URL and sync new app name +- Typos and naming +- Client and webhook secret disappear after sync +- Missing `mysql_password` API property +- Incorrect MongoDB init API property +- Old git versions does not have --cone implemented properly +- Don't allow editing traefik config +- Restart proxy +- Dev mode +- Ui +- Display actual values for disk space checks in installer script +- Proxy change behaviour +- Add warning color +- Import NotificationSlack correctly +- Add middleware to new abilities, better ux for selecting permissions, etc. +- Root + read:sensive could read senstive data with a middlewarew +- Always have download logs button on scheduled tasks +- Missing css +- Development image +- Dockerignore +- DB migration error +- Drop all unused smtp columns +- Backward compatibility +- Email notification channel enabled function +- Instance email settins +- Make sure resend is false if SMTP is true and vice versa +- Email Notification saving +- Slack and discord url now uses text filed because encryption makes the url very long +- Notification trait +- Encryption fixes +- Docker cleanup email template +- Add missing deployment notifications to telegram +- New docker cleanup settings are now saved to the DB correctly +- Ui + migrations +- Docker cleanup email notifications +- General notifications does not go through email channel +- Test notifications to only send it to the right channel +- Remove resale_license from db as well +- Nexus service +- Fileflows volume names +- --cone +- Provider error +- Database migration +- Seeder +- Migration call +- Slack helper +- Telegram helper +- Discord helper +- Telegram topic IDs +- Make pushover settings more clear +- Typo in pushover user key +- Use Livewire refresh method and lock properties +- Create pushover settings for existing teams +- Update token permission check from 'write' to 'root' +- Pushover +- Oauth seeder +- Correct heading display for OAuth settings in settings-oauth.blade.php +- Adjust spacing in login form for improved layout +- Services env values should be sensitive +- Documenso +- Dolibarr +- Typo +- Update OauthSettingSeeder to handle new provider definitions and ensure authentik is recreated if missing +- Improve OauthSettingSeeder to correctly delete non-existent providers and ensure proper handling of provider definitions +- Encrypt resend API key in instance settings +- Resend api key is already a text column +- Monaco editor light and dark mode switching +- Service status indicator + oauth saving +- Socialite for azure and authentik +- Saving oauth +- Fallback for copy button +- Copy the right text +- Maybe fallback is now working +- Only show copy button on secure context +- Render html on error page correctly +- Invalid API response on missing project +- Applications API response code + schema +- Applications API writing to unavailable models +- If an init script is renamed the old version is still on the server +- Oauthseeder +- Compose loading seq +- Resource clone name + volume name generation +- Update Dockerfile entrypoint path to /etc/entrypoint.d +- Debug mode +- Unreachable notifications +- Remove duplicated ServerCheckJob call +- Few fixes and use new ServerReachabilityChanged event +- Use serverStatus not just status +- Oauth seeder +- Service ui structure +- Check port 8080 and fallback to 80 +- Refactor database view +- Always use docker cleanup frequency +- Advanced server UI +- Html css +- Fix domain being override when update application +- Use nixpacks predefined build variables, but still could update the default values from Coolify +- Use local monaco-editor instead of Cloudflare +- N8n timezone +- Smtp encryption +- Bind() to 0.0.0.0:80 failed +- Oauth seeder +- Unreachable notifications +- Instance settings migration +- Only encrypt instance email settings if there are any +- Error message +- Update healthcheck and port configurations to use port 8080 +- Compose envs +- Scheduled tasks and backups are executed by server timezone. +- Show backup timezone on the UI +- Disappearing UI after livewire event received +- Add default vector db for anythingllm +- We need XSRF-TOKEN for terminal +- Prevent default link behavior for resource and settings actions in dashboard +- Increase default php memory limit +- Show if only build servers are added to your team +- Update Livewire button click method to use camelCase +- Local dropzonejs +- Import backups due to js stuff should not be navigated +- Install inetutils on Arch Linux +- Use ip in place of hostname from inetutils in arch +- Update import command to append file redirection for database restoration +- Ui bug on pw confirmation +- Exclude system and computed fields from model replication +- Service cloning on a separate server +- Application cloning +- `Undefined variable $fs_path` for databases +- Service and database cloning and label generation +- Labels and URL generation when cloning +- Clone naming for different database data volumes +- Implement all the cloneMe changes for ResourceOperations as well +- Volume and fileStorages cloning +- View text and helpers +- Teable +- Trigger with external db +- Set `EXPERIMENTAL_FEATURES` to false for labelstudio +- Monaco editor disabled state +- Edge case where executions could be null +- Create destination properly +- Getcontainer status should timeout after 30s +- Enable response for temporary unavailability in sentinel push endpoint +- Use timeout in cleanup resources +- Add timeout to sentinel process checks for improved reliability +- Horizon job checker +- Update response message for sentinel push route +- Add own servers on cloud +- Application deployment +- Service update statsu +- If $SERVICE found in the service specific configuration, then search for it in the db +- Instance wide GitHub apps are not available on other teams then the source team +- Function calls +- UI +- Deletion of single backup +- Backup job deletion - delete all backups from s3 and local +- Use new removeOldBackups function +- Retention functions and folder deletion for local backups +- Storage retention setting +- Db without s3 should still backup +- Wording +- `Undefined variable $service` when creating a new service +- Nodebb service +- Calibre-web service +- Rallly and actualbudget service +- Removed container_name +- Added healthcheck for gotenberg template +- Gotenberg +- *(template)* Gotenberg healthcheck, use /health instead of /version +- Use wire:navigate on sidebar +- Use wire:navigate on dashboard +- Use wire:navigate on projects page +- More wire:navigate +- Even more wire:navigate +- Service navigation +- Logs icons everywhere + terminal +- Redis DB should use the new resourceable columns +- Joomla service +- Add back letters to prod password requirement +- Check System and GitHub time and throw and error if it is over 50s out of sync +- Error message and server time getting +- Error rendering +- Render html correctly now +- Indent +- Potential fix for permissions update +- Expiration time claim ('exp') must be a numeric value +- Sanitize html error messages +- Production password rule and cleanup code +- Use json as it is just better than string for huge amount of logs +- Use `wire:navigate` on server sidebar +- Use finished_at for the end time instead of created_at +- Cancelled deployments should not show end and duration time +- Redirect to server index instead of show on error in Advanced and DockerCleanup components +- Disable registration after creating the root user +- RootUserSeeder +- Regex username validation +- Add spacing around echo outputs +- Success message +- Silent return if envs are empty or not set. +- Create the private key before the server in the prod seeder +- Update ProductionSeeder to check for private key instead of server's private key +- *(ui)* Missing underline for docs link in the Swarm section (#4860) +- *(service)* Change chatwoot service postgres image from `postgres:12` to `pgvector/pgvector:pg12` +- Docker image parser +- Add public key attribute to privatekey model +- Correct service update logic in Docker Compose parser +- Update CDN URL in install script to point to nightly version +- *(service)* Add healthcheck to Cloudflared service (#4859) +- Remove wire:navigate from import backups +- *(ui)* Backups link should not redirected to general +- Envs with special chars during build +- *(db)* `finished_at` timestamps are not set for existing deployments +- Load service templates on cloud +- *(email)* Transactional email sending +- *(ui)* Add missing save button for new Docker Cleanup page +- *(ui)* Show preview deployment environment variables +- *(ui)* Show error on terminal if container has no shell (bash/sh) +- *(parser)* Resource URL should only be parsed if there is one +- *(core)* Compose parsing for apps +- *(redis)* Update environment variable keys from standalone_redis_id to resourceable_id +- *(routes)* Local API docs not available on domain or IP +- *(routes)* Local API docs not available on domain or IP +- *(core)* Update application_id references to resourable_id and resourable_type for Nixpacks configuration +- *(core)* Correct spelling of 'resourable' to 'resourceable' in Nixpacks configuration for ApplicationDeploymentJob +- *(ui)* Traefik dashboard url not working +- *(ui)* Proxy status badge flashing during navigation +- *(core)* Update environment variable generation logic in ApplicationDeploymentJob to handle different build packs +- *(env)* Shared variables can not be updated +- *(ui)* Metrics stuck in loading state +- *(ui)* Use `wire:navigate` to navigate to the server settings page +- *(service)* Plunk API & health check endpoint (#4925) +- *(service)* Infinite loading and lag with invoiceninja service (#4876) +- *(service)* Invoiceninja service +- *(workflows)* `Waiting for changes` label should also be considered and improved messages +- *(workflows)* Remove tags only if the PR has been merged into the main branch +- *(terminal)* Terminal shows that it is not available, even though it is +- *(labels)* Docker labels do not generated correctly +- *(helper)* Downgrade Nixpacks to v1.29.0 +- *(labels)* Generate labels when they are empty not when they are already generated +- *(storage)* Hetzner storage buckets not working +- *(ui)* Update database control UI to check server functionality before displaying actions +- *(ui)* Typo in upgrade message +- *(ui)* Cloudflare tunnel configuration should be an info, not a warning +- *(s3)* DigitalOcean storage buckets do not work +- *(ui)* Correct typo in container label helper text +- Disable certain parts if readonly label is turned off +- Cleanup old scheduled_task_executions +- Validate cron expression in Scheduled Task update +- *(core)* Check cron expression on save +- *(database)* Detect more postgres database image types +- *(templates)* Update service templates +- Remove quotes in COOLIFY_CONTAINER_NAME +- *(templates)* Update Trigger.dev service templates with v3 configuration +- *(database)* Adjust MongoDB restore command and import view styling +- *(core)* Improve public repository URL parsing for branch and base directory +- *(core)* Increase HTTP/2 max concurrent streams to 250 (default) +- *(ui)* Update docker compose file helper text to clarify repository modification +- *(ui)* Skip SERVICE_FQDN and SERVICE_URL variables during update +- *(core)* Stopping database is not disabling db proxy +- *(core)* Remove --remove-orphans flag from proxy startup command to prevent other proxy deletions (db) +- *(api)* Domain check when updating domain +- *(ui)* Always redirect to dashboard after team switch +- *(backup)* Escape special characters in database backup commands +- *(core)* Improve deployment failure Slack notification formatting +- *(core)* Update Slack notification formatting to use bold correctly +- *(core)* Enhance Slack deployment success notification formatting +- *(ui)* Simplify service templates loading logic +- *(ui)* Align title and add button vertically in various views +- Handle pullrequest:updated for reliable preview deployments +- *(ui)* Fix typo on team page (#5105) +- Cal.com documentation link give 404 (#5070) +- *(slack)* Notification settings URL in `HighDiskUsage` message (#5071) +- *(ui)* Correct typo in Storage delete dialog (#5061) +- *(lang)* Add missing italian translations (#5057) +- *(service)* Improve duplicati.yaml (#4971) +- *(service)* Links in homepage service (#5002) +- *(service)* Added SMTP credentials to getoutline yaml template file (#5011) +- *(service)* Added `KEY` Variable to Beszel Template (#5021) +- *(cloudflare-tunnels)* Dead links to docs (#5104) +- System-wide GitHub apps (#5114) +- Pull latest image from registry when using build server +- *(deployment)* Improve server selection for deployment cancellation +- *(deployment)* Improve log line rendering and formatting +- *(s3-storage)* Optimize team admin notification query +- *(core)* Improve connection testing with dynamic disk configuration for s3 backups +- *(core)* Update service status refresh event handling +- *(ui)* Adjust polling intervals for database and service status checks +- *(service)* Update Fider service template healthcheck command +- *(core)* Improve server selection error handling in Docker component +- *(core)* Add server functionality check before dispatching container status +- *(ui)* Disable sticky scroll in Monaco editor +- *(ui)* Add literal and multiline env support to services. +- *(services)* Owncloud docs link +- *(template)* Remove db-migration step from `infisical.yaml` (#5209) +- *(service)* Penpot (#5047) +- *(core)* Production dockerfile +- *(ui)* Update storage configuration guidance link +- *(ui)* Set default SMTP encryption to starttls +- *(notifications)* Correct environment URL path in application notifications +- *(config)* Update default PostgreSQL host to coolify-db instead of postgres +- *(docker)* Improve Docker compose file validation process +- *(ui)* Restrict service retrieval to current team +- *(core)* Only validate custom compose files +- *(mail)* Set default mailer to array when not specified +- *(ui)* Correct redirect routes after task deletion +- *(core)* Adding a new server should not try to make the default docker network +- *(core)* Clean up unnecessary files during application image build +- *(core)* Improve label generation and merging for applications and services +- *(billing)* Handle 'past_due' subscription status in Stripe processing +- *(revert)* Label parsing +- *(helpers)* Initialize command variable in parseCommandFromMagicEnvVariable +- *(billing)* Restrict Stripe subscription status update to 'active' only +- *(api)* Docker compose based apps creationg through api +- *(database)* Improve database type detection for Supabase Postgres images +- *(ssl)* Permission of ssl crt and key inside the container +- *(ui)* Make sure file mounts do not showing the encrypted values +- *(ssl)* Make default ssl mode require not verify-full as it does not need a ca cert +- *(ui)* Select component should not always uses title case +- *(db)* SSL certificates table and model +- *(migration)* Ssl certificates table +- *(databases)* Fix database name users new `uuid` instead of DB one +- *(database)* Fix volume and file mounts and naming +- *(migration)* Store subjectAlternativeNames as a json array in the db +- *(ssl)* Make sure the subjectAlternativeNames are unique and stored correctly +- *(ui)* Certificate expiration data is null before starting the DB +- *(deletion)* Fix DB deletion +- *(ssl)* Improve SSL cert file mounts +- *(ssl)* Always create ca crt on disk even if it is already there +- *(ssl)* Use mountPath parameter not a hardcoded path +- *(ssl)* Use 1 instead of on for mysql +- *(ssl)* Do not remove SSL directory +- *(ssl)* Wrong ssl cert is loaded to the server and UI error when regenerating SSL +- *(ssl)* Make sure when regenerating the CA cert it is not overwritten with a server cert +- *(ssl)* Regenerating certs for a specific DB +- *(ssl)* Fix MariaDB and MySQL need CA cert +- *(ssl)* Add mount path to DB to fix regeneration of certs +- *(ssl)* Fix SSL regeneration to sign with CA cert and use mount path +- *(ssl)* Get caCert correctly +- *(ssl)* Remove caCert even if it is a folder by accident +- *(ssl)* Ger caCert and `mountPath` correctly +- *(ui)* Only show Regenerate SSL Certificates button when there is a cert +- *(ssl)* Server id +- *(ssl)* When regenerating SSL certs the cert is not singed with the new CN +- *(ssl)* Adjust ca paths for MySQL +- *(ssl)* Remove mode selection for MariaDB as it is not supported +- *(ssl)* Permission issue with MariDB cert and key and paths +- *(ssl)* Rename Redis mode to verify-ca as it is not verify-full +- *(ui)* Remove unused mode for MongoDB +- *(ssl)* KeyDB port and caCert args are missing +- *(ui)* Enable SSL is not working correctly for KeyDB +- *(ssl)* Add `--tls` arg to DrangflyDB +- *(notification)* Always send SSL notifications +- *(database)* Change default value of enable_ssl to false for multiple tables +- *(ui)* Correct grammatical error in 404 page +- *(seeder)* Update GitHub app name in GithubAppSeeder +- *(plane)* Update APP_RELEASE to v0.25.2 in environment configuration +- *(domain)* Dispatch refreshStatus event after successful domain update +- *(database)* Correct container name generation for service databases +- *(database)* Limit container name length for database proxy +- *(database)* Handle unsupported database types in StartDatabaseProxy +- *(database)* Simplify container name generation in StartDatabaseProxy +- *(install)* Handle potential errors in Docker address pool configuration +- *(backups)* Retention settings +- *(redis)* Set default redis_username for new instances +- *(core)* Improve instantSave logic and error handling +- *(general)* Correct link to framework specific documentation +- *(core)* Redirect healthcheck route for dockercompose applications +- *(api)* Use name from request payload +- *(issue#4746)* Do not use setGitImportSettings inside of generateGitLsRemoteCommands +- Correct some spellings +- *(service)* Replace deprecated credentials env variables on keycloak service +- *(keycloak)* Update keycloak image version to 26.1 +- *(console)* Handle missing root user in password reset command +- *(ssl)* Handle missing CA certificate in SSL regeneration job +- *(copy-button)* Ensure text is safely passed to clipboard +- *(file-storage)* Double save on compose volumes +- *(parser)* Add logging support for applications in services +- Only get apps for the current team +- *(DeployController)* Cast 'pr' query parameter to integer +- *(deploy)* Validate team ID before deployment +- *(wakapi)* Typo in env variables and add some useful variables to wakapi.yaml (#5424) +- *(ui)* Instance Backup settings +- *(docs)* Comment out execute for now +- *(installation)* Mount the docker config +- *(installation)* Path to config file for docker login +- *(service)* Add health check to Bugsink service (#5512) +- *(email)* Emails are not sent in multiple cases +- *(deployments)* Use graceful shutdown instead of `rm` +- *(docs)* Contribute service url (#5517) +- *(proxy)* Proxy restart does not work on domain +- *(ui)* Only show copy button on https +- *(database)* Custom config for MongoDB (#5471) +- *(api)* Used ssh keys can be deleted +- *(email)* Transactional emails not sending +- *(CheckProxy)* Update port conflict check to ensure accurate grep matching +- *(CheckProxy)* Refine port conflict detection with improved grep patterns +- *(CheckProxy)* Enhance port conflict detection by adjusting ss command for better output +- *(api)* Add back validateDataApplications (#5539) +- *(CheckProxy, Status)* Prevent proxy checks when force_stop is active; remove debug statement in General +- *(Status)* Conditionally check proxy status and refresh button based on force_stop state +- *(General)* Change redis_password property to nullable string +- *(DeployController)* Update request handling to use input method and enhance OpenAPI description for deployment endpoint +- *(pre-commit)* Correct input redirection for /dev/tty and add OpenAPI generation command +- *(pricing-plans)* Adjust grid class for improved layout consistency in subscription pricing plans +- *(migrations)* Make stripe_comment field nullable in subscriptions table +- *(mongodb)* Also apply custom config when SSL is enabled +- *(templates)* Correct casing of denoKV references in service templates and YAML files +- *(deployment)* Handle missing destination in deployment process to prevent errors +- *(parser)* Transform associative array labels into key=value format for better compatibility +- *(redis)* Update username and password input handling to clarify database sync requirements +- *(source)* Update connected source display to handle cases with no source connected +- *(application)* Append base directory to git branch URLs for improved path handling +- *(templates)* Correct casing of "denokv" to "denoKV" in service templates JSON +- *(navbar)* Update error message link to use route for environment variables navigation +- Unsend template +- Replace ports with expose +- *(templates)* Update Unsend compose configuration for improved service integration +- *(backup-edit)* Conditionally enable S3 checkbox based on available validated S3 storage +- *(source)* Update no sources found message for clarity +- *(api)* Correct middleware for service update route to ensure proper permissions +- *(api)* Handle JSON response in service creation and update methods for improved error handling +- Add 201 json code to servers validate api response +- *(docker)* Ensure password hashing only occurs when HTTP Basic Authentication is enabled +- *(docker)* Enhance hostname and GPU option validation in Docker run to compose conversion +- *(terminal)* Enhance WebSocket client verification with authorized IPs in terminal server +- *(ApplicationDeploymentJob)* Ensure source is an object before checking GitHub app properties +- *(ui)* Disable livewire navigate feature (causing spam of setInterval()) +- *(ui)* Remove required attribute from image input in service application view +- *(ui)* Change application image validation to be nullable in service application view +- *(Server)* Correct proxy path formatting for Traefik proxy type +- *(service)* Graceful shutdown of old container (#5731) +- *(ServerCheck)* Enhance proxy container check to ensure it is running before proceeding +- *(applications)* Include pull_request_id in deployment queue check to prevent duplicate deployments +- *(database)* Update label for image input field to improve clarity +- *(ServerCheck)* Set default proxy status to 'exited' to handle missing container state +- *(database)* Reduce container stop timeout from 300 to 30 seconds for improved responsiveness +- *(ui)* System theming for charts (#5740) +- *(dev)* Mount points?! +- *(dev)* Proxy mount point +- *(ui)* Allow adding scheduled backups for non-migrated databases +- *(DatabaseBackupJob)* Escape PostgreSQL password in backup command (#5759) +- *(ui)* Correct closing div tag in service index view +- *(select)* Update fallback logo path to use absolute URL for improved reliability +- *(constants)* Adding 'fedora-asahi-remix' as a supported OS (#5646) +- *(authentik)* Update docker-compose configuration for authentik service +- *(api)* Allow nullable destination_uuid (#5683) +- *(service)* Fix documenso startup and mail (#5737) +- *(docker)* Fix production dockerfile +- *(service)* Navidrome service +- *(service)* Passbolt +- *(service)* Add missing ENVs to NTFY service (#5629) +- *(service)* NTFY is behind a proxy +- *(service)* Vert logo and ENVs +- *(service)* Add platform to Observium service +- *(ActivityMonitor)* Prevent multiple event dispatches during polling +- *(service)* Convex ENVs and update image versions (#5827) +- *(service)* Paymenter +- *(ApplicationDeploymentJob)* Ensure correct COOLIFY_FQDN/COOLIFY_URL values (#4719) +- *(service)* Snapdrop no matching manifest error (#5849) +- *(service)* Use the same volume between chatwoot and sidekiq (#5851) +- *(api)* Validate docker_compose_raw input in ApplicationsController +- *(api)* Enhance validation for docker_compose_raw in ApplicationsController +- *(select)* Update PostgreSQL versions and titles in resource selection +- *(database)* Include DatabaseStatusChanged event in activityMonitor dispatch +- *(css)* Tailwind v5 things +- *(service)* Diun ENV for consistency +- *(service)* Memos service name +- *(css)* 8+ issue with new tailwind v4 +- *(css)* `bg-coollabs-gradient` not working anymore +- *(ui)* Add back missing service navbar components +- *(deploy)* Update resource timestamp handling in deploy_resource method +- *(patches)* DNF reboot logic is flipped +- *(deployment)* Correct syntax for else statement in docker compose build command +- *(shared)* Remove unused relation from queryDatabaseByUuidWithinTeam function +- *(deployment)* Correct COOLIFY_URL and COOLIFY_FQDN assignments based on parsing version in preview deployments +- *(docker)* Ensure correct parsing of environment variables by limiting explode to 2 parts +- *(project)* Update selected environment handling to use environment name instead of UUID +- *(ui)* Update server status display and improve server addition layout +- *(service)* Neon WS Proxy service not working on ARM64 (#5887) +- *(server)* Enhance error handling in server patch check notifications +- *(PushServerUpdateJob)* Add null checks before updating application and database statuses +- *(environment-variables)* Update label text for build variable checkboxes to improve clarity +- *(service-management)* Update service stop and restart messages for improved clarity and formatting +- *(preview-form)* Update helper text formatting in preview URL template input for better readability +- *(application-management)* Improve stop messages for application, database, and service to enhance clarity and formatting +- *(application-configuration)* Prevent access to preview deployments for deploy_key applications and update menu visibility accordingly +- *(select-component)* Handle exceptions during parameter retrieval and environment selection in the mount method +- *(previews)* Escape container names in stopContainers method to prevent shell injection vulnerabilities +- *(docker)* Add protection against empty container queries in GetContainersStatus to prevent unnecessary updates +- *(modal-confirmation)* Decode HTML entities in confirmation text to ensure proper display +- *(select-component)* Enhance user interaction by adding cursor styles and disabling selection during processing +- *(deployment-show)* Remove unnecessary fixed positioning for button container to improve layout responsiveness +- *(email-notifications)* Change notify method to notifyNow for immediate test email delivery +- *(service-templates)* Update Convex service configuration to use FQDN variables +- *(database-heading)* Simplify stop database message for clarity +- *(navbar)* Remove unnecessary x-init directive for loading proxy configuration +- *(patches)* Add padding to loading message for better visibility during update checks +- *(terminal-connection)* Improve error handling and stability for auto-connection; enhance component readiness checks and retry logic +- *(terminal)* Add unique wire:key to terminal component for improved reactivity and state management +- *(css)* Adjust utility classes in utilities.css for consistent application of Tailwind directives +- *(css)* Refine utility classes in utilities.css for proper Tailwind directive application +- *(install)* Update Docker installation script to use dynamic OS_TYPE and correct installation URL +- *(cloudflare)* Add error handling to automated Cloudflare configuration script +- *(navbar)* Add error handling for proxy status check to improve user feedback +- *(web)* Update user team retrieval method for consistent authentication handling +- *(cloudflare)* Update refresh method to correctly set Cloudflare tunnel status and improve user notification on IP address update +- *(service)* Update service template for affine and add migration service for improved deployment process +- *(supabase)* Update Supabase service images and healthcheck methods for improved reliability +- *(terminal)* Now it should work +- *(degraded-status)* Remove unnecessary whitespace in badge element for cleaner HTML +- *(routes)* Add name to security route for improved route management +- *(migration)* Update default value handling for is_sentinel_enabled column in server_settings +- *(seeder)* Conditionally dispatch CheckAndStartSentinelJob based on server's sentinel status +- *(service)* Disable healthcheck logging for Gotenberg (#6005) +- *(service)* Joplin volume name (#5930) +- *(server)* Update sentinelUpdatedAt assignment to use server's sentinel_updated_at property +- *(service)* Audiobookshelf healthcheck command (#5993) +- *(service)* Downgrade Evolution API phone version (#5977) +- *(service)* Pingvinshare-with-clamav +- *(ssh)* Scp requires square brackets for ipv6 (#6001) +- *(github)* Changing github app breaks the webhook. it does not anymore +- *(parser)* Improve FQDN generation and update environment variable handling +- *(ui)* Enhance status refresh buttons with loading indicators +- *(ui)* Update confirmation button text for stopping database and service +- *(routes)* Update middleware for deploy route to use 'api.ability:deploy' +- *(ui)* Refine API token creation form and update helper text for clarity +- *(ui)* Adjust layout of deployments section for improved alignment +- *(ui)* Adjust project grid layout and refine server border styling for better visibility +- *(ui)* Update border styling for consistency across components and enhance loading indicators +- *(ui)* Add padding to section headers in settings views for improved spacing +- *(ui)* Reduce gap between input fields in email settings for better alignment +- *(docker)* Conditionally enable gzip compression in Traefik labels based on configuration +- *(parser)* Enable gzip compression conditionally for Pocketbase images and streamline service creation logic +- *(ui)* Update padding for trademarks policy and enhance spacing in advanced settings section +- *(ui)* Correct closing tag for sponsorship link in layout popups +- *(ui)* Refine wording in sponsorship donation prompt in layout popups +- *(ui)* Update navbar icon color and enhance popup layout for sponsorship support +- *(ui)* Add target="_blank" to sponsorship links in layout popups for improved user experience +- *(models)* Refine comment wording in User model for clarity on user deletion criteria +- *(models)* Improve user deletion logic in User model to handle team member roles and prevent deletion if user is alone in root team +- *(ui)* Update wording in sponsorship prompt for clarity and engagement +- *(shared)* Refactor gzip handling for Pocketbase in newParser function for improved clarity +- *(server)* Prepend 'mux_' to UUID in muxFilename method for consistent naming +- *(ui)* Enhance terminal access messaging to clarify server functionality and terminal status +- *(database)* Proxy ssl port if ssl is enabled +- *(terminal)* Ensure shell execution only uses valid shell if available in terminal command +- *(ui)* Improve destination selection description for clarity in resource segregation +- *(jobs)* Update middleware to use expireAfter for WithoutOverlapping in multiple job classes +- Removing eager loading (#6071) +- *(template)* Adjust health check interval and retries for excalidraw service +- *(ui)* Env variable settings wrong order +- *(service)* Ensure configuration changes are properly tracked and dispatched +- *(service)* Update Postiz compose configuration for improved server availability +- *(install.sh)* Use IPV4_PUBLIC_IP variable in output instead of repeated curl +- *(env)* Generate literal env variables better +- *(deployment)* Update x-data initialization in deployment view for improved functionality +- *(deployment)* Enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility +- *(deployment)* Improve docker-compose domain handling and environment variable generation +- *(deployment)* Refactor domain parsing and environment variable generation using Spatie URL library +- *(deployment)* Update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy +- *(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 +- *(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 +- *(horizon)* Add silenced jobs +- *(application)* Sanitize service names for HTML form binding and ensure original names are stored in docker compose domains +- *(previews)* Adjust padding for rate limit message in application previews +- *(previews)* Order application previews by pull request ID in descending order +- *(previews)* Add unique wire keys for preview containers and services based on pull request ID +- *(previews)* Enhance domain generation logic for application previews, ensuring unique domains are created when none are set +- *(previews)* Refine preview domain generation for Docker Compose applications, ensuring correct method usage based on build pack type +- *(ui)* Typo on proxy request handler tooltip (#6192) +- *(backups)* Large database backups are not working (#6217) +- *(backups)* Error message if there is no exception +- *(installer)* Public IPv4 link does not work +- *(composer)* Version constraint of prompts +- *(service)* Budibase secret keys (#6205) +- *(service)* Wg-easy host should be just the FQDN +- *(ui)* Search box overlaps the sidebar navigation (#6176) +- *(webhooks)* Exclude webhook routes from CSRF protection (#6200) +- *(services)* Update environment variable naming convention to use underscores instead of dashes for SERVICE_FQDN and SERVICE_URL +- *(service)* Triliumnext platform and link +- *(application)* Update service environment variables when generating domain for Docker Compose +- *(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) +- *(docker)* Volumes get delete when stopping a service if `Delete Unused Volumes` is activated (#6317) +- *(docker)* Cleanup always running on deletion +- *(proxy)* Remove hardcoded port 80/443 checks (#6275) +- *(service)* Update healthcheck of penpot backend container (#6272) +- *(api)* Duplicated logs in application endpoint (#6292) +- *(service)* Documenso signees always pending (#6334) +- *(api)* Update service upsert to retain name and description values if not set +- *(database)* Custom postgres configs with SSL (#6352) +- *(policy)* Update delete method to check for admin status in S3StoragePolicy +- *(container)* Sort containers alphabetically by name in ExecuteContainerCommand and update filtering in Terminal Index +- *(application)* Streamline environment variable updates for Docker Compose services and enhance FQDN generation logic +- *(constants)* Update 'Change Log' to 'Changelog' in settings dropdown +- *(constants)* Update coolify version to 4.0.0-beta.420.7 +- *(parsers)* Clarify comments and update variable checks for FQDN and URL handling +- *(terminal)* Update text color for terminal availability message and improve readability +- *(drizzle-gateway)* Remove healthcheck from drizzle-gateway compose file and update service template +- *(templates)* Should generate old SERVICE_FQDN service templates as well +- *(constants)* Update official service template URL to point to the v4.x branch for accuracy +- *(git)* Use exact refspec in ls-remote to avoid matching similarly named branches (e.g., changeset-release/main). Use refs/heads/ or provider-specific PR refs. +- *(ApplicationPreview)* Change null check to empty check for fqdn in generate_preview_fqdn method +- *(email notifications)* Enhance EmailChannel to validate team membership for recipients and handle errors gracefully +- *(service api)* Separate create and update service functionalities +- *(templates)* Added a category tag for the docs service filter +- *(application)* Clear Docker Compose specific data when switching away from dockercompose +- *(database)* Conditionally set started_at only if the database is running +- *(ui)* Handle null values in postgres metrics (#6388) +- Disable env sorting by default +- *(proxy)* Filter host network from default proxy (#6383) +- *(modal)* Enhance confirmation text handling +- *(notification)* Update unread count display and improve HTML rendering +- *(select)* Remove unnecessary sanitization for logo rendering +- *(tags)* Update tag display to limit name length and adjust styling +- *(init)* Improve error handling for deployment and template pulling processes +- *(settings-dropdown)* Adjust unread count badge size and display logic for better consistency +- *(sanitization)* Enhance DOMPurify hook to remove Alpine.js directives for improved XSS protection +- *(servercheck)* Properly check server statuses with and without Sentinel +- *(errors)* Update error pages to provide navigation options +- *(github-deploy-key)* Update background color for selected private keys in deployment key selection UI +- *(auth)* Enhance authorization checks in application management +- *(backups)* S3 backup upload is failing +- *(backups)* Rollback helper update for now +- *(parsers)* Replace hyphens with underscores in service names for consistency. this allows to properly parse custom domains in docker compose based applications +- *(parsers)* Implement parseDockerVolumeString function to handle various Docker volume formats and modes, including environment variables and Windows paths. Add unit tests for comprehensive coverage. +- *(git)* Submodule update command uses an unsupported option (#6454) +- *(service)* Swap URL for FQDN on matrix template (#6466) +- *(parsers)* Enhance volume string handling by preserving mode in application and service parsers. Update related unit tests for validation. +- *(docker)* Update parser version in FQDN generation for service-specific URLs +- *(parsers)* Do not modify service names, only for getting fqdns and related envs +- *(compose)* Temporary allow to edit volumes in apps (compose based) and services +- *(previews)* Simplify FQDN generation logic by removing unnecessary empty check +- *(templates)* Update Matrix service compose configuration for improved compatibility and clarity ### 💼 Other -- Pocketbase release - -## [3.11.10] - 2022-11-16 - -### 🚀 Features - -- Only show expose if no proxy conf defined in template -- Custom/private docker registries - -### 🐛 Bug Fixes - -- Local dev api/ws urls -- Wrong template/type -- Gitea icon is svg -- Gh actions -- Gh actions -- Replace $$generate vars -- Webhook traefik -- Exposed ports -- Wrong icons on dashboard -- Escape % in secrets -- Move debug log settings to build logs -- Storage for compose bp + debug on -- Hasura admin secret -- Logs -- Mounts -- Load logs after build failed -- Accept logged and not logged user in /base -- Remote haproxy password/etc -- Remove hardcoded sentry dsn -- Nope in database strings - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Version++ -- Version++ - -## [3.11.9] - 2022-11-15 - -### 🐛 Bug Fixes - -- IsBot issue - -## [3.11.8] - 2022-11-14 - -### 🐛 Bug Fixes - -- Default icon for new services - -## [3.11.1] - 2022-11-08 - -### 🚀 Features - -- Rollback coolify - -### 🐛 Bug Fixes - -- Remove contribution docs -- Umami template -- Compose webhooks fixed -- Variable replacements -- Doc links -- For rollback -- N8n and weblate icon -- Expose ports for services -- Wp + mysql on arm -- Show rollback button loading -- No tags error -- Update on mobile -- Dashboard error -- GetTemplates -- Docker compose persistent volumes -- Application persistent storage things -- Volume names for undefined volume names in compose -- Empty secrets on UI -- Ports for services - -### 💼 Other - -- Secrets on apps +- Only allow cleanup in production +- Make copy/password visible +- Dns check +- Remote docker engine +- Colorful states +- Application start +- Colors on svelte-select +- Improvements +- Fix +- Better layout for root team - Fix - Fixes -- Reload compose loading - -### 🚜 Refactor - -- Code - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Add jda icon for lavalink service -- Version++ - -### ◀️ Revert - -- Revert: revert - -## [3.11.0] - 2022-11-07 - -### 🚀 Features - -- Initial support for specific git commit -- Add default to latest commit and support for gitlab -- Redirect catch-all rule - -### 🐛 Bug Fixes - -- Secret errors -- Service logs -- Heroku bp -- Expose port is readonly on the wrong condition -- Toast -- Traefik proxy q 10s -- App logs view -- Tooltip -- Toast, rde, webhooks -- Pathprefix -- Load public repos -- Webhook simplified -- Remote webhooks -- Previews wbh -- Webhooks -- Websecure redirect -- Wb for previews -- Pr stopps main deployment -- Preview wbh -- Wh catchall for all -- Remove old minio proxies -- Template files -- Compose icon -- Templates -- Confirm restart service -- Template -- Templates -- Templates -- Plausible analytics things -- Appwrite webhook -- Coolify instance proxy -- Migrate template -- Preview webhooks -- Simplify webhooks -- Remove ghost-mariadb from the list -- More simplified webhooks -- Umami + ghost issues - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.10.16] - 2022-10-12 - -### 🐛 Bug Fixes - -- Single container logs and usage with compose - -### 💼 Other - -- New resource label - -## [3.10.15] - 2022-10-12 - -### 🚀 Features - -- Monitoring by container - -### 🐛 Bug Fixes - -- Do not show nope as ip address for dbs -- Add git sha to build args -- Smart search for new services -- Logs for not running containers -- Update docker binaries -- Gh release -- Dev container -- Gitlab auth and compose reload -- Check compose domains in general -- Port required if fqdn is set -- Appwrite v1 missing containers -- Dockerfile -- Pull does not work remotely on huge compose file - -### ⚙️ Miscellaneous Tasks - -- Update staging release - -## [3.10.14] - 2022-10-05 - -### 🚀 Features - -- Docker compose support -- Docker compose -- Docker compose - -### 🐛 Bug Fixes - -- Do not use npx -- Pure docker based development - -### 💼 Other - -- Docker-compose support -- Docker compose -- Remove worker jobs -- One less worker thread - -### 🧪 Testing - -- Remove prisma - -## [3.10.5] - 2022-09-26 - -### 🚀 Features - -- Add migration button to appwrite -- Custom certificate -- Ssl cert on traefik config -- Refresh resource status on dashboard -- Ssl certificate sets custom ssl for applications -- System-wide github apps -- Cleanup unconfigured applications -- Cleanup unconfigured services and databases - -### 🐛 Bug Fixes - -- Ui -- Tooltip -- Dropdown -- Ssl certificate distribution -- Db migration -- Multiplex ssh connections -- Able to search with id -- Not found redirect -- Settings db requests -- Error during saving logs -- Consider base directory in heroku bp -- Basedirectory should be empty if null -- Allow basedirectory for heroku -- Stream logs for heroku bp -- Debug log for bp -- Scp without host verification & cert copy -- Base directory & docker bp -- Laravel php chooser -- Multiplex ssh and ssl copy -- Seed new preview secret types -- Error notification -- Empty preview value -- Error notification -- Seed -- Service logs -- Appwrite function network is not the default -- Logs in docker bp -- Able to delete apps in unconfigured state -- Disable development low disk space -- Only log things to console in dev mode -- Do not get status of more than 10 resources defined by category -- BaseDirectory -- Dashboard statuses -- Default buildImage and baseBuildImage -- Initial deploy status -- Show logs better -- Do not start tcp proxy without main container -- Cleanup stucked tcp proxies -- Default 0 pending invitations -- Handle forked repositories -- Typo -- Pr branches -- Fork pr previews -- Remove unnecessary things -- Meilisearch data dir -- Verify and configure remote docker engines -- Add buildkit features -- Nope if you are not logged in - -### 💼 Other - +- Fix +- Fix +- Fix +- Fix +- Fix +- Fix +- Fix +- Insane amount +- Fix +- Fixes +- Fixes +- Fix +- Fixes +- Fixes +- Show extraconfig if wp is running +- Umami service +- Base image selector +- Laravel +- Appwrite +- Testing WS +- Traefik?! +- Traefik +- Traefik +- Traefik migration +- Traefik +- Traefik +- Traefik +- Notifications and application usage +- *(fix)* Traefik +- Css +- Error message https://github.com/coollabsio/coolify/issues/502 +- Changes +- Settings +- For removing app +- Local ssh port +- Redesign a lot +- Fixes +- Loading indicator for plausible buttons +- Fix +- Fider +- Typing +- Fixes here and there +- Dashboard fine-tunes +- Fine-tune +- Fixes +- Fix +- Dashbord fixes +- Fixes +- Fixes +- Route to the correct path when creating destination from db config +- Fixes +- Change tooltips and info boxes +- Added rc release +- Database_branches +- Login page +- Fix login/register page +- Update devcontainer +- Add debug log +- Fix initial loading icon bg +- Fix loading start/stop db/services +- Dashboard updates and a lot more +- Dashboard updates +- Fix tooltip +- Fix button +- Fix follow button +- Arm should be on next all the time +- Fix plausible +- Fix cleanup button +- Fix buttons - Responsive! - Fixes - Fix git icon @@ -6807,1851 +4033,1491 @@ ### 💼 Other - Iam & settings update - Send 200 for ping and installation wh - Settings icon - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Version++ -- Version++ -- Version++ -- Version++ -- Version++ - -### ◀️ Revert - -- Show usage everytime - -## [3.10.2] - 2022-09-11 - -### 🚀 Features - -- Add queue reset button -- Previewapplications init -- PreviewApplications finalized -- Fluentbit -- Show remote servers -- *(layout)* Added drawer when user is in mobile -- Re-apply ui improves -- *(ui)* Improve header of pages -- *(styles)* Make header css component -- *(routes)* Improve ui for apps, databases and services logs - -### 🐛 Bug Fixes - -- Changing umami image URL to get latest version -- Gitlab importer for public repos -- Show error logs -- Umami init sql -- Plausible analytics actions -- Login -- Dev url -- UpdateMany build logs -- Fallback to db logs -- Fluentbit configuration -- Coolify update -- Fluentbit and logs -- Canceling build -- Logging -- Load more -- Build logs -- Versions of appwrite -- Appwrite?! -- Get building status -- Await -- Await #2 -- Update PR building status -- Appwrite default version 1.0 -- Undead endpoint does not require JWT -- *(routes)* Improve design of application page -- *(routes)* Improve design of git sources page -- *(routes)* Ui from destinations page -- *(routes)* Ui from databases page -- *(routes)* Ui from databases page -- *(routes)* Ui from databases page -- *(routes)* Ui from services page -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- *(routes)* Ui from settings page -- *(routes)* Duplicates classes in services page -- *(routes)* Searchbar ui -- Github conflicts -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- *(routes)* More ui tweaks -- Ui with headers -- *(routes)* Header of settings page in databases -- *(routes)* Ui from secrets table - -### 💼 Other - -- Fix plausible -- Fix cleanup button -- Fix buttons - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Minor changes -- Minor changes -- Minor changes -- Whoops - -## [3.10.1] - 2022-09-10 - -### 🐛 Bug Fixes - -- Show restarting apps -- Show restarting application & logs -- Remove unnecessary gitlab group name -- Secrets for PR -- Volumes for services -- Build secrets for apps -- Delete resource use window location - -### 💼 Other - -- Fix button -- Fix follow button -- Arm should be on next all the time - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.10.0] - 2022-09-08 - -### 🚀 Features - -- New servers view - -### 🐛 Bug Fixes - -- Change to execa from utils -- Save search input -- Ispublic status on databases -- Port checkers -- Ui variables -- Glitchtip env to pyhton boolean -- Autoupdater - -### 💼 Other - -- Dashboard updates -- Fix tooltip - -## [3.9.4] - 2022-09-07 - -### 🐛 Bug Fixes - -- DnsServer formatting -- Settings for service - -## [3.9.3] - 2022-09-07 - -### 🐛 Bug Fixes - -- Pr previews - -## [3.9.2] - 2022-09-07 - -### 🚀 Features - -- Add traefik acme json to coolify container -- Database secrets - -### 🐛 Bug Fixes - -- Gitlab webhook -- Use ip address instead of window location -- Use ip instead of window location host -- Service state update -- Add initial DNS servers -- Revert last change with domain check -- Service volume generation -- Minio default env variables -- Add php 8.1/8.2 -- Edgedb ui -- Edgedb stuff -- Edgedb - -### 💼 Other - -- Fix login/register page -- Update devcontainer -- Add debug log -- Fix initial loading icon bg -- Fix loading start/stop db/services -- Dashboard updates and a lot more - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ - -## [3.9.0] - 2022-09-06 - -### 🐛 Bug Fixes - -- Debug api logging + gh actions -- Workdir -- Move restart button to settings - -## [3.9.1-rc.1] - 2022-09-06 - -### 🚀 Features - -- *(routes)* Rework ui from login and register page - -### 🐛 Bug Fixes - -- Ssh pid agent name -- Repository link trim -- Fqdn or expose port required -- Service deploymentEnabled -- Expose port is not required -- Remote verification -- Dockerfile - -### 💼 Other - -- Database_branches -- Login page - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ - -## [3.9.0-rc.1] - 2022-09-02 - -### 🚀 Features - -- New service - weblate -- Restart application -- Show elapsed time on running builds -- Github allow fual branches -- Gitlab dual branch -- Taiga - -### 🐛 Bug Fixes - -- Glitchtip things -- Loading state on start -- Ui -- Submodule -- Gitlab webhooks -- UI + refactor -- Exposedport on save -- Appwrite letsencrypt -- Traefik appwrite -- Traefik -- Finally works! :) -- Rename components + remove PR/MR deployment from public repos -- Settings missing id -- Explainer component -- Database name on logs view -- Taiga - -### 💼 Other - +- Docker-compose support +- Docker compose +- Remove worker jobs +- One less worker thread +- New resource label +- Secrets on apps +- Fix - Fixes -- Change tooltips and info boxes -- Added rc release +- Reload compose loading +- Pocketbase release +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Conditional on environment +- Add missing variables +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Trpc +- Extract process handling from async job. +- Extract process handling from async job. +- Extract process handling from async job. +- Extract process handling from async job. +- Extract process handling from async job. +- Extract process handling from async job. +- Extract process handling from async job. +- Persisting data +- Scheduled backups +- Boarding +- Backup existing database +- User should know that the public key +- Services are not availble yet +- Show registered users on waitlist page +- Nixpacksarchive +- Add Plausible analytics +- Global env variables +- Fix +- Trial emails +- Server check instead of app check +- Show trial instead of sub +- Server lost connection +- Services +- Services +- Services +- Ui for services +- Services +- Services +- Services +- Fixes +- Fix typo +- Fixed z-index for version link. +- Add source button +- Fixed z-index for magicbar +- A bit better error +- More visible feedback button +- Update help modal +- Help +- Marketing emails +- Fix previews to preview +- Uptime kume hc updated +- Switch back to /data (volume errors) +- Notifications +- Add shared email option to everyone +- Dockerimage +- Updated dashboard +- Fix +- Fix +- Coolify proxy access logs exposed in dev +- Able to select environment on new resource +- Delete server +- Redis +- Wordpress +- Add helper to service domains +- PAT by team +- Generate services +- Mongodb backup +- Mongodb backup +- Updates +- Fix subs +- New deployment jobs +- Compose based apps +- Swarm +- Swarm +- Swarm +- Swarm +- Disable trial +- Meilisearch +- Broadcast +- 🌮 +- Env vars +- Migrate to livewire 3 +- Fix for comma in labels +- Add image name to service stack + better options visibility +- Swarm +- Swarm +- Send notification email if payment +- New modal component +- Specific about newrelic logdrains +- Updates +- Change + icon to hamburger. +- Redesign +- Redesign +- Run cleanup every day +- Fix +- Fix log outputs +- Automatic cloudflare tunnels +- Backup executions +- Light buttons +- Multiple server view +- New pricing +- Fix allowTab logic +- Use 2 space instead of tab +- Non-root user for remote servers +- Non-root +- Update resource operations view +- Fix tag view +- Fix a few boxes here and there +- Responsive here and there +- Rocketchat +- New services based git apps +- Unnecessary notification +- Update process +- Glances service +- Glances +- Able to update application +- Add basedir + compose file in new compose based apps +- Formbricks template add required CRON_SECRET +- Add required CRON_SECRET to Formbricks template +- Service env parsing +- Actually update timezone on the server +- Cron jobs are executed based on the server timezone +- Server timezone seeder +- Recent backups UI +- Use apt-get instead of apt +- Typo +- Only pull helper image if the version is newer than the one +- Plunk svg +- Pull helper image if not available otherwise s3 backup upload fails +- Set a default server timezone +- Implement SSH Multiplexing +- Enabel mux +- Cleanup stale multiplexing connections +- Remote servers with port and user +- Do not change localhost server name on revalidation +- Release.md file +- SSH Multiplexing on docker desktop on Windows +- Remove labels and assignees on issue close +- Make sure this action is also triggered on PR issue close +- Volumes on development environment +- Clean new volume name for dev volumes +- Persist DBs, services and so on stored in data/coolify +- Add SSH Key fingerprint to DB +- Add a fingerprint to every private key on save, create... +- Make sure invalid private keys can not be added +- Encrypt private SSH keys in the DB +- Add is_sftp and is_server_ssh_key coloums +- New ssh key file name on disk +- Store all keys on disk by default +- Populate SSH key folder +- Populate SSH keys in dev +- Use new function names and logic everywhere +- Create a Multiplexing Helper +- SSH multiplexing +- Remove unused code form multiplexing +- SSH Key cleanup job +- Private key with ID 2 on dev +- Move more functions to the PrivateKey Model +- Add ssh key fingerprint and generate one for existing keys +- ID issues on dev seeders +- Server ID 0 +- Make sure in use private keys are not deleted +- Do not delete SSH Key from disk during server validation error +- UI bug, do not write ssh key to disk in server dialog +- SSH Multiplexing for Jobs +- SSH algorhytm text +- Few multiplexing things +- Clear mux directory +- Multiplexing do not write file manually +- Integrate tow step process in the modal component WIP +- Ability to hide labels +- DB start, stop confirm +- Del init script +- General confirm +- Preview deployments and typos +- Service confirmation +- Confirm file storage +- Stop service confirm +- DB image cleanup +- Confirm ressource operation +- Environment variabel deletion +- Confirm scheduled tasks +- Confirm API token +- Confirm private key +- Confirm server deletion +- Confirm server settings +- Proxy stop and restart confirmation +- GH app deletion confirmation +- Redeploy all confirmation +- User deletion confirmation +- Team deletion confirmation +- Backup job confirmation +- Delete volume confirmation +- More conformations and fixes +- Delete unused private keys button +- Ray error because port is not uncommented +- #3322 deploy DB alterations before updating +- Css issue with advanced settings and remove cf tunnel in onboarding +- New cf tunnel install flow +- Made help text more clear +- Cloudflare tunnel +- Make helper text more clean to use a FQDN and not an URL +- Manual cleanup button and unused volumes and network deletion +- Force helper image removal +- Use the new confirmation flow +- Typo +- Typo in install script +- If API is disabeled do not show API token creation stuff +- Disable API by default +- Add debug bar +- Remove memlock as it caused problems for some users +- Server storage check +- Show backup button on supported db service stacks +- Update helper version +- Outline +- Directus +- Supertokens +- Supertokens json +- Rabbitmq +- Easyappointments +- Soketi +- Dozzle +- Windmill +- Coolify.json +- Keycloak +- Other DB options for freshrss +- Nextcloud MariaDB and MySQL versions +- Add peppermint +- Loggy +- Add UI for redis password and username +- Wireguard-easy template +- Https://github.com/coollabsio/coolify/issues/4186 +- Separate resources by type in projects view +- Improve s3 add view +- Caddy docker labels do not honor "strip prefix" option +- Test rename GitHub app +- Checkmate service and fix prowlar slogan (too long) +- Arrrrr +- Dep +- Docker dep +- Trigger.dev templates - wrong key length issue +- Trigger.dev template - missing ports and wrong env usage +- Trigger.dev template - fixed otel config +- Trigger.dev template - fixed otel config +- Trigger.dev template - fixed port config +- Bump all dependencies (#5216) +- Bump Coolify to 4.0.0-beta.398 +- Bump Coolify to 4.0.0-beta.400 +- *(migration)* Add SSL fields to database tables +- SSL Support for KeyDB +- Add missing UUID to openapi spec +- Add missing openapi items to PrivateKey +- Adjust Workflows for v5 (#5689) +- Add support for postmarketOS (#5608) +- *(core)* Simplify events for app/db/service status changes +- *(settings-dropdown)* Add icons to buttons for improved UI in settings dropdown +- *(ui)* Introduce task for simplifying resource operations UI by replacing boxes with dropdown selections to enhance user experience and streamline interactions + +### 🚜 Refactor + +- Code +- Env variable generator +- Service logs are now on one page +- Application status changed realtime +- Custom labels +- Clone project +- Compose file and install script +- Add SCHEDULER environment variable to StartSentinel.php +- Update edit-domain form in project service view +- Add Huly services to compose file +- Remove redundant heading in backup settings page +- Add isBuildServer method to Server model +- Update docker network creation in ApplicationDeploymentJob +- Update destination.blade.php to add group class for better styling +- Applicationdeploymentjob +- Improve code structure in ApplicationDeploymentJob.php +- Remove unnecessary debug statement in ApplicationDeploymentJob.php +- Remove unnecessary debug statements and improve code structure in RunRemoteProcess.php and ApplicationDeploymentJob.php +- Remove unnecessary logging statements from UpdateCoolify +- Update storage form inputs in show.blade.php +- Improve Docker Compose parsing for services +- Remove unnecessary port appending in updateCompose function +- Remove unnecessary form class in profile index.blade.php +- Update form layout in invite-link.blade.php +- Add log entry when starting new application deployment +- Improve Docker Compose parsing for services +- Update Docker Compose parsing for services +- Update slogan in shlink.yaml +- Improve display of deployment time in index.blade.php +- Remove commented out code for clearing Ray logs +- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview +- Append utm_source parameter to documentation URL +- Update save_environment_variables method to use application's environment_variables instead of environment_variables_preview +- Update deployment previews heading to "Deployments" +- Remove unused variables and improve code readability +- Initialize null properties in Github Change component +- Improve pre and post deployment command inputs +- Improve handling of Docker volumes in parseDockerComposeFile function +- Replaces duplications in code with a single function +- Update text color for stderr output in deployment show view +- Update text color for stderr output in deployment show view +- Remove debug code for saving environment variables +- Update Docker build commands for better performance and flexibility +- Update image sizes and add new logos to README.md +- Update README.md with new logos and fix styling +- Update shared.php to use correct key for retrieving sentinel version +- Update container name assignment in Application model +- Remove commented code for docker container removal +- Update Application model to include getDomainsByUuid method +- Update Project/Show component to sort environments by created_at +- Update profile index view to display 2FA QR code in a centered container +- Update dashboard.blade.php to use project's default environment for redirection +- Update gitCommitLink method to handle null values in source.html_url +- Update docker-compose generation to use multi-line literal block +- Update Service model's saveComposeConfigs method +- Add default environment to Service model's saveComposeConfigs method +- Improve handling of default environment in Service model's saveComposeConfigs method +- Remove commented out code in Service model's saveComposeConfigs method +- Update stack-form.blade.php to include wire:target attribute for submit button +- Update code to use str() instead of Str::of() for string manipulation +- Improve formatting and readability of source.blade.php +- Add is_build_time property to nixpacks_php_fallback_path and nixpacks_php_root_dir +- Simplify code for retrieving subscription in Stripe webhook +- Add force parameter to StartProxy handle method +- Comment out unused code for network cleanup +- Reset default labels when docker_compose_domains is modified +- Webhooks view +- Tags view +- Only get instanceSettings once from db +- Update Dockerfile to set CI environment variable to true +- Remove unnecessary code in AppServiceProvider.php +- Update Livewire configuration views +- Update Webhooks.php to use nullable type for webhook URLs +- Add lazy loading to tags in Livewire configuration view +- Update metrics.blade.php to improve alert message clarity +- Update version numbers to 4.0.0-beta.312 +- Update version numbers to 4.0.0-beta.314 +- Remove unused code and fix storage form layout +- Update Docker Compose build command to include --pull flag +- Update DockerCleanupJob to handle nullable usageBefore property +- Server status job and docker cleanup job +- Update DockerCleanupJob to use server settings for force cleanup +- Update DockerCleanupJob to use server settings for force cleanup +- Disable health check for Rust applications during deployment +- Update CleanupDatabase.php to adjust keep_days based on environment +- Adjust keep_days in CleanupDatabase.php based on environment +- Remove commented out code for cleaning up networks in CleanupDocker.php +- Update livewire polling interval in heading.blade.php +- Remove unused code for checking server status in Heading.php +- Simplify log drain installation in ServerCheckJob +- Remove unnecessary debug statement in ServerCheckJob +- Simplify log drain installation and stop log drain if necessary +- Cleanup unnecessary dynamic proxy configuration in Init command +- Remove unnecessary debug statement in ApplicationDeploymentJob +- Update timeout for graceful_shutdown_container in ApplicationDeploymentJob +- Remove unused code and optimize CheckForUpdatesJob +- Update ProxyTypes enum values to use TRAEFIK instead of TRAEFIK_V2 +- Update Traefik labels on init and cleanup unnecessary dynamic proxy configuration +- Update StandalonePostgresql database initialization and backup handling +- Update cron expressions and add helper text for scheduled tasks +- Update Server model getContainers method to use collect() for containers and containerReplicates +- Import ProxyTypes enum and use TRAEFIK instead of TRAEFIK_V2 +- Update event listeners in Show components +- Refresh application to get latest database changes +- Update RabbitMQ configuration to use environment variable for port +- Remove debug statement in parseDockerComposeFile function +- ParseServiceVolumes +- Update OpenApi command to generate documentation +- Remove unnecessary server status check in destination view +- Remove unnecessary admin user email and password in budibase.yaml +- Improve saving of custom internal name in Advanced.php +- Add conditional check for volumes in generate_compose_file() +- Improve storage mount forms in add.blade.php +- Load environment variables based on resource type in sortEnvironmentVariables() +- Remove unnecessary network cleanup in Init.php +- Remove unnecessary environment variable checks in parseDockerComposeFile() +- Add null check for docker_compose_raw in parseCompose() +- Update dockerComposeParser to use YAML data from $yaml instead of $compose +- Convert service variables to key-value pairs in parseDockerComposeFile function +- Update database service name from mariadb to mysql +- Remove unnecessary code in DatabaseBackupJob and BackupExecutions +- Update Docker Compose parsing function to convert service variables to key-value pairs +- Update Docker Compose parsing function to convert service variables to key-value pairs +- Remove unused server timezone seeder and related code +- Remove unused server timezone seeder and related code +- Remove unused PullCoolifyImageJob from schedule +- Update parse method in Advanced, All, ApplicationPreview, General, and ApplicationDeploymentJob classes +- Remove commented out code for getIptables() in Dashboard.php +- Update .env file path in install.sh script +- Update SELF_HOSTED environment variable in docker-compose.prod.yml +- Remove unnecessary code for creating coolify network in upgrade.sh +- Update environment variable handling in StartClickhouse.php and ApplicationDeploymentJob.php +- Improve handling of COOLIFY_URL in shared.php +- Update build_args property type in ApplicationDeploymentJob +- Update background color of sponsor section in README.md +- Update Docker Compose location handling in PublicGitRepository +- Upgrade process of Coolify +- Improve handling of server timezones in scheduled backups and tasks +- Improve handling of server timezones in scheduled backups and tasks +- Improve handling of server timezones in scheduled backups and tasks +- Update cleanup schedule to run daily at midnight +- Skip returning volume if driver type is cifs or nfs +- Improve environment variable handling in shared.php +- Improve handling of environment variable merging in upgrade script +- Remove unnecessary code in ExecuteContainerCommand.php +- Improve Docker network connection command in StartService.php +- Terminal / run command +- Add authorization check in ExecuteContainerCommand mount method +- Remove unnecessary code in Terminal.php +- Remove unnecessary code in Terminal.blade.php +- Update WebSocket connection initialization in terminal.blade.php +- Remove unnecessary console.log statements in terminal.blade.php +- Update Docker cleanup label in Heading.php and Navbar.php +- Remove commented out code in Navbar.php +- Remove CleanupSshKeysJob from schedule in Kernel.php +- Update getAJoke function to exclude offensive jokes +- Update getAJoke function to use HTTPS for API request +- Update CleanupHelperContainersJob to use more efficient Docker command +- Update PrivateKey model to improve code readability and maintainability +- Remove unnecessary code in PrivateKey model +- Update PrivateKey model to use ownedByCurrentTeam() scope for cleanupUnusedKeys() +- Update install.sh script to check if coolify-db volume exists before generating SSH key +- Update ServerSeeder and PopulateSshKeysDirectorySeeder +- Improve attribute sanitization in Server model +- Update confirmation button text for deletion actions +- Remove unnecessary code in shared.php file +- Update environment variables for services in compose files +- Update select.blade.php to improve trademarks policy display +- Update select.blade.php to improve trademarks policy display +- Fix typo in subscription URLs +- Add Postiz service to compose file (disabled for now) +- Update shared.php to include predefined ports for services +- Simplify SSH key synchronization logic +- Remove unused code in DatabaseBackupStatusJob and PopulateSshKeysDirectorySeeder +- Remove commented out code and improve environment variable handling in newParser function +- Improve label positioning in input and checkbox components +- Group and sort fields in StackForm by service name and password status +- Improve layout and add checkbox for task enablement in scheduled task form +- Update checkbox component to support full width option +- Update confirmation label in danger.blade.php template +- Fix typo in execute-container-command.blade.php +- Update OS_TYPE for Asahi Linux in install.sh script +- Add localhost as Server if it doesn't exist and not in cloud environment +- Add localhost as Server if it doesn't exist and not in cloud environment +- Update ProductionSeeder to fix issue with coolify_key assignment +- Improve modal confirmation titles and button labels +- Update install.sh script to remove redirection of upgrade output to /dev/null +- Fix modal input closeOutside prop in configuration.blade.php +- Add support for IPv6 addresses in sslip function +- Update environment variable name for uptime-kuma service +- Improve start proxy script to handle existing containers gracefully +- Update delete server confirmation modal buttons +- Remove unnecessary code +- Update search input placeholder in resource index view +- Remove deployment queue when deleting an application +- Improve SSH command generation in Terminal.php and terminal-server.js +- Fix indentation in modal-confirmation.blade.php +- Improve parsing of commands for sudo in parseCommandsByLineForSudo +- Improve popup component styling and button behavior +- Encode delimiter in SshMultiplexingHelper +- Remove inactivity timer in terminal-server.js +- Improve socket reconnection interval in terminal.js +- Remove unnecessary watch command from soketi service entrypoint +- Update Traefik configuration for improved security and logging +- Improve proxy configuration and code consistency in Server model +- Rename name method to sanitizedName in BaseModel for clarity +- Improve migration command and enhance application model with global scope and status checks +- Unify notification icon +- Remove unused Azure and Authentik service configurations from services.php +- Change email column types in instance_settings migration from string to text +- Change OauthSetting creation to updateOrCreate for better handling of existing records +- Rename `coolify.environment` to `coolify.environmentName` +- Rename parameter in DatabaseBackupJob for clarity +- Improve checkbox component accessibility and styling +- Remove unused tags method from ApplicationDeploymentJob +- Improve deployment status check in isAnyDeploymentInprogress function +- Extend HorizonServiceProvider from HorizonApplicationServiceProvider +- Streamline job status retrieval and clean up repository interface +- Enhance ApplicationDeploymentJob and HorizonServiceProvider for improved job handling +- Remove commented-out unsubscribe route from API +- Update redirect calls to use a consistent navigation method in deployment functions +- AppServiceProvider +- Github.php +- Improve data formatting and UI +- Comment out RootUserSeeder call in ProductionSeeder for clarity +- Streamline ProductionSeeder by removing debug logs and unnecessary checks, while ensuring essential seeding operations remain intact +- Remove debug echo statements from Init command to clean up output and improve readability +- *(workflows)* Replace jq with PHP script for version retrieval in workflows +- *(s3)* Improve S3 bucket endpoint formatting +- *(vite)* Improve environment variable handling in Vite configuration +- *(ui)* Simplify GitHub App registration UI and layout +- Simplify service start and restart workflows +- Use pull flag on docker compose up +- *(ui)* Simplify file storage modal confirmations +- *(notifications)* Improve transactional email settings handling +- *(scheduled-tasks)* Improve scheduled task creation and management +- *(billing)* Enhance Stripe subscription status handling and notifications +- *(ui)* Unhide log toggle in application settings +- *(nginx)* Streamline default Nginx configuration and improve error handling +- *(install)* Clean up install script and enhance Docker installation logic +- *(ScheduledTask)* Clean up code formatting and remove unused import +- *(app)* Remove unused MagicBar component and related code +- *(database)* Streamline SSL configuration handling across database types +- *(application)* Streamline healthcheck parsing from Dockerfile +- *(notifications)* Standardize getRecipients method signatures +- *(configuration)* Centralize configuration management in ConfigurationRepository +- *(docker)* Update image references to use centralized registry URL +- *(env)* Add centralized registry URL to environment configuration +- *(storage)* Simplify file storage iteration in Blade template +- *(models)* Add is_directory attribute to LocalFileVolume model +- *(modal)* Add ignoreWire attribute to modal-confirmation component +- *(invite-link)* Adjust layout for better responsiveness in form +- *(invite-link)* Enhance form layout for improved responsiveness +- *(network)* Enhance docker network creation with ipv6 fallback +- *(network)* Check for existing coolify network before creation +- *(database)* Enhance encryption process for local file volumes +- *(proxy)* Improve port availability checks with multiple methods +- *(database)* Update MongoDB SSL configuration for improved security +- *(database)* Enhance SSL configuration handling for various databases +- *(notifications)* Update Telegram button URL for staging environment +- *(models)* Remove unnecessary cloud check in isEnabled method +- *(database)* Streamline event listeners in Redis General component +- *(database)* Remove redundant database status display in MongoDB view +- *(database)* Update import statements for Auth in database components +- *(database)* Require PEM key file for SSL certificate regeneration +- *(database)* Change MySQL daemon command to MariaDB daemon +- *(nightly)* Update version numbers and enhance upgrade script +- *(versions)* Update version numbers for coolify and nightly +- *(email)* Validate team membership for email recipients +- *(shared)* Simplify deployment status check logic +- *(shared)* Add logging for running deployment jobs +- *(shared)* Enhance job status check to include 'reserved' +- *(email)* Improve error handling by passing context to handleError +- *(email)* Streamline email sending logic and improve configuration handling +- *(email)* Remove unnecessary whitespace in email sending logic +- *(email)* Allow custom email recipients in email sending logic +- *(email)* Enhance sender information formatting in email logic +- *(proxy)* Remove redundant stop call in restart method +- *(file-storage)* Add loadStorageOnServer method for improved error handling +- *(docker)* Parse and sanitize YAML compose file before encoding +- *(file-storage)* Improve layout and structure of input fields +- *(email)* Update label for test email recipient input +- *(database-backup)* Remove existing Docker container before backup upload +- *(database)* Improve decryption and deduplication of local file volumes +- *(database)* Remove debug output from volume update process +- *(dev)* Remove OpenAPI generation functionality +- *(migration)* Enhance local file volumes migration with logging +- *(CheckProxy)* Replace 'which' with 'command -v' for command availability checks +- *(Server)* Use data_get for safer access to settings properties in isFunctional method +- *(Application)* Rename network_aliases to custom_network_aliases across the application for clarity and consistency +- *(ApplicationDeploymentJob)* Streamline environment variable handling by introducing generate_coolify_env_variables method and consolidating logic for pull request and main branch scenarios +- *(ApplicationDeploymentJob, ApplicationDeploymentQueue)* Improve deployment status handling and log entry management with transaction support +- *(SourceManagement)* Sort sources by name and improve UI for changing Git source with better error handling +- *(Email)* Streamline SMTP and resend settings handling in copyFromInstanceSettings method +- *(Email)* Enhance error handling in SMTP and resend methods by passing context to handleError function +- *(DynamicConfigurations)* Improve handling of dynamic configuration content by ensuring fallback to empty string when content is null +- *(ServicesGenerate)* Update command signature from 'services:generate' to 'generate:services' for consistency; update Dockerfile to run service generation during build; update Odoo image version to 18 and add extra addons volume in compose configuration +- *(Dockerfile)* Streamline RUN commands for improved readability and maintainability by adding line continuations +- *(Dockerfile)* Reintroduce service generation command in the build process for consistency and ensure proper asset compilation +- *(commands)* Reorganize OpenAPI and Services generation commands into a new namespace for better structure; remove old command files +- *(Dockerfile)* Remove service generation command from the build process to streamline Dockerfile and improve build efficiency +- *(navbar-delete-team)* Simplify modal confirmation layout and enhance button styling for better user experience +- *(Server)* Remove debug logging from isReachableChanged method to clean up code and improve performance +- *(source)* Conditionally display connected source and change source options based on private key presence +- *(jobs)* Update WithoutOverlapping middleware to use expireAfter for better queue management +- *(jobs)* Comment out unused Caddy label handling in ApplicationDeploymentJob and simplify proxy path logic in Server model +- *(database)* Simplify database type checks in ServiceDatabase and enhance image validation in Docker helper +- *(shared)* Remove unused ray debugging statement from newParser function +- *(applications)* Remove redundant error response in create_env method +- *(api)* Restructure routes to include versioning and maintain existing feedback endpoint +- *(api)* Remove token variable from OpenAPI specifications for clarity +- *(environment-variables)* Remove protected variable checks from delete methods for cleaner logic +- *(http-basic-auth)* Rename 'http_basic_auth_enable' to 'http_basic_auth_enabled' across application files for consistency +- *(docker)* Remove debug statement and enhance hostname handling in Docker run conversion +- *(server)* Simplify proxy path logic and remove unnecessary conditions +- *(Database)* Streamline container shutdown process and reduce timeout duration +- *(core)* Streamline container stopping process and reduce timeout duration; update related methods for consistency +- *(database)* Update DB facade usage for consistency across service files +- *(database)* Enhance application conversion logic and add existence checks for databases and applications +- *(actions)* Standardize method naming for network and configuration deletion across application and service classes +- *(logdrain)* Consolidate log drain stopping logic to reduce redundancy +- *(StandaloneMariadb)* Add type hint for destination method to improve code clarity +- *(DeleteResourceJob)* Streamline resource deletion logic and improve conditional checks for database types +- *(jobs)* Update middleware to prevent job release after expiration for CleanupInstanceStuffsJob, RestartProxyJob, and ServerCheckJob +- *(jobs)* Unify middleware configuration to prevent job release after expiration for DockerCleanupJob and PushServerUpdateJob +- *(service)* Observium +- *(service)* Improve leantime +- *(service)* Imporve limesurvey +- *(service)* Improve CodiMD +- *(service)* Typsense +- *(services)* Improve yamtrack +- *(service)* Improve paymenter +- *(service)* Consolidate configuration change dispatch logic and remove unused navbar component +- *(sidebar)* Simplify server patching link by removing button element +- *(slide-over)* Streamline button element and improve code readability +- *(service)* Enhance modal confirmation component with event dispatching for service stop actions +- *(slide-over)* Enhance class merging for improved component styling +- *(core)* Use property promotion +- *(service)* Improve maybe +- *(applications)* Remove unused docker compose raw decoding +- *(service)* Make TYPESENSE_API_KEY required +- *(ui)* Show toast when server does not work and on stop +- *(service)* Improve superset +- *(service)* Improve Onetimesecret +- *(service)* Improve Seafile +- *(service)* Improve orangehrm +- *(service)* Improve grist +- *(application)* Enhance application stopping logic to support multiple servers +- *(pricing-plans)* Improve label class binding for payment frequency selection +- *(error-handling)* Replace generic Exception with RuntimeException for improved error specificity +- *(error-handling)* Change Exception to RuntimeException for clearer error reporting +- *(service)* Remove informational dispatch during service stop for cleaner execution +- *(server-ui)* Improve layout and messaging in advanced settings and charts views +- *(terminal-access)* Streamline resource retrieval and enhance terminal access messaging in UI +- *(terminal)* Enhance terminal connection management and error handling, including improved reconnection logic and cleanup procedures +- *(application-deployment)* Separate handling of FAILED and CANCELLED_BY_USER statuses for clearer logic and notification +- *(jobs)* Update middleware to include job-specific identifiers for WithoutOverlapping +- *(jobs)* Modify middleware to use job-specific identifier for WithoutOverlapping +- *(environment-variables)* Remove debug logging from bulk submit handling for cleaner code +- *(environment-variables)* Simplify application build pack check in environment variable handling +- *(logs)* Adjust padding in logs view for improved layout consistency +- *(application-deployment)* Streamline post-deployment process by always dispatching container status check +- *(service-management)* Enhance container stopping logic by implementing parallel processing and removing deprecated methods +- *(activity-monitor)* Change activity property visibility and update view references for consistency +- *(activity-monitor)* Enhance layout responsiveness by adjusting class bindings and structure for better display +- *(service-management)* Update stopContainersInParallel method to enforce Server type hint for improved type safety +- *(service-management)* Rearrange docker cleanup logic in StopService to improve readability +- *(database-management)* Simplify docker cleanup logic in StopDatabase to enhance readability +- *(activity-monitor)* Consolidate activity monitoring logic and remove deprecated NewActivityMonitor component +- *(activity-monitor)* Update dispatch method to use activityMonitor instead of deprecated newActivityMonitor +- *(push-server-update)* Enhance application preview handling by incorporating pull request IDs and adding status update protections +- *(docker-compose)* Replace hardcoded Docker Compose configuration with external YAML template for improved database detection testing +- *(test-database-detection)* Rename services for clarity, add new database configurations, and update application service dependencies +- *(database-detection)* Enhance isDatabaseImage function to utilize service configuration for improved detection accuracy +- *(install-scripts)* Update Docker installation process to include manual installation fallback and improve error handling +- *(logs-view)* Update logs display for service containers with improved headings and dynamic key binding +- *(logs)* Enhance container loading logic and improve UI for logs display across various resource types +- *(cloudflare-tunnel)* Enhance layout and structure of Cloudflare Tunnel documentation and confirmation modal +- *(terminal-connection)* Streamline auto-connection logic and improve component readiness checks +- *(logs)* Remove unused methods and debug functionality from Logs.php for cleaner code +- *(remoteProcess)* Update sanitize_utf8_text function to accept nullable string parameter for improved type safety +- *(events)* Remove ProxyStarted event and associated ProxyStartedNotification listener for code cleanup +- *(navbar)* Remove unnecessary parameters from server navbar component for cleaner implementation +- *(proxy)* Remove commented-out listener and method for cleaner code structure +- *(events)* Update ProxyStatusChangedUI constructor to accept nullable teamId for improved flexibility +- *(cloudflare)* Update server retrieval method for improved query efficiency +- *(navbar)* Remove unused PHP use statement for cleaner code +- *(proxy)* Streamline proxy status handling and improve dashboard availability checks +- *(navbar)* Simplify proxy status handling and enhance loading indicators for better user experience +- *(resource-operations)* Filter out build servers from the server list and clean up commented-out code in the resource operations view +- *(execute-container-command)* Simplify connection logic and improve terminal availability checks +- *(navigation)* Remove wire:navigate directive from configuration links for cleaner HTML structure +- *(proxy)* Update StartProxy calls to use named parameter for async option +- *(clone-project)* Enhance server retrieval by including destinations and filtering out build servers +- *(ui)* Terminal +- *(ui)* Remove terminal header from execute-container-command view +- *(ui)* Remove unnecessary padding from deployment, backup, and logs sections +- *(service)* Update Hoarder to their new name karakeep (#5964) +- *(service)* Karakeep naming and formatting +- *(service)* Improve miniflux +- *(core)* Rename API rate limit ENV +- *(ui)* Simplify container selection form in execute-container-command view +- *(email)* Streamline SMTP and resend settings logic for improved clarity +- *(invitation)* Rename methods for consistency and enhance invitation deletion logic +- *(user)* Streamline user deletion process and enhance team management logic +- *(ui)* Separate views for instance settings to separate paths to make it cleaner +- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code +- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection +- *(terminal)* Simplify command construction for SSH execution +- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components +- *(policy)* Optimize team membership checks in S3StoragePolicy +- *(popup)* Improve styling and structure of the small popup component +- *(shared)* Enhance FQDN generation logic for services in newParser function +- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic +- *(init)* Standardize method naming conventions and improve command structure in Init.php +- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml +- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy +- *(previews)* Streamline preview URL generation by utilizing application method +- *(application)* Adjust layout and spacing in general application view for improved UI +- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency +- *(scheduling)* Replace deprecated job checks with ScheduledJobManager and ServerResourceManager for improved scheduling efficiency +- *(previews)* Move preview domain generation logic to ApplicationPreview model for better encapsulation and consistency across webhook handlers +- *(service)* Improve gowa +- *(previews)* Streamline preview domain generation logic in ApplicationDeploymentJob for improved clarity and maintainability +- *(services)* Simplify environment variable updates by using updateOrCreate and add cleanup for removed FQDNs +- *(jobs)* Remove logging for ScheduledJobManager and ServerResourceManager start and completion +- *(services)* Update validation rules to be optional +- *(service)* Improve langfuse +- *(service)* Improve openpanel template +- *(service)* Improve librechat +- *(public-git-repository)* Enhance form structure and add autofocus to repository URL input +- *(public-git-repository)* Remove commented-out code for cleaner template +- *(templates)* Update service template file handling to use dynamic file name from constants +- *(parsers)* Streamline domain handling in applicationParser and improve DNS validation logic +- *(templates)* Replace SERVICE_FQDN variables with SERVICE_URL in compose files for consistency +- *(links)* Replace inline SVGs with reusable external link component for consistency and improved maintainability +- *(previews)* Improve layout and add deployment/application logs links for previews +- *(docker compose)* Remove deprecated newParser function and associated test file to streamline codebase +- *(shared helpers)* Remove unused parseServiceVolumes function to clean up codebase +- *(parsers)* Update volume parsing logic to use beforeLast and afterLast for improved accuracy +- *(validation)* Implement centralized validation patterns across components +- *(jobs)* Rename job classes to indicate deprecation status +- Update check frequency logic for cloud and self-hosted environments; streamline server task scheduling and timezone handling +- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility +- *(policies)* Remove Response type hint from update methods in ApplicationPreviewPolicy and DatabasePolicy for improved flexibility +- *(git)* Improve submodule cloning +- *(parsers)* Remove unnecessary hyphen-to-underscore replacement for service names in serviceParser function +- *(urls)* Replace generateFqdn with generateUrl for consistent URL generation across applications +- *(domains)* Rename check_domain_usage to checkDomainUsage and update references across the application +- *(auth)* Simplify access control logic in CanAccessTerminal and ServerPolicy by allowing all users to perform actions +- *(policy)* Simplify ServiceDatabasePolicy methods to always return true and add manageBackups method + +### 📚 Documentation + +- Contribution guide +- How to add new services +- Update +- Update +- Update Plunk documentation link in compose/plunk.yaml +- Update link to deploy api docs +- Add TECH_STACK.md (#4883) +- *(services)* Reword nitropage url and slogan +- *(readme)* Add Convex to special sponsors section +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- *(CONTRIBUTING)* Add note about Laravel Horizon accessibility +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- *(service)* Add new docs link for zipline (#5912) +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- *(claude)* Clarify that artisan commands should only be run inside the "coolify" container during development +- Add AGENTS.md for project guidance and development instructions +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog + +### 🎨 Styling + +- Linting +- *(css)* Update padding utility for password input and add newline in app.css +- *(css)* Refine badge utility styles in utilities.css +- *(css)* Enhance badge utility styles in utilities.css ### 🧪 Testing - Native binary target - Dockerfile - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.8.9] - 2022-08-30 - -### 🐛 Bug Fixes - -- Oh god Prisma - -## [3.8.8] - 2022-08-30 - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.8.6] - 2022-08-30 - -### 🐛 Bug Fixes - -- Pr deployment -- CompareVersions -- Include -- Include -- Gitlab apps - -### 💼 Other - -- Fixes -- Route to the correct path when creating destination from db config - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.8.5] - 2022-08-27 - -### 🐛 Bug Fixes - -- Copy all files during install process -- Typo -- Process -- White labeled icon on navbar -- Whitelabeled icon -- Next/nuxt deployment type -- Again - -## [3.8.4] - 2022-08-27 - -### 🐛 Bug Fixes - -- UI thinkgs -- Delete team while it is active -- Team switching -- Queue cleanup -- Decrypt secrets -- Cleanup build cache as well -- Pr deployments + remove public gits - -### 💼 Other - -- Dashbord fixes -- Fixes - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.8.3] - 2022-08-26 - -### 🐛 Bug Fixes - -- Secrets decryption - -## [3.8.2] - 2022-08-26 - -### 🚀 Features - -- *(ui)* Rework home UI and with responsive design - -### 🐛 Bug Fixes - -- Never stop deplyo queue -- Build queue system -- High cpu usage -- Worker -- Better worker system - -### 💼 Other - -- Dashboard fine-tunes -- Fine-tune -- Fixes -- Fix - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.8.1] - 2022-08-24 - -### 🐛 Bug Fixes - -- Ui buttons -- Clear queue on cancelling jobs -- Cancelling jobs -- Dashboard for admins - -## [3.8.0] - 2022-08-23 - -### 🚀 Features - -- Searxng service - -### 🐛 Bug Fixes - -- Port checker -- Cancel build after 5 seconds -- ExposedPort checker -- Batch secret = -- Dashboard for non-root users -- Stream build logs -- Show build log start/end - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.7.0] - 2022-08-19 - -### 🚀 Features - -- Add GlitchTip service - -### 🐛 Bug Fixes - -- Missing commas -- ExposedPort is just optional - -### ⚙️ Miscellaneous Tasks - -- Add .pnpm-store in .gitignore -- Version++ - -## [3.6.0] - 2022-08-18 - -### 🚀 Features - -- Import public repos (wip) -- Public repo deployment -- Force rebuild + env.PORT for port + public repo build - -### 🐛 Bug Fixes - -- Bots without exposed ports - -### 💼 Other - -- Fixes here and there - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.5.2] - 2022-08-17 - -### 🐛 Bug Fixes - -- Restart containers on-failure instead of always -- Show that Ghost values could be changed - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.5.1] - 2022-08-17 - -### 🐛 Bug Fixes - -- Revert docker compose version to 2.6.1 -- Trim secrets - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.5.0] - 2022-08-17 - -### 🚀 Features - -- Deploy bots (no domains) -- Custom dns servers - -### 🐛 Bug Fixes - -- Dns button ui -- Bot deployments -- Bots -- AutoUpdater & cleanupStorage jobs - -### 💼 Other - -- Typing - -## [3.4.0] - 2022-08-16 - -### 🚀 Features - -- Appwrite service -- Heroku deployments - -### 🐛 Bug Fixes - -- Replace docker compose with docker-compose on CSB -- Dashboard ui -- Create coolify-infra, if it does not exists -- Gitpod conf and heroku buildpacks -- Appwrite -- Autoimport + readme -- Services import -- Heroku icon -- Heroku icon - -## [3.3.4] - 2022-08-15 - -### 🐛 Bug Fixes - -- Make it public button -- Loading indicator - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.3.3] - 2022-08-14 - -### 🐛 Bug Fixes - -- Decryption errors -- Postgresql on ARM - -## [3.3.2] - 2022-08-12 - -### 🐛 Bug Fixes - -- Debounce dashboard status requests - -### 💼 Other - -- Fider - -## [3.3.1] - 2022-08-12 - -### 🐛 Bug Fixes - -- Empty buildpack icons - -## [3.2.3] - 2022-08-12 - -### 🚀 Features - -- Databases on ARM -- Mongodb arm support -- New dashboard - -### 🐛 Bug Fixes - -- Cleanup stucked prisma-engines -- Toast -- Secrets -- Cleanup prisma engine if there is more than 1 -- !isARM to isARM -- Enterprise GH link - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.2.2] - 2022-08-11 - -### 🐛 Bug Fixes - -- Coolify-network on verification - -## [3.2.1] - 2022-08-11 - -### 🚀 Features - -- Init heroku buildpacks - -### 🐛 Bug Fixes - -- Follow/cancel buttons -- Only remove coolify managed containers -- White-labeled env -- Schema - -### 💼 Other - -- Fix - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.2.0] - 2022-08-11 - -### 🚀 Features - -- Persistent storage for all services -- Cleanup clickhouse db - -### 🐛 Bug Fixes - -- Rde local ports -- Empty remote destinations could be removed -- Tips -- Lowercase issues fider -- Tooltip colors -- Update clickhouse configuration -- Cleanup command -- Enterprise Github instance endpoint - -### 💼 Other - -- Local ssh port -- Redesign a lot -- Fixes -- Loading indicator for plausible buttons - -## [3.1.4] - 2022-08-01 - -### 🚀 Features - -- Moodle init -- Remote docker engine init -- Working on remote docker engine -- Rde -- Remote docker engine -- Ipv4 and ipv6 -- Contributors -- Add arch to database -- Stop preview deployment - -### 🐛 Bug Fixes - -- Settings from api -- Selectable destinations -- Gitpod hardcodes -- Typo -- Typo -- Expose port checker -- States and exposed ports -- CleanupStorage -- Remote traefik webhook -- Remote engine ip address -- RemoteipAddress -- Explanation for remote engine url -- Tcp proxy -- Lol -- Webhook -- Dns check for rde -- Gitpod -- Revert last commit -- Dns check -- Dns checker -- Webhook -- Df and more debug -- Webhooks -- Load previews async -- Destination icon -- Pr webhook -- Cache image -- No ssh key found -- Prisma migration + update of docker and stuffs -- Ui -- Ui -- Only 1 ssh-agent is needed -- Reuse ssh connection -- Ssh tunnel -- Dns checking -- Fider BASE_URL set correctly - -### 💼 Other - -- Error message https://github.com/coollabsio/coolify/issues/502 -- Changes -- Settings -- For removing app - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.1.3] - 2022-07-18 - -### 🚀 Features - -- Init moodle and separate stuffs to shared package - -### 🐛 Bug Fixes - -- More types for API -- More types -- Do not rebuild in case image exists and sha not changed -- Gitpod urls -- Remove new service start process -- Remove shared dir, deployment does not work -- Gitlab custom url -- Location url for services and apps - -## [3.1.2] - 2022-07-14 - -### 🐛 Bug Fixes - -- Admin password reset should not timeout -- Message for double branches -- Turn off autodeploy if double branch is configured - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.1.1] - 2022-07-13 - -### 🚀 Features - -- Gitpod integration - -### 🐛 Bug Fixes - -- Cleanup less often and can do it manually - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ - -## [3.1.0] - 2022-07-12 - -### 🚀 Features - -- Ability to change deployment type for nextjs -- Ability to change deployment type for nuxtjs -- Gitpod ready code(almost) -- Add Docker buildpack exposed port setting -- Custom port for git instances - -### 🐛 Bug Fixes - -- GitLab pagination load data -- Service domain checker -- Wp missing ftp solution -- Ftp WP issues -- Ftp?! -- Gitpod updates -- Gitpod -- Gitpod -- Wordpress FTP permission issues -- GitLab search fields -- GitHub App button -- GitLab loop on misconfigured source -- Gitpod - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [3.0.3] - 2022-07-06 - -### 🐛 Bug Fixes - -- Domain check -- Domain check -- TrustProxy for Fastify -- Hostname issue - -## [3.0.2] - 2022-07-06 - -### 🐛 Bug Fixes - -- New destination can be created -- Include post -- New destinations - -## [3.0.1] - 2022-07-06 - -### 🐛 Bug Fixes - -- Seeding -- Forgot that the version bump changed 😅 - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.11] - 2022-06-20 - -### 🐛 Bug Fixes - -- Be able to change database + service versions -- Lock file - -## [2.9.10] - 2022-06-17 - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.9] - 2022-06-10 - -### 🐛 Bug Fixes - -- Host and reload for uvicorn -- Remove package-lock - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.8] - 2022-06-10 - -### 🐛 Bug Fixes - -- Persistent nocodb -- Nocodb persistency - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.7] - 2022-06-09 - -### 🐛 Bug Fixes - -- Plausible custom script -- Plausible script and middlewares -- Remove console log -- Remove comments -- Traefik middleware - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.6] - 2022-06-02 - -### 🐛 Bug Fixes - -- Fider changed an env variable name -- Pnpm command - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.5] - 2022-06-02 - -### 🐛 Bug Fixes - -- Proxy stop missing argument - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.4] - 2022-06-01 - -### 🐛 Bug Fixes - -- Demo version forms -- Typo -- Revert gh and gl cloning - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.3] - 2022-05-31 - -### 🐛 Bug Fixes - -- Recurisve clone instead of submodule -- Versions -- Only reconfigure coolify proxy if its missconfigured - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.2] - 2022-05-31 - -### 🐛 Bug Fixes - -- TrustProxy -- Force restart proxy -- Only restart coolify proxy in case of version prior to 2.9.2 -- Force restart proxy on seeding -- Add GIT ENV variable for submodules - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.1] - 2022-05-31 - -### 🐛 Bug Fixes - -- GitHub fixes - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.9.0] - 2022-05-31 - -### 🚀 Features - -- PageLoader -- Database + service usage - -### 🐛 Bug Fixes - -- Service checks -- Remove console.log -- Traefik -- Remove debug things -- WIP Traefik -- Proxy for http -- PR deployments view -- Minio urls + domain checks -- Remove gh token on git source changes -- Do not fetch app state in case of missconfiguration -- Demo instance save domain instantly -- Instant save on demo instance -- New source canceled view -- Lint errors in database services -- Otherfqdns -- Host key verification -- Ftp connection - -### 💼 Other - -- Appwrite -- Testing WS -- Traefik?! -- Traefik -- Traefik -- Traefik migration -- Traefik -- Traefik -- Traefik -- Notifications and application usage -- *(fix)* Traefik -- Css - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.8.2] - 2022-05-16 - -### 🐛 Bug Fixes - -- Gastby buildpack - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.8.1] - 2022-05-10 - -### 🐛 Bug Fixes - -- WP custom db -- UI - -## [2.6.1] - 2022-05-03 - -### 🚀 Features - -- Basic server usage on dashboard -- Show usage trends -- Usage on dashboard -- Custom script path for Plausible -- WP could have custom db -- Python image selection - -### 🐛 Bug Fixes - -- ExposedPorts -- Logos for dbs -- Do not run SSL renew in development -- Check domain for coolify before saving -- Remove debug info -- Cancel jobs -- Cancel old builds in database -- Better DNS check to prevent errors -- Check DNS in prod only -- DNS check -- Disable sentry for now -- Cancel -- Sentry -- No image for Docker buildpack -- Default packagemanager -- Server usage only shown for root team -- Expose ports for services -- UI -- Navbar UI -- UI -- UI -- Remove RC python -- UI -- UI -- UI -- Default Python package - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Version++ -- Version++ - -## [2.6.0] - 2022-05-02 - -### 🚀 Features - -- Hasura as a service -- Gzip compression -- Laravel buildpack is working! -- Laravel -- Fider service -- Database and services logs -- DNS check settings for SSL generation -- Cancel builds! - -### 🐛 Bug Fixes - -- Unami svg size -- Team switching moved to IAM menu -- Always use IP address for webhooks -- Remove unnecessary test endpoint -- UI -- Migration -- Fider envs -- Checking low disk space -- Build image -- Update autoupdate env variable -- Renew certificates -- Webhook build images -- Missing node versions - -### 💼 Other - -- Laravel - -## [2.4.11] - 2022-04-20 - -### 🚀 Features - -- Deno DB migration -- Show exited containers on UI & better UX -- Query container state periodically -- Install svelte-18n and init setup -- Umami service -- Coolify auto-updater -- Autoupdater -- Select base image for buildpacks - -### 🐛 Bug Fixes - -- Deno configurations -- Text on deno buildpack -- Correct branch shown in build logs -- Vscode permission fix -- I18n -- Locales -- Application logs is not reversed and queried better -- Do not activate i18n for now -- GitHub token cleanup on team switch -- No logs found -- Code cleanups -- Reactivate posgtres password -- Contribution guide -- Simplify list services -- Contribution -- Contribution guide -- Contribution guide -- Packagemanager finder - -### 💼 Other - -- Umami service -- Base image selector - -### 📚 Documentation - -- How to add new services -- Update -- Update - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Version++ - -## [2.4.10] - 2022-04-17 - -### 🚀 Features - -- Add persistent storage for services -- Multiply dockerfile locations for docker buildpack -- Testing fluentd logging driver -- Fluentbit investigation -- Initial deno support - -### 🐛 Bug Fixes - -- Switch from bitnami/redis to normal redis -- Use redis-alpine -- Wordpress extra config -- Stop sFTP connection on wp stop -- Change user's id in sftp wp instance -- Use arm based certbot on arm -- Buildlog line number is not string -- Application logs paginated -- Switch to stream on applications logs -- Scroll to top for logs -- Pull new images for services all the time it's started. -- White-labeled custom logo -- Application logs - -### 💼 Other - -- Show extraconfig if wp is running - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ - -## [2.4.9] - 2022-04-14 - -### 🐛 Bug Fixes - -- Postgres root pw is pw field -- Teams view -- Improved tcp proxy monitoring for databases/ftp -- Add HTTP proxy checks -- Loading of new destinations -- Better performance for cleanup images -- Remove proxy container in case of dependent container is down -- Restart local docker coolify proxy in case of something happens to it -- Id of service container - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.8] - 2022-04-13 - -### 🐛 Bug Fixes - -- Register should happen if coolify proxy cannot be started -- GitLab typo -- Remove system wide pw reset - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.7] - 2022-04-13 - -### 🐛 Bug Fixes - -- Destinations to HAProxy - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.6] - 2022-04-13 - -### 🐛 Bug Fixes - -- Cleanup images older than a day -- Meilisearch service -- Load all branches, not just the first 30 -- ProjectID for Github -- DNS check before creating SSL cert -- Try catch me -- Restart policy for resources -- No permission on first registration -- Reverting postgres password for now - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.5] - 2022-04-12 - -### 🐛 Bug Fixes - -- Types -- Invitations -- Timeout values - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.4] - 2022-04-12 - -### 🐛 Bug Fixes - -- Haproxy build stuffs -- Proxy - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.3] - 2022-04-12 - -### 🐛 Bug Fixes - -- Remove unnecessary save button haha -- Update dockerfile - -### ⚙️ Miscellaneous Tasks - -- Update packages -- Version++ -- Update build scripts -- Update build packages - -## [2.4.2] - 2022-04-09 - -### 🐛 Bug Fixes - -- Missing install repositories GitHub -- Return own and other sources better -- Show config missing on sources - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.1] - 2022-04-09 - -### 🐛 Bug Fixes - -- Enable https for Ghost -- Postgres root passwor shown and set -- Able to change postgres user password from ui -- DB Connecting string generator - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.4.0] - 2022-04-08 - -### 🚀 Features - -- Wordpress on-demand SFTP -- Finalize on-demand sftp for wp -- PHP Composer support -- Working on-demand sftp to wp data -- Admin team sees everything -- Able to change service version/tag -- Basic white labeled version -- Able to modify database passwords - -### 🐛 Bug Fixes - -- Add openssl to image -- Permission issues -- On-demand sFTP for wp -- Fix for fix haha -- Do not pull latest image -- Updated db versions -- Only show proxy for admin team -- Team view for root team -- Do not trigger >1 webhooks on GitLab -- Possible fix for spikes in CPU usage -- Last commit -- Www or not-www, that's the question -- Fix for the fix that fixes the fix -- Ton of updates for users/teams -- Small typo -- Unique storage paths -- Self-hosted GitLab URL -- No line during buildLog -- Html/apiUrls cannot end with / -- Typo -- Missing buildpack - -### 💼 Other - -- Fix -- Better layout for root team -- Fix -- Fixes -- Fix -- Fix -- Fix -- Fix -- Fix -- Fix -- Fix -- Insane amount -- Fix -- Fixes -- Fixes -- Fix -- Fixes -- Fixes - -### 📚 Documentation - -- Contribution guide - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.3.3] - 2022-04-05 - -### 🐛 Bug Fixes - -- Add git lfs while deploying -- Try to update build status several times -- Update stucked builds -- Update stucked builds on startup -- Revert seed -- Lame fixing -- Remove asyncUntil - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.3.2] - 2022-04-04 - -### 🐛 Bug Fixes - -- *(php)* If .htaccess file found use apache -- Add default webhook domain for n8n - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.3.1] - 2022-04-04 - -### 🐛 Bug Fixes - -- Secrets build/runtime coudl be changed after save -- Default configuration - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.3.0] - 2022-04-04 - -### 🚀 Features - -- Initial python support -- Add loading on register button -- *(dev)* Allow windows users to use pnpm dev -- MeiliSearch service -- Add abilitry to paste env files - -### 🐛 Bug Fixes - -- Ignore coolify proxy error for now -- Python no wsgi -- If user not found -- Rename envs to secrets -- Infinite loop on www domains -- No need to paste clear text env for previews -- Build log fix attempt #1 -- Small UI fix on logs -- Lets await! -- Async progress -- Remove console.log -- Build log -- UI -- Gitlab & Github urls - -### 💼 Other - -- Improvements - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Lock file + fix packages - -## [2.2.7] - 2022-04-01 - -### 🐛 Bug Fixes - -- Haproxy errors -- Build variables -- Use NodeJS for sveltekit for now - -## [2.2.6] - 2022-03-31 - -### 🐛 Bug Fixes - -- Add PROTO headers - -## [2.2.5] - 2022-03-31 - -### 🐛 Bug Fixes - -- Registration enabled/disabled - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.2.4] - 2022-03-31 - -### 🐛 Bug Fixes - -- Gitlab repo url -- No need to dashify anymore - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.2.3] - 2022-03-31 - -### 🐛 Bug Fixes - -- List ghost services -- Reload window on settings saved -- Persistent storage on webhooks -- Add license -- Space in repo names - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ -- Version++ -- Fixed typo on New Git Source view - -## [2.2.0] - 2022-03-27 - -### 🚀 Features - -- Add n8n.io service -- Add update kuma service -- Ghost service - -### 🐛 Bug Fixes - -- Ghost logo size -- Ghost icon, remove console.log - -### 💼 Other - -- Colors on svelte-select - -### ⚙️ Miscellaneous Tasks - -- Version ++ - -## [2.1.1] - 2022-03-25 - -### 🐛 Bug Fixes - -- Cleanup only 2 hours+ old images - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.1.0] - 2022-03-23 - -### 🚀 Features - -- Use compose instead of normal docker cmd -- Be able to redeploy PRs - -### 🐛 Bug Fixes - -- Skip ssl cert in case of error -- Volumes - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.31] - 2022-03-20 - -### 🚀 Features - -- Add PHP modules - -### 🐛 Bug Fixes - -- Cleanup old builds -- Only cleanup same app -- Add nginx + htaccess files - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.30] - 2022-03-19 - -### 🐛 Bug Fixes - -- No cookie found -- Missing session data -- No error if GitSource is missing -- No webhook secret found? -- Basedir for dockerfiles -- Better queue system + more support on monorepos -- Remove build logs in case of app removed - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.29] - 2022-03-11 - -### 🚀 Features - -- Webhooks inititate all applications with the correct branch -- Check ssl for new apps/services first -- Autodeploy pause -- Install pnpm into docker image if pnpm lock file is used - -### 🐛 Bug Fixes - -- Personal Gitlab repos -- Autodeploy true by default for GH repos - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.28] - 2022-03-04 - -### 🚀 Features - -- Service secrets - -### 🐛 Bug Fixes - -- Do not error if proxy is not running - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.27] - 2022-03-02 - -### 🚀 Features - -- Send version with update request - -### 🐛 Bug Fixes - -- Check when a container is running -- Reload haproxy if new cert is added -- Cleanup coolify images -- Application state in UI - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.26] - 2022-03-02 - -### 🐛 Bug Fixes - -- Update process - -## [2.0.25] - 2022-03-02 - -### 🚀 Features - -- Languagetool service - -### 🐛 Bug Fixes - -- Reload proxy on ssl cert -- Volume name - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.24] - 2022-03-02 - -### 🐛 Bug Fixes - -- Better proxy check -- Ssl + sslrenew -- Null proxyhash on restart -- Reconfigure proxy on restart -- Update process - -## [2.0.23] - 2022-02-28 - -### 🐛 Bug Fixes - -- Be sure .env exists -- Missing fqdn for services -- Default npm command -- Add coolify-image label for build images -- Cleanup old images, > 3 days - -### 💼 Other - -- Colorful states -- Application start - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.22] - 2022-02-27 - -### 🐛 Bug Fixes - -- Coolify image pulls -- Remove wrong/stuck proxy configurations -- Always use a buildpack -- Add icons for eleventy + astro -- Fix proxy every 10 secs -- Do not remove coolify proxy -- Update version - -### 💼 Other - -- Remote docker engine - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.21] - 2022-02-24 - -### 🚀 Features - -- Random subdomain for demo -- Random domain for services -- Astro buildpack -- 11ty buildpack -- Registration page - -### 🐛 Bug Fixes - -- Http for demo, oops -- Docker scanner -- Improvement on image pulls - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.20] - 2022-02-23 - -### 🐛 Bug Fixes - -- Revert default network - -### 💼 Other - -- Dns check - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.19] - 2022-02-23 - -### 🐛 Bug Fixes - -- Random network name for demo -- Settings fqdn grr - -## [2.0.18] - 2022-02-22 - -### 🚀 Features - -- Ports range - -### 🐛 Bug Fixes - -- Email is lowercased in login -- Lowercase email everywhere -- Use normal docker-compose in dev - -### 💼 Other - -- Make copy/password visible - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.17] - 2022-02-21 - -### 🐛 Bug Fixes - -- Move tokens from session to cookie/store - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.14] - 2022-02-18 - -### 🚀 Features - -- Basic password reset form -- Scan for lock files and set right commands -- Public port range (WIP) - -### 🐛 Bug Fixes - -- SSL app off -- Local docker host -- Typo -- Lets encrypt -- Remove SSL with stop -- SSL off for services -- Grr -- Running state css -- Minor fixes -- Remove force SSL when doing let's encrypt request -- GhToken in session now -- Random port for certbot -- Follow icon -- Plausible volume fixed -- Database connection strings -- Gitlab webhooks fixed -- If DNS not found, do not redirect -- Github token - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version ++ - -## [2.0.13] - 2022-02-17 - -### 🐛 Bug Fixes - -- Login issues - -## [2.0.11] - 2022-02-15 - -### 🚀 Features - -- Follow logs -- Generate www & non-www SSL certs - -### 🐛 Bug Fixes - -- Window error in SSR -- GitHub sync PR's -- Load more button -- Small fixes -- Typo -- Error with follow logs -- IsDomainConfigured -- TransactionIds -- Coolify image cleanup -- Cleanup every 10 mins -- Cleanup images -- Add no user redis to uri -- Secure cookie disabled by default -- Buggy svelte-kit-cookie-session - -### 💼 Other - -- Only allow cleanup in production - -### ⚙️ Miscellaneous Tasks - -- Version++ -- Version++ - -## [2.0.10] - 2022-02-15 - -### 🐛 Bug Fixes - -- Typo -- Error handling -- Stopping service without proxy -- Coolify proxy start - -### ⚙️ Miscellaneous Tasks - -- Version++ - -## [2.0.8] - 2022-02-14 - -### 🐛 Bug Fixes - -- Validate secrets -- Truncate git clone errors -- Branch used does not throw error - -## [2.0.7] - 2022-02-13 - -### 🚀 Features - -- Www <-> non-www redirection for apps -- Www <-> non-www redirection - -### 🐛 Bug Fixes - -- Package.json -- Build secrets should be visible in runtime -- New secret should have default values - -## [2.0.5] - 2022-02-11 - -### 🚀 Features - -- VaultWarden service - -### 🐛 Bug Fixes - -- PreventDefault on a button, thats all -- Haproxy check should not throw error -- Delete all build files -- Cleanup images -- More error handling in proxy configuration + cleanups -- Local static assets -- Check sentry -- Typo - -### ⚙️ Miscellaneous Tasks - -- Version -- Version - -## [2.0.4] - 2022-02-11 - -### 🚀 Features - -- Use tags in update -- New update process (#115) - -### 🐛 Bug Fixes - -- Docker Engine bug related to live-restore and IPs -- Version - -## [2.0.3] - 2022-02-10 - -### 🐛 Bug Fixes - -- Capture non-error as error -- Only delete id.rsa in case of it exists -- Status is not available yet +- Remove prisma +- More tests +- Setup database for upcoming tests ### ⚙️ Miscellaneous Tasks - Version bump +- Version +- Version +- Version++ +- Version++ +- Version++ +- Version++ +- Version ++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version ++ +- Version++ +- Version++ +- Version++ +- Fixed typo on New Git Source view +- Version++ +- Version++ +- Version++ +- Version++ +- Lock file + fix packages +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Update packages +- Version++ +- Update build scripts +- Update build packages +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Add .pnpm-store in .gitignore +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Minor changes +- Minor changes +- Minor changes +- Whoops +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Update staging release +- Version++ +- Version++ +- Add jda icon for lavalink service +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Version++ +- Update version to 4.0.0-beta.275 +- Update DNS server validation helper text +- Dark mode should be the default +- Improve menu item styling and spacing in service configuration and index views +- Improve menu item styling and spacing in service configuration and index views +- Improve menu item styling and spacing in project index and show views +- Remove docker compose versions +- Add Listmonk service template and logo +- Refactor GetContainersStatus.php for improved readability and maintainability +- Refactor ApplicationDeploymentJob.php for improved readability and maintainability +- Add metrics and logs directories to installation script +- Update sentinel version to 0.0.2 in versions.json +- Update permissions on metrics and logs directories +- Comment out server sentinel check in ServerStatusJob +- Update version numbers to 4.0.0-beta.278 +- Update hover behavior and cursor style in scheduled task executions view +- Refactor scheduled task view to improve code readability and maintainability +- Skip scheduled tasks if application or service is not running +- Remove debug logging statements in Kernel.php +- Handle invalid cron strings in Kernel.php +- Refactor Service.php to handle missing admin user in extraFields() method +- Update twenty CRM template with environment variables and dependencies +- Refactor applications.php to remove unused imports and improve code readability +- Refactor deployment index.blade.php for improved readability and rollback handling +- Refactor GitHub app selection UI in project creation form +- Update ServerLimitCheckJob.php to handle missing serverLimit value +- Remove unnecessary code for saving commit message +- Update DOCKER_VERSION to 26.0 in install.sh script +- Update Docker and Docker Compose versions in Dockerfiles +- Update version numbers to 4.0.0-beta.279 +- Limit commit message length to 50 characters in ApplicationDeploymentJob +- Update version to 4.0.0-beta.283 +- Change pre and post deployment command length in applications table +- Refactor container name logic in GetContainersStatus.php and ForcePasswordReset.php +- Remove unnecessary content from Docker Compose file +- Update Sentry release version to 4.0.0-beta.287 +- Add Thompson Edolo as a sponsor +- Add null checks for team in Stripe webhook +- Update Sentry release version to 4.0.0-beta.288 +- Update for version 289 +- Fix formatting issue in deployment index.blade.php file +- Remove unnecessary wire:navigate attribute in breadcrumbs.blade.php +- Rename docker dirs +- Update laravel/socialite to version v5.14.0 and livewire/livewire to version 3.4.9 +- Update modal styles for better user experience +- Update deployment index.blade.php script for better performance +- Update version numbers to 4.0.0-beta.290 +- Update version numbers to 4.0.0-beta.291 +- Update version numbers to 4.0.0-beta.292 +- Update version numbers to 4.0.0-beta.293 +- Add upgrade guide link to upgrade.blade.php +- Improve upgrade.blade.php with clearer instructions and formatting +- Update version numbers to 4.0.0-beta.294 +- Add Lightspeed.run as a sponsor +- Update Dockerfile to install vim +- Update Dockerfile with latest versions of Docker, Docker Compose, Docker Buildx, Pack, and Nixpacks +- Update version numbers to 4.0.0-beta.295 +- Update supported OS list with almalinux +- Update install.sh to support PopOS +- Update install.sh script to version 1.3.2 and handle Linux Mint as Ubuntu +- Update page title in resource index view +- Update logo file path in logto.yaml +- Update logo file path in logto.yaml +- Remove commented out code for docker container removal +- Add isAnyDeploymentInprogress function to check if any deployments are in progress +- Add ApplicationDeploymentJob and pint.json +- Update version numbers to 4.0.0-beta.298 +- Switch to database sessions from redis +- Update dependencies and remove unused code +- Update tailwindcss and vue versions in package.json +- Update service template URL in constants.php +- Update sentinel version to 0.0.8 +- Update chart styling and loading text +- Update sentinel version to 0.0.9 +- Update Spanish translation for failed authentication messages +- Add portuguese traslation +- Add Turkish translations +- Add Vietnamese translate +- Add Treive logo to donations section +- Update README.md with latest release version badge +- Update latest release version badge in README.md +- Update version to 4.0.0-beta.299 +- Move server delete component to the bottom of the page +- Update version to 4.0.0-beta.301 +- Update version to 4.0.0-beta.302 +- Update version to 4.0.0-beta.303 +- Update version to 4.0.0-beta.305 +- Update version to 4.0.0-beta.306 +- Add log1x/laravel-webfonts package +- Update version to 4.0.0-beta.307 +- Refactor ServerStatusJob constructor formatting +- Update Monaco Editor for Docker Compose and Proxy Configuration +- More details +- Refactor shared.php helper functions +- Update Plausible docker compose template to Plausible 2.1.0 +- Update Plausible docker compose template to Plausible 2.1.0 +- Update livewire/livewire dependency to version 3.4.9 +- Refactor checkIfDomainIsAlreadyUsed function +- Update storage.blade.php view for livewire project service +- Update version to 4.0.0-beta.310 +- Update composer dependencies +- Add new logo for Latitude +- Bump version to 4.0.0-beta.311 +- Update version to 4.0.0-beta.315 +- Update version to 4.0.0-beta.316 +- Update bug report template +- Update repository form with simplified URL input field +- Update width of container in general.blade.php +- Update checkbox labels in general.blade.php +- Update general page of apps +- Handle JSON parsing errors in format_docker_command_output_to_json +- Update Traefik image version to v2.11 +- Update version to 4.0.0-beta.317 +- Update version to 4.0.0-beta.318 +- Update helper message with link to documentation +- Disable health check by default +- Remove commented out code for sending internal notification +- Update APP_BASE_URL to use SERVICE_FQDN_PLANE +- Update resource-limits.blade.php with improved input field helpers +- Update version numbers to 4.0.0-beta.319 +- Remove commented out code for docker image pruning +- Collect/create/update volumes in parseDockerComposeFile function +- Update version to 4.0.0-beta.320 +- Add pull_request image builds to GH actions +- Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy() +- Update formbricks template +- Update registration view to display a notice for first user that it will be an admin +- Update server form to use password input for IP Address/Domain field +- Update navbar to include service status check +- Update navbar and configuration to improve service status check functionality +- Update workflows to include PR build and merge manifest steps +- Update UpdateCoolifyJob timeout to 10 minutes +- Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously +- Update version to 4.0.0-beta.321 +- Update version to 4.0.0-beta.322 +- Update version to 4.0.0-beta.323 +- Update version to 4.0.0-beta.324 +- New compose parser with tests +- Update version to 1.3.4 in install.sh and 1.0.6 in upgrade.sh +- Update memory limit to 64MB in horizon configuration +- Update php packages +- Update axios npm dependency to version 1.7.5 +- Update Coolify version to 4.0.0-beta.324 and fix file paths in upgrade script +- Update Coolify version to 4.0.0-beta.324 +- Update Coolify version to 4.0.0-beta.325 +- Update Coolify version to 4.0.0-beta.326 +- Add cd command to change directory before removing .env file +- Update Coolify version to 4.0.0-beta.327 +- Update Coolify version to 4.0.0-beta.328 +- Update sponsor links in README.md +- Update version.json to versions.json in GitHub workflow +- Cleanup stucked resources and scheduled backups +- Update GitHub workflow to use versions.json instead of version.json +- Update GitHub workflow to use versions.json instead of version.json +- Update GitHub workflow to use versions.json instead of version.json +- Update GitHub workflow to use jq container for version extraction +- Update GitHub workflow to use jq container for version extraction +- Update UI for displaying no executions found in scheduled task list +- Update UI for displaying deployment status in deployment list +- Update UI for displaying deployment status in deployment list +- Ignore unnecessary files in production build workflow +- Update server form layout and settings +- Update Dockerfile with latest versions of PACK and NIXPACKS +- Update coolify-helper.yml to get version from versions.json +- Disable Ray by default +- Enable Ray by default and update Dockerfile with latest versions of PACK and NIXPACKS +- Update Ray configuration and Dockerfile +- Add middleware for updating environment variables by UUID in `api.php` routes +- Expose port 3000 in browserless.yaml template +- Update Ray configuration and Dockerfile +- Update coolify version to 4.0.0-beta.331 +- Update versions.json and sentry.php to 4.0.0-beta.332 +- Update version to 4.0.0-beta.332 +- Update DATABASE_URL in plunk.yaml to use plunk database +- Add coolify.managed=true label to Docker image builds +- Update docker image pruning command to exclude managed images +- Update docker cleanup schedule to run daily at midnight +- Update versions.json to version 1.0.1 +- Update coolify-helper.yml to include "next" branch in push trigger +- Set timeout for ServerCheckJob to 60 seconds +- Update appwrite.yaml to include OpenSSL key variable assignment +- Update version numbers to 4.0.0-beta.333 +- Copy .env file to .env-{DATE} if it exists +- Update .env file with new values +- Update server check job middleware to use server ID instead of UUID +- Add reminder to backup .env file before running install script again +- Copy .env file to backup location during installation script +- Add reminder to backup .env file during installation script +- Update permissions in pr-build.yml and version numbers +- Add minio/mc command to Dockerfile +- Remove itsgoingd/clockwork from require-dev in composer.json +- Update 'key' value of gitlab in Service.php to use environment variable +- Update release version to 4.0.0-beta.335 +- Update constants.ssh.mux_enabled in remoteProcess.php +- Update listeners and proxy settings in server form and new server components +- Remove unnecessary null check for proxy_type in generate_default_proxy_configuration +- Remove unnecessary SSH command execution time logging +- Update release version to 4.0.0-beta.336 +- Update coolify environment variable assignment with double quotes +- Update shared.php to fix issues with source and network variables +- Update terminal styling for better readability +- Update button text for container connection form +- Update Dockerfile and workflow for Coolify Realtime (v4) +- Remove unused entrypoint script and update volume mapping +- Update .env file and docker-compose configuration +- Update APP_NAME environment variable in docker-compose.prod.yml +- Update WebSocket URL in terminal.blade.php +- Update Dockerfile and workflow for Coolify Realtime (v4) +- Update Dockerfile and workflow for Coolify Realtime (v4) +- Update Dockerfile and workflow for Coolify Realtime (v4) +- Rename Command Center to Terminal in code and views +- Update branch restriction for push event in coolify-helper.yml +- Update terminal button text and layout in application heading view +- Refactor terminal component and select form layout +- Update coolify nightly version to 4.0.0-beta.335 +- Update helper version to 1.0.1 +- Fix syntax error in versions.json +- Update version numbers to 4.0.0-beta.337 +- Update Coolify installer and scripts to include a function for fetching programming jokes +- Update docker network connection command in ApplicationDeploymentJob.php +- Add validation to prevent selecting 'default' server or container in RunCommand.php +- Update versions.json to reflect latest version of realtime container +- Update soketi image to version 1.0.1 +- Nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container +- Update version numbers to 4.0.0-beta.339 +- Update version numbers to 4.0.0-beta.340 +- Update version numbers to 4.0.0-beta.341 +- Update version numbers to 4.0.0-beta.342 +- Update remove-labels-and-assignees-on-close.yml +- Add SSH key for localhost in ProductionSeeder +- Update SSH key generation in install.sh script +- Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder +- Update install.sh to support Asahi Linux +- Update install.sh version to 1.6 +- Remove unused middleware and uniqueId method in DockerCleanupJob +- Refactor DockerCleanupJob to remove unused middleware and uniqueId method +- Remove unused migration file for populating SSH keys and clearing mux directory +- Add modified files to the commit +- Refactor pre-commit hook to improve performance and readability +- Update CONTRIBUTING.md with troubleshooting note about database migrations +- Refactor pre-commit hook to improve performance and readability +- Update cleanup command to use Redis instead of queue +- Update Docker commands to start proxy +- Update version numbers to 4.0.0-beta.343 +- Update version numbers to 4.0.0-beta.344 +- Update version numbers to 4.0.0-beta.345 +- Update version numbers to 4.0.0-beta.346 +- Add autocomplete attribute to input fields +- Refactor API Tokens component to use isApiEnabled flag +- Update versions.json file +- Remove unused .env.development.example file +- Update API Tokens view to include link to Settings menu +- Update web.php to cast server port as integer +- Update backup deletion labels to use language files +- Update database startup heading title +- Update database startup heading title +- Custom vite envs +- Update version numbers to 4.0.0-beta.348 +- Refactor code to improve SSH key handling and storage +- Update Mailpit logo to use SVG format +- Fix docs link in running state +- Update Coolify Realtime workflow to only trigger on the main branch +- Refactor instanceSettings() function to improve code readability +- Update Coolify Realtime image to version 1.0.2 +- Remove unnecessary code in DatabaseBackupJob.php +- Add "Not Usable" indicator for storage items +- Refactor instanceSettings() function and improve code readability +- Update version numbers to 4.0.0-beta.349 and 4.0.0-beta.350 +- Update version numbers to 4.0.0-beta.350 in configuration files +- Update command signature and description for cleanup application deployment queue +- Add missing import for Attribute class in ApplicationDeploymentQueue model +- Update modal input in server form to prevent closing on outside click +- Remove unnecessary command from SshMultiplexingHelper +- Remove commented out code for uploading to S3 in DatabaseBackupJob +- Update soketi service image to version 1.0.3 +- Update version to 4.0.0-beta.352 +- Refactor DatabaseBackupJob to handle missing team +- Update version to 4.0.0-beta.353 +- Update service application view +- Update version to 4.0.0-beta.354 +- Remove debug statement in Service model +- Remove commented code in Server model +- Fix application deployment queue filter logic +- Refactor modal-confirmation component +- Update it-tools service template and port configuration +- Update homarr service template and remove unnecessary code +- Update homarr service template and remove unnecessary code +- Update version to 4.0.0-beta.355 +- Update version to 4.0.0-beta.356 +- Remove commented code for shared variable type validation +- Update MariaDB image to version 11 and fix service environment variable orders +- Update anythingllm.yaml volumes configuration +- Update proxy configuration paths for Caddy and Nginx in dev +- Update password form submission in modal-confirmation component +- Update project query to order by name in uppercase +- Update project query to order by name in lowercase +- Update select.blade.php with improved search functionality +- Add Nitropage service template and logo +- Bump coolify-helper version to 1.0.2 +- Refactor loadServices2 method and remove unused code +- Update version to 4.0.0-beta.357 +- Update service names and volumes in windmill.yaml +- Update version to 4.0.0-beta.358 +- Ignore .ignition.json files in Docker and Git +- Add mattermost logo as svg +- Add mattermost svg to compose +- Update version to 4.0.0-beta.357 +- Fix form submission and keydown event handling in modal-confirmation.blade.php +- Update version numbers to 4.0.0-beta.359 in configuration files +- Disable adding default environment variables in shared.php +- Update laravel/horizon dependency to version 5.29.1 +- Update service extra fields to use dynamic keys +- Update livewire/livewire dependency to version 3.4.9 +- Add transmission template desc +- Update transmission docs link +- Update version numbers to 4.0.0-beta.360 in configuration files +- Update AWS environment variable names in unsend.yaml +- Update AWS environment variable names in unsend.yaml +- Update livewire/livewire dependency to version 3.4.9 +- Update version to 4.0.0-beta.361 +- Update Docker build and push actions to v6 +- Update Docker build and push actions to v6 +- Update Docker build and push actions to v6 +- Sync coolify-helper to dockerhub as well +- Push realtime to dockerhub +- Sync coolify-realtime to dockerhub +- Rename workflows +- Rename development to staging build +- Sync coolify-testing-host to dockerhbu +- Sync coolify prod image to dockerhub as well +- Update Docker version to 26.0 +- Update project resource index page +- Update project service configuration view +- Edit www helper +- Update dep +- Regenerate openapi spec +- Composer dep bump +- Dep bump +- Upgrade cloudflared and minio +- Remove comments and improve DB column naming +- Remove unused seeder +- Remove unused waitlist stuff +- Remove wired.php (not used anymore) +- Remove unused resale license job +- Remove commented out internal notification +- Remove more waitlist stuff +- Remove commented out notification +- Remove more waitlist stuff +- Remove unused code +- Fix typo +- Remove comment out code +- Some reordering +- Remove resale license reference +- Remove functions from shared.php +- Public settings for email notification +- Remove waitlist redirect +- Remove log +- Use new notification trait +- Remove unused route +- Remove unused email component +- Comment status changes as it is disabled for now +- Bump dep +- Reorder navbar +- Rename topicID to threadId like in the telegram API response +- Update PHP configuration to set memory limit using environment variable +- Regenerate API spec, removing notification fields +- Remove ray debugging +- Version ++ +- Improve Penpot healthchecks +- Switch up readonly lables to make more sense +- Remove unused computed fields +- Use the new job dispatch +- Disable volume data cloning for now +- Improve code +- Lowcoder service naming +- Use new functions +- Improve error styling +- Css +- More css as it still looks like shit +- Final css touches +- Ajust time to 50s (tests done) +- Remove debug log, finally found it +- Remove more logging +- Remove limit on commit message +- Remove dayjs +- Remove unused code and fix import +- *(dep)* Bump nixpacks version +- *(dep)* Version++ +- *(dep)* Bump helper version to 1.0.5 +- *(docker)* Add blank line for readability in Dockerfile +- *(versions)* Update coolify versions to v4.0.0-beta.388 +- *(versions)* Update coolify versions to v4.0.0-beta.389 and add helper version retrieval script +- *(versions)* Update coolify versions to v4.0.0-beta.389 +- *(core)* EnvironmentVariable Model now extends BaseModel to remove duplicated code +- *(versions)* Update coolify versions to v4.0.0-beta.3909 +- *(version)* Bump Coolify version to 4.0.0-beta.391 +- *(config)* Increase default PHP memory limit to 256M +- Add openapi response +- *(workflows)* Make naming more clear and remove unused code +- Bump Coolify version to 4.0.0-beta.392/393 +- *(ci)* Update changelog generation workflow to target 'next' branch +- *(ci)* Update changelog generation workflow to target main branch +- Rollback Coolify version to 4.0.0-beta.392 +- Bump Coolify version to 4.0.0-beta.393 +- Bump Coolify version to 4.0.0-beta.394 +- Bump Coolify version to 4.0.0-beta.395 +- Bump Coolify version to 4.0.0-beta.396 +- *(services)* Update zipline to use new Database env var. (#5210) +- *(service)* Upgrade authentik service +- *(service)* Remove unused env from zipline +- Bump helper and realtime version +- *(migration)* Remove unused columns +- *(ssl)* Improve code in ssl helper +- *(migration)* Ssl cert and key should not be nullable +- *(ssl)* Rename CA cert to `coolify-ca.crt` because of conflicts +- Rename ca crt folder to ssl +- *(ui)* Improve valid until handling +- Improve code quality suggested by code rabbit +- *(supabase)* Update Supabase service template and Postgres image version +- *(versions)* Update version numbers for coolify and nightly +- *(versions)* Update version numbers for coolify and nightly +- *(service)* Update minecraft service ENVs +- *(service)* Add more vars to infisical.yaml (#5418) +- *(service)* Add google variables to plausible.yaml (#5429) +- *(service)* Update authentik.yaml versions (#5373) +- *(core)* Remove redocs +- *(versions)* Update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404 +- *(service)* Remove unused code in Bugsink service +- *(versions)* Update version to 404 +- *(versions)* Bump version to 403 (#5520) +- *(versions)* Bump version to 404 +- *(versions)* Bump version to 406 +- *(versions)* Bump version to 407 +- *(versions)* Bump version to 406 +- *(versions)* Bump version to 407 and 408 for coolify and nightly +- *(versions)* Bump version to 408 for coolify and 409 for nightly +- *(versions)* Update nightly version to 4.0.0-beta.410 +- *(pre-commit)* Remove OpenAPI generation command from pre-commit hook +- *(versions)* Update realtime version to 1.0.7 and bump dependencies in package.json +- *(versions)* Bump coolify version to 4.0.0-beta.409 in configuration files +- *(versions)* Bump coolify version to 4.0.0-beta.410 and update nightly version to 4.0.0-beta.411 in configuration files +- *(templates)* Update plausible and clickhouse images to latest versions and remove mail service +- *(versions)* Update coolify version to 4.0.0-beta.411 and nightly version to 4.0.0-beta.412 in configuration files +- *(versions)* Update coolify version to 4.0.0-beta.412 and nightly version to 4.0.0-beta.413 in configuration files +- *(versions)* Update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files +- *(versions)* Update realtime version to 1.0.8 in versions.json +- *(versions)* Update realtime version to 1.0.8 in versions.json +- *(docker)* Update soketi image version to 1.0.8 in production configuration files +- *(versions)* Update coolify version to 4.0.0-beta.414 and nightly version to 4.0.0-beta.415 in configuration files +- *(workflows)* Adjust workflow for announcement +- *(versions)* Update coolify version to 4.0.0-beta.416 and nightly version to 4.0.0-beta.417 in configuration files; fix links in deployment view +- *(seeder)* Update git branch from 'main' to 'v4.x' for multiple examples in ApplicationSeeder +- *(versions)* Update coolify version to 4.0.0-beta.417 and nightly version to 4.0.0-beta.418 +- *(versions)* Update coolify version to 4.0.0-beta.418 +- *(versions)* Update coolify version to 4.0.0-beta.419 and nightly version to 4.0.0-beta.420 in configuration files +- *(service)* Rename hoarder server to karakeep (#5607) +- *(service)* Update Supabase services (#5708) +- *(service)* Remove unused documenso env +- *(service)* Formatting and cleanup of ryot +- *(docs)* Remove changelog and add it to gitignore +- *(versions)* Update version to 4.0.0-beta.419 +- *(service)* Diun formatting +- *(docs)* Update CHANGELOG.md +- *(service)* Switch convex vars +- *(service)* Pgbackweb formatting and naming update +- *(service)* Remove typesense default API key +- *(service)* Format yamtrack healthcheck +- *(core)* Remove unused function +- *(ui)* Remove unused stopEvent code +- *(service)* Remove unused env +- *(tests)* Update test environment database name and add new feature test for converting container environment variables to array +- *(service)* Update Immich service (#5886) +- *(service)* Remove unused logo +- *(api)* Update API docs +- *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance +- *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features +- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files +- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421 +- *(service)* Changedetection remove unused code +- *(service)* Update Evolution API image to the official one (#6031) +- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421 +- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0 +- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates +- *(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)* Improve matrix service +- *(service)* Format runner service +- *(service)* Improve sequin +- *(service)* Add `NOT_SECURED` env to Postiz (#6243) +- *(service)* Improve evolution-api environment variables (#6283) +- *(service)* Update Langfuse template to v3 (#6301) +- *(core)* Remove unused argument +- *(deletion)* Rename isDeleteOperation to deleteConnectedNetworks +- *(docker)* Remove unused arguments on StopService +- *(service)* Homebox formatting +- Clarify usage of custom redis configuration (#6321) +- *(changelogs)* Add .gitignore for changelogs directory and remove outdated changelog files for May, June, and July 2025 +- *(service)* Change affine images (#6366) +- Elasticsearch URL, fromatting and add category +- Update service-templates json files +- *(docs)* Remove AGENTS.md file; enhance CLAUDE.md with detailed form authorization patterns and service configuration examples +- *(cleanup)* Remove unused GitLab view files for change, new, and show pages +- *(workflows)* Add backlog directory to build triggers for production and staging workflows +- *(config)* Disable auto_commit in backlog configuration to prevent automatic commits +- *(versions)* Update coolify version to 4.0.0-beta.420.8 and nightly version to 4.0.0-beta.420.9 in versions.json and constants.php +- *(docker)* Update soketi image version to 1.0.10 in production and Windows configurations +- *(core)* Update version +- *(core)* Update version +- *(versions)* Update coolify version to 4.0.0-beta.421 and nightly version to 4.0.0-beta.422 +- Update version +- Update development node version +- Update coolify version to 4.0.0-beta.423 and nightly version to 4.0.0-beta.424 +- Update coolify version to 4.0.0-beta.424 and nightly version to 4.0.0-beta.425 +- Update coolify version to 4.0.0-beta.425 and nightly version to 4.0.0-beta.426 +- Update coolify version to 4.0.0-beta.426 and nightly version to 4.0.0-beta.427 -## [2.0.2] - 2022-02-10 +### ◀️ Revert -### 🐛 Bug Fixes - -- Secrets join -- ENV variables set differently - -## [1.0.0] - 2021-03-24 +- Show usage everytime +- Revert: revert +- Wip +- Variable parsing +- Hc return code check +- Instancesettings +- Pull policy +- Advanced dropdown +- Databasebackup +- Remove Cloudflare async tag attributes +- Encrypting mount and fs_path +- *(parser)* Enhance FQDN generation logic for services and applications diff --git a/CLAUDE.md b/CLAUDE.md index 6c594955c..34149d28a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,6 +160,7 @@ ### Database Patterns - Use database transactions for critical operations - Leverage query scopes for reusable queries - Apply indexes for performance-critical queries +- **CRITICAL**: When adding new database columns, ALWAYS update the model's `$fillable` array to allow mass assignment ### Security Best Practices - **Authentication**: Multi-provider auth via Laravel Fortify & Sanctum diff --git a/README.md b/README.md index 1c88f4c54..9be4130c2 100644 --- a/README.md +++ b/README.md @@ -53,40 +53,40 @@ # Donations ## Big Sponsors -* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy -* [GlueOps](https://www.glueops.dev?ref=coolify.io) - DevOps automation and infrastructure management +* [23M](https://23m.com?ref=coolify.io) - Your experts for high-availability hosting solutions! * [Algora](https://algora.io?ref=coolify.io) - Open source contribution platform -* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform -* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions -* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers -* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions -* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers -* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase -* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers -* [Trieve](https://trieve.ai?ref=coolify.io) - AI-powered search and analytics -* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data -* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions -* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions -* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor -* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform -* [WZ-IT](https://wz-it.com/?ref=coolify.io) - German agency for customised cloud solutions -* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner -* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform -* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions -* [QuantCDN](https://www.quantcdn.io?ref=coolify.io) - Enterprise-grade content delivery network -* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang -* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers -* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital PR & AI Authority Building Agency -* [Cloudify.ro](https://cloudify.ro?ref=coolify.io) - Cloud hosting solutions -* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half * [American Cloud](https://americancloud.com?ref=coolify.io) - US-based cloud infrastructure services -* [MassiveGrid](https://massivegrid.com?ref=coolify.io) - Enterprise cloud hosting solutions -* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers -* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform +* [Arcjet](https://arcjet.com?ref=coolify.io) - Advanced web security and performance solutions +* [BC Direct](https://bc.direct?ref=coolify.io) - Your trusted technology consulting partner +* [Blacksmith](https://blacksmith.sh?ref=coolify.io) - Infrastructure automation platform +* [Brand.dev](https://brand.dev?ref=coolify.io) - API to personalize your product with logos, colors, and company info from any domain +* [ByteBase](https://www.bytebase.com?ref=coolify.io) - Database CI/CD and Security at Scale +* [CodeRabbit](https://coderabbit.ai?ref=coolify.io) - Cut Code Review Time & Bugs in Half +* [COMIT](https://comit.international?ref=coolify.io) - New York Times award–winning contractor * [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform +* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers +* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy +* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions +* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform * [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions * [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure +* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions +* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions +* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers +* [Juxtdigital](https://juxtdigital.com?ref=coolify.io) - Digital PR & AI Authority Building Agency +* [LiquidWeb](https://liquidweb.com?ref=coolify.io) - Premium managed hosting solutions +* [Logto](https://logto.io?ref=coolify.io) - The better identity infrastructure for developers * [Macarne](https://macarne.com?ref=coolify.io) - Best IP Transit & Carrier Ethernet Solutions for Simplified Network Connectivity +* [Mobb](https://vibe.mobb.ai/?ref=coolify.io) - Secure Your AI-Generated Code to Unlock Dev Productivity +* [PFGLabs](https://pfglabs.com?ref=coolify.io) - Build Real Projects with Golang +* [Ramnode](https://ramnode.com/?ref=coolify.io) - High Performance Cloud VPS Hosting +* [SaasyKit](https://saasykit.com?ref=coolify.io) - Complete SaaS starter kit for developers +* [SupaGuide](https://supa.guide?ref=coolify.io) - Your comprehensive guide to Supabase +* [Supadata AI](https://supadata.ai/?ref=coolify.io) - Scrape YouTube, web, and files. Get AI-ready, clean data +* [Syntax.fm](https://syntax.fm?ref=coolify.io) - Podcast for web developers +* [Tigris](https://www.tigrisdata.com?ref=coolify.io) - Modern developer data platform +* [Tolgee](https://tolgee.io?ref=coolify.io) - The open source localization platform +* [Ubicloud](https://www.ubicloud.com?ref=coolify.io) - Open source cloud infrastructure platform ## Small Sponsors diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index f218fcabb..38f6d7bc8 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -105,6 +105,8 @@ public function handle(StandaloneClickhouse $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 38ad99d2e..300221d24 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -55,11 +55,11 @@ public function handle(StandaloneDragonfly $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -192,6 +192,8 @@ public function handle(StandaloneDragonfly $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 59bcd4123..3a2ceebb3 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -56,11 +56,11 @@ public function handle(StandaloneKeydb $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -208,6 +208,8 @@ public function handle(StandaloneKeydb $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 13dba4b43..8a936c8ae 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -57,11 +57,11 @@ public function handle(StandaloneMariadb $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -209,6 +209,8 @@ public function handle(StandaloneMariadb $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; if ($this->database->enable_ssl) { diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 870b5b7e5..19699d684 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -61,11 +61,11 @@ public function handle(StandaloneMongodb $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -260,6 +260,8 @@ public function handle(StandaloneMongodb $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; if ($this->database->enable_ssl) { $this->commands[] = executeInDocker($this->database->uuid, 'chown mongodb:mongodb /etc/mongo/certs/server.pem'); diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 5d5611e07..25546fa9d 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -57,11 +57,11 @@ public function handle(StandaloneMysql $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -210,6 +210,8 @@ public function handle(StandaloneMysql $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; if ($this->database->enable_ssl) { diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 38d46b3c1..ac011acbe 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -62,11 +62,11 @@ public function handle(StandalonePostgresql $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -223,6 +223,8 @@ public function handle(StandalonePostgresql $database) $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; if ($this->database->enable_ssl) { $this->commands[] = executeInDocker($this->database->uuid, "chown {$this->database->postgres_user}:{$this->database->postgres_user} /var/lib/postgresql/certs/server.key /var/lib/postgresql/certs/server.crt"); diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 68a1f3fe3..8a7ae42a4 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -56,11 +56,11 @@ public function handle(StandaloneRedis $database) $this->commands[] = "mkdir -p $this->configuration_dir/ssl"; $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { @@ -205,6 +205,8 @@ public function handle(StandaloneRedis $database) if ($this->database->enable_ssl) { $this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt"; } + $this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true"; + $this->commands[] = "docker rm -f $container_name 2>/dev/null || true"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "echo 'Database started.'"; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index ecfb13d0b..8671a5f27 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -19,6 +19,11 @@ public function handle(Server $server, bool $async = true, bool $force = false): if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { return 'OK'; } + $server->proxy->set('status', 'starting'); + $server->save(); + $server->refresh(); + ProxyStatusChangedUI::dispatch($server->team_id); + $commands = collect([]); $proxy_path = $server->proxyPath(); $configuration = GetProxyConfiguration::run($server); @@ -64,14 +69,12 @@ public function handle(Server $server, bool $async = true, bool $force = false): ]); $commands = $commands->merge(connectProxyToNetworks($server)); } - $server->proxy->set('status', 'starting'); - $server->save(); - ProxyStatusChangedUI::dispatch($server->team_id); if ($async) { return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id); } else { instant_remote_process($commands, $server); + $server->proxy->set('type', $proxyType); $server->save(); ProxyStatusChanged::dispatch($server->id); diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php index 15c892e75..45ec68abc 100644 --- a/app/Actions/Server/DeleteServer.php +++ b/app/Actions/Server/DeleteServer.php @@ -2,16 +2,102 @@ namespace App\Actions\Server; +use App\Models\CloudProviderToken; use App\Models\Server; +use App\Models\Team; +use App\Notifications\Server\HetznerDeletionFailed; +use App\Services\HetznerService; use Lorisleiva\Actions\Concerns\AsAction; class DeleteServer { use AsAction; - public function handle(Server $server) + public function handle(int $serverId, bool $deleteFromHetzner = false, ?int $hetznerServerId = null, ?int $cloudProviderTokenId = null, ?int $teamId = null) { - StopSentinel::run($server); - $server->forceDelete(); + $server = Server::withTrashed()->find($serverId); + + // Delete from Hetzner even if server is already gone from Coolify + if ($deleteFromHetzner && ($hetznerServerId || ($server && $server->hetzner_server_id))) { + $this->deleteFromHetznerById( + $hetznerServerId ?? $server->hetzner_server_id, + $cloudProviderTokenId ?? $server->cloud_provider_token_id, + $teamId ?? $server->team_id + ); + } + + ray($server ? 'Deleting server from Coolify' : 'Server already deleted from Coolify, skipping Coolify deletion'); + + // If server is already deleted from Coolify, skip this part + if (! $server) { + return; // Server already force deleted from Coolify + } + + ray('force deleting server from Coolify', ['server_id' => $server->id]); + + try { + $server->forceDelete(); + } catch (\Throwable $e) { + ray('Failed to force delete server from Coolify', [ + 'error' => $e->getMessage(), + 'server_id' => $server->id, + ]); + logger()->error('Failed to force delete server from Coolify', [ + 'error' => $e->getMessage(), + 'server_id' => $server->id, + ]); + } + } + + private function deleteFromHetznerById(int $hetznerServerId, ?int $cloudProviderTokenId, int $teamId): void + { + try { + // Use the provided token, or fallback to first available team token + $token = null; + + if ($cloudProviderTokenId) { + $token = CloudProviderToken::find($cloudProviderTokenId); + } + + if (! $token) { + $token = CloudProviderToken::where('team_id', $teamId) + ->where('provider', 'hetzner') + ->first(); + } + + if (! $token) { + ray('No Hetzner token found for team, skipping Hetzner deletion', [ + 'team_id' => $teamId, + 'hetzner_server_id' => $hetznerServerId, + ]); + + return; + } + + $hetznerService = new HetznerService($token->token); + $hetznerService->deleteServer($hetznerServerId); + + ray('Deleted server from Hetzner', [ + 'hetzner_server_id' => $hetznerServerId, + 'team_id' => $teamId, + ]); + } catch (\Throwable $e) { + ray('Failed to delete server from Hetzner', [ + 'error' => $e->getMessage(), + 'hetzner_server_id' => $hetznerServerId, + 'team_id' => $teamId, + ]); + + // Log the error but don't prevent the server from being deleted from Coolify + logger()->error('Failed to delete server from Hetzner', [ + 'error' => $e->getMessage(), + 'hetzner_server_id' => $hetznerServerId, + 'team_id' => $teamId, + ]); + + // Notify the team about the failure + $team = Team::find($teamId); + $team?->notify(new HetznerDeletionFailed($hetznerServerId, $teamId, $e->getMessage())); + } } } diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index 5410b1cbd..10589c8b9 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -4,7 +4,6 @@ use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneDocker; use Lorisleiva\Actions\Concerns\AsAction; @@ -20,7 +19,7 @@ public function handle(Server $server) throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - if (! SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->exists()) { + if (! $server->sslCertificates()->where('is_ca_certificate', true)->exists()) { $serverCert = SslHelper::generateSslCertificate( commonName: 'Coolify CA Certificate', serverId: $server->id, diff --git a/app/Actions/Stripe/CancelSubscription.php b/app/Actions/Stripe/CancelSubscription.php index 859aec6f6..71b6ed52b 100644 --- a/app/Actions/Stripe/CancelSubscription.php +++ b/app/Actions/Stripe/CancelSubscription.php @@ -30,7 +30,7 @@ public function getSubscriptionsPreview(): Collection $subscriptions = collect(); // Get all teams the user belongs to - $teams = $this->user->teams; + $teams = $this->user->teams()->get(); foreach ($teams as $team) { // Only include subscriptions from teams where user is owner @@ -49,6 +49,64 @@ public function getSubscriptionsPreview(): Collection return $subscriptions; } + /** + * Verify subscriptions exist and are active in Stripe API + * + * @return array ['verified' => Collection, 'not_found' => Collection, 'errors' => array] + */ + public function verifySubscriptionsInStripe(): array + { + if (! isCloud()) { + return [ + 'verified' => collect(), + 'not_found' => collect(), + 'errors' => [], + ]; + } + + $stripe = new StripeClient(config('subscription.stripe_api_key')); + $subscriptions = $this->getSubscriptionsPreview(); + + $verified = collect(); + $notFound = collect(); + $errors = []; + + foreach ($subscriptions as $subscription) { + try { + $stripeSubscription = $stripe->subscriptions->retrieve($subscription->stripe_subscription_id); + + // Check if subscription is actually active in Stripe + if (in_array($stripeSubscription->status, ['active', 'trialing', 'past_due'])) { + $verified->push([ + 'subscription' => $subscription, + 'stripe_status' => $stripeSubscription->status, + 'current_period_end' => $stripeSubscription->current_period_end, + ]); + } else { + $notFound->push([ + 'subscription' => $subscription, + 'reason' => "Status in Stripe: {$stripeSubscription->status}", + ]); + } + } catch (\Stripe\Exception\InvalidRequestException $e) { + // Subscription doesn't exist in Stripe + $notFound->push([ + 'subscription' => $subscription, + 'reason' => 'Not found in Stripe', + ]); + } catch (\Exception $e) { + $errors[] = "Error verifying subscription {$subscription->stripe_subscription_id}: ".$e->getMessage(); + \Log::error("Error verifying subscription {$subscription->stripe_subscription_id}: ".$e->getMessage()); + } + } + + return [ + 'verified' => $verified, + 'not_found' => $notFound, + 'errors' => $errors, + ]; + } + public function execute(): array { if ($this->isDryRun) { diff --git a/app/Actions/User/DeleteUserResources.php b/app/Actions/User/DeleteUserResources.php index 7b2e7318d..3c539d7c5 100644 --- a/app/Actions/User/DeleteUserResources.php +++ b/app/Actions/User/DeleteUserResources.php @@ -24,23 +24,46 @@ public function getResourcesPreview(): array $services = collect(); // Get all teams the user belongs to - $teams = $this->user->teams; + $teams = $this->user->teams()->get(); foreach ($teams as $team) { + // Only delete resources from teams that will be FULLY DELETED + // This means: user is the ONLY member of the team + // + // DO NOT delete resources if: + // - User is just a member (not owner) + // - Team has other members (ownership will be transferred or user just removed) + + $userRole = $team->pivot->role; + $memberCount = $team->members->count(); + + // Skip if user is not owner + if ($userRole !== 'owner') { + continue; + } + + // Skip if team has other members (will be transferred/user removed, not deleted) + if ($memberCount > 1) { + continue; + } + + // Only delete resources from teams where user is the ONLY member + // These teams will be fully deleted + // Get all servers for this team - $servers = $team->servers; + $servers = $team->servers()->get(); foreach ($servers as $server) { - // Get applications - $serverApplications = $server->applications; + // Get applications (custom method returns Collection) + $serverApplications = $server->applications(); $applications = $applications->merge($serverApplications); - // Get databases - $serverDatabases = $this->getAllDatabasesForServer($server); + // Get databases (custom method returns Collection) + $serverDatabases = $server->databases(); $databases = $databases->merge($serverDatabases); - // Get services - $serverServices = $server->services; + // Get services (relationship needs ->get()) + $serverServices = $server->services()->get(); $services = $services->merge($serverServices); } } @@ -105,21 +128,4 @@ public function execute(): array return $deletedCounts; } - - private function getAllDatabasesForServer($server): Collection - { - $databases = collect(); - - // Get all standalone database types - $databases = $databases->merge($server->postgresqls); - $databases = $databases->merge($server->mysqls); - $databases = $databases->merge($server->mariadbs); - $databases = $databases->merge($server->mongodbs); - $databases = $databases->merge($server->redis); - $databases = $databases->merge($server->keydbs); - $databases = $databases->merge($server->dragonflies); - $databases = $databases->merge($server->clickhouses); - - return $databases; - } } diff --git a/app/Actions/User/DeleteUserServers.php b/app/Actions/User/DeleteUserServers.php index d8caae54d..ca29dd49d 100644 --- a/app/Actions/User/DeleteUserServers.php +++ b/app/Actions/User/DeleteUserServers.php @@ -23,13 +23,13 @@ public function getServersPreview(): Collection $servers = collect(); // Get all teams the user belongs to - $teams = $this->user->teams; + $teams = $this->user->teams()->get(); foreach ($teams as $team) { // Only include servers from teams where user is owner or admin $userRole = $team->pivot->role; if ($userRole === 'owner' || $userRole === 'admin') { - $teamServers = $team->servers; + $teamServers = $team->servers()->get(); $servers = $servers->merge($teamServers); } } diff --git a/app/Console/Commands/AdminDeleteUser.php b/app/Console/Commands/AdminDeleteUser.php new file mode 100644 index 000000000..9b803b1f7 --- /dev/null +++ b/app/Console/Commands/AdminDeleteUser.php @@ -0,0 +1,1173 @@ + false, + 'phase_2_resources' => false, + 'phase_3_servers' => false, + 'phase_4_teams' => false, + 'phase_5_user_profile' => false, + 'phase_6_stripe' => false, + 'db_committed' => false, + ]; + + public function handle() + { + // Register signal handlers for graceful shutdown (Ctrl+C handling) + $this->registerSignalHandlers(); + + $email = $this->argument('email'); + $this->isDryRun = $this->option('dry-run'); + $this->skipStripe = $this->option('skip-stripe'); + $this->skipResources = $this->option('skip-resources'); + $force = $this->option('force'); + + if ($force) { + $this->warn('⚠️ FORCE MODE - Lock check will be bypassed'); + $this->warn(' Use this flag only if you are certain no other deletion is running'); + $this->newLine(); + } + + if ($this->isDryRun) { + $this->info('🔍 DRY RUN MODE - No data will be deleted'); + $this->newLine(); + } + + if ($this->output->isVerbose()) { + $this->info('📊 VERBOSE MODE - Full stack traces will be shown on errors'); + $this->newLine(); + } else { + $this->comment('💡 Tip: Use -v flag for detailed error stack traces'); + $this->newLine(); + } + + if (! $this->isDryRun && ! $this->option('auto-confirm')) { + $this->info('🔄 INTERACTIVE MODE - You will be asked to confirm after each phase'); + $this->comment(' Use --auto-confirm to skip phase confirmations'); + $this->newLine(); + } + + // Notify about instance type and Stripe + if (isCloud()) { + $this->comment('☁️ Cloud instance - Stripe subscriptions will be handled'); + } else { + $this->comment('🏠 Self-hosted instance - Stripe operations will be skipped'); + } + $this->newLine(); + + try { + $this->user = User::whereEmail($email)->firstOrFail(); + } catch (\Exception $e) { + $this->error("User with email '{$email}' not found."); + + return 1; + } + + // Implement file lock to prevent concurrent deletions of the same user + $lockKey = "user_deletion_{$this->user->id}"; + $this->lock = Cache::lock($lockKey, 600); // 10 minute lock + + if (! $force) { + if (! $this->lock->get()) { + $this->error('Another deletion process is already running for this user.'); + $this->error('Use --force to bypass this lock (use with extreme caution).'); + $this->logAction("Deletion blocked for user {$email}: Another process is already running"); + + return 1; + } + } else { + // In force mode, try to get lock but continue even if it fails + if (! $this->lock->get()) { + $this->warn('⚠️ Lock exists but proceeding due to --force flag'); + $this->warn(' There may be another deletion process running!'); + $this->newLine(); + } + } + + try { + $this->logAction("Starting user deletion process for: {$email}"); + + // Phase 1: Show User Overview (outside transaction) + if (! $this->showUserOverview()) { + $this->info('User deletion cancelled by operator.'); + + return 0; + } + $this->deletionState['phase_1_overview'] = true; + + // If not dry run, wrap DB operations in a transaction + // NOTE: Stripe cancellations happen AFTER commit to avoid inconsistent state + if (! $this->isDryRun) { + try { + DB::beginTransaction(); + + // Phase 2: Delete Resources + // WARNING: This triggers Docker container deletion via SSH which CANNOT be rolled back + if (! $this->skipResources) { + if (! $this->deleteResources()) { + DB::rollBack(); + $this->displayErrorState('Phase 2: Resource Deletion'); + $this->error('❌ User deletion failed at resource deletion phase.'); + $this->warn('⚠️ Some Docker containers may have been deleted on remote servers and cannot be restored.'); + $this->displayRecoverySteps(); + + return 1; + } + } + $this->deletionState['phase_2_resources'] = true; + + // Confirmation to continue after Phase 2 + if (! $this->skipResources && ! $this->option('auto-confirm')) { + $this->newLine(); + if (! $this->confirm('Phase 2 completed. Continue to Phase 3 (Delete Servers)?', true)) { + DB::rollBack(); + $this->info('User deletion cancelled by operator after Phase 2.'); + $this->info('Database changes have been rolled back.'); + + return 0; + } + } + + // Phase 3: Delete Servers + // WARNING: This may trigger cleanup operations on remote servers which CANNOT be rolled back + if (! $this->deleteServers()) { + DB::rollBack(); + $this->displayErrorState('Phase 3: Server Deletion'); + $this->error('❌ User deletion failed at server deletion phase.'); + $this->warn('⚠️ Some server cleanup operations may have been performed and cannot be restored.'); + $this->displayRecoverySteps(); + + return 1; + } + $this->deletionState['phase_3_servers'] = true; + + // Confirmation to continue after Phase 3 + if (! $this->option('auto-confirm')) { + $this->newLine(); + if (! $this->confirm('Phase 3 completed. Continue to Phase 4 (Handle Teams)?', true)) { + DB::rollBack(); + $this->info('User deletion cancelled by operator after Phase 3.'); + $this->info('Database changes have been rolled back.'); + + return 0; + } + } + + // Phase 4: Handle Teams + if (! $this->handleTeams()) { + DB::rollBack(); + $this->displayErrorState('Phase 4: Team Handling'); + $this->error('❌ User deletion failed at team handling phase.'); + $this->displayRecoverySteps(); + + return 1; + } + $this->deletionState['phase_4_teams'] = true; + + // Confirmation to continue after Phase 4 + if (! $this->option('auto-confirm')) { + $this->newLine(); + if (! $this->confirm('Phase 4 completed. Continue to Phase 5 (Delete User Profile)?', true)) { + DB::rollBack(); + $this->info('User deletion cancelled by operator after Phase 4.'); + $this->info('Database changes have been rolled back.'); + + return 0; + } + } + + // Phase 5: Delete User Profile + if (! $this->deleteUserProfile()) { + DB::rollBack(); + $this->displayErrorState('Phase 5: User Profile Deletion'); + $this->error('❌ User deletion failed at user profile deletion phase.'); + $this->displayRecoverySteps(); + + return 1; + } + $this->deletionState['phase_5_user_profile'] = true; + + // CRITICAL CONFIRMATION: Database commit is next (PERMANENT) + if (! $this->option('auto-confirm')) { + $this->newLine(); + $this->warn('⚠️ CRITICAL DECISION POINT'); + $this->warn('Next step: COMMIT database changes (PERMANENT and IRREVERSIBLE)'); + $this->warn('All resources, servers, teams, and user profile will be permanently deleted'); + $this->newLine(); + if (! $this->confirm('Phase 5 completed. Commit database changes? (THIS IS PERMANENT)', false)) { + DB::rollBack(); + $this->info('User deletion cancelled by operator before commit.'); + $this->info('Database changes have been rolled back.'); + $this->warn('⚠️ Note: Some Docker containers may have been deleted on remote servers.'); + + return 0; + } + } + + // Commit the database transaction + DB::commit(); + $this->deletionState['db_committed'] = true; + + $this->newLine(); + $this->info('✅ Database operations completed successfully!'); + $this->info('✅ Transaction committed - database changes are now PERMANENT.'); + $this->logAction("Database deletion completed for: {$email}"); + + // Confirmation to continue to Stripe (after commit) + if (! $this->skipStripe && isCloud() && ! $this->option('auto-confirm')) { + $this->newLine(); + $this->warn('⚠️ Database changes are committed (permanent)'); + $this->info('Next: Cancel Stripe subscriptions'); + if (! $this->confirm('Continue to Phase 6 (Cancel Stripe Subscriptions)?', true)) { + $this->warn('User deletion stopped after database commit.'); + $this->error('⚠️ IMPORTANT: User deleted from database but Stripe subscriptions remain active!'); + $this->error('You must cancel subscriptions manually in Stripe Dashboard.'); + $this->error('Go to: https://dashboard.stripe.com/'); + $this->error('Search for: '.$email); + + return 1; + } + } + + // Phase 6: Cancel Stripe Subscriptions (AFTER DB commit) + // This is done AFTER commit because Stripe API calls cannot be rolled back + // If this fails, DB changes are already committed but subscriptions remain active + if (! $this->skipStripe && isCloud()) { + if (! $this->cancelStripeSubscriptions()) { + $this->newLine(); + $this->error('═══════════════════════════════════════'); + $this->error('⚠️ CRITICAL: INCONSISTENT STATE DETECTED'); + $this->error('═══════════════════════════════════════'); + $this->error('✓ User data DELETED from database (committed)'); + $this->error('✗ Stripe subscription cancellation FAILED'); + $this->newLine(); + $this->displayErrorState('Phase 6: Stripe Cancellation (Post-Commit)'); + $this->newLine(); + $this->error('MANUAL ACTION REQUIRED:'); + $this->error('1. Go to Stripe Dashboard: https://dashboard.stripe.com/'); + $this->error('2. Search for customer email: '.$email); + $this->error('3. Cancel all active subscriptions'); + $this->error('4. Check storage/logs/user-deletions.log for subscription IDs'); + $this->newLine(); + $this->logAction("INCONSISTENT STATE: User {$email} deleted but Stripe cancellation failed"); + + return 1; + } + } + $this->deletionState['phase_6_stripe'] = true; + + $this->newLine(); + $this->info('✅ User deletion completed successfully!'); + $this->logAction("User deletion completed for: {$email}"); + + } catch (\Exception $e) { + DB::rollBack(); + $this->newLine(); + $this->error('═══════════════════════════════════════'); + $this->error('❌ EXCEPTION DURING USER DELETION'); + $this->error('═══════════════════════════════════════'); + $this->error('Exception: '.get_class($e)); + $this->error('Message: '.$e->getMessage()); + $this->error('File: '.$e->getFile().':'.$e->getLine()); + $this->newLine(); + + if ($this->output->isVerbose()) { + $this->error('Stack Trace:'); + $this->error($e->getTraceAsString()); + $this->newLine(); + } else { + $this->info('Run with -v for full stack trace'); + $this->newLine(); + } + + $this->displayErrorState('Exception during execution'); + $this->displayRecoverySteps(); + + $this->logAction("User deletion failed for {$email}: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}"); + + return 1; + } + } else { + // Dry run mode - just run through the phases without transaction + // Phase 2: Delete Resources + if (! $this->skipResources) { + if (! $this->deleteResources()) { + $this->info('User deletion would be cancelled at resource deletion phase.'); + + return 0; + } + } + + // Phase 3: Delete Servers + if (! $this->deleteServers()) { + $this->info('User deletion would be cancelled at server deletion phase.'); + + return 0; + } + + // Phase 4: Handle Teams + if (! $this->handleTeams()) { + $this->info('User deletion would be cancelled at team handling phase.'); + + return 0; + } + + // Phase 5: Delete User Profile + if (! $this->deleteUserProfile()) { + $this->info('User deletion would be cancelled at user profile deletion phase.'); + + return 0; + } + + // Phase 6: Cancel Stripe Subscriptions (shown after DB operations in dry run too) + if (! $this->skipStripe && isCloud()) { + if (! $this->cancelStripeSubscriptions()) { + $this->info('User deletion would be cancelled at Stripe cancellation phase.'); + + return 0; + } + } + + $this->newLine(); + $this->info('✅ DRY RUN completed successfully! No data was deleted.'); + } + + return 0; + } finally { + // Ensure lock is always released + $this->releaseLock(); + } + } + + private function showUserOverview(): bool + { + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 1: USER OVERVIEW'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $teams = $this->user->teams()->get(); + $ownedTeams = $teams->filter(fn ($team) => $team->pivot->role === 'owner'); + $memberTeams = $teams->filter(fn ($team) => $team->pivot->role !== 'owner'); + + // Collect servers and resources ONLY from teams that will be FULLY DELETED + // This means: user is owner AND is the ONLY member + // + // Resources from these teams will NOT be deleted: + // - Teams where user is just a member + // - Teams where user is owner but has other members (will be transferred/user removed) + $allServers = collect(); + $allApplications = collect(); + $allDatabases = collect(); + $allServices = collect(); + $activeSubscriptions = collect(); + + foreach ($teams as $team) { + $userRole = $team->pivot->role; + $memberCount = $team->members->count(); + + // Only show resources from teams where user is the ONLY member + // These are the teams that will be fully deleted + if ($userRole !== 'owner' || $memberCount > 1) { + continue; + } + + $servers = $team->servers()->get(); + $allServers = $allServers->merge($servers); + + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + if ($resource instanceof \App\Models\Application) { + $allApplications->push($resource); + } elseif ($resource instanceof \App\Models\Service) { + $allServices->push($resource); + } else { + $allDatabases->push($resource); + } + } + } + + // Only collect subscriptions on cloud instances + if (isCloud() && $team->subscription && $team->subscription->stripe_subscription_id) { + $activeSubscriptions->push($team->subscription); + } + } + + // Build table data + $tableData = [ + ['User', $this->user->email], + ['User ID', $this->user->id], + ['Created', $this->user->created_at->format('Y-m-d H:i:s')], + ['Last Login', $this->user->updated_at->format('Y-m-d H:i:s')], + ['Teams (Total)', $teams->count()], + ['Teams (Owner)', $ownedTeams->count()], + ['Teams (Member)', $memberTeams->count()], + ['Servers', $allServers->unique('id')->count()], + ['Applications', $allApplications->count()], + ['Databases', $allDatabases->count()], + ['Services', $allServices->count()], + ]; + + // Only show Stripe subscriptions on cloud instances + if (isCloud()) { + $tableData[] = ['Active Stripe Subscriptions', $activeSubscriptions->count()]; + } + + $this->table(['Property', 'Value'], $tableData); + + $this->newLine(); + + $this->warn('⚠️ WARNING: This will permanently delete the user and all associated data!'); + $this->newLine(); + + if (! $this->confirm('Do you want to continue with the deletion process?', false)) { + return false; + } + + return true; + } + + private function deleteResources(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 2: DELETE RESOURCES'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserResources($this->user, $this->isDryRun); + $resources = $action->getResourcesPreview(); + + if ($resources['applications']->isEmpty() && + $resources['databases']->isEmpty() && + $resources['services']->isEmpty()) { + $this->info('No resources to delete.'); + + return true; + } + + $this->info('Resources to be deleted:'); + $this->newLine(); + + if ($resources['applications']->isNotEmpty()) { + $this->warn("Applications to be deleted ({$resources['applications']->count()}):"); + $this->table( + ['Name', 'UUID', 'Server', 'Status'], + $resources['applications']->map(function ($app) { + return [ + $app->name, + $app->uuid, + $app->destination->server->name, + $app->status ?? 'unknown', + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($resources['databases']->isNotEmpty()) { + $this->warn("Databases to be deleted ({$resources['databases']->count()}):"); + $this->table( + ['Name', 'Type', 'UUID', 'Server'], + $resources['databases']->map(function ($db) { + return [ + $db->name, + class_basename($db), + $db->uuid, + $db->destination->server->name, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($resources['services']->isNotEmpty()) { + $this->warn("Services to be deleted ({$resources['services']->count()}):"); + $this->table( + ['Name', 'UUID', 'Server'], + $resources['services']->map(function ($service) { + return [ + $service->name, + $service->uuid, + $service->server->name, + ]; + })->toArray() + ); + $this->newLine(); + } + + $this->error('⚠️ THIS ACTION CANNOT BE UNDONE!'); + if (! $this->confirm('Are you sure you want to delete all these resources?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting resources...'); + try { + $result = $action->execute(); + $this->info("✓ Deleted: {$result['applications']} applications, {$result['databases']} databases, {$result['services']} services"); + $this->logAction("Deleted resources for user {$this->user->email}: {$result['applications']} apps, {$result['databases']} databases, {$result['services']} services"); + } catch (\Exception $e) { + $this->error('Failed to delete resources:'); + $this->error('Exception: '.get_class($e)); + $this->error('Message: '.$e->getMessage()); + $this->error('File: '.$e->getFile().':'.$e->getLine()); + + if ($this->output->isVerbose()) { + $this->error('Stack Trace:'); + $this->error($e->getTraceAsString()); + } + + throw $e; // Re-throw to trigger rollback + } + } + + return true; + } + + private function deleteServers(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 3: DELETE SERVERS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserServers($this->user, $this->isDryRun); + $servers = $action->getServersPreview(); + + if ($servers->isEmpty()) { + $this->info('No servers to delete.'); + + return true; + } + + $this->warn("Servers to be deleted ({$servers->count()}):"); + $this->table( + ['ID', 'Name', 'IP', 'Description', 'Resources Count'], + $servers->map(function ($server) { + $resourceCount = $server->definedResources()->count(); + + return [ + $server->id, + $server->name, + $server->ip, + $server->description ?? '-', + $resourceCount, + ]; + })->toArray() + ); + $this->newLine(); + + $this->error('⚠️ WARNING: Deleting servers will remove all server configurations!'); + if (! $this->confirm('Are you sure you want to delete all these servers?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting servers...'); + try { + $result = $action->execute(); + $this->info("✓ Deleted {$result['servers']} servers"); + $this->logAction("Deleted {$result['servers']} servers for user {$this->user->email}"); + } catch (\Exception $e) { + $this->error('Failed to delete servers:'); + $this->error('Exception: '.get_class($e)); + $this->error('Message: '.$e->getMessage()); + $this->error('File: '.$e->getFile().':'.$e->getLine()); + + if ($this->output->isVerbose()) { + $this->error('Stack Trace:'); + $this->error($e->getTraceAsString()); + } + + throw $e; // Re-throw to trigger rollback + } + } + + return true; + } + + private function handleTeams(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 4: HANDLE TEAMS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new DeleteUserTeams($this->user, $this->isDryRun); + $preview = $action->getTeamsPreview(); + + // Check for edge cases first - EXIT IMMEDIATELY if found + if ($preview['edge_cases']->isNotEmpty()) { + $this->error('═══════════════════════════════════════'); + $this->error('⚠️ EDGE CASES DETECTED - CANNOT PROCEED'); + $this->error('═══════════════════════════════════════'); + $this->newLine(); + + foreach ($preview['edge_cases'] as $edgeCase) { + $team = $edgeCase['team']; + $reason = $edgeCase['reason']; + $this->error("Team: {$team->name} (ID: {$team->id})"); + $this->error("Issue: {$reason}"); + + // Show team members for context + $this->info('Current members:'); + foreach ($team->members as $member) { + $role = $member->pivot->role; + $this->line(" - {$member->name} ({$member->email}) - Role: {$role}"); + } + + // Check for active resources + $resourceCount = 0; + foreach ($team->servers()->get() as $server) { + $resources = $server->definedResources(); + $resourceCount += $resources->count(); + } + + if ($resourceCount > 0) { + $this->warn(" ⚠️ This team has {$resourceCount} active resources!"); + } + + // Show subscription details if relevant + if ($team->subscription && $team->subscription->stripe_subscription_id) { + $this->warn(' ⚠️ Active Stripe subscription details:'); + $this->warn(" Subscription ID: {$team->subscription->stripe_subscription_id}"); + $this->warn(" Customer ID: {$team->subscription->stripe_customer_id}"); + + // Show other owners who could potentially take over + $otherOwners = $team->members + ->where('id', '!=', $this->user->id) + ->filter(function ($member) { + return $member->pivot->role === 'owner'; + }); + + if ($otherOwners->isNotEmpty()) { + $this->info(' Other owners who could take over billing:'); + foreach ($otherOwners as $owner) { + $this->line(" - {$owner->name} ({$owner->email})"); + } + } + } + + $this->newLine(); + } + + $this->error('Please resolve these issues manually before retrying:'); + + // Check if any edge case involves subscription payment issues + $hasSubscriptionIssue = $preview['edge_cases']->contains(function ($edgeCase) { + return str_contains($edgeCase['reason'], 'Stripe subscription'); + }); + + if ($hasSubscriptionIssue) { + $this->info('For teams with subscription payment issues:'); + $this->info('1. Cancel the subscription through Stripe dashboard, OR'); + $this->info('2. Transfer the subscription to another owner\'s payment method, OR'); + $this->info('3. Have the other owner create a new subscription after cancelling this one'); + $this->newLine(); + } + + $hasNoOwnerReplacement = $preview['edge_cases']->contains(function ($edgeCase) { + return str_contains($edgeCase['reason'], 'No suitable owner replacement'); + }); + + if ($hasNoOwnerReplacement) { + $this->info('For teams with no suitable owner replacement:'); + $this->info('1. Assign an admin role to a trusted member, OR'); + $this->info('2. Transfer team resources to another team, OR'); + $this->info('3. Delete the team manually if no longer needed'); + $this->newLine(); + } + + $this->error('USER DELETION ABORTED DUE TO EDGE CASES'); + $this->logAction("User deletion aborted for {$this->user->email}: Edge cases in team handling"); + + // Return false to trigger proper cleanup and lock release + return false; + } + + if ($preview['to_delete']->isEmpty() && + $preview['to_transfer']->isEmpty() && + $preview['to_leave']->isEmpty()) { + $this->info('No team changes needed.'); + + return true; + } + + if ($preview['to_delete']->isNotEmpty()) { + $this->warn('Teams to be DELETED (user is the only member):'); + $this->table( + ['ID', 'Name', 'Resources', 'Subscription'], + $preview['to_delete']->map(function ($team) { + $resourceCount = 0; + foreach ($team->servers()->get() as $server) { + $resourceCount += $server->definedResources()->count(); + } + $hasSubscription = $team->subscription && $team->subscription->stripe_subscription_id + ? '⚠️ YES - '.$team->subscription->stripe_subscription_id + : 'No'; + + return [ + $team->id, + $team->name, + $resourceCount, + $hasSubscription, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($preview['to_transfer']->isNotEmpty()) { + $this->warn('Teams where ownership will be TRANSFERRED:'); + $this->table( + ['Team ID', 'Team Name', 'New Owner', 'New Owner Email'], + $preview['to_transfer']->map(function ($item) { + return [ + $item['team']->id, + $item['team']->name, + $item['new_owner']->name, + $item['new_owner']->email, + ]; + })->toArray() + ); + $this->newLine(); + } + + if ($preview['to_leave']->isNotEmpty()) { + $this->warn('Teams where user will be REMOVED (other owners/admins exist):'); + $userId = $this->user->id; + $this->table( + ['ID', 'Name', 'User Role', 'Other Members'], + $preview['to_leave']->map(function ($team) use ($userId) { + $userRole = $team->members->where('id', $userId)->first()->pivot->role; + $otherMembers = $team->members->count() - 1; + + return [ + $team->id, + $team->name, + $userRole, + $otherMembers, + ]; + })->toArray() + ); + $this->newLine(); + } + + $this->error('⚠️ WARNING: Team changes affect access control and ownership!'); + if (! $this->confirm('Are you sure you want to proceed with these team changes?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Processing team changes...'); + try { + $result = $action->execute(); + $this->info("✓ Teams deleted: {$result['deleted']}, ownership transferred: {$result['transferred']}, left: {$result['left']}"); + $this->logAction("Team changes for user {$this->user->email}: deleted {$result['deleted']}, transferred {$result['transferred']}, left {$result['left']}"); + } catch (\Exception $e) { + $this->error('Failed to process team changes:'); + $this->error('Exception: '.get_class($e)); + $this->error('Message: '.$e->getMessage()); + $this->error('File: '.$e->getFile().':'.$e->getLine()); + + if ($this->output->isVerbose()) { + $this->error('Stack Trace:'); + $this->error($e->getTraceAsString()); + } + + throw $e; // Re-throw to trigger rollback + } + } + + return true; + } + + private function cancelStripeSubscriptions(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 6: CANCEL STRIPE SUBSCRIPTIONS'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $action = new CancelSubscription($this->user, $this->isDryRun); + $subscriptions = $action->getSubscriptionsPreview(); + + if ($subscriptions->isEmpty()) { + $this->info('No Stripe subscriptions to cancel.'); + + return true; + } + + // Verify subscriptions in Stripe before showing details + $this->info('Verifying subscriptions in Stripe...'); + $verification = $action->verifySubscriptionsInStripe(); + + if (! empty($verification['errors'])) { + $this->warn('⚠️ Errors occurred during verification:'); + foreach ($verification['errors'] as $error) { + $this->warn(" - {$error}"); + } + $this->newLine(); + } + + if ($verification['not_found']->isNotEmpty()) { + $this->warn('⚠️ Subscriptions not found or inactive in Stripe:'); + foreach ($verification['not_found'] as $item) { + $subscription = $item['subscription']; + $reason = $item['reason']; + $this->line(" - {$subscription->stripe_subscription_id} (Team: {$subscription->team->name}) - {$reason}"); + } + $this->newLine(); + } + + if ($verification['verified']->isEmpty()) { + $this->info('No active subscriptions found in Stripe to cancel.'); + + return true; + } + + $this->info('Active Stripe subscriptions to cancel:'); + $this->newLine(); + + $totalMonthlyValue = 0; + foreach ($verification['verified'] as $item) { + $subscription = $item['subscription']; + $stripeStatus = $item['stripe_status']; + $team = $subscription->team; + $planId = $subscription->stripe_plan_id; + + // Try to get the price from config + $monthlyValue = $this->getSubscriptionMonthlyValue($planId); + $totalMonthlyValue += $monthlyValue; + + $this->line(" - {$subscription->stripe_subscription_id} (Team: {$team->name})"); + $this->line(" Stripe Status: {$stripeStatus}"); + if ($monthlyValue > 0) { + $this->line(" Monthly value: \${$monthlyValue}"); + } + if ($subscription->stripe_cancel_at_period_end) { + $this->line(' ⚠️ Already set to cancel at period end'); + } + } + + if ($totalMonthlyValue > 0) { + $this->newLine(); + $this->warn("Total monthly value: \${$totalMonthlyValue}"); + } + $this->newLine(); + + $this->error('⚠️ WARNING: Subscriptions will be cancelled IMMEDIATELY (not at period end)!'); + $this->warn('⚠️ NOTE: This operation happens AFTER database commit and cannot be rolled back!'); + if (! $this->confirm('Are you sure you want to cancel all these subscriptions immediately?', false)) { + return false; + } + + if (! $this->isDryRun) { + $this->info('Cancelling subscriptions...'); + $result = $action->execute(); + $this->info("Cancelled {$result['cancelled']} subscriptions, {$result['failed']} failed"); + if ($result['failed'] > 0 && ! empty($result['errors'])) { + $this->error('Failed subscriptions:'); + foreach ($result['errors'] as $error) { + $this->error(" - {$error}"); + } + + return false; + } + $this->logAction("Cancelled {$result['cancelled']} Stripe subscriptions for user {$this->user->email}"); + } + + return true; + } + + private function deleteUserProfile(): bool + { + $this->newLine(); + $this->info('═══════════════════════════════════════'); + $this->info('PHASE 5: DELETE USER PROFILE'); + $this->info('═══════════════════════════════════════'); + $this->newLine(); + + $this->warn('⚠️ FINAL STEP - This action is IRREVERSIBLE!'); + $this->newLine(); + + $this->info('User profile to be deleted:'); + $this->table( + ['Property', 'Value'], + [ + ['Email', $this->user->email], + ['Name', $this->user->name], + ['User ID', $this->user->id], + ['Created', $this->user->created_at->format('Y-m-d H:i:s')], + ['Email Verified', $this->user->email_verified_at ? 'Yes' : 'No'], + ['2FA Enabled', $this->user->two_factor_confirmed_at ? 'Yes' : 'No'], + ] + ); + + $this->newLine(); + + $this->warn("Type 'DELETE {$this->user->email}' to confirm final deletion:"); + $confirmation = $this->ask('Confirmation'); + + if ($confirmation !== "DELETE {$this->user->email}") { + $this->error('Confirmation text does not match. Deletion cancelled.'); + + return false; + } + + if (! $this->isDryRun) { + $this->info('Deleting user profile...'); + + try { + $this->user->delete(); + $this->info('✓ User profile deleted successfully.'); + $this->logAction("User profile deleted: {$this->user->email}"); + } catch (\Exception $e) { + $this->error('Failed to delete user profile:'); + $this->error('Exception: '.get_class($e)); + $this->error('Message: '.$e->getMessage()); + $this->error('File: '.$e->getFile().':'.$e->getLine()); + + if ($this->output->isVerbose()) { + $this->error('Stack Trace:'); + $this->error($e->getTraceAsString()); + } + + $this->logAction("Failed to delete user profile {$this->user->email}: {$e->getMessage()}"); + + throw $e; // Re-throw to trigger rollback + } + } + + return true; + } + + private function getSubscriptionMonthlyValue(string $planId): int + { + // Try to get pricing from subscription metadata or config + // Since we're using dynamic pricing, return 0 for now + // This could be enhanced by fetching the actual price from Stripe API + + // Check if this is a dynamic pricing plan + $dynamicMonthlyPlanId = config('subscription.stripe_price_id_dynamic_monthly'); + $dynamicYearlyPlanId = config('subscription.stripe_price_id_dynamic_yearly'); + + if ($planId === $dynamicMonthlyPlanId || $planId === $dynamicYearlyPlanId) { + // For dynamic pricing, we can't determine the exact amount without calling Stripe API + // Return 0 to indicate dynamic/usage-based pricing + return 0; + } + + // For any other plans, return 0 as we don't have hardcoded prices + return 0; + } + + private function logAction(string $message): void + { + $logMessage = "[CloudDeleteUser] {$message}"; + + if ($this->isDryRun) { + $logMessage = "[DRY RUN] {$logMessage}"; + } + + Log::channel('single')->info($logMessage); + + // Also log to a dedicated user deletion log file + $logFile = storage_path('logs/user-deletions.log'); + + // Ensure the logs directory exists + $logDir = dirname($logFile); + if (! is_dir($logDir)) { + mkdir($logDir, 0755, true); + } + + $timestamp = now()->format('Y-m-d H:i:s'); + file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX); + } + + private function displayErrorState(string $failedAt): void + { + $this->newLine(); + $this->error('═══════════════════════════════════════'); + $this->error('DELETION STATE AT FAILURE'); + $this->error('═══════════════════════════════════════'); + $this->error("Failed at: {$failedAt}"); + $this->newLine(); + + $stateTable = []; + foreach ($this->deletionState as $phase => $completed) { + $phaseLabel = str_replace('_', ' ', ucwords($phase, '_')); + $status = $completed ? '✓ Completed' : '✗ Not completed'; + $stateTable[] = [$phaseLabel, $status]; + } + + $this->table(['Phase', 'Status'], $stateTable); + $this->newLine(); + + // Show what was rolled back vs what remains + if ($this->deletionState['db_committed']) { + $this->error('⚠️ DATABASE COMMITTED - Changes CANNOT be rolled back!'); + } else { + $this->info('✓ Database changes were ROLLED BACK'); + } + + $this->newLine(); + $this->error('User email: '.$this->user->email); + $this->error('User ID: '.$this->user->id); + $this->error('Timestamp: '.now()->format('Y-m-d H:i:s')); + $this->newLine(); + } + + private function displayRecoverySteps(): void + { + $this->error('═══════════════════════════════════════'); + $this->error('RECOVERY STEPS'); + $this->error('═══════════════════════════════════════'); + + if (! $this->deletionState['db_committed']) { + $this->info('✓ Database was rolled back - no recovery needed for database'); + $this->newLine(); + + if ($this->deletionState['phase_2_resources'] || $this->deletionState['phase_3_servers']) { + $this->warn('However, some remote operations may have occurred:'); + $this->newLine(); + + if ($this->deletionState['phase_2_resources']) { + $this->warn('Phase 2 (Resources) was attempted:'); + $this->warn('- Check remote servers for orphaned Docker containers'); + $this->warn('- Use: docker ps -a | grep coolify'); + $this->warn('- Manually remove if needed: docker rm -f '); + $this->newLine(); + } + + if ($this->deletionState['phase_3_servers']) { + $this->warn('Phase 3 (Servers) was attempted:'); + $this->warn('- Check for orphaned server configurations'); + $this->warn('- Verify SSH access to servers listed for this user'); + $this->newLine(); + } + } + } else { + $this->error('⚠️ DATABASE WAS COMMITTED - Manual recovery required!'); + $this->newLine(); + $this->error('The following data has been PERMANENTLY deleted:'); + + if ($this->deletionState['phase_5_user_profile']) { + $this->error('- User profile (email: '.$this->user->email.')'); + } + if ($this->deletionState['phase_4_teams']) { + $this->error('- Team memberships and owned teams'); + } + if ($this->deletionState['phase_3_servers']) { + $this->error('- Server records and configurations'); + } + if ($this->deletionState['phase_2_resources']) { + $this->error('- Applications, databases, and services'); + } + + $this->newLine(); + + if (! $this->deletionState['phase_6_stripe']) { + $this->error('Stripe subscriptions were NOT cancelled:'); + $this->error('1. Go to Stripe Dashboard: https://dashboard.stripe.com/'); + $this->error('2. Search for: '.$this->user->email); + $this->error('3. Cancel all active subscriptions manually'); + $this->newLine(); + } + } + + $this->error('Log file: storage/logs/user-deletions.log'); + $this->error('Check logs for detailed error information'); + $this->newLine(); + } + + /** + * Register signal handlers for graceful shutdown on Ctrl+C (SIGINT) and SIGTERM + */ + private function registerSignalHandlers(): void + { + if (! function_exists('pcntl_signal')) { + // pcntl extension not available, skip signal handling + return; + } + + // Handle Ctrl+C (SIGINT) + pcntl_signal(SIGINT, function () { + $this->newLine(); + $this->warn('═══════════════════════════════════════'); + $this->warn('⚠️ PROCESS INTERRUPTED (Ctrl+C)'); + $this->warn('═══════════════════════════════════════'); + $this->info('Cleaning up and releasing lock...'); + $this->releaseLock(); + $this->info('Lock released. Exiting gracefully.'); + exit(130); // Standard exit code for SIGINT + }); + + // Handle SIGTERM + pcntl_signal(SIGTERM, function () { + $this->newLine(); + $this->warn('═══════════════════════════════════════'); + $this->warn('⚠️ PROCESS TERMINATED (SIGTERM)'); + $this->warn('═══════════════════════════════════════'); + $this->info('Cleaning up and releasing lock...'); + $this->releaseLock(); + $this->info('Lock released. Exiting gracefully.'); + exit(143); // Standard exit code for SIGTERM + }); + + // Enable async signal handling + pcntl_async_signals(true); + } + + /** + * Release the lock if it exists + */ + private function releaseLock(): void + { + if ($this->lock) { + try { + $this->lock->release(); + } catch (\Exception $e) { + // Silently ignore lock release errors + // Lock will expire after 10 minutes anyway + } + } + } +} diff --git a/app/Console/Commands/AdminRemoveUser.php b/app/Console/Commands/AdminRemoveUser.php deleted file mode 100644 index d4534399c..000000000 --- a/app/Console/Commands/AdminRemoveUser.php +++ /dev/null @@ -1,56 +0,0 @@ -argument('email'); - $confirm = $this->confirm('Are you sure you want to remove user with email: '.$email.'?'); - if (! $confirm) { - $this->info('User removal cancelled.'); - - return; - } - $this->info("Removing user with email: $email"); - $user = User::whereEmail($email)->firstOrFail(); - $teams = $user->teams; - foreach ($teams as $team) { - if ($team->members->count() > 1) { - $this->error('User is a member of a team with more than one member. Please remove user from team first.'); - - return; - } - $team->delete(); - } - $user->delete(); - } catch (\Exception $e) { - $this->error('Failed to remove user.'); - $this->error($e->getMessage()); - - return; - } - } -} diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index ce2d6d598..0b13462ef 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -13,6 +13,7 @@ use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; +use App\Models\SslCertificate; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; @@ -58,6 +59,15 @@ private function cleanup_stucked_resources() } catch (\Throwable $e) { echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; } + try { + $servers = Server::onlyTrashed()->get(); + foreach ($servers as $server) { + echo "Force deleting stuck server: {$server->name}\n"; + $server->forceDelete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning stuck servers: {$e->getMessage()}\n"; + } try { $applicationsDeploymentQueue = ApplicationDeploymentQueue::get(); foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) { @@ -427,5 +437,18 @@ private function cleanup_stucked_resources() } catch (\Throwable $e) { echo "Error in ServiceDatabases: {$e->getMessage()}\n"; } + + try { + $orphanedCerts = SslCertificate::whereNotIn('server_id', function ($query) { + $query->select('id')->from('servers'); + })->get(); + + foreach ($orphanedCerts as $cert) { + echo "Deleting orphaned SSL certificate: {$cert->id} (server_id: {$cert->server_id})\n"; + $cert->delete(); + } + } catch (\Throwable $e) { + echo "Error in cleaning orphaned SSL certificates: {$e->getMessage()}\n"; + } } } diff --git a/app/Console/Commands/ClearGlobalSearchCache.php b/app/Console/Commands/ClearGlobalSearchCache.php new file mode 100644 index 000000000..a368b0bad --- /dev/null +++ b/app/Console/Commands/ClearGlobalSearchCache.php @@ -0,0 +1,83 @@ +option('all')) { + return $this->clearAllTeamsCache(); + } + + if ($teamId = $this->option('team')) { + return $this->clearTeamCache($teamId); + } + + // If no options provided, clear cache for current user's team + if (! auth()->check()) { + $this->error('No authenticated user found. Use --team=ID or --all option.'); + + return Command::FAILURE; + } + + $teamId = auth()->user()->currentTeam()->id; + + return $this->clearTeamCache($teamId); + } + + private function clearTeamCache(int $teamId): int + { + $team = Team::find($teamId); + + if (! $team) { + $this->error("Team with ID {$teamId} not found."); + + return Command::FAILURE; + } + + GlobalSearch::clearTeamCache($teamId); + $this->info("✓ Cleared global search cache for team: {$team->name} (ID: {$teamId})"); + + return Command::SUCCESS; + } + + private function clearAllTeamsCache(): int + { + $teams = Team::all(); + + if ($teams->isEmpty()) { + $this->warn('No teams found.'); + + return Command::SUCCESS; + } + + $count = 0; + foreach ($teams as $team) { + GlobalSearch::clearTeamCache($team->id); + $count++; + } + + $this->info("✓ Cleared global search cache for {$count} team(s)"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/Cloud/CloudDeleteUser.php b/app/Console/Commands/Cloud/CloudDeleteUser.php deleted file mode 100644 index a2ea9b3e5..000000000 --- a/app/Console/Commands/Cloud/CloudDeleteUser.php +++ /dev/null @@ -1,744 +0,0 @@ -error('This command is only available on cloud instances.'); - - return 1; - } - - $email = $this->argument('email'); - $this->isDryRun = $this->option('dry-run'); - $this->skipStripe = $this->option('skip-stripe'); - $this->skipResources = $this->option('skip-resources'); - - if ($this->isDryRun) { - $this->info('🔍 DRY RUN MODE - No data will be deleted'); - $this->newLine(); - } - - try { - $this->user = User::whereEmail($email)->firstOrFail(); - } catch (\Exception $e) { - $this->error("User with email '{$email}' not found."); - - return 1; - } - - // Implement file lock to prevent concurrent deletions of the same user - $lockKey = "user_deletion_{$this->user->id}"; - $lock = Cache::lock($lockKey, 600); // 10 minute lock - - if (! $lock->get()) { - $this->error('Another deletion process is already running for this user. Please try again later.'); - $this->logAction("Deletion blocked for user {$email}: Another process is already running"); - - return 1; - } - - try { - $this->logAction("Starting user deletion process for: {$email}"); - - // Phase 1: Show User Overview (outside transaction) - if (! $this->showUserOverview()) { - $this->info('User deletion cancelled.'); - $lock->release(); - - return 0; - } - - // If not dry run, wrap everything in a transaction - if (! $this->isDryRun) { - try { - DB::beginTransaction(); - - // Phase 2: Delete Resources - if (! $this->skipResources) { - if (! $this->deleteResources()) { - DB::rollBack(); - $this->error('User deletion failed at resource deletion phase. All changes rolled back.'); - - return 1; - } - } - - // Phase 3: Delete Servers - if (! $this->deleteServers()) { - DB::rollBack(); - $this->error('User deletion failed at server deletion phase. All changes rolled back.'); - - return 1; - } - - // Phase 4: Handle Teams - if (! $this->handleTeams()) { - DB::rollBack(); - $this->error('User deletion failed at team handling phase. All changes rolled back.'); - - return 1; - } - - // Phase 5: Cancel Stripe Subscriptions - if (! $this->skipStripe && isCloud()) { - if (! $this->cancelStripeSubscriptions()) { - DB::rollBack(); - $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.'); - - return 1; - } - } - - // Phase 6: Delete User Profile - if (! $this->deleteUserProfile()) { - DB::rollBack(); - $this->error('User deletion failed at final phase. All changes rolled back.'); - - return 1; - } - - // Commit the transaction - DB::commit(); - - $this->newLine(); - $this->info('✅ User deletion completed successfully!'); - $this->logAction("User deletion completed for: {$email}"); - - } catch (\Exception $e) { - DB::rollBack(); - $this->error('An error occurred during user deletion: '.$e->getMessage()); - $this->logAction("User deletion failed for {$email}: ".$e->getMessage()); - - return 1; - } - } else { - // Dry run mode - just run through the phases without transaction - // Phase 2: Delete Resources - if (! $this->skipResources) { - if (! $this->deleteResources()) { - $this->info('User deletion would be cancelled at resource deletion phase.'); - - return 0; - } - } - - // Phase 3: Delete Servers - if (! $this->deleteServers()) { - $this->info('User deletion would be cancelled at server deletion phase.'); - - return 0; - } - - // Phase 4: Handle Teams - if (! $this->handleTeams()) { - $this->info('User deletion would be cancelled at team handling phase.'); - - return 0; - } - - // Phase 5: Cancel Stripe Subscriptions - if (! $this->skipStripe && isCloud()) { - if (! $this->cancelStripeSubscriptions()) { - $this->info('User deletion would be cancelled at Stripe cancellation phase.'); - - return 0; - } - } - - // Phase 6: Delete User Profile - if (! $this->deleteUserProfile()) { - $this->info('User deletion would be cancelled at final phase.'); - - return 0; - } - - $this->newLine(); - $this->info('✅ DRY RUN completed successfully! No data was deleted.'); - } - - return 0; - } finally { - // Ensure lock is always released - $lock->release(); - } - } - - private function showUserOverview(): bool - { - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 1: USER OVERVIEW'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $teams = $this->user->teams; - $ownedTeams = $teams->filter(fn ($team) => $team->pivot->role === 'owner'); - $memberTeams = $teams->filter(fn ($team) => $team->pivot->role !== 'owner'); - - // Collect all servers from all teams - $allServers = collect(); - $allApplications = collect(); - $allDatabases = collect(); - $allServices = collect(); - $activeSubscriptions = collect(); - - foreach ($teams as $team) { - $servers = $team->servers; - $allServers = $allServers->merge($servers); - - foreach ($servers as $server) { - $resources = $server->definedResources(); - foreach ($resources as $resource) { - if ($resource instanceof \App\Models\Application) { - $allApplications->push($resource); - } elseif ($resource instanceof \App\Models\Service) { - $allServices->push($resource); - } else { - $allDatabases->push($resource); - } - } - } - - if ($team->subscription && $team->subscription->stripe_subscription_id) { - $activeSubscriptions->push($team->subscription); - } - } - - $this->table( - ['Property', 'Value'], - [ - ['User', $this->user->email], - ['User ID', $this->user->id], - ['Created', $this->user->created_at->format('Y-m-d H:i:s')], - ['Last Login', $this->user->updated_at->format('Y-m-d H:i:s')], - ['Teams (Total)', $teams->count()], - ['Teams (Owner)', $ownedTeams->count()], - ['Teams (Member)', $memberTeams->count()], - ['Servers', $allServers->unique('id')->count()], - ['Applications', $allApplications->count()], - ['Databases', $allDatabases->count()], - ['Services', $allServices->count()], - ['Active Stripe Subscriptions', $activeSubscriptions->count()], - ] - ); - - $this->newLine(); - - $this->warn('⚠️ WARNING: This will permanently delete the user and all associated data!'); - $this->newLine(); - - if (! $this->confirm('Do you want to continue with the deletion process?', false)) { - return false; - } - - return true; - } - - private function deleteResources(): bool - { - $this->newLine(); - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 2: DELETE RESOURCES'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $action = new DeleteUserResources($this->user, $this->isDryRun); - $resources = $action->getResourcesPreview(); - - if ($resources['applications']->isEmpty() && - $resources['databases']->isEmpty() && - $resources['services']->isEmpty()) { - $this->info('No resources to delete.'); - - return true; - } - - $this->info('Resources to be deleted:'); - $this->newLine(); - - if ($resources['applications']->isNotEmpty()) { - $this->warn("Applications to be deleted ({$resources['applications']->count()}):"); - $this->table( - ['Name', 'UUID', 'Server', 'Status'], - $resources['applications']->map(function ($app) { - return [ - $app->name, - $app->uuid, - $app->destination->server->name, - $app->status ?? 'unknown', - ]; - })->toArray() - ); - $this->newLine(); - } - - if ($resources['databases']->isNotEmpty()) { - $this->warn("Databases to be deleted ({$resources['databases']->count()}):"); - $this->table( - ['Name', 'Type', 'UUID', 'Server'], - $resources['databases']->map(function ($db) { - return [ - $db->name, - class_basename($db), - $db->uuid, - $db->destination->server->name, - ]; - })->toArray() - ); - $this->newLine(); - } - - if ($resources['services']->isNotEmpty()) { - $this->warn("Services to be deleted ({$resources['services']->count()}):"); - $this->table( - ['Name', 'UUID', 'Server'], - $resources['services']->map(function ($service) { - return [ - $service->name, - $service->uuid, - $service->server->name, - ]; - })->toArray() - ); - $this->newLine(); - } - - $this->error('⚠️ THIS ACTION CANNOT BE UNDONE!'); - if (! $this->confirm('Are you sure you want to delete all these resources?', false)) { - return false; - } - - if (! $this->isDryRun) { - $this->info('Deleting resources...'); - $result = $action->execute(); - $this->info("Deleted: {$result['applications']} applications, {$result['databases']} databases, {$result['services']} services"); - $this->logAction("Deleted resources for user {$this->user->email}: {$result['applications']} apps, {$result['databases']} databases, {$result['services']} services"); - } - - return true; - } - - private function deleteServers(): bool - { - $this->newLine(); - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 3: DELETE SERVERS'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $action = new DeleteUserServers($this->user, $this->isDryRun); - $servers = $action->getServersPreview(); - - if ($servers->isEmpty()) { - $this->info('No servers to delete.'); - - return true; - } - - $this->warn("Servers to be deleted ({$servers->count()}):"); - $this->table( - ['ID', 'Name', 'IP', 'Description', 'Resources Count'], - $servers->map(function ($server) { - $resourceCount = $server->definedResources()->count(); - - return [ - $server->id, - $server->name, - $server->ip, - $server->description ?? '-', - $resourceCount, - ]; - })->toArray() - ); - $this->newLine(); - - $this->error('⚠️ WARNING: Deleting servers will remove all server configurations!'); - if (! $this->confirm('Are you sure you want to delete all these servers?', false)) { - return false; - } - - if (! $this->isDryRun) { - $this->info('Deleting servers...'); - $result = $action->execute(); - $this->info("Deleted {$result['servers']} servers"); - $this->logAction("Deleted {$result['servers']} servers for user {$this->user->email}"); - } - - return true; - } - - private function handleTeams(): bool - { - $this->newLine(); - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 4: HANDLE TEAMS'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $action = new DeleteUserTeams($this->user, $this->isDryRun); - $preview = $action->getTeamsPreview(); - - // Check for edge cases first - EXIT IMMEDIATELY if found - if ($preview['edge_cases']->isNotEmpty()) { - $this->error('═══════════════════════════════════════'); - $this->error('⚠️ EDGE CASES DETECTED - CANNOT PROCEED'); - $this->error('═══════════════════════════════════════'); - $this->newLine(); - - foreach ($preview['edge_cases'] as $edgeCase) { - $team = $edgeCase['team']; - $reason = $edgeCase['reason']; - $this->error("Team: {$team->name} (ID: {$team->id})"); - $this->error("Issue: {$reason}"); - - // Show team members for context - $this->info('Current members:'); - foreach ($team->members as $member) { - $role = $member->pivot->role; - $this->line(" - {$member->name} ({$member->email}) - Role: {$role}"); - } - - // Check for active resources - $resourceCount = 0; - foreach ($team->servers as $server) { - $resources = $server->definedResources(); - $resourceCount += $resources->count(); - } - - if ($resourceCount > 0) { - $this->warn(" ⚠️ This team has {$resourceCount} active resources!"); - } - - // Show subscription details if relevant - if ($team->subscription && $team->subscription->stripe_subscription_id) { - $this->warn(' ⚠️ Active Stripe subscription details:'); - $this->warn(" Subscription ID: {$team->subscription->stripe_subscription_id}"); - $this->warn(" Customer ID: {$team->subscription->stripe_customer_id}"); - - // Show other owners who could potentially take over - $otherOwners = $team->members - ->where('id', '!=', $this->user->id) - ->filter(function ($member) { - return $member->pivot->role === 'owner'; - }); - - if ($otherOwners->isNotEmpty()) { - $this->info(' Other owners who could take over billing:'); - foreach ($otherOwners as $owner) { - $this->line(" - {$owner->name} ({$owner->email})"); - } - } - } - - $this->newLine(); - } - - $this->error('Please resolve these issues manually before retrying:'); - - // Check if any edge case involves subscription payment issues - $hasSubscriptionIssue = $preview['edge_cases']->contains(function ($edgeCase) { - return str_contains($edgeCase['reason'], 'Stripe subscription'); - }); - - if ($hasSubscriptionIssue) { - $this->info('For teams with subscription payment issues:'); - $this->info('1. Cancel the subscription through Stripe dashboard, OR'); - $this->info('2. Transfer the subscription to another owner\'s payment method, OR'); - $this->info('3. Have the other owner create a new subscription after cancelling this one'); - $this->newLine(); - } - - $hasNoOwnerReplacement = $preview['edge_cases']->contains(function ($edgeCase) { - return str_contains($edgeCase['reason'], 'No suitable owner replacement'); - }); - - if ($hasNoOwnerReplacement) { - $this->info('For teams with no suitable owner replacement:'); - $this->info('1. Assign an admin role to a trusted member, OR'); - $this->info('2. Transfer team resources to another team, OR'); - $this->info('3. Delete the team manually if no longer needed'); - $this->newLine(); - } - - $this->error('USER DELETION ABORTED DUE TO EDGE CASES'); - $this->logAction("User deletion aborted for {$this->user->email}: Edge cases in team handling"); - - // Exit immediately - don't proceed with deletion - if (! $this->isDryRun) { - DB::rollBack(); - } - exit(1); - } - - if ($preview['to_delete']->isEmpty() && - $preview['to_transfer']->isEmpty() && - $preview['to_leave']->isEmpty()) { - $this->info('No team changes needed.'); - - return true; - } - - if ($preview['to_delete']->isNotEmpty()) { - $this->warn('Teams to be DELETED (user is the only member):'); - $this->table( - ['ID', 'Name', 'Resources', 'Subscription'], - $preview['to_delete']->map(function ($team) { - $resourceCount = 0; - foreach ($team->servers as $server) { - $resourceCount += $server->definedResources()->count(); - } - $hasSubscription = $team->subscription && $team->subscription->stripe_subscription_id - ? '⚠️ YES - '.$team->subscription->stripe_subscription_id - : 'No'; - - return [ - $team->id, - $team->name, - $resourceCount, - $hasSubscription, - ]; - })->toArray() - ); - $this->newLine(); - } - - if ($preview['to_transfer']->isNotEmpty()) { - $this->warn('Teams where ownership will be TRANSFERRED:'); - $this->table( - ['Team ID', 'Team Name', 'New Owner', 'New Owner Email'], - $preview['to_transfer']->map(function ($item) { - return [ - $item['team']->id, - $item['team']->name, - $item['new_owner']->name, - $item['new_owner']->email, - ]; - })->toArray() - ); - $this->newLine(); - } - - if ($preview['to_leave']->isNotEmpty()) { - $this->warn('Teams where user will be REMOVED (other owners/admins exist):'); - $userId = $this->user->id; - $this->table( - ['ID', 'Name', 'User Role', 'Other Members'], - $preview['to_leave']->map(function ($team) use ($userId) { - $userRole = $team->members->where('id', $userId)->first()->pivot->role; - $otherMembers = $team->members->count() - 1; - - return [ - $team->id, - $team->name, - $userRole, - $otherMembers, - ]; - })->toArray() - ); - $this->newLine(); - } - - $this->error('⚠️ WARNING: Team changes affect access control and ownership!'); - if (! $this->confirm('Are you sure you want to proceed with these team changes?', false)) { - return false; - } - - if (! $this->isDryRun) { - $this->info('Processing team changes...'); - $result = $action->execute(); - $this->info("Teams deleted: {$result['deleted']}, ownership transferred: {$result['transferred']}, left: {$result['left']}"); - $this->logAction("Team changes for user {$this->user->email}: deleted {$result['deleted']}, transferred {$result['transferred']}, left {$result['left']}"); - } - - return true; - } - - private function cancelStripeSubscriptions(): bool - { - $this->newLine(); - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 5: CANCEL STRIPE SUBSCRIPTIONS'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $action = new CancelSubscription($this->user, $this->isDryRun); - $subscriptions = $action->getSubscriptionsPreview(); - - if ($subscriptions->isEmpty()) { - $this->info('No Stripe subscriptions to cancel.'); - - return true; - } - - $this->info('Stripe subscriptions to cancel:'); - $this->newLine(); - - $totalMonthlyValue = 0; - foreach ($subscriptions as $subscription) { - $team = $subscription->team; - $planId = $subscription->stripe_plan_id; - - // Try to get the price from config - $monthlyValue = $this->getSubscriptionMonthlyValue($planId); - $totalMonthlyValue += $monthlyValue; - - $this->line(" - {$subscription->stripe_subscription_id} (Team: {$team->name})"); - if ($monthlyValue > 0) { - $this->line(" Monthly value: \${$monthlyValue}"); - } - if ($subscription->stripe_cancel_at_period_end) { - $this->line(' ⚠️ Already set to cancel at period end'); - } - } - - if ($totalMonthlyValue > 0) { - $this->newLine(); - $this->warn("Total monthly value: \${$totalMonthlyValue}"); - } - $this->newLine(); - - $this->error('⚠️ WARNING: Subscriptions will be cancelled IMMEDIATELY (not at period end)!'); - if (! $this->confirm('Are you sure you want to cancel all these subscriptions immediately?', false)) { - return false; - } - - if (! $this->isDryRun) { - $this->info('Cancelling subscriptions...'); - $result = $action->execute(); - $this->info("Cancelled {$result['cancelled']} subscriptions, {$result['failed']} failed"); - if ($result['failed'] > 0 && ! empty($result['errors'])) { - $this->error('Failed subscriptions:'); - foreach ($result['errors'] as $error) { - $this->error(" - {$error}"); - } - } - $this->logAction("Cancelled {$result['cancelled']} Stripe subscriptions for user {$this->user->email}"); - } - - return true; - } - - private function deleteUserProfile(): bool - { - $this->newLine(); - $this->info('═══════════════════════════════════════'); - $this->info('PHASE 6: DELETE USER PROFILE'); - $this->info('═══════════════════════════════════════'); - $this->newLine(); - - $this->warn('⚠️ FINAL STEP - This action is IRREVERSIBLE!'); - $this->newLine(); - - $this->info('User profile to be deleted:'); - $this->table( - ['Property', 'Value'], - [ - ['Email', $this->user->email], - ['Name', $this->user->name], - ['User ID', $this->user->id], - ['Created', $this->user->created_at->format('Y-m-d H:i:s')], - ['Email Verified', $this->user->email_verified_at ? 'Yes' : 'No'], - ['2FA Enabled', $this->user->two_factor_confirmed_at ? 'Yes' : 'No'], - ] - ); - - $this->newLine(); - - $this->warn("Type 'DELETE {$this->user->email}' to confirm final deletion:"); - $confirmation = $this->ask('Confirmation'); - - if ($confirmation !== "DELETE {$this->user->email}") { - $this->error('Confirmation text does not match. Deletion cancelled.'); - - return false; - } - - if (! $this->isDryRun) { - $this->info('Deleting user profile...'); - - try { - $this->user->delete(); - $this->info('User profile deleted successfully.'); - $this->logAction("User profile deleted: {$this->user->email}"); - } catch (\Exception $e) { - $this->error('Failed to delete user profile: '.$e->getMessage()); - $this->logAction("Failed to delete user profile {$this->user->email}: ".$e->getMessage()); - - return false; - } - } - - return true; - } - - private function getSubscriptionMonthlyValue(string $planId): int - { - // Try to get pricing from subscription metadata or config - // Since we're using dynamic pricing, return 0 for now - // This could be enhanced by fetching the actual price from Stripe API - - // Check if this is a dynamic pricing plan - $dynamicMonthlyPlanId = config('subscription.stripe_price_id_dynamic_monthly'); - $dynamicYearlyPlanId = config('subscription.stripe_price_id_dynamic_yearly'); - - if ($planId === $dynamicMonthlyPlanId || $planId === $dynamicYearlyPlanId) { - // For dynamic pricing, we can't determine the exact amount without calling Stripe API - // Return 0 to indicate dynamic/usage-based pricing - return 0; - } - - // For any other plans, return 0 as we don't have hardcoded prices - return 0; - } - - private function logAction(string $message): void - { - $logMessage = "[CloudDeleteUser] {$message}"; - - if ($this->isDryRun) { - $logMessage = "[DRY RUN] {$logMessage}"; - } - - Log::channel('single')->info($logMessage); - - // Also log to a dedicated user deletion log file - $logFile = storage_path('logs/user-deletions.log'); - - // Ensure the logs directory exists - $logDir = dirname($logFile); - if (! is_dir($logDir)) { - mkdir($logDir, 0755, true); - } - - $timestamp = now()->format('Y-m-d H:i:s'); - file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX); - } -} diff --git a/app/Events/ServerValidated.php b/app/Events/ServerValidated.php new file mode 100644 index 000000000..95a116ebe --- /dev/null +++ b/app/Events/ServerValidated.php @@ -0,0 +1,51 @@ +check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; + } + $this->teamId = $teamId; + $this->serverUuid = $serverUuid; + } + + public function broadcastOn(): array + { + if (is_null($this->teamId)) { + return []; + } + + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } + + public function broadcastAs(): string + { + return 'ServerValidated'; + } + + public function broadcastWith(): array + { + return [ + 'teamId' => $this->teamId, + 'serverUuid' => $this->serverUuid, + ]; + } +} diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 065d7f767..e9c52d2f5 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -17,6 +17,7 @@ use App\Models\Service; use App\Rules\ValidGitBranch; use App\Rules\ValidGitRepositoryUrl; +use App\Services\DockerImageParser; use Illuminate\Http\Request; use Illuminate\Validation\Rule; use OpenApi\Attributes as OA; @@ -1512,31 +1513,32 @@ private function create_application(Request $request, $type) if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - // Process docker image name and tag for SHA256 digests + // Process docker image name and tag using DockerImageParser $dockerImageName = $request->docker_registry_image_name; $dockerImageTag = $request->docker_registry_image_tag; - // Strip 'sha256:' prefix if user provided it in the tag + // Build the full Docker image string for parsing if ($dockerImageTag) { - $dockerImageTag = preg_replace('/^sha256:/i', '', trim($dockerImageTag)); + $dockerImageString = $dockerImageName.':'.$dockerImageTag; + } else { + $dockerImageString = $dockerImageName; } - // Remove @sha256 from image name if user added it - if ($dockerImageName) { - $dockerImageName = preg_replace('/@sha256$/i', '', trim($dockerImageName)); - } + // Parse using DockerImageParser to normalize the image reference + $parser = new DockerImageParser; + $parser->parse($dockerImageString); - // Check if tag is a valid SHA256 hash (64 hex characters) - $isSha256Hash = $dockerImageTag && preg_match('/^[a-f0-9]{64}$/i', $dockerImageTag); + // Get normalized image name and tag + $normalizedImageName = $parser->getFullImageNameWithoutTag(); - // Append @sha256 to image name if using digest and not already present - if ($isSha256Hash && ! str_ends_with($dockerImageName, '@sha256')) { - $dockerImageName .= '@sha256'; + // Append @sha256 to image name if using digest + if ($parser->isImageHash() && ! str_ends_with($normalizedImageName, '@sha256')) { + $normalizedImageName .= '@sha256'; } // Set processed values back to request - $request->offsetSet('docker_registry_image_name', $dockerImageName); - $request->offsetSet('docker_registry_image_tag', $dockerImageTag ?: 'latest'); + $request->offsetSet('docker_registry_image_name', $normalizedImageName); + $request->offsetSet('docker_registry_image_tag', $parser->getTag()); $application = new Application; removeUnnecessaryFieldsFromRequest($request); @@ -2492,7 +2494,7 @@ public function envs(Request $request) )] public function update_env_by_uuid(Request $request) { - $allowedFields = ['key', 'value', 'is_preview', 'is_literal']; + $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -2520,6 +2522,8 @@ public function update_env_by_uuid(Request $request) 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', + 'is_runtime' => 'boolean', + 'is_buildtime' => 'boolean', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); @@ -2715,7 +2719,7 @@ public function create_bulk_envs(Request $request) ], 400); } $bulk_data = collect($bulk_data)->map(function ($item) { - return collect($item)->only(['key', 'value', 'is_preview', 'is_literal']); + return collect($item)->only(['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime']); }); $returnedEnvs = collect(); foreach ($bulk_data as $item) { @@ -2726,6 +2730,8 @@ public function create_bulk_envs(Request $request) 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', + 'is_runtime' => 'boolean', + 'is_buildtime' => 'boolean', ]); if ($validator->fails()) { return response()->json([ @@ -2885,7 +2891,7 @@ public function create_bulk_envs(Request $request) )] public function create_env(Request $request) { - $allowedFields = ['key', 'value', 'is_preview', 'is_literal']; + $allowedFields = ['key', 'value', 'is_preview', 'is_literal', 'is_multiline', 'is_shown_once', 'is_runtime', 'is_buildtime']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -2908,6 +2914,8 @@ public function create_env(Request $request) 'is_literal' => 'boolean', 'is_multiline' => 'boolean', 'is_shown_once' => 'boolean', + 'is_runtime' => 'boolean', + 'is_buildtime' => 'boolean', ]); $extraFields = array_diff(array_keys($request->all()), $allowedFields); diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 5871f481a..46282fddb 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -317,6 +317,10 @@ public function database_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_by_uuid(Request $request) @@ -593,6 +597,224 @@ public function update_by_uuid(Request $request) ]); } + #[OA\Post( + summary: 'Create Backup', + description: 'Create a new scheduled backup configuration for a database', + path: '/databases/{uuid}/backups', + operationId: 'create-database-backup', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Backup configuration data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['frequency'], + properties: [ + 'frequency' => ['type' => 'string', 'description' => 'Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)'], + 'enabled' => ['type' => 'boolean', 'description' => 'Whether the backup is enabled', 'default' => true], + 'save_s3' => ['type' => 'boolean', 'description' => 'Whether to save backups to S3', 'default' => false], + 's3_storage_uuid' => ['type' => 'string', 'description' => 'S3 storage UUID (required if save_s3 is true)'], + 'databases_to_backup' => ['type' => 'string', 'description' => 'Comma separated list of databases to backup'], + 'dump_all' => ['type' => 'boolean', 'description' => 'Whether to dump all databases', 'default' => false], + 'backup_now' => ['type' => 'boolean', 'description' => 'Whether to trigger backup immediately after creation'], + 'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Number of backups to retain locally'], + 'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Number of days to retain backups locally'], + 'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage (MB) for local backups'], + 'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Number of backups to retain in S3'], + 'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'], + 'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage (MB) for S3 backups'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 201, + description: 'Backup configuration created successfully', + content: new OA\JsonContent( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'format' => 'uuid', 'example' => '550e8400-e29b-41d4-a716-446655440000'], + 'message' => ['type' => 'string', 'example' => 'Backup configuration created successfully.'], + ] + ) + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), + ] + )] + public function create_backup(Request $request) + { + $backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', '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', 's3_storage_uuid']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + // Validate incoming request is valid JSON + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'frequency' => 'required|string', + 'enabled' => 'boolean', + 'save_s3' => 'boolean', + 'dump_all' => 'boolean', + 'backup_now' => 'boolean|nullable', + 's3_storage_uuid' => 'string|exists:s3_storages,uuid|nullable', + 'databases_to_backup' => 'string|nullable', + 'database_backup_retention_amount_locally' => 'integer|min:0', + 'database_backup_retention_days_locally' => 'integer|min:0', + 'database_backup_retention_max_storage_locally' => 'integer|min:0', + 'database_backup_retention_amount_s3' => 'integer|min:0', + 'database_backup_retention_days_s3' => 'integer|min:0', + 'database_backup_retention_max_storage_s3' => 'integer|min:0', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + + $uuid = $request->uuid; + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + $this->authorize('manageBackups', $database); + + // Validate frequency is a valid cron expression + $isValid = validate_cron_expression($request->frequency); + if (! $isValid) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['frequency' => ['Invalid cron expression or frequency format.']], + ], 422); + } + + // Validate S3 storage if save_s3 is true + if ($request->boolean('save_s3') && ! $request->filled('s3_storage_uuid')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The s3_storage_uuid field is required when save_s3 is true.']], + ], 422); + } + + if ($request->filled('s3_storage_uuid')) { + $existsInTeam = S3Storage::ownedByCurrentTeam()->where('uuid', $request->s3_storage_uuid)->exists(); + if (! $existsInTeam) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']], + ], 422); + } + } + + // Check for extra fields + $extraFields = array_diff(array_keys($request->all()), $backupConfigFields, ['backup_now']); + if (! empty($extraFields)) { + $errors = $validator->errors(); + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $backupData = $request->only($backupConfigFields); + + // Convert s3_storage_uuid to s3_storage_id + if (isset($backupData['s3_storage_uuid'])) { + $s3Storage = S3Storage::ownedByCurrentTeam()->where('uuid', $backupData['s3_storage_uuid'])->first(); + if ($s3Storage) { + $backupData['s3_storage_id'] = $s3Storage->id; + } elseif ($request->boolean('save_s3')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['s3_storage_uuid' => ['The selected S3 storage is invalid for this team.']], + ], 422); + } + unset($backupData['s3_storage_uuid']); + } + + // Set default databases_to_backup based on database type if not provided + if (! isset($backupData['databases_to_backup']) || empty($backupData['databases_to_backup'])) { + if ($database->type() === 'standalone-postgresql') { + $backupData['databases_to_backup'] = $database->postgres_db; + } elseif ($database->type() === 'standalone-mysql') { + $backupData['databases_to_backup'] = $database->mysql_database; + } elseif ($database->type() === 'standalone-mariadb') { + $backupData['databases_to_backup'] = $database->mariadb_database; + } + } + + // Add required fields + $backupData['database_id'] = $database->id; + $backupData['database_type'] = $database->getMorphClass(); + $backupData['team_id'] = $teamId; + + // Set defaults + if (! isset($backupData['enabled'])) { + $backupData['enabled'] = true; + } + + $backupConfig = ScheduledDatabaseBackup::create($backupData); + + // Trigger immediate backup if requested + if ($request->backup_now) { + dispatch(new DatabaseBackupJob($backupConfig)); + } + + return response()->json([ + 'uuid' => $backupConfig->uuid, + 'message' => 'Backup configuration created successfully.', + ], 201); + } + #[OA\Patch( summary: 'Update', description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID', @@ -666,6 +888,10 @@ public function update_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_backup(Request $request) @@ -844,6 +1070,10 @@ public function update_backup(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_postgresql(Request $request) @@ -907,6 +1137,10 @@ public function create_database_postgresql(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_clickhouse(Request $request) @@ -969,6 +1203,10 @@ public function create_database_clickhouse(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_dragonfly(Request $request) @@ -1032,6 +1270,10 @@ public function create_database_dragonfly(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_redis(Request $request) @@ -1095,6 +1337,10 @@ public function create_database_redis(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_keydb(Request $request) @@ -1161,6 +1407,10 @@ public function create_database_keydb(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_mariadb(Request $request) @@ -1227,6 +1477,10 @@ public function create_database_mariadb(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_mysql(Request $request) @@ -1290,6 +1544,10 @@ public function create_database_mysql(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_database_mongodb(Request $request) @@ -1941,7 +2199,7 @@ public function delete_by_uuid(Request $request) content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup configuration and all executions deleted.'), + new OA\Property(property: 'message', type: 'string', example: 'Backup configuration and all executions deleted.'), ] ) ), @@ -1951,7 +2209,7 @@ public function delete_by_uuid(Request $request) content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup configuration not found.'), + new OA\Property(property: 'message', type: 'string', example: 'Backup configuration not found.'), ] ) ), @@ -2065,7 +2323,7 @@ public function delete_backup_by_uuid(Request $request) content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup execution deleted.'), + new OA\Property(property: 'message', type: 'string', example: 'Backup execution deleted.'), ] ) ), @@ -2075,7 +2333,7 @@ public function delete_backup_by_uuid(Request $request) content: new OA\JsonContent( type: 'object', properties: [ - 'message' => new OA\Schema(type: 'string', example: 'Backup execution not found.'), + new OA\Property(property: 'message', type: 'string', example: 'Backup execution not found.'), ] ) ), @@ -2171,17 +2429,18 @@ public function delete_execution_by_uuid(Request $request) content: new OA\JsonContent( type: 'object', properties: [ - 'executions' => new OA\Schema( + new OA\Property( + property: 'executions', type: 'array', items: new OA\Items( type: 'object', properties: [ - 'uuid' => ['type' => 'string'], - 'filename' => ['type' => 'string'], - 'size' => ['type' => 'integer'], - 'created_at' => ['type' => 'string'], - 'message' => ['type' => 'string'], - 'status' => ['type' => 'string'], + new OA\Property(property: 'uuid', type: 'string'), + new OA\Property(property: 'filename', type: 'string'), + new OA\Property(property: 'size', type: 'integer'), + new OA\Property(property: 'created_at', type: 'string'), + new OA\Property(property: 'message', type: 'string'), + new OA\Property(property: 'status', type: 'string'), ] ) ), diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index c4d603392..16a7b6f71 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -131,6 +131,161 @@ public function deployment_by_uuid(Request $request) return response()->json($this->removeSensitiveData($deployment)); } + #[OA\Post( + summary: 'Cancel', + description: 'Cancel a deployment by UUID.', + path: '/deployments/{uuid}/cancel', + operationId: 'cancel-deployment-by-uuid', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Deployment cancelled successfully.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Deployment cancelled successfully.'], + 'deployment_uuid' => ['type' => 'string', 'example' => 'cm37r6cqj000008jm0veg5tkm'], + 'status' => ['type' => 'string', 'example' => 'cancelled-by-user'], + ] + ) + ), + ]), + new OA\Response( + response: 400, + description: 'Deployment cannot be cancelled (already finished/failed/cancelled).', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Deployment cannot be cancelled. Current status: finished'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 403, + description: 'User doesn\'t have permission to cancel this deployment.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'You do not have permission to cancel this deployment.'], + ] + ) + ), + ]), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function cancel_deployment(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + + // Find the deployment by UUID + $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first(); + if (! $deployment) { + return response()->json(['message' => 'Deployment not found.'], 404); + } + + // Check if the deployment belongs to the user's team + $servers = Server::whereTeamId($teamId)->pluck('id'); + if (! $servers->contains($deployment->server_id)) { + return response()->json(['message' => 'You do not have permission to cancel this deployment.'], 403); + } + + // Check if deployment can be cancelled (must be queued or in_progress) + $cancellableStatuses = [ + \App\Enums\ApplicationDeploymentStatus::QUEUED->value, + \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ]; + + if (! in_array($deployment->status, $cancellableStatuses)) { + return response()->json([ + 'message' => "Deployment cannot be cancelled. Current status: {$deployment->status}", + ], 400); + } + + // Perform the cancellation + try { + $deployment_uuid = $deployment->deployment_uuid; + $kill_command = "docker rm -f {$deployment_uuid}"; + $build_server_id = $deployment->build_server_id ?? $deployment->server_id; + + // Mark deployment as cancelled + $deployment->update([ + 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + + // Get the server + $server = Server::find($build_server_id); + + if ($server) { + // Add cancellation log entry + $deployment->addLogEntry('Deployment cancelled by user via API.', 'stderr'); + + // Check if container exists and kill it + $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'"; + $containerExists = instant_remote_process([$checkCommand], $server); + + if ($containerExists && str($containerExists)->trim()->isNotEmpty()) { + instant_remote_process([$kill_command], $server); + $deployment->addLogEntry('Deployment container stopped.'); + } else { + $deployment->addLogEntry('Deployment container not yet started. Will be cancelled when job checks status.'); + } + + // Kill running process if process ID exists + if ($deployment->current_process_id) { + try { + $processKillCommand = "kill -9 {$deployment->current_process_id}"; + instant_remote_process([$processKillCommand], $server); + } catch (\Throwable $e) { + // Process might already be gone + } + } + } + + return response()->json([ + 'message' => 'Deployment cancelled successfully.', + 'deployment_uuid' => $deployment->deployment_uuid, + 'status' => $deployment->status, + ]); + } catch (\Throwable $e) { + return response()->json([ + 'message' => 'Failed to cancel deployment: '.$e->getMessage(), + ], 500); + } + } + #[OA\Get( summary: 'Deploy', description: 'Deploy by tag or uuid. `Post` request also accepted with `uuid` and `tag` json body.', diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index 8c8c87238..f6a6b3513 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -12,6 +12,88 @@ class GithubController extends Controller { + private function removeSensitiveData($githubApp) + { + $githubApp->makeHidden([ + 'client_secret', + 'webhook_secret', + ]); + + return serializeApiResponse($githubApp); + } + + #[OA\Get( + summary: 'List', + description: 'List all GitHub apps.', + path: '/github-apps', + operationId: 'list-github-apps', + security: [ + ['bearerAuth' => []], + ], + tags: ['GitHub Apps'], + responses: [ + new OA\Response( + response: 200, + description: 'List of GitHub apps.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'organization' => ['type' => 'string', 'nullable' => true], + 'api_url' => ['type' => 'string'], + 'html_url' => ['type' => 'string'], + 'custom_user' => ['type' => 'string'], + 'custom_port' => ['type' => 'integer'], + 'app_id' => ['type' => 'integer'], + 'installation_id' => ['type' => 'integer'], + 'client_id' => ['type' => 'string'], + 'private_key_id' => ['type' => 'integer'], + 'is_system_wide' => ['type' => 'boolean'], + 'is_public' => ['type' => 'boolean'], + 'team_id' => ['type' => 'integer'], + 'type' => ['type' => 'string'], + ] + ) + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function list_github_apps(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $githubApps = GithubApp::where(function ($query) use ($teamId) { + $query->where('team_id', $teamId) + ->orWhere('is_system_wide', true); + })->get(); + + $githubApps = $githubApps->map(function ($app) { + return $this->removeSensitiveData($app); + }); + + return response()->json($githubApps); + } + #[OA\Post( summary: 'Create GitHub App', description: 'Create a new GitHub app.', @@ -219,7 +301,8 @@ public function create_github_app(Request $request) schema: new OA\Schema( type: 'object', properties: [ - 'repositories' => new OA\Schema( + new OA\Property( + property: 'repositories', type: 'array', items: new OA\Items(type: 'object') ), @@ -335,7 +418,8 @@ public function load_repositories($github_app_id) schema: new OA\Schema( type: 'object', properties: [ - 'branches' => new OA\Schema( + new OA\Property( + property: 'branches', type: 'array', items: new OA\Items(type: 'object') ), @@ -457,7 +541,7 @@ public function load_branches($github_app_id, $owner, $repo) ), new OA\Response(response: 401, description: 'Unauthorized'), new OA\Response(response: 404, description: 'GitHub app not found'), - new OA\Response(response: 422, description: 'Validation error'), + new OA\Response(response: 422, ref: '#/components/responses/422'), ] )] public function update_github_app(Request $request, $github_app_id) diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php index 60337a76c..69f71feaf 100644 --- a/app/Http/Controllers/Api/OpenApi.php +++ b/app/Http/Controllers/Api/OpenApi.php @@ -40,6 +40,27 @@ new OA\Property(property: 'message', type: 'string', example: 'Resource not found.'), ] )), + new OA\Response( + response: 422, + description: 'Validation error.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'Validation error.'), + new OA\Property( + property: 'errors', + type: 'object', + additionalProperties: new OA\AdditionalProperties( + type: 'array', + items: new OA\Items(type: 'string') + ), + example: [ + 'name' => ['The name field is required.'], + 'api_url' => ['The api url field is required.', 'The api url format is invalid.'], + ] + ), + ] + )), ], )] class OpenApi diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 303e6535d..8f2ba25c8 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -21,8 +21,9 @@ class OtherController extends Controller new OA\Response( response: 200, description: 'Returns the version of the application', - content: new OA\JsonContent( - type: 'string', + content: new OA\MediaType( + mediaType: 'text/html', + schema: new OA\Schema(type: 'string'), example: 'v4.0.0', )), new OA\Response( @@ -166,8 +167,9 @@ public function feedback(Request $request) new OA\Response( response: 200, description: 'Healthcheck endpoint.', - content: new OA\JsonContent( - type: 'string', + content: new OA\MediaType( + mediaType: 'text/html', + schema: new OA\Schema(type: 'string'), example: 'OK', )), new OA\Response( diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index e688b8980..951a1f5d9 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -134,6 +134,10 @@ public function project_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function environment_details(Request $request) @@ -214,6 +218,10 @@ public function environment_details(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_project(Request $request) @@ -324,6 +332,10 @@ public function create_project(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_project(Request $request) @@ -425,6 +437,10 @@ public function update_project(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function delete_project(Request $request) @@ -487,6 +503,10 @@ public function delete_project(Request $request) response: 404, description: 'Project not found.', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function get_environments(Request $request) @@ -566,6 +586,10 @@ public function get_environments(Request $request) response: 409, description: 'Environment with this name already exists.', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_environment(Request $request) @@ -663,6 +687,10 @@ public function create_environment(Request $request) response: 404, description: 'Project or environment not found.', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function delete_environment(Request $request) diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index 55a6cd9f4..e7b36cb9a 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -163,6 +163,10 @@ public function key_by_uuid(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_key(Request $request) @@ -282,6 +286,10 @@ public function create_key(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_key(Request $request) diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index cbd20400a..06baf2dde 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -447,6 +447,10 @@ public function domains_by_server(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_server(Request $request) @@ -604,6 +608,10 @@ public function create_server(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_server(Request $request) @@ -722,6 +730,10 @@ public function update_server(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function delete_server(Request $request) @@ -746,7 +758,13 @@ public function delete_server(Request $request) return response()->json(['message' => 'Local server cannot be deleted.'], 400); } $server->delete(); - DeleteServer::dispatch($server); + DeleteServer::dispatch( + $server->id, + false, // Don't delete from Hetzner via API + $server->hetzner_server_id, + $server->cloud_provider_token_id, + $server->team_id + ); return response()->json(['message' => 'Server deleted.']); } @@ -790,6 +808,10 @@ public function delete_server(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function validate_server(Request $request) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index e240e326e..b3565a933 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -235,6 +235,10 @@ public function services(Request $request) response: 400, ref: '#/components/responses/400', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_service(Request $request) @@ -324,9 +328,23 @@ public function create_service(Request $request) }); } if ($oneClickService) { - $service_payload = [ + $dockerComposeRaw = base64_decode($oneClickService); + + // Validate for command injection BEFORE creating service + try { + validateDockerComposeForInjection($dockerComposeRaw); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => $e->getMessage(), + ], + ], 422); + } + + $servicePayload = [ 'name' => "$oneClickServiceName-".str()->random(10), - 'docker_compose_raw' => base64_decode($oneClickService), + 'docker_compose_raw' => $dockerComposeRaw, 'environment_id' => $environment->id, 'service_type' => $oneClickServiceName, 'server_id' => $server->id, @@ -334,9 +352,9 @@ public function create_service(Request $request) 'destination_type' => $destination->getMorphClass(), ]; if ($oneClickServiceName === 'cloudflared') { - data_set($service_payload, 'connect_to_docker_network', true); + data_set($servicePayload, 'connect_to_docker_network', true); } - $service = Service::create($service_payload); + $service = Service::create($servicePayload); $service->name = "$oneClickServiceName-".$service->uuid; $service->save(); if ($oneClickDotEnvs?->count() > 0) { @@ -458,6 +476,18 @@ public function create_service(Request $request) $dockerCompose = base64_decode($request->docker_compose_raw); $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + // Validate for command injection BEFORE saving to database + try { + validateDockerComposeForInjection($dockerComposeRaw); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => $e->getMessage(), + ], + ], 422); + } + $connectToDockerNetwork = $request->connect_to_docker_network ?? false; $instantDeploy = $request->instant_deploy ?? false; @@ -704,6 +734,10 @@ public function delete_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_by_uuid(Request $request) @@ -769,6 +803,19 @@ public function update_by_uuid(Request $request) } $dockerCompose = base64_decode($request->docker_compose_raw); $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // Validate for command injection BEFORE saving to database + try { + validateDockerComposeForInjection($dockerComposeRaw); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => $e->getMessage(), + ], + ], 422); + } + $service->docker_compose_raw = $dockerComposeRaw; } @@ -954,6 +1001,10 @@ public function envs(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function update_env_by_uuid(Request $request) @@ -1075,6 +1126,10 @@ public function update_env_by_uuid(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_bulk_envs(Request $request) @@ -1191,6 +1246,10 @@ public function create_bulk_envs(Request $request) response: 404, ref: '#/components/responses/404', ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), ] )] public function create_env(Request $request) diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e9d7b82b2..515d40c62 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -14,7 +14,7 @@ class Kernel extends HttpKernel * @var array */ protected $middleware = [ - // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php index c9c58bddc..c2a2cb41a 100644 --- a/app/Http/Middleware/TrustHosts.php +++ b/app/Http/Middleware/TrustHosts.php @@ -2,7 +2,10 @@ namespace App\Http\Middleware; +use App\Models\InstanceSettings; use Illuminate\Http\Middleware\TrustHosts as Middleware; +use Illuminate\Support\Facades\Cache; +use Spatie\Url\Url; class TrustHosts extends Middleware { @@ -13,8 +16,37 @@ class TrustHosts extends Middleware */ public function hosts(): array { - return [ - $this->allSubdomainsOfApplicationUrl(), - ]; + $trustedHosts = []; + + // Trust the configured FQDN from InstanceSettings (cached to avoid DB query on every request) + // Use empty string as sentinel value instead of null so negative results are cached + $fqdnHost = Cache::remember('instance_settings_fqdn_host', 300, function () { + try { + $settings = InstanceSettings::get(); + if ($settings && $settings->fqdn) { + $url = Url::fromString($settings->fqdn); + $host = $url->getHost(); + + return $host ?: ''; + } + } catch (\Exception $e) { + // If instance settings table doesn't exist yet (during installation), + // return empty string (sentinel) so this result is cached + } + + return ''; + }); + + // Convert sentinel value back to null for consumption + $fqdnHost = $fqdnHost !== '' ? $fqdnHost : null; + + if ($fqdnHost) { + $trustedHosts[] = $fqdnHost; + } + + // Trust all subdomains of APP_URL as fallback + $trustedHosts[] = $this->allSubdomainsOfApplicationUrl(); + + return array_filter($trustedHosts); } } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 381857f83..aac7408ce 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -484,9 +484,18 @@ private function deploy_simple_dockerfile() ); $this->generate_image_names(); $this->generate_compose_file(); + + // Save build-time .env file BEFORE the build + $this->save_buildtime_environment_variables(); + $this->generate_build_env_variables(); $this->add_build_env_variables_to_dockerfile(); $this->build_image(); + + // Save runtime environment variables AFTER the build + // This overwrites the build-time .env with ALL variables (build-time + runtime) + $this->save_runtime_environment_variables(); + $this->push_to_docker_registry(); $this->rolling_update(); } @@ -1310,12 +1319,18 @@ private function save_runtime_environment_variables() private function generate_buildtime_environment_variables() { + if (isDev()) { + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry('[DEBUG] Generating build-time environment variables'); + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + } + $envs = collect([]); $coolify_envs = $this->generate_coolify_env_variables(); // Add COOLIFY variables $coolify_envs->each(function ($item, $key) use ($envs) { - $envs->push($key.'='.$item); + $envs->push($key.'='.escapeBashEnvValue($item)); }); // Add SERVICE_NAME variables for Docker Compose builds @@ -1329,7 +1344,7 @@ private function generate_buildtime_environment_variables() } $services = data_get($dockerCompose, 'services', []); foreach ($services as $serviceName => $_) { - $envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.$serviceName); + $envs->push('SERVICE_NAME_'.str($serviceName)->upper().'='.escapeBashEnvValue($serviceName)); } // Generate SERVICE_FQDN & SERVICE_URL for non-PR deployments @@ -1342,8 +1357,8 @@ private function generate_buildtime_environment_variables() $coolifyScheme = $coolifyUrl->getScheme(); $coolifyFqdn = $coolifyUrl->getHost(); $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); - $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); - $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.escapeBashEnvValue($coolifyUrl->__toString())); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.escapeBashEnvValue($coolifyFqdn)); } } } else { @@ -1351,7 +1366,7 @@ private function generate_buildtime_environment_variables() $rawDockerCompose = Yaml::parse($this->application->docker_compose_raw); $rawServices = data_get($rawDockerCompose, 'services', []); foreach ($rawServices as $rawServiceName => $_) { - $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id)); + $envs->push('SERVICE_NAME_'.str($rawServiceName)->upper().'='.escapeBashEnvValue(addPreviewDeploymentSuffix($rawServiceName, $this->pull_request_id))); } // Generate SERVICE_FQDN & SERVICE_URL for preview deployments with PR-specific domains @@ -1364,8 +1379,8 @@ private function generate_buildtime_environment_variables() $coolifyScheme = $coolifyUrl->getScheme(); $coolifyFqdn = $coolifyUrl->getHost(); $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); - $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); - $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.escapeBashEnvValue($coolifyUrl->__toString())); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.escapeBashEnvValue($coolifyFqdn)); } } } @@ -1387,7 +1402,32 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { - $envs->push($env->key.'='.$env->real_value); + // For literal/multiline vars, real_value includes quotes that we need to remove + if ($env->is_literal || $env->is_multiline) { + // Strip outer quotes from real_value and apply proper bash escaping + $value = trim($env->real_value, "'"); + $escapedValue = escapeBashEnvValue($value); + $envs->push($env->key.'='.$escapedValue); + + if (isDev()) { + $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] Type: literal/multiline'); + $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] stripped value: {$value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); + } + } else { + // For normal vars, use double quotes to allow $VAR expansion + $escapedValue = escapeBashDoubleQuoted($env->real_value); + $envs->push($env->key.'='.$escapedValue); + + if (isDev()) { + $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] Type: normal (allows expansion)'); + $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); + } + } } } else { $sorted_environment_variables = $this->application->environment_variables_preview() @@ -1404,11 +1444,42 @@ private function generate_buildtime_environment_variables() } foreach ($sorted_environment_variables as $env) { - $envs->push($env->key.'='.$env->real_value); + // For literal/multiline vars, real_value includes quotes that we need to remove + if ($env->is_literal || $env->is_multiline) { + // Strip outer quotes from real_value and apply proper bash escaping + $value = trim($env->real_value, "'"); + $escapedValue = escapeBashEnvValue($value); + $envs->push($env->key.'='.$escapedValue); + + if (isDev()) { + $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] Type: literal/multiline'); + $this->application_deployment_queue->addLogEntry("[DEBUG] raw real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] stripped value: {$value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); + } + } else { + // For normal vars, use double quotes to allow $VAR expansion + $escapedValue = escapeBashDoubleQuoted($env->real_value); + $envs->push($env->key.'='.$escapedValue); + + if (isDev()) { + $this->application_deployment_queue->addLogEntry("[DEBUG] Build-time env: {$env->key}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] Type: normal (allows expansion)'); + $this->application_deployment_queue->addLogEntry("[DEBUG] real_value: {$env->real_value}"); + $this->application_deployment_queue->addLogEntry("[DEBUG] final escaped: {$escapedValue}"); + } + } } } // Return the generated environment variables + if (isDev()) { + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + $this->application_deployment_queue->addLogEntry("[DEBUG] Total build-time env variables: {$envs->count()}"); + $this->application_deployment_queue->addLogEntry('[DEBUG] ========================================'); + } + return $envs; } @@ -1432,9 +1503,9 @@ private function save_buildtime_environment_variables() 'hidden' => true, ], ); - } elseif ($this->build_pack === 'dockercompose') { - // For Docker Compose, create an empty .env file even if there are no build-time variables - // This ensures the file exists when referenced in docker-compose commands + } elseif ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerfile') { + // For Docker Compose and Dockerfile, create an empty .env file even if there are no build-time variables + // This ensures the file exists when referenced in build commands $this->application_deployment_queue->addLogEntry('Creating empty build-time .env file in /artifacts (no build-time variables defined).', hidden: true); $this->execute_remote_command( @@ -1888,9 +1959,27 @@ private function check_git_if_build_needed() ); } if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) { - $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t"); - $this->application_deployment_queue->commit = $this->commit; - $this->application_deployment_queue->save(); + // Extract commit SHA from git ls-remote output, handling multi-line output (e.g., redirect warnings) + // Expected format: "commit_sha\trefs/heads/branch" possibly preceded by warning lines + // Note: Git warnings can be on the same line as the result (no newline) + $lsRemoteOutput = $this->saved_outputs->get('git_commit_sha'); + + // Find the part containing a tab (the actual ls-remote result) + // Handle cases where warning is on the same line as the result + if ($lsRemoteOutput->contains("\t")) { + // Get everything from the last occurrence of a valid commit SHA pattern before the tab + // A valid commit SHA is 40 hex characters + $output = $lsRemoteOutput->value(); + + // Extract the line with the tab (actual ls-remote result) + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + + if (isset($matches[1])) { + $this->commit = $matches[1]; + $this->application_deployment_queue->commit = $this->commit; + $this->application_deployment_queue->save(); + } + } } $this->set_coolify_variables(); @@ -1905,7 +1994,7 @@ private function clone_repository() { $importCommands = $this->generate_git_import_commands(); $this->application_deployment_queue->addLogEntry("\n----------------------------------------"); - $this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}."); + $this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->commit}) to {$this->basedir}."); if ($this->pull_request_id !== 0) { $this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head."); } @@ -2701,10 +2790,12 @@ private function build_image() ] ); } + $publishDir = trim($this->application->publish_directory, '/'); + $publishDir = $publishDir ? "/{$publishDir}" : ''; $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} -COPY --from=$this->build_image_name /app/{$this->application->publish_directory} . +COPY --from=$this->build_image_name /app{$publishDir} . COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { $nginx_config = base64_encode($this->application->custom_nginx_configuration); @@ -3196,7 +3287,7 @@ private function add_build_env_variables_to_dockerfile() } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); - $this->application_deployment_queue->addLogEntry('Final Dockerfile:', type: 'info'); + $this->application_deployment_queue->addLogEntry('Final Dockerfile:', type: 'info', hidden: true); $this->execute_remote_command( [ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"), diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index 471edb4c6..d2e3cc964 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -35,20 +35,24 @@ public function handle() if ($this->application->is_public_repository()) { return; } + + $serviceName = $this->application->name; + if ($this->status === ProcessStatus::CLOSED) { $this->delete_comment(); return; - } elseif ($this->status === ProcessStatus::IN_PROGRESS) { - $this->body = "The preview deployment is in progress. 🟡\n\n"; - } elseif ($this->status === ProcessStatus::FINISHED) { - $this->body = "The preview deployment is ready. 🟢\n\n"; - if ($this->preview->fqdn) { - $this->body .= "[Open Preview]({$this->preview->fqdn}) | "; - } - } elseif ($this->status === ProcessStatus::ERROR) { - $this->body = "The preview deployment failed. 🔴\n\n"; } + + match ($this->status) { + ProcessStatus::QUEUED => $this->body = "The preview deployment for **{$serviceName}** is queued. ⏳\n\n", + ProcessStatus::IN_PROGRESS => $this->body = "The preview deployment for **{$serviceName}** is in progress. 🟡\n\n", + ProcessStatus::FINISHED => $this->body = "The preview deployment for **{$serviceName}** is ready. 🟢\n\n".($this->preview->fqdn ? "[Open Preview]({$this->preview->fqdn}) | " : ''), + ProcessStatus::ERROR => $this->body = "The preview deployment for **{$serviceName}** failed. 🔴\n\n", + ProcessStatus::KILLED => $this->body = "The preview deployment for **{$serviceName}** was killed. ⚫\n\n", + ProcessStatus::CANCELLED => $this->body = "The preview deployment for **{$serviceName}** was cancelled. 🚫\n\n", + ProcessStatus::CLOSED => '', // Already handled above, but included for completeness + }; $this->build_logs_url = base_url()."/project/{$this->application->environment->project->uuid}/environment/{$this->application->environment->uuid}/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; $this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n"; diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 3cc372fd1..11da6fac1 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -69,13 +69,12 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 3600; - public string $backup_log_uuid; + public ?string $backup_log_uuid = null; public function __construct(public ScheduledDatabaseBackup $backup) { $this->onQueue('high'); $this->timeout = $backup->timeout; - $this->backup_log_uuid = (string) new Cuid2; } public function handle(): void diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php index cf598c75c..c0284e1ee 100644 --- a/app/Jobs/RegenerateSslCertJob.php +++ b/app/Jobs/RegenerateSslCertJob.php @@ -45,7 +45,7 @@ public function handle() $query->cursor()->each(function ($certificate) use ($regenerated) { try { - $caCert = SslCertificate::where('server_id', $certificate->server_id) + $caCert = $certificate->server->sslCertificates() ->where('is_ca_certificate', true) ->first(); diff --git a/app/Jobs/SendWebhookJob.php b/app/Jobs/SendWebhookJob.php new file mode 100644 index 000000000..607fda3fe --- /dev/null +++ b/app/Jobs/SendWebhookJob.php @@ -0,0 +1,60 @@ +onQueue('high'); + } + + /** + * Execute the job. + */ + public function handle(): void + { + if (isDev()) { + ray('Sending webhook notification', [ + 'url' => $this->webhookUrl, + 'payload' => $this->payload, + ]); + } + + $response = Http::post($this->webhookUrl, $this->payload); + + if (isDev()) { + ray('Webhook response', [ + 'status' => $response->status(), + 'body' => $response->body(), + 'successful' => $response->successful(), + ]); + } + } +} diff --git a/app/Jobs/ServerConnectionCheckJob.php b/app/Jobs/ServerConnectionCheckJob.php index 8b55434f6..9dbce4bfe 100644 --- a/app/Jobs/ServerConnectionCheckJob.php +++ b/app/Jobs/ServerConnectionCheckJob.php @@ -54,6 +54,11 @@ public function handle() return; } + // Check Hetzner server status if applicable + if ($this->server->hetzner_server_id && $this->server->cloudProviderToken) { + $this->checkHetznerStatus(); + } + // Temporarily disable mux if requested if ($this->disableMux) { $this->disableSshMux(); @@ -86,6 +91,11 @@ public function handle() ]); } catch (\Throwable $e) { + + Log::error('ServerConnectionCheckJob failed', [ + 'error' => $e->getMessage(), + 'server_id' => $this->server->id, + ]); $this->server->settings->update([ 'is_reachable' => false, 'is_usable' => false, @@ -95,6 +105,30 @@ public function handle() } } + private function checkHetznerStatus(): void + { + try { + $hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token); + $serverData = $hetznerService->getServer($this->server->hetzner_server_id); + $status = $serverData['status'] ?? null; + + } catch (\Throwable $e) { + Log::debug('ServerConnectionCheck: Hetzner status check failed', [ + 'server_id' => $this->server->id, + 'error' => $e->getMessage(), + ]); + } + if ($this->server->hetzner_server_status !== $status) { + $this->server->update(['hetzner_server_status' => $status]); + $this->server->hetzner_server_status = $status; + if ($status === 'off') { + ray('Server is powered off, marking as unreachable'); + throw new \Exception('Server is powered off'); + } + } + + } + private function checkConnection(): bool { try { diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php new file mode 100644 index 000000000..388791f10 --- /dev/null +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -0,0 +1,162 @@ +onQueue('high'); + } + + public function handle(): void + { + try { + // Mark validation as in progress + $this->server->update(['is_validating' => true]); + + Log::info('ValidateAndInstallServer: Starting validation', [ + 'server_id' => $this->server->id, + 'server_name' => $this->server->name, + 'attempt' => $this->numberOfTries + 1, + ]); + + // 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; + $this->server->update([ + 'validation_logs' => $errorMessage, + 'is_validating' => false, + ]); + Log::error('ValidateAndInstallServer: Server not reachable', [ + 'server_id' => $this->server->id, + 'error' => $error, + ]); + + return; + } + + // Validate OS + $supportedOsType = $this->server->validateOS(); + if (! $supportedOsType) { + $errorMessage = 'Server OS type is not supported. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $errorMessage, + 'is_validating' => false, + ]); + Log::error('ValidateAndInstallServer: OS not supported', [ + 'server_id' => $this->server->id, + ]); + + return; + } + + // Check if Docker is installed + $dockerInstalled = $this->server->validateDockerEngine(); + $dockerComposeInstalled = $this->server->validateDockerCompose(); + + if (! $dockerInstalled || ! $dockerComposeInstalled) { + // Try to install Docker + if ($this->numberOfTries >= $this->maxTries) { + $errorMessage = 'Docker Engine could not be installed after '.$this->maxTries.' attempts. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $errorMessage, + 'is_validating' => false, + ]); + Log::error('ValidateAndInstallServer: Docker installation failed after max tries', [ + 'server_id' => $this->server->id, + 'attempts' => $this->numberOfTries, + ]); + + return; + } + + Log::info('ValidateAndInstallServer: Installing Docker', [ + 'server_id' => $this->server->id, + 'attempt' => $this->numberOfTries + 1, + ]); + + // Install Docker + $this->server->installDocker(); + + // Retry validation after installation + self::dispatch($this->server, $this->numberOfTries + 1)->delay(now()->addSeconds(30)); + + return; + } + + // Validate Docker version + $dockerVersion = $this->server->validateDockerEngineVersion(); + if (! $dockerVersion) { + $requiredDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); + $errorMessage = 'Minimum Docker Engine version '.$requiredDockerVersion.' is not installed. Please install Docker manually before continuing: documentation.'; + $this->server->update([ + 'validation_logs' => $errorMessage, + 'is_validating' => false, + ]); + Log::error('ValidateAndInstallServer: Docker version not sufficient', [ + 'server_id' => $this->server->id, + ]); + + return; + } + + // Validation successful! + Log::info('ValidateAndInstallServer: Validation successful', [ + 'server_id' => $this->server->id, + 'server_name' => $this->server->name, + ]); + + // Start proxy if needed + if (! $this->server->isBuildServer()) { + $proxyShouldRun = CheckProxy::run($this->server, true); + if ($proxyShouldRun) { + StartProxy::dispatch($this->server); + } + } + + // Mark validation as complete + $this->server->update(['is_validating' => false]); + + // Refresh server to get latest state + $this->server->refresh(); + + // Broadcast events to update UI + ServerValidated::dispatch($this->server->team_id, $this->server->uuid); + ServerReachabilityChanged::dispatch($this->server); + + } catch (\Throwable $e) { + Log::error('ValidateAndInstallServer: Exception occurred', [ + 'server_id' => $this->server->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + $this->server->update([ + 'validation_logs' => 'An error occurred during validation: '.$e->getMessage(), + 'is_validating' => false, + ]); + } + } +} diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 430470fa0..ac2b9213b 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -16,14 +16,18 @@ class Index extends Component { protected $listeners = ['refreshBoardingIndex' => 'validateServer']; + #[\Livewire\Attributes\Url(as: 'step', history: true)] public string $currentState = 'welcome'; + #[\Livewire\Attributes\Url(keep: true)] public ?string $selectedServerType = null; public ?Collection $privateKeys = null; + #[\Livewire\Attributes\Url(keep: true)] public ?int $selectedExistingPrivateKey = null; + #[\Livewire\Attributes\Url(keep: true)] public ?string $privateKeyType = null; public ?string $privateKey = null; @@ -38,6 +42,7 @@ class Index extends Component public ?Collection $servers = null; + #[\Livewire\Attributes\Url(keep: true)] public ?int $selectedExistingServer = null; public ?string $remoteServerName = null; @@ -58,6 +63,7 @@ class Index extends Component public Collection $projects; + #[\Livewire\Attributes\Url(keep: true)] public ?int $selectedProject = null; public ?Project $createdProject = null; @@ -79,17 +85,68 @@ public function mount() $this->minDockerVersion = str(config('constants.docker.minimum_required_version'))->before('.'); $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); - if (isDev()) { - $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk -hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA -AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV -uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== ------END OPENSSH PRIVATE KEY-----'; - $this->privateKeyDescription = 'Created by Coolify'; - $this->remoteServerDescription = 'Created by Coolify'; - $this->remoteServerHost = 'coolify-testing-host'; + + // Initialize collections to avoid null errors + if ($this->privateKeys === null) { + $this->privateKeys = collect(); + } + if ($this->servers === null) { + $this->servers = collect(); + } + if (! isset($this->projects)) { + $this->projects = collect(); + } + + // Restore state when coming from URL with query params + if ($this->selectedServerType === 'localhost' && $this->selectedExistingServer === 0) { + $this->createdServer = Server::find(0); + if ($this->createdServer) { + $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); + } + } + + if ($this->selectedServerType === 'remote') { + if ($this->privateKeys->isEmpty()) { + $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); + } + if ($this->servers->isEmpty()) { + $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); + } + + if ($this->selectedExistingServer) { + $this->createdServer = Server::find($this->selectedExistingServer); + if ($this->createdServer) { + $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); + $this->updateServerDetails(); + } + } + + if ($this->selectedExistingPrivateKey) { + $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id) + ->where('id', $this->selectedExistingPrivateKey) + ->first(); + if ($this->createdPrivateKey) { + $this->privateKey = $this->createdPrivateKey->private_key; + $this->publicKey = $this->createdPrivateKey->getPublicKey(); + } + } + + // Auto-regenerate key pair for "Generate with Coolify" mode on page refresh + if ($this->privateKeyType === 'create' && empty($this->privateKey)) { + $this->createNewPrivateKey(); + } + } + + if ($this->selectedProject) { + $this->createdProject = Project::find($this->selectedProject); + if (! $this->createdProject) { + $this->projects = Project::ownedByCurrentTeam(['name'])->get(); + } + } + + // Load projects when on create-project state (for page refresh) + if ($this->currentState === 'create-project' && $this->projects->isEmpty()) { + $this->projects = Project::ownedByCurrentTeam(['name'])->get(); } } @@ -129,41 +186,16 @@ public function setServerType(string $type) return $this->validateServer('localhost'); } elseif ($this->selectedServerType === 'remote') { - if (isDev()) { - $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->get(); - } else { - $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); - } + $this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); + // Auto-select first key if available for better UX if ($this->privateKeys->count() > 0) { $this->selectedExistingPrivateKey = $this->privateKeys->first()->id; } - $this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get(); - if ($this->servers->count() > 0) { - $this->selectedExistingServer = $this->servers->first()->id; - $this->updateServerDetails(); - $this->currentState = 'select-existing-server'; - - return; - } + // Onboarding always creates new servers, skip existing server selection $this->currentState = 'private-key'; } } - public function selectExistingServer() - { - $this->createdServer = Server::find($this->selectedExistingServer); - if (! $this->createdServer) { - $this->dispatch('error', 'Server is not found.'); - $this->currentState = 'private-key'; - - return; - } - $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id; - $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey(); - $this->updateServerDetails(); - $this->currentState = 'validate-server'; - } - private function updateServerDetails() { if ($this->createdServer) { @@ -181,7 +213,7 @@ public function getProxyType() public function selectExistingPrivateKey() { if (is_null($this->selectedExistingPrivateKey)) { - $this->restartBoarding(); + $this->dispatch('error', 'Please select a private key.'); return; } @@ -202,6 +234,9 @@ public function setPrivateKey(string $type) $this->privateKeyType = $type; if ($type === 'create') { $this->createNewPrivateKey(); + } else { + $this->privateKey = null; + $this->publicKey = null; } $this->currentState = 'create-private-key'; } diff --git a/app/Livewire/Concerns/SynchronizesModelData.php b/app/Livewire/Concerns/SynchronizesModelData.php new file mode 100644 index 000000000..f8218c715 --- /dev/null +++ b/app/Livewire/Concerns/SynchronizesModelData.php @@ -0,0 +1,35 @@ + Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content']) + */ + abstract protected function getModelBindings(): array; + + /** + * Synchronize component properties TO the model. + * Copies values from component properties to the model. + */ + protected function syncToModel(): void + { + foreach ($this->getModelBindings() as $property => $modelKey) { + data_set($this, $modelKey, $this->{$property}); + } + } + + /** + * Synchronize component properties FROM the model. + * Copies values from the model to component properties. + */ + protected function syncFromModel(): void + { + foreach ($this->getModelBindings() as $property => $modelKey) { + $this->{$property} = data_get($this, $modelKey); + } + } +} diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php index 87008e45e..e1dd678ff 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -617,7 +617,14 @@ private function loadSearchableItems() 'type' => 'navigation', 'description' => 'Manage private keys and API tokens', 'link' => route('security.private-key.index'), - 'search_text' => 'security private keys ssh api tokens', + 'search_text' => 'security private keys ssh api tokens cloud-init scripts', + ], + [ + 'name' => 'Cloud-Init Scripts', + 'type' => 'navigation', + 'description' => 'Manage reusable cloud-init scripts', + 'link' => route('security.cloud-init-scripts'), + 'search_text' => 'cloud-init scripts cloud init cloudinit initialization startup server setup', ], [ 'name' => 'Sources', @@ -1140,6 +1147,9 @@ private function navigateToResourceCreation($type) $this->selectedResourceType = $type; $this->isSelectingResource = true; + // Clear search query to show selection UI instead of creatable items + $this->searchQuery = ''; + // Reset selections $this->selectedServerId = null; $this->selectedDestinationUuid = null; @@ -1309,10 +1319,10 @@ private function completeResourceCreation() $queryParams['database_image'] = 'postgres:16-alpine'; } - return redirect()->route('project.resource.create', [ + $this->redirect(route('project.resource.create', [ 'project_uuid' => $this->selectedProjectUuid, 'environment_uuid' => $this->selectedEnvironmentUuid, - ] + $queryParams); + ] + $queryParams)); } } diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php index 53ca1d386..f660f9c13 100644 --- a/app/Livewire/MonacoEditor.php +++ b/app/Livewire/MonacoEditor.php @@ -25,6 +25,7 @@ public function __construct( public bool $readonly, public bool $allowTab, public bool $spellcheck, + public bool $autofocus, public ?string $helper, public bool $realtimeValidation, public bool $allowToPeak, diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php new file mode 100644 index 000000000..cf4e71105 --- /dev/null +++ b/app/Livewire/Notifications/Webhook.php @@ -0,0 +1,196 @@ +team = auth()->user()->currentTeam(); + $this->settings = $this->team->webhookNotificationSettings; + $this->authorize('view', $this->settings); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->authorize('update', $this->settings); + $this->settings->webhook_enabled = $this->webhookEnabled; + $this->settings->webhook_url = $this->webhookUrl; + + $this->settings->deployment_success_webhook_notifications = $this->deploymentSuccessWebhookNotifications; + $this->settings->deployment_failure_webhook_notifications = $this->deploymentFailureWebhookNotifications; + $this->settings->status_change_webhook_notifications = $this->statusChangeWebhookNotifications; + $this->settings->backup_success_webhook_notifications = $this->backupSuccessWebhookNotifications; + $this->settings->backup_failure_webhook_notifications = $this->backupFailureWebhookNotifications; + $this->settings->scheduled_task_success_webhook_notifications = $this->scheduledTaskSuccessWebhookNotifications; + $this->settings->scheduled_task_failure_webhook_notifications = $this->scheduledTaskFailureWebhookNotifications; + $this->settings->docker_cleanup_success_webhook_notifications = $this->dockerCleanupSuccessWebhookNotifications; + $this->settings->docker_cleanup_failure_webhook_notifications = $this->dockerCleanupFailureWebhookNotifications; + $this->settings->server_disk_usage_webhook_notifications = $this->serverDiskUsageWebhookNotifications; + $this->settings->server_reachable_webhook_notifications = $this->serverReachableWebhookNotifications; + $this->settings->server_unreachable_webhook_notifications = $this->serverUnreachableWebhookNotifications; + $this->settings->server_patch_webhook_notifications = $this->serverPatchWebhookNotifications; + + $this->settings->save(); + refreshSession(); + } else { + $this->webhookEnabled = $this->settings->webhook_enabled; + $this->webhookUrl = $this->settings->webhook_url; + + $this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications; + $this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications; + $this->statusChangeWebhookNotifications = $this->settings->status_change_webhook_notifications; + $this->backupSuccessWebhookNotifications = $this->settings->backup_success_webhook_notifications; + $this->backupFailureWebhookNotifications = $this->settings->backup_failure_webhook_notifications; + $this->scheduledTaskSuccessWebhookNotifications = $this->settings->scheduled_task_success_webhook_notifications; + $this->scheduledTaskFailureWebhookNotifications = $this->settings->scheduled_task_failure_webhook_notifications; + $this->dockerCleanupSuccessWebhookNotifications = $this->settings->docker_cleanup_success_webhook_notifications; + $this->dockerCleanupFailureWebhookNotifications = $this->settings->docker_cleanup_failure_webhook_notifications; + $this->serverDiskUsageWebhookNotifications = $this->settings->server_disk_usage_webhook_notifications; + $this->serverReachableWebhookNotifications = $this->settings->server_reachable_webhook_notifications; + $this->serverUnreachableWebhookNotifications = $this->settings->server_unreachable_webhook_notifications; + $this->serverPatchWebhookNotifications = $this->settings->server_patch_webhook_notifications; + } + } + + public function instantSaveWebhookEnabled() + { + try { + $original = $this->webhookEnabled; + $this->validate([ + 'webhookUrl' => 'required', + ], [ + 'webhookUrl.required' => 'Webhook URL is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->webhookEnabled = $original; + + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function saveModel() + { + $this->syncData(true); + refreshSession(); + + if (isDev()) { + ray('Webhook settings saved', [ + 'webhook_enabled' => $this->settings->webhook_enabled, + 'webhook_url' => $this->settings->webhook_url, + ]); + } + + $this->dispatch('success', 'Settings saved.'); + } + + public function sendTestNotification() + { + try { + $this->authorize('sendTest', $this->settings); + + if (isDev()) { + ray('Sending test webhook notification', [ + 'team_id' => $this->team->id, + 'webhook_url' => $this->settings->webhook_url, + ]); + } + + $this->team->notify(new Test(channel: 'webhook')); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.notifications.webhook'); + } +} diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index b42f29fa5..a733d8cb3 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\GenerateConfig; +use App\Livewire\Concerns\SynchronizesModelData; use App\Models\Application; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -14,6 +15,7 @@ class General extends Component { use AuthorizesRequests; + use SynchronizesModelData; public string $applicationId; @@ -23,6 +25,8 @@ class General extends Component public string $name; + public ?string $description = null; + public ?string $fqdn = null; public string $git_repository; @@ -31,14 +35,82 @@ class General extends Component public ?string $git_commit_sha = null; + public ?string $install_command = null; + + public ?string $build_command = null; + + public ?string $start_command = null; + public string $build_pack; + public string $static_image; + + public string $base_directory; + + public ?string $publish_directory = null; + public ?string $ports_exposes = null; + public ?string $ports_mappings = null; + + public ?string $custom_network_aliases = null; + + public ?string $dockerfile = null; + + public ?string $dockerfile_location = null; + + public ?string $dockerfile_target_build = null; + + public ?string $docker_registry_image_name = null; + + public ?string $docker_registry_image_tag = null; + + public ?string $docker_compose_location = null; + + public ?string $docker_compose = null; + + public ?string $docker_compose_raw = null; + + public ?string $docker_compose_custom_start_command = null; + + public ?string $docker_compose_custom_build_command = null; + + public ?string $custom_labels = null; + + public ?string $custom_docker_run_options = null; + + public ?string $pre_deployment_command = null; + + public ?string $pre_deployment_command_container = null; + + public ?string $post_deployment_command = null; + + public ?string $post_deployment_command_container = null; + + public ?string $custom_nginx_configuration = null; + + public bool $is_static = false; + + public bool $is_spa = false; + + public bool $is_build_server_enabled = false; + public bool $is_preserve_repository_enabled = false; public bool $is_container_label_escape_enabled = true; + public bool $is_container_label_readonly_enabled = false; + + public bool $is_http_basic_auth_enabled = false; + + public ?string $http_basic_auth_username = null; + + public ?string $http_basic_auth_password = null; + + public ?string $watch_paths = null; + + public string $redirect; + public $customLabels; public bool $labelsChanged = false; @@ -66,50 +138,50 @@ class General extends Component protected function rules(): array { return [ - 'application.name' => ValidationPatterns::nameRules(), - 'application.description' => ValidationPatterns::descriptionRules(), - 'application.fqdn' => 'nullable', - 'application.git_repository' => 'required', - 'application.git_branch' => 'required', - 'application.git_commit_sha' => 'nullable', - 'application.install_command' => 'nullable', - 'application.build_command' => 'nullable', - 'application.start_command' => 'nullable', - 'application.build_pack' => 'required', - 'application.static_image' => 'required', - 'application.base_directory' => 'required', - 'application.publish_directory' => 'nullable', - 'application.ports_exposes' => 'required', - 'application.ports_mappings' => 'nullable', - 'application.custom_network_aliases' => 'nullable', - 'application.dockerfile' => 'nullable', - 'application.docker_registry_image_name' => 'nullable', - 'application.docker_registry_image_tag' => 'nullable', - 'application.dockerfile_location' => 'nullable', - 'application.docker_compose_location' => 'nullable', - 'application.docker_compose' => 'nullable', - 'application.docker_compose_raw' => 'nullable', - 'application.dockerfile_target_build' => 'nullable', - 'application.docker_compose_custom_start_command' => 'nullable', - 'application.docker_compose_custom_build_command' => 'nullable', - 'application.custom_labels' => 'nullable', - 'application.custom_docker_run_options' => 'nullable', - 'application.pre_deployment_command' => 'nullable', - 'application.pre_deployment_command_container' => 'nullable', - 'application.post_deployment_command' => 'nullable', - 'application.post_deployment_command_container' => 'nullable', - 'application.custom_nginx_configuration' => 'nullable', - 'application.settings.is_static' => 'boolean|required', - 'application.settings.is_spa' => 'boolean|required', - 'application.settings.is_build_server_enabled' => 'boolean|required', - 'application.settings.is_container_label_escape_enabled' => 'boolean|required', - 'application.settings.is_container_label_readonly_enabled' => 'boolean|required', - 'application.settings.is_preserve_repository_enabled' => 'boolean|required', - 'application.is_http_basic_auth_enabled' => 'boolean|required', - 'application.http_basic_auth_username' => 'string|nullable', - 'application.http_basic_auth_password' => 'string|nullable', - 'application.watch_paths' => 'nullable', - 'application.redirect' => 'string|required', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'fqdn' => 'nullable', + 'git_repository' => 'required', + 'git_branch' => 'required', + 'git_commit_sha' => 'nullable', + 'install_command' => 'nullable', + 'build_command' => 'nullable', + 'start_command' => 'nullable', + 'build_pack' => 'required', + 'static_image' => 'required', + 'base_directory' => 'required', + 'publish_directory' => 'nullable', + 'ports_exposes' => 'required', + 'ports_mappings' => 'nullable', + 'custom_network_aliases' => 'nullable', + 'dockerfile' => 'nullable', + 'docker_registry_image_name' => 'nullable', + 'docker_registry_image_tag' => 'nullable', + 'dockerfile_location' => 'nullable', + 'docker_compose_location' => 'nullable', + 'docker_compose' => 'nullable', + 'docker_compose_raw' => 'nullable', + 'dockerfile_target_build' => 'nullable', + 'docker_compose_custom_start_command' => 'nullable', + 'docker_compose_custom_build_command' => 'nullable', + 'custom_labels' => 'nullable', + 'custom_docker_run_options' => 'nullable', + 'pre_deployment_command' => 'nullable', + 'pre_deployment_command_container' => 'nullable', + 'post_deployment_command' => 'nullable', + 'post_deployment_command_container' => 'nullable', + 'custom_nginx_configuration' => 'nullable', + 'is_static' => 'boolean|required', + 'is_spa' => 'boolean|required', + 'is_build_server_enabled' => 'boolean|required', + 'is_container_label_escape_enabled' => 'boolean|required', + 'is_container_label_readonly_enabled' => 'boolean|required', + 'is_preserve_repository_enabled' => 'boolean|required', + 'is_http_basic_auth_enabled' => 'boolean|required', + 'http_basic_auth_username' => 'string|nullable', + 'http_basic_auth_password' => 'string|nullable', + 'watch_paths' => 'nullable', + 'redirect' => 'string|required', ]; } @@ -118,31 +190,31 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'application.name.required' => 'The Name field is required.', - 'application.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'application.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'application.git_repository.required' => 'The Git Repository field is required.', - 'application.git_branch.required' => 'The Git Branch field is required.', - 'application.build_pack.required' => 'The Build Pack field is required.', - 'application.static_image.required' => 'The Static Image field is required.', - 'application.base_directory.required' => 'The Base Directory field is required.', - 'application.ports_exposes.required' => 'The Exposed Ports field is required.', - 'application.settings.is_static.required' => 'The Static setting is required.', - 'application.settings.is_static.boolean' => 'The Static setting must be true or false.', - 'application.settings.is_spa.required' => 'The SPA setting is required.', - 'application.settings.is_spa.boolean' => 'The SPA setting must be true or false.', - 'application.settings.is_build_server_enabled.required' => 'The Build Server setting is required.', - 'application.settings.is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.', - 'application.settings.is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.', - 'application.settings.is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.', - 'application.settings.is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.', - 'application.settings.is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.', - 'application.settings.is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.', - 'application.settings.is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.', - 'application.is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.', - 'application.is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.', - 'application.redirect.required' => 'The Redirect setting is required.', - 'application.redirect.string' => 'The Redirect setting must be a string.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'git_repository.required' => 'The Git Repository field is required.', + 'git_branch.required' => 'The Git Branch field is required.', + 'build_pack.required' => 'The Build Pack field is required.', + 'static_image.required' => 'The Static Image field is required.', + 'base_directory.required' => 'The Base Directory field is required.', + 'ports_exposes.required' => 'The Exposed Ports field is required.', + 'is_static.required' => 'The Static setting is required.', + 'is_static.boolean' => 'The Static setting must be true or false.', + 'is_spa.required' => 'The SPA setting is required.', + 'is_spa.boolean' => 'The SPA setting must be true or false.', + 'is_build_server_enabled.required' => 'The Build Server setting is required.', + 'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.', + 'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.', + 'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.', + 'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.', + 'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.', + 'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.', + 'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.', + 'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.', + 'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.', + 'redirect.required' => 'The Redirect setting is required.', + 'redirect.string' => 'The Redirect setting must be a string.', ] ); } @@ -193,11 +265,15 @@ public function mount() $this->parsedServices = $this->application->parse(); if (is_null($this->parsedServices) || empty($this->parsedServices)) { $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); + // Still sync data even if parse fails, so form fields are populated + $this->syncFromModel(); return; } } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); + // Still sync data even on error, so form fields are populated + $this->syncFromModel(); } if ($this->application->build_pack === 'dockercompose') { // Only update if user has permission @@ -218,9 +294,6 @@ public function mount() } $this->parsedServiceDomains = $sanitizedDomains; - $this->ports_exposes = $this->application->ports_exposes; - $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; - $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->customLabels = $this->application->parseContainerLabels(); if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) { // Only update custom labels if user has permission @@ -249,6 +322,60 @@ public function mount() if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) { $this->dispatch('configurationChanged'); } + + // Sync data from model to properties at the END, after all business logic + // This ensures any modifications to $this->application during mount() are reflected in properties + $this->syncFromModel(); + } + + protected function getModelBindings(): array + { + return [ + 'name' => 'application.name', + 'description' => 'application.description', + 'fqdn' => 'application.fqdn', + 'git_repository' => 'application.git_repository', + 'git_branch' => 'application.git_branch', + 'git_commit_sha' => 'application.git_commit_sha', + 'install_command' => 'application.install_command', + 'build_command' => 'application.build_command', + 'start_command' => 'application.start_command', + 'build_pack' => 'application.build_pack', + 'static_image' => 'application.static_image', + 'base_directory' => 'application.base_directory', + 'publish_directory' => 'application.publish_directory', + 'ports_exposes' => 'application.ports_exposes', + 'ports_mappings' => 'application.ports_mappings', + 'custom_network_aliases' => 'application.custom_network_aliases', + 'dockerfile' => 'application.dockerfile', + 'dockerfile_location' => 'application.dockerfile_location', + 'dockerfile_target_build' => 'application.dockerfile_target_build', + 'docker_registry_image_name' => 'application.docker_registry_image_name', + 'docker_registry_image_tag' => 'application.docker_registry_image_tag', + 'docker_compose_location' => 'application.docker_compose_location', + 'docker_compose' => 'application.docker_compose', + 'docker_compose_raw' => 'application.docker_compose_raw', + 'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command', + 'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command', + 'custom_labels' => 'application.custom_labels', + 'custom_docker_run_options' => 'application.custom_docker_run_options', + 'pre_deployment_command' => 'application.pre_deployment_command', + 'pre_deployment_command_container' => 'application.pre_deployment_command_container', + 'post_deployment_command' => 'application.post_deployment_command', + 'post_deployment_command_container' => 'application.post_deployment_command_container', + 'custom_nginx_configuration' => 'application.custom_nginx_configuration', + 'is_static' => 'application.settings.is_static', + 'is_spa' => 'application.settings.is_spa', + 'is_build_server_enabled' => 'application.settings.is_build_server_enabled', + 'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled', + 'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled', + 'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled', + 'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled', + 'http_basic_auth_username' => 'application.http_basic_auth_username', + 'http_basic_auth_password' => 'application.http_basic_auth_password', + 'watch_paths' => 'application.watch_paths', + 'redirect' => 'application.redirect', + ]; } public function instantSave() @@ -256,6 +383,12 @@ public function instantSave() try { $this->authorize('update', $this->application); + $oldPortsExposes = $this->application->ports_exposes; + $oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; + $oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled; + + $this->syncToModel(); + if ($this->application->settings->isDirty('is_spa')) { $this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static'); } @@ -265,20 +398,21 @@ public function instantSave() $this->application->settings->save(); $this->dispatch('success', 'Settings saved.'); $this->application->refresh(); + $this->syncFromModel(); // If port_exposes changed, reset default labels - if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { + if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) { $this->resetDefaultLabels(false); } - if ($this->is_preserve_repository_enabled !== $this->application->settings->is_preserve_repository_enabled) { - if ($this->application->settings->is_preserve_repository_enabled === false) { + if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) { + if ($this->is_preserve_repository_enabled === false) { $this->application->fileStorages->each(function ($storage) { - $storage->is_based_on_git = $this->application->settings->is_preserve_repository_enabled; + $storage->is_based_on_git = $this->is_preserve_repository_enabled; $storage->save(); }); } } - if ($this->application->settings->is_container_label_readonly_enabled) { + if ($this->is_container_label_readonly_enabled) { $this->resetDefaultLabels(false); } } catch (\Throwable $e) { @@ -366,21 +500,21 @@ public function generateDomain(string $serviceName) } } - public function updatedApplicationBaseDirectory() + public function updatedBaseDirectory() { - if ($this->application->build_pack === 'dockercompose') { + if ($this->build_pack === 'dockercompose') { $this->loadComposeFile(); } } - public function updatedApplicationSettingsIsStatic($value) + public function updatedIsStatic($value) { if ($value) { $this->generateNginxConfiguration(); } } - public function updatedApplicationBuildPack() + public function updatedBuildPack() { // Check if user has permission to update try { @@ -388,21 +522,28 @@ public function updatedApplicationBuildPack() } catch (\Illuminate\Auth\Access\AuthorizationException $e) { // User doesn't have permission, revert the change and return $this->application->refresh(); + $this->syncFromModel(); return; } - if ($this->application->build_pack !== 'nixpacks') { + // Sync property to model before checking/modifying + $this->syncToModel(); + + if ($this->build_pack !== 'nixpacks') { + $this->is_static = false; $this->application->settings->is_static = false; $this->application->settings->save(); } else { - $this->application->ports_exposes = $this->ports_exposes = 3000; + $this->ports_exposes = 3000; + $this->application->ports_exposes = 3000; $this->resetDefaultLabels(false); } - if ($this->application->build_pack === 'dockercompose') { + if ($this->build_pack === 'dockercompose') { // Only update if user has permission try { $this->authorize('update', $this->application); + $this->fqdn = null; $this->application->fqdn = null; $this->application->settings->save(); } catch (\Illuminate\Auth\Access\AuthorizationException $e) { @@ -421,8 +562,9 @@ public function updatedApplicationBuildPack() $this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete(); } } - if ($this->application->build_pack === 'static') { - $this->application->ports_exposes = $this->ports_exposes = 80; + if ($this->build_pack === 'static') { + $this->ports_exposes = 80; + $this->application->ports_exposes = 80; $this->resetDefaultLabels(false); $this->generateNginxConfiguration(); } @@ -438,8 +580,11 @@ public function getWildcardDomain() $server = data_get($this->application, 'destination.server'); if ($server) { $fqdn = generateUrl(server: $server, random: $this->application->uuid); - $this->application->fqdn = $fqdn; + $this->fqdn = $fqdn; + $this->syncToModel(); $this->application->save(); + $this->application->refresh(); + $this->syncFromModel(); $this->resetDefaultLabels(); $this->dispatch('success', 'Wildcard domain generated.'); } @@ -453,8 +598,11 @@ public function generateNginxConfiguration($type = 'static') try { $this->authorize('update', $this->application); - $this->application->custom_nginx_configuration = defaultNginxConfiguration($type); + $this->custom_nginx_configuration = defaultNginxConfiguration($type); + $this->syncToModel(); $this->application->save(); + $this->application->refresh(); + $this->syncFromModel(); $this->dispatch('success', 'Nginx configuration generated.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -464,15 +612,16 @@ public function generateNginxConfiguration($type = 'static') public function resetDefaultLabels($manualReset = false) { try { - if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { + if (! $this->is_container_label_readonly_enabled && ! $manualReset) { return; } $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); - $this->ports_exposes = $this->application->ports_exposes; - $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; - $this->application->custom_labels = base64_encode($this->customLabels); + $this->custom_labels = base64_encode($this->customLabels); + $this->syncToModel(); $this->application->save(); - if ($this->application->build_pack === 'dockercompose') { + $this->application->refresh(); + $this->syncFromModel(); + if ($this->build_pack === 'dockercompose') { $this->loadComposeFile(showToast: false); } $this->dispatch('configurationChanged'); @@ -483,8 +632,8 @@ public function resetDefaultLabels($manualReset = false) public function checkFqdns($showToaster = true) { - if (data_get($this->application, 'fqdn')) { - $domains = str($this->application->fqdn)->trim()->explode(','); + if ($this->fqdn) { + $domains = str($this->fqdn)->trim()->explode(','); if ($this->application->additional_servers->count() === 0) { foreach ($domains as $domain) { if (! validateDNSEntry($domain, $this->application->destination->server)) { @@ -507,7 +656,8 @@ public function checkFqdns($showToaster = true) $this->forceSaveDomains = false; } - $this->application->fqdn = $domains->implode(','); + $this->fqdn = $domains->implode(','); + $this->application->fqdn = $this->fqdn; $this->resetDefaultLabels(false); } @@ -547,21 +697,27 @@ public function submit($showToaster = true) $this->validate(); - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $oldPortsExposes = $this->application->ports_exposes; + $oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled; + $oldDockerComposeLocation = $this->initialDockerComposeLocation; + + // Process FQDN with intermediate variable to avoid Collection/string confusion + $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); + $this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString(); + $domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) { $domain = trim($domain); Url::fromString($domain, ['http', 'https']); return str($domain)->lower(); }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $warning = sslipDomainWarning($this->application->fqdn); + $this->fqdn = $domains->unique()->implode(','); + $warning = sslipDomainWarning($this->fqdn); if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - // $this->resetDefaultLabels(); + + $this->syncToModel(); if ($this->application->isDirty('redirect')) { $this->setRedirect(); @@ -581,38 +737,42 @@ public function submit($showToaster = true) $this->application->save(); } - if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { + if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) { $compose_return = $this->loadComposeFile(showToast: false); if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) { return; } } - if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) { + if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) { $this->resetDefaultLabels(); } - if (data_get($this->application, 'build_pack') === 'dockerimage') { + if ($this->build_pack === 'dockerimage') { $this->validate([ - 'application.docker_registry_image_name' => 'required', + 'docker_registry_image_name' => 'required', ]); } - if (data_get($this->application, 'custom_docker_run_options')) { - $this->application->custom_docker_run_options = str($this->application->custom_docker_run_options)->trim(); + if ($this->custom_docker_run_options) { + $this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString(); + $this->application->custom_docker_run_options = $this->custom_docker_run_options; } - if (data_get($this->application, 'dockerfile')) { - $port = get_port_from_dockerfile($this->application->dockerfile); - if ($port && ! $this->application->ports_exposes) { + if ($this->dockerfile) { + $port = get_port_from_dockerfile($this->dockerfile); + if ($port && ! $this->ports_exposes) { + $this->ports_exposes = $port; $this->application->ports_exposes = $port; } } - if ($this->application->base_directory && $this->application->base_directory !== '/') { - $this->application->base_directory = rtrim($this->application->base_directory, '/'); + if ($this->base_directory && $this->base_directory !== '/') { + $this->base_directory = rtrim($this->base_directory, '/'); + $this->application->base_directory = $this->base_directory; } - if ($this->application->publish_directory && $this->application->publish_directory !== '/') { - $this->application->publish_directory = rtrim($this->application->publish_directory, '/'); + if ($this->publish_directory && $this->publish_directory !== '/') { + $this->publish_directory = rtrim($this->publish_directory, '/'); + $this->application->publish_directory = $this->publish_directory; } - if ($this->application->build_pack === 'dockercompose') { + if ($this->build_pack === 'dockercompose') { $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); if ($this->application->isDirty('docker_compose_domains')) { foreach ($this->parsedServiceDomains as $service) { @@ -643,12 +803,12 @@ public function submit($showToaster = true) } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); + $this->application->refresh(); + $this->syncFromModel(); $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { - $originalFqdn = $this->application->getOriginal('fqdn'); - if ($originalFqdn !== $this->application->fqdn) { - $this->application->fqdn = $originalFqdn; - } + $this->application->refresh(); + $this->syncFromModel(); return handleError($e, $this); } finally { diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 7a6e8c415..45371678b 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -33,14 +33,34 @@ class Previews extends Component public $pendingPreviewId = null; + public array $previewFqdns = []; + protected $rules = [ - 'application.previews.*.fqdn' => 'string|nullable', + 'previewFqdns.*' => 'string|nullable', ]; public function mount() { $this->pull_requests = collect(); $this->parameters = get_route_parameters(); + $this->syncData(false); + } + + private function syncData(bool $toModel = false): void + { + if ($toModel) { + foreach ($this->previewFqdns as $key => $fqdn) { + $preview = $this->application->previews->get($key); + if ($preview) { + $preview->fqdn = $fqdn; + } + } + } else { + $this->previewFqdns = []; + foreach ($this->application->previews as $key => $preview) { + $this->previewFqdns[$key] = $preview->fqdn; + } + } } public function load_prs() @@ -73,35 +93,52 @@ public function save_preview($preview_id) $this->authorize('update', $this->application); $success = true; $preview = $this->application->previews->find($preview_id); - if (data_get_str($preview, 'fqdn')->isNotEmpty()) { - $preview->fqdn = str($preview->fqdn)->replaceEnd(',', '')->trim(); - $preview->fqdn = str($preview->fqdn)->replaceStart(',', '')->trim(); - $preview->fqdn = str($preview->fqdn)->trim()->lower(); - if (! validateDNSEntry($preview->fqdn, $this->application->destination->server)) { - $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$preview->fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); - $success = false; - } - // Check for domain conflicts if not forcing save - if (! $this->forceSaveDomains) { - $result = checkDomainUsage(resource: $this->application, domain: $preview->fqdn); - if ($result['hasConflicts']) { - $this->domainConflicts = $result['conflicts']; - $this->showDomainConflictModal = true; - $this->pendingPreviewId = $preview_id; - - return; - } - } else { - // Reset the force flag after using it - $this->forceSaveDomains = false; - } - } if (! $preview) { throw new \Exception('Preview not found'); } - $success && $preview->save(); - $success && $this->dispatch('success', 'Preview saved.

Do not forget to redeploy the preview to apply the changes.'); + + // Find the key for this preview in the collection + $previewKey = $this->application->previews->search(function ($item) use ($preview_id) { + return $item->id == $preview_id; + }); + + if ($previewKey !== false && isset($this->previewFqdns[$previewKey])) { + $fqdn = $this->previewFqdns[$previewKey]; + + if (! empty($fqdn)) { + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $fqdn = str($fqdn)->trim()->lower(); + $this->previewFqdns[$previewKey] = $fqdn; + + if (! validateDNSEntry($fqdn, $this->application->destination->server)) { + $this->dispatch('error', 'Validating DNS failed.', "Make sure you have added the DNS records correctly.

$fqdn->{$this->application->destination->server->ip}

Check this documentation for further help."); + $success = false; + } + + // Check for domain conflicts if not forcing save + if (! $this->forceSaveDomains) { + $result = checkDomainUsage(resource: $this->application, domain: $fqdn); + if ($result['hasConflicts']) { + $this->domainConflicts = $result['conflicts']; + $this->showDomainConflictModal = true; + $this->pendingPreviewId = $preview_id; + + return; + } + } else { + // Reset the force flag after using it + $this->forceSaveDomains = false; + } + } + } + + if ($success) { + $this->syncData(true); + $preview->save(); + $this->dispatch('success', 'Preview saved.

Do not forget to redeploy the preview to apply the changes.'); + } } catch (\Throwable $e) { return handleError($e, $this); } @@ -121,6 +158,7 @@ public function generate_preview($preview_id) if ($this->application->build_pack === 'dockercompose') { $preview->generate_preview_fqdn_compose(); $this->application->refresh(); + $this->syncData(false); $this->dispatch('success', 'Domain generated.'); return; @@ -128,6 +166,7 @@ public function generate_preview($preview_id) $preview->generate_preview_fqdn(); $this->application->refresh(); + $this->syncData(false); $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); } catch (\Throwable $e) { @@ -152,6 +191,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) } $found->generate_preview_fqdn_compose(); $this->application->refresh(); + $this->syncData(false); } else { $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); @@ -164,6 +204,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) } $found->generate_preview_fqdn(); $this->application->refresh(); + $this->syncData(false); $this->dispatch('update_links'); $this->dispatch('success', 'Preview added.'); } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index cfb364b6d..942dfeb37 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -18,6 +18,13 @@ class PreviewsCompose extends Component public ApplicationPreview $preview; + public ?string $domain = null; + + public function mount() + { + $this->domain = data_get($this->service, 'domain'); + } + public function render() { return view('livewire.project.application.previews-compose'); @@ -28,10 +35,10 @@ public function save() try { $this->authorize('update', $this->preview->application); - $domain = data_get($this->service, 'domain'); $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$this->serviceName]['domain'] = $domain; + $docker_compose_domains = json_decode($docker_compose_domains, true) ?: []; + $docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? []; + $docker_compose_domains[$this->serviceName]['domain'] = $this->domain; $this->preview->docker_compose_domains = json_encode($docker_compose_domains); $this->preview->save(); $this->dispatch('update_links'); @@ -46,7 +53,7 @@ public function generate() try { $this->authorize('update', $this->preview->application); - $domains = collect(json_decode($this->preview->application->docker_compose_domains)) ?? collect(); + $domains = collect(json_decode($this->preview->application->docker_compose_domains, true) ?: []); $domain = $domains->first(function ($_, $key) { return $key === $this->serviceName; }); @@ -68,24 +75,40 @@ public function generate() $preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn; } else { // Use the existing domain from the main application - $url = Url::fromString($domain_string); + // Handle multiple domains separated by commas + $domain_list = explode(',', $domain_string); + $preview_fqdns = []; $template = $this->preview->application->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $portInt = $url->getPort(); - $port = $portInt !== null ? ':'.$portInt : ''; $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); - $preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; + + foreach ($domain_list as $single_domain) { + $single_domain = trim($single_domain); + if (empty($single_domain)) { + continue; + } + + $url = Url::fromString($single_domain); + $host = $url->getHost(); + $schema = $url->getScheme(); + $portInt = $url->getPort(); + $port = $portInt !== null ? ':'.$portInt : ''; + + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); + $preview_fqdn = str_replace('{{port}}', $port, $preview_fqdn); + $preview_fqdns[] = "$schema://$preview_fqdn"; + } + + $preview_fqdn = implode(',', $preview_fqdns); } // Save the generated domain + $this->domain = $preview_fqdn; $docker_compose_domains = data_get($this->preview, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn; + $docker_compose_domains = json_decode($docker_compose_domains, true) ?: []; + $docker_compose_domains[$this->serviceName] = $docker_compose_domains[$this->serviceName] ?? []; + $docker_compose_domains[$this->serviceName]['domain'] = $this->domain; $this->preview->docker_compose_domains = json_encode($docker_compose_domains); $this->preview->save(); diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index b3df79008..7deaa82a9 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -85,6 +85,7 @@ class BackupEdit extends Component public function mount() { try { + $this->authorize('view', $this->backup->database); $this->parameters = get_route_parameters(); $this->syncData(); } catch (Exception $e) { diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index b80775853..c4a7983b8 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -16,7 +16,7 @@ class General extends Component { use AuthorizesRequests; - public Server $server; + public ?Server $server = null; public StandaloneClickhouse $database; @@ -56,8 +56,14 @@ public function getListeners() public function mount() { try { + $this->authorize('view', $this->database); $this->syncData(); $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); + + return; + } } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Database/Configuration.php b/app/Livewire/Project/Database/Configuration.php index 88ecccf99..7c64a6eef 100644 --- a/app/Livewire/Project/Database/Configuration.php +++ b/app/Livewire/Project/Database/Configuration.php @@ -3,10 +3,13 @@ namespace App\Livewire\Project\Database; use Auth; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Configuration extends Component { + use AuthorizesRequests; + public $currentRoute; public $database; @@ -42,6 +45,8 @@ public function mount() ->where('uuid', request()->route('database_uuid')) ->firstOrFail(); + $this->authorize('view', $database); + $this->database = $database; $this->project = $project; $this->environment = $environment; diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index fabbc7cb4..9052a4749 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneDragonfly; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,7 +18,7 @@ class General extends Component { use AuthorizesRequests; - public Server $server; + public ?Server $server = null; public StandaloneDragonfly $database; @@ -63,8 +62,14 @@ public function getListeners() public function mount() { try { + $this->authorize('view', $this->database); $this->syncData(); $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); + + return; + } $existingCert = $this->database->sslCertificates()->first(); @@ -249,13 +254,13 @@ public function regenerateSslCertificate() $server = $this->database->destination->server; - $caCert = SslCertificate::where('server_id', $server->id) + $caCert = $server->sslCertificates() ->where('is_ca_certificate', true) ->first(); if (! $caCert) { $server->generateCaCertificate(); - $caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $caCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); } if (! $caCert) { diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index 3f974f63d..7d6ac3131 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -131,6 +131,7 @@ public function getContainers() if (is_null($resource)) { abort(404); } + $this->authorize('view', $resource); $this->resource = $resource; $this->server = $this->resource->destination->server; $this->container = $this->resource->uuid; diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 7502d001d..6d21988e7 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneKeydb; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,7 +18,7 @@ class General extends Component { use AuthorizesRequests; - public Server $server; + public ?Server $server = null; public StandaloneKeydb $database; @@ -59,15 +58,20 @@ public function getListeners() return [ "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', - 'refresh' => '$refresh', ]; } public function mount() { try { + $this->authorize('view', $this->database); $this->syncData(); $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); + + return; + } $existingCert = $this->database->sslCertificates()->first(); @@ -255,7 +259,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id) + $caCert = $this->server->sslCertificates() ->where('is_ca_certificate', true) ->first(); diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index c82c4538f..429cfc387 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneMariadb; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,12 +18,38 @@ class General extends Component { use AuthorizesRequests; - protected $listeners = ['refresh']; - - public Server $server; + public ?Server $server = null; public StandaloneMariadb $database; + public string $name; + + public ?string $description = null; + + public string $mariadbRootPassword; + + public string $mariadbUser; + + public string $mariadbPassword; + + public string $mariadbDatabase; + + public ?string $mariadbConf = null; + + public string $image; + + public ?string $portsMappings = null; + + public ?bool $isPublic = null; + + public ?int $publicPort = null; + + public bool $isLogDrainEnabled = false; + + public ?string $customDockerRunOptions = null; + + public bool $enableSsl = false; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -37,27 +62,26 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', - 'refresh' => '$refresh', ]; } protected function rules(): array { return [ - 'database.name' => ValidationPatterns::nameRules(), - 'database.description' => ValidationPatterns::descriptionRules(), - 'database.mariadb_root_password' => 'required', - 'database.mariadb_user' => 'required', - 'database.mariadb_password' => 'required', - 'database.mariadb_database' => 'required', - 'database.mariadb_conf' => 'nullable', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - 'database.enable_ssl' => 'boolean', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'mariadbRootPassword' => 'required', + 'mariadbUser' => 'required', + 'mariadbPassword' => 'required', + 'mariadbDatabase' => 'required', + 'mariadbConf' => 'nullable', + 'image' => 'required', + 'portsMappings' => 'nullable', + 'isPublic' => 'nullable|boolean', + 'publicPort' => 'nullable|integer', + 'isLogDrainEnabled' => 'nullable|boolean', + 'customDockerRunOptions' => 'nullable', + 'enableSsl' => 'boolean', ]; } @@ -66,45 +90,96 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'database.name.required' => 'The Name field is required.', - 'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'database.mariadb_root_password.required' => 'The Root Password field is required.', - 'database.mariadb_user.required' => 'The MariaDB User field is required.', - 'database.mariadb_password.required' => 'The MariaDB Password field is required.', - 'database.mariadb_database.required' => 'The MariaDB Database field is required.', - 'database.image.required' => 'The Docker Image field is required.', - 'database.public_port.integer' => 'The Public Port must be an integer.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'mariadbRootPassword.required' => 'The Root Password field is required.', + 'mariadbUser.required' => 'The MariaDB User field is required.', + 'mariadbPassword.required' => 'The MariaDB Password field is required.', + '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.', ] ); } protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.mariadb_root_password' => 'Root Password', - 'database.mariadb_user' => 'User', - 'database.mariadb_password' => 'Password', - 'database.mariadb_database' => 'Database', - 'database.mariadb_conf' => 'MariaDB Configuration', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Options', - 'database.enable_ssl' => 'Enable SSL', + 'name' => 'Name', + 'description' => 'Description', + 'mariadbRootPassword' => 'Root Password', + 'mariadbUser' => 'User', + 'mariadbPassword' => 'Password', + 'mariadbDatabase' => 'Database', + 'mariadbConf' => 'MariaDB Configuration', + 'image' => 'Image', + 'portsMappings' => 'Port Mapping', + 'isPublic' => 'Is Public', + 'publicPort' => 'Public Port', + 'customDockerRunOptions' => 'Custom Docker Options', + 'enableSsl' => 'Enable SSL', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->authorize('view', $this->database); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); - $existingCert = $this->database->sslCertificates()->first(); + return; + } - if ($existingCert) { - $this->certificateValidUntil = $existingCert->valid_until; + $existingCert = $this->database->sslCertificates()->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->mariadb_root_password = $this->mariadbRootPassword; + $this->database->mariadb_user = $this->mariadbUser; + $this->database->mariadb_password = $this->mariadbPassword; + $this->database->mariadb_database = $this->mariadbDatabase; + $this->database->mariadb_conf = $this->mariadbConf; + $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->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->enable_ssl = $this->enableSsl; + $this->database->save(); + + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->mariadbRootPassword = $this->database->mariadb_root_password; + $this->mariadbUser = $this->database->mariadb_user; + $this->mariadbPassword = $this->database->mariadb_password; + $this->mariadbDatabase = $this->database->mariadb_database; + $this->mariadbConf = $this->database->mariadb_conf; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->enableSsl = $this->database->enable_ssl; + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; } } @@ -114,12 +189,12 @@ public function instantSaveAdvanced() $this->authorize('update', $this->database); if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -132,11 +207,10 @@ public function submit() try { $this->authorize('update', $this->database); - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -154,16 +228,16 @@ public function instantSave() try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -173,10 +247,9 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; return handleError($e, $this); } @@ -187,7 +260,7 @@ public function instantSaveSSL() try { $this->authorize('update', $this->database); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'SSL configuration updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -207,7 +280,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first(); + $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); SslHelper::generateSslCertificate( commonName: $existingCert->common_name, @@ -231,6 +304,7 @@ public function regenerateSslCertificate() public function refresh(): void { $this->database->refresh(); + $this->syncData(); } public function render() diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 4fbc45437..ae725fa4b 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneMongodb; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,12 +18,38 @@ class General extends Component { use AuthorizesRequests; - protected $listeners = ['refresh']; - - public Server $server; + public ?Server $server = null; public StandaloneMongodb $database; + public string $name; + + public ?string $description = null; + + public ?string $mongoConf = null; + + public string $mongoInitdbRootUsername; + + public string $mongoInitdbRootPassword; + + public string $mongoInitdbDatabase; + + public string $image; + + public ?string $portsMappings = null; + + public ?bool $isPublic = null; + + public ?int $publicPort = null; + + public bool $isLogDrainEnabled = false; + + public ?string $customDockerRunOptions = null; + + public bool $enableSsl = false; + + public ?string $sslMode = null; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -37,27 +62,26 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', - 'refresh' => '$refresh', ]; } protected function rules(): array { return [ - 'database.name' => ValidationPatterns::nameRules(), - 'database.description' => ValidationPatterns::descriptionRules(), - 'database.mongo_conf' => 'nullable', - 'database.mongo_initdb_root_username' => 'required', - 'database.mongo_initdb_root_password' => 'required', - 'database.mongo_initdb_database' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - 'database.enable_ssl' => 'boolean', - 'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'mongoConf' => 'nullable', + 'mongoInitdbRootUsername' => 'required', + 'mongoInitdbRootPassword' => 'required', + 'mongoInitdbDatabase' => 'required', + 'image' => 'required', + 'portsMappings' => 'nullable', + 'isPublic' => 'nullable|boolean', + 'publicPort' => 'nullable|integer', + 'isLogDrainEnabled' => 'nullable|boolean', + 'customDockerRunOptions' => 'nullable', + 'enableSsl' => 'boolean', + 'sslMode' => 'nullable|string|in:allow,prefer,require,verify-full', ]; } @@ -66,45 +90,96 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'database.name.required' => 'The Name field is required.', - 'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'database.mongo_initdb_root_username.required' => 'The Root Username field is required.', - 'database.mongo_initdb_root_password.required' => 'The Root Password field is required.', - 'database.mongo_initdb_database.required' => 'The MongoDB Database field is required.', - 'database.image.required' => 'The Docker Image field is required.', - 'database.public_port.integer' => 'The Public Port must be an integer.', - 'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'mongoInitdbRootUsername.required' => 'The Root Username field is required.', + 'mongoInitdbRootPassword.required' => 'The Root Password field is required.', + '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.', + 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.', ] ); } protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.mongo_conf' => 'Mongo Configuration', - 'database.mongo_initdb_root_username' => 'Root Username', - 'database.mongo_initdb_root_password' => 'Root Password', - 'database.mongo_initdb_database' => 'Database', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - 'database.enable_ssl' => 'Enable SSL', - 'database.ssl_mode' => 'SSL Mode', + 'name' => 'Name', + 'description' => 'Description', + 'mongoConf' => 'Mongo Configuration', + 'mongoInitdbRootUsername' => 'Root Username', + 'mongoInitdbRootPassword' => 'Root Password', + 'mongoInitdbDatabase' => 'Database', + 'image' => 'Image', + 'portsMappings' => 'Port Mapping', + 'isPublic' => 'Is Public', + 'publicPort' => 'Public Port', + 'customDockerRunOptions' => 'Custom Docker Run Options', + 'enableSsl' => 'Enable SSL', + 'sslMode' => 'SSL Mode', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->authorize('view', $this->database); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); - $existingCert = $this->database->sslCertificates()->first(); + return; + } - if ($existingCert) { - $this->certificateValidUntil = $existingCert->valid_until; + $existingCert = $this->database->sslCertificates()->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->mongo_conf = $this->mongoConf; + $this->database->mongo_initdb_root_username = $this->mongoInitdbRootUsername; + $this->database->mongo_initdb_root_password = $this->mongoInitdbRootPassword; + $this->database->mongo_initdb_database = $this->mongoInitdbDatabase; + $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->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->enable_ssl = $this->enableSsl; + $this->database->ssl_mode = $this->sslMode; + $this->database->save(); + + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->mongoConf = $this->database->mongo_conf; + $this->mongoInitdbRootUsername = $this->database->mongo_initdb_root_username; + $this->mongoInitdbRootPassword = $this->database->mongo_initdb_root_password; + $this->mongoInitdbDatabase = $this->database->mongo_initdb_database; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->enableSsl = $this->database->enable_ssl; + $this->sslMode = $this->database->ssl_mode; + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; } } @@ -114,12 +189,12 @@ public function instantSaveAdvanced() $this->authorize('update', $this->database); if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -132,14 +207,13 @@ public function submit() try { $this->authorize('update', $this->database); - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - if (str($this->database->mongo_conf)->isEmpty()) { - $this->database->mongo_conf = null; + if (str($this->mongoConf)->isEmpty()) { + $this->mongoConf = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -157,16 +231,16 @@ public function instantSave() try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -176,16 +250,15 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; return handleError($e, $this); } } - public function updatedDatabaseSslMode() + public function updatedSslMode() { $this->instantSaveSSL(); } @@ -195,7 +268,7 @@ public function instantSaveSSL() try { $this->authorize('update', $this->database); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'SSL configuration updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -215,7 +288,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first(); + $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); SslHelper::generateSslCertificate( commonName: $existingCert->common_name, @@ -239,6 +312,7 @@ public function regenerateSslCertificate() public function refresh(): void { $this->database->refresh(); + $this->syncData(); } public function render() diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index ada1b3a2c..cffedcd23 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneMysql; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,11 +18,39 @@ class General extends Component { use AuthorizesRequests; - protected $listeners = ['refresh']; - public StandaloneMysql $database; - public Server $server; + public ?Server $server = null; + + public string $name; + + public ?string $description = null; + + public string $mysqlRootPassword; + + public string $mysqlUser; + + public string $mysqlPassword; + + public string $mysqlDatabase; + + public ?string $mysqlConf = null; + + public string $image; + + public ?string $portsMappings = null; + + public ?bool $isPublic = null; + + public ?int $publicPort = null; + + public bool $isLogDrainEnabled = false; + + public ?string $customDockerRunOptions = null; + + public bool $enableSsl = false; + + public ?string $sslMode = null; public ?string $db_url = null; @@ -37,28 +64,27 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', - 'refresh' => '$refresh', ]; } protected function rules(): array { return [ - 'database.name' => ValidationPatterns::nameRules(), - 'database.description' => ValidationPatterns::descriptionRules(), - 'database.mysql_root_password' => 'required', - 'database.mysql_user' => 'required', - 'database.mysql_password' => 'required', - 'database.mysql_database' => 'required', - 'database.mysql_conf' => 'nullable', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - 'database.enable_ssl' => 'boolean', - 'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'mysqlRootPassword' => 'required', + 'mysqlUser' => 'required', + 'mysqlPassword' => 'required', + 'mysqlDatabase' => 'required', + 'mysqlConf' => 'nullable', + 'image' => 'required', + 'portsMappings' => 'nullable', + 'isPublic' => 'nullable|boolean', + 'publicPort' => 'nullable|integer', + 'isLogDrainEnabled' => 'nullable|boolean', + 'customDockerRunOptions' => 'nullable', + 'enableSsl' => 'boolean', + 'sslMode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY', ]; } @@ -67,47 +93,100 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'database.name.required' => 'The Name field is required.', - 'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'database.mysql_root_password.required' => 'The Root Password field is required.', - 'database.mysql_user.required' => 'The MySQL User field is required.', - 'database.mysql_password.required' => 'The MySQL Password field is required.', - 'database.mysql_database.required' => 'The MySQL Database field is required.', - 'database.image.required' => 'The Docker Image field is required.', - 'database.public_port.integer' => 'The Public Port must be an integer.', - 'database.ssl_mode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'mysqlRootPassword.required' => 'The Root Password field is required.', + 'mysqlUser.required' => 'The MySQL User field is required.', + 'mysqlPassword.required' => 'The MySQL Password field is required.', + '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.', + 'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.', ] ); } protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.mysql_root_password' => 'Root Password', - 'database.mysql_user' => 'User', - 'database.mysql_password' => 'Password', - 'database.mysql_database' => 'Database', - 'database.mysql_conf' => 'MySQL Configuration', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - 'database.enable_ssl' => 'Enable SSL', - 'database.ssl_mode' => 'SSL Mode', + 'name' => 'Name', + 'description' => 'Description', + 'mysqlRootPassword' => 'Root Password', + 'mysqlUser' => 'User', + 'mysqlPassword' => 'Password', + 'mysqlDatabase' => 'Database', + 'mysqlConf' => 'MySQL Configuration', + 'image' => 'Image', + 'portsMappings' => 'Port Mapping', + 'isPublic' => 'Is Public', + 'publicPort' => 'Public Port', + 'customDockerRunOptions' => 'Custom Docker Run Options', + 'enableSsl' => 'Enable SSL', + 'sslMode' => 'SSL Mode', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->authorize('view', $this->database); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); - $existingCert = $this->database->sslCertificates()->first(); + return; + } - if ($existingCert) { - $this->certificateValidUntil = $existingCert->valid_until; + $existingCert = $this->database->sslCertificates()->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->mysql_root_password = $this->mysqlRootPassword; + $this->database->mysql_user = $this->mysqlUser; + $this->database->mysql_password = $this->mysqlPassword; + $this->database->mysql_database = $this->mysqlDatabase; + $this->database->mysql_conf = $this->mysqlConf; + $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->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->enable_ssl = $this->enableSsl; + $this->database->ssl_mode = $this->sslMode; + $this->database->save(); + + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->mysqlRootPassword = $this->database->mysql_root_password; + $this->mysqlUser = $this->database->mysql_user; + $this->mysqlPassword = $this->database->mysql_password; + $this->mysqlDatabase = $this->database->mysql_database; + $this->mysqlConf = $this->database->mysql_conf; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->enableSsl = $this->database->enable_ssl; + $this->sslMode = $this->database->ssl_mode; + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; } } @@ -117,12 +196,12 @@ public function instantSaveAdvanced() $this->authorize('update', $this->database); if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -135,11 +214,10 @@ public function submit() try { $this->authorize('update', $this->database); - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -157,16 +235,16 @@ public function instantSave() try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -176,16 +254,15 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; return handleError($e, $this); } } - public function updatedDatabaseSslMode() + public function updatedSslMode() { $this->instantSaveSSL(); } @@ -195,7 +272,7 @@ public function instantSaveSSL() try { $this->authorize('update', $this->database); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'SSL configuration updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -215,7 +292,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first(); + $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); SslHelper::generateSslCertificate( commonName: $existingCert->common_name, @@ -239,6 +316,7 @@ public function regenerateSslCertificate() public function refresh(): void { $this->database->refresh(); + $this->syncData(); } public function render() diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 2d37620b9..3240aadd2 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandalonePostgresql; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -21,7 +20,41 @@ class General extends Component public StandalonePostgresql $database; - public Server $server; + public ?Server $server = null; + + public string $name; + + public ?string $description = null; + + public string $postgresUser; + + public string $postgresPassword; + + public string $postgresDb; + + public ?string $postgresInitdbArgs = null; + + public ?string $postgresHostAuthMethod = null; + + public ?string $postgresConf = null; + + public ?array $initScripts = null; + + public string $image; + + public ?string $portsMappings = null; + + public ?bool $isPublic = null; + + public ?int $publicPort = null; + + public bool $isLogDrainEnabled = false; + + public ?string $customDockerRunOptions = null; + + public bool $enableSsl = false; + + public ?string $sslMode = null; public string $new_filename; @@ -39,7 +72,6 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', - 'refresh' => '$refresh', 'save_init_script', 'delete_init_script', ]; @@ -48,23 +80,23 @@ public function getListeners() protected function rules(): array { return [ - 'database.name' => ValidationPatterns::nameRules(), - 'database.description' => ValidationPatterns::descriptionRules(), - 'database.postgres_user' => 'required', - 'database.postgres_password' => 'required', - 'database.postgres_db' => 'required', - 'database.postgres_initdb_args' => 'nullable', - 'database.postgres_host_auth_method' => 'nullable', - 'database.postgres_conf' => 'nullable', - 'database.init_scripts' => 'nullable', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - 'database.enable_ssl' => 'boolean', - 'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'postgresUser' => 'required', + 'postgresPassword' => 'required', + 'postgresDb' => 'required', + 'postgresInitdbArgs' => 'nullable', + 'postgresHostAuthMethod' => 'nullable', + 'postgresConf' => 'nullable', + 'initScripts' => 'nullable', + 'image' => 'required', + 'portsMappings' => 'nullable', + 'isPublic' => 'nullable|boolean', + 'publicPort' => 'nullable|integer', + 'isLogDrainEnabled' => 'nullable|boolean', + 'customDockerRunOptions' => 'nullable', + 'enableSsl' => 'boolean', + 'sslMode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full', ]; } @@ -73,48 +105,105 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'database.name.required' => 'The Name field is required.', - 'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'database.postgres_user.required' => 'The Postgres User field is required.', - 'database.postgres_password.required' => 'The Postgres Password field is required.', - 'database.postgres_db.required' => 'The Postgres Database field is required.', - 'database.image.required' => 'The Docker Image field is required.', - 'database.public_port.integer' => 'The Public Port must be an integer.', - 'database.ssl_mode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'postgresUser.required' => 'The Postgres User field is required.', + 'postgresPassword.required' => 'The Postgres Password field is required.', + '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.', + 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.', ] ); } protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.postgres_user' => 'Postgres User', - 'database.postgres_password' => 'Postgres Password', - 'database.postgres_db' => 'Postgres DB', - 'database.postgres_initdb_args' => 'Postgres Initdb Args', - 'database.postgres_host_auth_method' => 'Postgres Host Auth Method', - 'database.postgres_conf' => 'Postgres Configuration', - 'database.init_scripts' => 'Init Scripts', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - 'database.enable_ssl' => 'Enable SSL', - 'database.ssl_mode' => 'SSL Mode', + 'name' => 'Name', + 'description' => 'Description', + 'postgresUser' => 'Postgres User', + 'postgresPassword' => 'Postgres Password', + 'postgresDb' => 'Postgres DB', + 'postgresInitdbArgs' => 'Postgres Initdb Args', + 'postgresHostAuthMethod' => 'Postgres Host Auth Method', + 'postgresConf' => 'Postgres Configuration', + 'initScripts' => 'Init Scripts', + 'image' => 'Image', + 'portsMappings' => 'Port Mapping', + 'isPublic' => 'Is Public', + 'publicPort' => 'Public Port', + 'customDockerRunOptions' => 'Custom Docker Run Options', + 'enableSsl' => 'Enable SSL', + 'sslMode' => 'SSL Mode', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->authorize('view', $this->database); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); - $existingCert = $this->database->sslCertificates()->first(); + return; + } - if ($existingCert) { - $this->certificateValidUntil = $existingCert->valid_until; + $existingCert = $this->database->sslCertificates()->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->postgres_user = $this->postgresUser; + $this->database->postgres_password = $this->postgresPassword; + $this->database->postgres_db = $this->postgresDb; + $this->database->postgres_initdb_args = $this->postgresInitdbArgs; + $this->database->postgres_host_auth_method = $this->postgresHostAuthMethod; + $this->database->postgres_conf = $this->postgresConf; + $this->database->init_scripts = $this->initScripts; + $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->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->enable_ssl = $this->enableSsl; + $this->database->ssl_mode = $this->sslMode; + $this->database->save(); + + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgresUser = $this->database->postgres_user; + $this->postgresPassword = $this->database->postgres_password; + $this->postgresDb = $this->database->postgres_db; + $this->postgresInitdbArgs = $this->database->postgres_initdb_args; + $this->postgresHostAuthMethod = $this->database->postgres_host_auth_method; + $this->postgresConf = $this->database->postgres_conf; + $this->initScripts = $this->database->init_scripts; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->enableSsl = $this->database->enable_ssl; + $this->sslMode = $this->database->ssl_mode; + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; } } @@ -124,12 +213,12 @@ public function instantSaveAdvanced() $this->authorize('update', $this->database); if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -137,7 +226,7 @@ public function instantSaveAdvanced() } } - public function updatedDatabaseSslMode() + public function updatedSslMode() { $this->instantSaveSSL(); } @@ -147,10 +236,8 @@ public function instantSaveSSL() try { $this->authorize('update', $this->database); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'SSL configuration updated.'); - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; } catch (Exception $e) { return handleError($e, $this); } @@ -169,7 +256,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first(); + $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); SslHelper::generateSslCertificate( commonName: $existingCert->common_name, @@ -195,16 +282,16 @@ public function instantSave() try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -214,10 +301,9 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; return handleError($e, $this); } @@ -227,7 +313,7 @@ public function save_init_script($script) { $this->authorize('update', $this->database); - $initScripts = collect($this->database->init_scripts ?? []); + $initScripts = collect($this->initScripts ?? []); $existingScript = $initScripts->firstWhere('filename', $script['filename']); $oldScript = $initScripts->firstWhere('index', $script['index']); @@ -263,7 +349,7 @@ public function save_init_script($script) $initScripts->push($script); } - $this->database->init_scripts = $initScripts->values() + $this->initScripts = $initScripts->values() ->map(function ($item, $index) { $item['index'] = $index; @@ -271,7 +357,7 @@ public function save_init_script($script) }) ->all(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Init script saved and updated.'); } @@ -279,7 +365,7 @@ public function delete_init_script($script) { $this->authorize('update', $this->database); - $collection = collect($this->database->init_scripts); + $collection = collect($this->initScripts); $found = $collection->firstWhere('filename', $script['filename']); if ($found) { $container_name = $this->database->uuid; @@ -304,8 +390,8 @@ public function delete_init_script($script) }) ->all(); - $this->database->init_scripts = $updatedScripts; - $this->database->save(); + $this->initScripts = $updatedScripts; + $this->syncData(true); $this->dispatch('refresh')->self(); $this->dispatch('success', 'Init script deleted from the database and the server.'); } @@ -319,23 +405,23 @@ public function save_new_init_script() 'new_filename' => 'required|string', 'new_content' => 'required|string', ]); - $found = collect($this->database->init_scripts)->firstWhere('filename', $this->new_filename); + $found = collect($this->initScripts)->firstWhere('filename', $this->new_filename); if ($found) { $this->dispatch('error', 'Filename already exists.'); return; } - if (! isset($this->database->init_scripts)) { - $this->database->init_scripts = []; + if (! isset($this->initScripts)) { + $this->initScripts = []; } - $this->database->init_scripts = array_merge($this->database->init_scripts, [ + $this->initScripts = array_merge($this->initScripts, [ [ - 'index' => count($this->database->init_scripts), + 'index' => count($this->initScripts), 'filename' => $this->new_filename, 'content' => $this->new_content, ], ]); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Init script added.'); $this->new_content = ''; $this->new_filename = ''; @@ -346,11 +432,10 @@ public function submit() try { $this->authorize('update', $this->database); - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 1eb4f5c8d..846614d21 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -6,7 +6,6 @@ use App\Actions\Database\StopDatabaseProxy; use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use App\Models\StandaloneRedis; use App\Support\ValidationPatterns; use Carbon\Carbon; @@ -19,19 +18,39 @@ class General extends Component { use AuthorizesRequests; - public Server $server; + public ?Server $server = null; public StandaloneRedis $database; - public string $redis_username; + public string $name; - public ?string $redis_password; + public ?string $description = null; - public string $redis_version; + public ?string $redisConf = null; - public ?string $db_url = null; + public string $image; - public ?string $db_url_public = null; + public ?string $portsMappings = null; + + public ?bool $isPublic = null; + + public ?int $publicPort = null; + + public bool $isLogDrainEnabled = false; + + public ?string $customDockerRunOptions = null; + + public string $redisUsername; + + public string $redisPassword; + + public string $redisVersion; + + public ?string $dbUrl = null; + + public ?string $dbUrlPublic = null; + + public bool $enableSsl = false; public ?Carbon $certificateValidUntil = null; @@ -42,25 +61,24 @@ public function getListeners() return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', 'envsUpdated' => 'refresh', - 'refresh', ]; } protected function rules(): array { return [ - 'database.name' => ValidationPatterns::nameRules(), - 'database.description' => ValidationPatterns::descriptionRules(), - 'database.redis_conf' => 'nullable', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - 'redis_username' => 'required', - 'redis_password' => 'required', - 'database.enable_ssl' => 'boolean', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'redisConf' => 'nullable', + 'image' => 'required', + 'portsMappings' => 'nullable', + 'isPublic' => 'nullable|boolean', + 'publicPort' => 'nullable|integer', + 'isLogDrainEnabled' => 'nullable|boolean', + 'customDockerRunOptions' => 'nullable', + 'redisUsername' => 'required', + 'redisPassword' => 'required', + 'enableSsl' => 'boolean', ]; } @@ -69,39 +87,87 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'database.name.required' => 'The Name field is required.', - 'database.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'database.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'database.image.required' => 'The Docker Image field is required.', - 'database.public_port.integer' => 'The Public Port must be an integer.', - 'redis_username.required' => 'The Redis Username field is required.', - 'redis_password.required' => 'The Redis Password field is required.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'image.required' => 'The Docker Image field is required.', + 'publicPort.integer' => 'The Public Port must be an integer.', + 'redisUsername.required' => 'The Redis Username field is required.', + 'redisPassword.required' => 'The Redis Password field is required.', ] ); } protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.redis_conf' => 'Redis Configuration', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Options', - 'redis_username' => 'Redis Username', - 'redis_password' => 'Redis Password', - 'database.enable_ssl' => 'Enable SSL', + 'name' => 'Name', + 'description' => 'Description', + 'redisConf' => 'Redis Configuration', + 'image' => 'Image', + 'portsMappings' => 'Port Mapping', + 'isPublic' => 'Is Public', + 'publicPort' => 'Public Port', + 'customDockerRunOptions' => 'Custom Docker Options', + 'redisUsername' => 'Redis Username', + 'redisPassword' => 'Redis Password', + 'enableSsl' => 'Enable SSL', ]; public function mount() { - $this->server = data_get($this->database, 'destination.server'); - $this->refreshView(); - $existingCert = $this->database->sslCertificates()->first(); + try { + $this->authorize('view', $this->database); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + if (! $this->server) { + $this->dispatch('error', 'Database destination server is not configured.'); - if ($existingCert) { - $this->certificateValidUntil = $existingCert->valid_until; + return; + } + + $existingCert = $this->database->sslCertificates()->first(); + + if ($existingCert) { + $this->certificateValidUntil = $existingCert->valid_until; + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->redis_conf = $this->redisConf; + $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->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->enable_ssl = $this->enableSsl; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->redisConf = $this->database->redis_conf; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->enableSsl = $this->database->enable_ssl; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + $this->redisVersion = $this->database->getRedisVersion(); + $this->redisUsername = $this->database->redis_username; + $this->redisPassword = $this->database->redis_password; } } @@ -111,12 +177,12 @@ public function instantSaveAdvanced() $this->authorize('update', $this->database); if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -129,20 +195,19 @@ public function submit() try { $this->authorize('manageEnvironment', $this->database); - $this->validate(); + $this->syncData(true); - if (version_compare($this->redis_version, '6.0', '>=')) { + if (version_compare($this->redisVersion, '6.0', '>=')) { $this->database->runtime_environment_variables()->updateOrCreate( ['key' => 'REDIS_USERNAME'], - ['value' => $this->redis_username, 'resourceable_id' => $this->database->id] + ['value' => $this->redisUsername, 'resourceable_id' => $this->database->id] ); } $this->database->runtime_environment_variables()->updateOrCreate( ['key' => 'REDIS_PASSWORD'], - ['value' => $this->redis_password, 'resourceable_id' => $this->database->id] + ['value' => $this->redisPassword, 'resourceable_id' => $this->database->id] ); - $this->database->save(); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -156,16 +221,16 @@ public function instantSave() try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -175,10 +240,11 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } @@ -189,7 +255,7 @@ public function instantSaveSSL() try { $this->authorize('update', $this->database); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'SSL configuration updated.'); } catch (Exception $e) { return handleError($e, $this); @@ -209,7 +275,7 @@ public function regenerateSslCertificate() return; } - $caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first(); + $caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); SslHelper::generateSslCertificate( commonName: $existingCert->commonName, @@ -233,16 +299,7 @@ public function regenerateSslCertificate() public function refresh(): void { $this->database->refresh(); - $this->refreshView(); - } - - private function refreshView() - { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->redis_version = $this->database->getRedisVersion(); - $this->redis_username = $this->database->redis_username; - $this->redis_password = $this->database->redis_password; + $this->syncData(); } public function render() diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php index 5cda1dedd..a88a62d88 100644 --- a/app/Livewire/Project/New/DockerCompose.php +++ b/app/Livewire/Project/New/DockerCompose.php @@ -37,6 +37,10 @@ public function submit() 'dockerComposeRaw' => 'required', ]); $this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // 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(); diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index e105c956a..9d04ca9a5 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -28,18 +28,60 @@ public function mount() $this->query = request()->query(); } + /** + * Auto-parse image name when user pastes a complete Docker image reference + * Examples: + * - nginx:stable-alpine3.21-perl@sha256:4e272eef... + * - ghcr.io/user/app:v1.2.3 + * - nginx@sha256:abc123... + */ + public function updatedImageName(): void + { + if (empty($this->imageName)) { + return; + } + + // Don't auto-parse if user has already manually filled tag or sha256 fields + if (! empty($this->imageTag) || ! empty($this->imageSha256)) { + return; + } + + // Only auto-parse if the image name contains a tag (:) or digest (@) + if (! str_contains($this->imageName, ':') && ! str_contains($this->imageName, '@')) { + return; + } + + try { + $parser = new DockerImageParser; + $parser->parse($this->imageName); + + // Extract the base image name (without tag/digest) + $baseImageName = $parser->getFullImageNameWithoutTag(); + + // Only update if parsing resulted in different base name + // This prevents unnecessary updates when user types just the name + if ($baseImageName !== $this->imageName) { + if ($parser->isImageHash()) { + // It's a SHA256 digest (takes priority over tag) + $this->imageSha256 = $parser->getTag(); + $this->imageTag = ''; + } elseif ($parser->getTag() !== 'latest' || str_contains($this->imageName, ':')) { + // It's a regular tag (only set if not default 'latest' or explicitly specified) + $this->imageTag = $parser->getTag(); + $this->imageSha256 = ''; + } + + // Update imageName to just the base name + $this->imageName = $baseImageName; + } + } catch (\Exception $e) { + // If parsing fails, leave the image name as-is + // User will see validation error on submit + } + } + public function submit() { - // Strip 'sha256:' prefix if user pasted it - if ($this->imageSha256) { - $this->imageSha256 = preg_replace('/^sha256:/i', '', trim($this->imageSha256)); - } - - // Remove @sha256 from image name if user added it - if ($this->imageName) { - $this->imageName = preg_replace('/@sha256$/i', '', trim($this->imageName)); - } - $this->validate([ 'imageName' => ['required', 'string'], 'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'], @@ -56,13 +98,16 @@ public function submit() // Build the full Docker image string if ($this->imageSha256) { - $dockerImage = $this->imageName.'@sha256:'.$this->imageSha256; + // Strip 'sha256:' prefix if user pasted it + $sha256Hash = preg_replace('/^sha256:/i', '', trim($this->imageSha256)); + $dockerImage = $this->imageName.'@sha256:'.$sha256Hash; } elseif ($this->imageTag) { $dockerImage = $this->imageName.':'.$this->imageTag; } else { $dockerImage = $this->imageName.':latest'; } + // Parse using DockerImageParser to normalize the image reference $parser = new DockerImageParser; $parser->parse($dockerImage); @@ -79,15 +124,15 @@ public function submit() $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first(); - // Determine the image tag based on whether it's a hash or regular tag - $imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag(); - // Append @sha256 to image name if using digest and not already present $imageName = $parser->getFullImageNameWithoutTag(); if ($parser->isImageHash() && ! str_ends_with($imageName, '@sha256')) { $imageName .= '@sha256'; } + // 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([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, @@ -96,7 +141,7 @@ public function submit() 'build_pack' => 'dockerimage', 'ports_exposes' => 80, 'docker_registry_image_name' => $imageName, - 'docker_registry_image_tag' => $parser->getTag(), + 'docker_registry_image_tag' => $imageTag, 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination_class, diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 73960d288..cdf95d2e4 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -81,7 +81,7 @@ public function mount() 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), ]; - if ($oneClickServiceName === 'cloudflared') { + if ($oneClickServiceName === 'cloudflared' || $oneClickServiceName === 'pgadmin') { data_set($service_payload, 'connect_to_docker_network', true); } $service = Service::create($service_payload); diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 559851e3a..2d69ceb12 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -33,6 +33,8 @@ public function getListeners() return [ "echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked', + 'refreshServices' => 'refreshServices', + 'refresh' => 'refreshServices', ]; } diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index abf4c45a7..4bcf866d3 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -24,16 +24,30 @@ class Database extends Component public $parameters; + public ?string $humanName = null; + + public ?string $description = null; + + public ?string $image = null; + + public bool $excludeFromStatus = false; + + public ?int $publicPort = null; + + public bool $isPublic = false; + + public bool $isLogDrainEnabled = false; + protected $listeners = ['refreshFileStorages']; protected $rules = [ - 'database.human_name' => 'nullable', - 'database.description' => 'nullable', - 'database.image' => 'required', - 'database.exclude_from_status' => 'required|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_public' => 'required|boolean', - 'database.is_log_drain_enabled' => 'required|boolean', + 'humanName' => 'nullable', + 'description' => 'nullable', + 'image' => 'required', + 'excludeFromStatus' => 'required|boolean', + 'publicPort' => 'nullable|integer', + 'isPublic' => 'required|boolean', + 'isLogDrainEnabled' => 'required|boolean', ]; public function render() @@ -50,11 +64,33 @@ public function mount() $this->db_url_public = $this->database->getServiceDatabaseUrl(); } $this->refreshFileStorages(); + $this->syncData(false); } catch (\Throwable $e) { return handleError($e, $this); } } + private function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->database->human_name = $this->humanName; + $this->database->description = $this->description; + $this->database->image = $this->image; + $this->database->exclude_from_status = $this->excludeFromStatus; + $this->database->public_port = $this->publicPort; + $this->database->is_public = $this->isPublic; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + } else { + $this->humanName = $this->database->human_name; + $this->description = $this->database->description; + $this->image = $this->database->image; + $this->excludeFromStatus = $this->database->exclude_from_status ?? false; + $this->publicPort = $this->database->public_port; + $this->isPublic = $this->database->is_public ?? false; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled ?? false; + } + } + public function delete($password) { try { @@ -92,7 +128,7 @@ public function instantSaveLogDrain() try { $this->authorize('update', $this->database); if (! $this->database->service->destination->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; @@ -145,15 +181,17 @@ public function instantSave() { try { $this->authorize('update', $this->database); - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } + $this->syncData(true); if ($this->database->is_public) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); + $this->isPublic = false; $this->database->is_public = false; return; @@ -182,7 +220,10 @@ public function submit() try { $this->authorize('update', $this->database); $this->validate(); + $this->syncData(true); $this->database->save(); + $this->database->refresh(); + $this->syncData(false); updateCompose($this->database); $this->dispatch('success', 'Database saved.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index b5f208941..32cf72067 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -11,6 +11,12 @@ class EditCompose extends Component public $serviceId; + public ?string $dockerComposeRaw = null; + + public ?string $dockerCompose = null; + + public bool $isContainerLabelEscapeEnabled = false; + protected $listeners = [ 'refreshEnvs', 'envsUpdated', @@ -18,30 +24,45 @@ class EditCompose extends Component ]; protected $rules = [ - 'service.docker_compose_raw' => 'required', - 'service.docker_compose' => 'required', - 'service.is_container_label_escape_enabled' => 'required', + 'dockerComposeRaw' => 'required', + 'dockerCompose' => 'required', + 'isContainerLabelEscapeEnabled' => 'required', ]; public function envsUpdated() { - $this->dispatch('saveCompose', $this->service->docker_compose_raw); + $this->dispatch('saveCompose', $this->dockerComposeRaw); $this->refreshEnvs(); } public function refreshEnvs() { $this->service = Service::ownedByCurrentTeam()->find($this->serviceId); + $this->syncData(false); } public function mount() { $this->service = Service::ownedByCurrentTeam()->find($this->serviceId); + $this->syncData(false); + } + + private function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->service->docker_compose_raw = $this->dockerComposeRaw; + $this->service->docker_compose = $this->dockerCompose; + $this->service->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled; + } else { + $this->dockerComposeRaw = $this->service->docker_compose_raw; + $this->dockerCompose = $this->service->docker_compose; + $this->isContainerLabelEscapeEnabled = $this->service->is_container_label_escape_enabled ?? false; + } } public function validateCompose() { - $isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id); + $isValid = validateComposeFile($this->dockerComposeRaw, $this->service->server_id); if ($isValid !== 'OK') { $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); } else { @@ -52,16 +73,17 @@ public function validateCompose() public function saveEditedCompose() { $this->dispatch('info', 'Saving new docker compose...'); - $this->dispatch('saveCompose', $this->service->docker_compose_raw); + $this->dispatch('saveCompose', $this->dockerComposeRaw); $this->dispatch('refreshStorages'); } public function instantSave() { $this->validate([ - 'service.is_container_label_escape_enabled' => 'required', + 'isContainerLabelEscapeEnabled' => 'required', ]); - $this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]); + $this->syncData(true); + $this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]); $this->dispatch('success', 'Service updated successfully'); } diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 7c718393d..43d885238 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -2,12 +2,14 @@ namespace App\Livewire\Project\Service; +use App\Livewire\Concerns\SynchronizesModelData; use App\Models\ServiceApplication; use Livewire\Component; use Spatie\Url\Url; class EditDomain extends Component { + use SynchronizesModelData; public $applicationId; public ServiceApplication $application; @@ -18,14 +20,24 @@ class EditDomain extends Component public $forceSaveDomains = false; + public ?string $fqdn = null; + protected $rules = [ - 'application.fqdn' => 'nullable', - 'application.required_fqdn' => 'required|boolean', + 'fqdn' => 'nullable', ]; public function mount() { - $this->application = ServiceApplication::find($this->applicationId); + $this->application = ServiceApplication::query()->findOrFail($this->applicationId); + $this->authorize('view', $this->application); + $this->syncFromModel(); + } + + protected function getModelBindings(): array + { + return [ + 'fqdn' => 'application.fqdn', + ]; } public function confirmDomainUsage() @@ -38,19 +50,22 @@ public function confirmDomainUsage() public function submit() { try { - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $this->authorize('update', $this->application); + $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); + $this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString(); + $domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) { $domain = trim($domain); Url::fromString($domain, ['http', 'https']); return str($domain)->lower(); }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $warning = sslipDomainWarning($this->application->fqdn); + $this->fqdn = $domains->unique()->implode(','); + $warning = sslipDomainWarning($this->fqdn); if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } + // Sync to model for domain conflict check + $this->syncToModel(); // Check for domain conflicts if not forcing save if (! $this->forceSaveDomains) { $result = checkDomainUsage(resource: $this->application); @@ -67,17 +82,21 @@ public function submit() $this->validate(); $this->application->save(); + $this->application->refresh(); + $this->syncData(false); updateCompose($this->application); if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } $this->application->service->parse(); $this->dispatch('refresh'); + $this->dispatch('refreshServices'); $this->dispatch('configurationChanged'); } catch (\Throwable $e) { $originalFqdn = $this->application->getOriginal('fqdn'); if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; + $this->syncData(false); } return handleError($e, $this); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 7f0caaba3..40539b13e 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Service; +use App\Livewire\Concerns\SynchronizesModelData; use App\Models\Application; use App\Models\InstanceSettings; use App\Models\LocalFileVolume; @@ -22,7 +23,7 @@ class FileStorage extends Component { - use AuthorizesRequests; + use AuthorizesRequests, SynchronizesModelData; public LocalFileVolume $fileStorage; @@ -36,12 +37,16 @@ class FileStorage extends Component public bool $isReadOnly = false; + public ?string $content = null; + + public bool $isBasedOnGit = false; + protected $rules = [ 'fileStorage.is_directory' => 'required', 'fileStorage.fs_path' => 'required', 'fileStorage.mount_path' => 'required', - 'fileStorage.content' => 'nullable', - 'fileStorage.is_based_on_git' => 'required|boolean', + 'content' => 'nullable', + 'isBasedOnGit' => 'required|boolean', ]; public function mount() @@ -56,6 +61,15 @@ public function mount() } $this->isReadOnly = $this->fileStorage->isReadOnlyVolume(); + $this->syncFromModel(); + } + + protected function getModelBindings(): array + { + return [ + 'content' => 'fileStorage.content', + 'isBasedOnGit' => 'fileStorage.is_based_on_git', + ]; } public function convertToDirectory() @@ -82,6 +96,7 @@ public function loadStorageOnServer() $this->authorize('update', $this->resource); $this->fileStorage->loadStorageOnServer(); + $this->syncFromModel(); $this->dispatch('success', 'File storage loaded from server.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -148,14 +163,16 @@ public function submit() try { $this->validate(); if ($this->fileStorage->is_directory) { - $this->fileStorage->content = null; + $this->content = null; } + $this->syncToModel(); $this->fileStorage->save(); $this->fileStorage->saveStorageOnServer(); $this->dispatch('success', 'File updated.'); } catch (\Throwable $e) { $this->fileStorage->setRawAttributes($original); $this->fileStorage->save(); + $this->syncFromModel(); return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index e37b6ad86..20358218f 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Service; +use App\Livewire\Concerns\SynchronizesModelData; use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -14,6 +15,7 @@ class ServiceApplicationView extends Component { use AuthorizesRequests; + use SynchronizesModelData; public ServiceApplication $application; @@ -29,16 +31,32 @@ class ServiceApplicationView extends Component public $forceSaveDomains = false; + public ?string $humanName = null; + + public ?string $description = null; + + public ?string $fqdn = null; + + public ?string $image = null; + + public bool $excludeFromStatus = false; + + public bool $isLogDrainEnabled = false; + + public bool $isGzipEnabled = false; + + public bool $isStripprefixEnabled = false; + protected $rules = [ - 'application.human_name' => 'nullable', - 'application.description' => 'nullable', - 'application.fqdn' => 'nullable', - 'application.image' => 'string|nullable', - 'application.exclude_from_status' => 'required|boolean', + 'humanName' => 'nullable', + 'description' => 'nullable', + 'fqdn' => 'nullable', + 'image' => 'string|nullable', + 'excludeFromStatus' => 'required|boolean', 'application.required_fqdn' => 'required|boolean', - 'application.is_log_drain_enabled' => 'nullable|boolean', - 'application.is_gzip_enabled' => 'nullable|boolean', - 'application.is_stripprefix_enabled' => 'nullable|boolean', + 'isLogDrainEnabled' => 'nullable|boolean', + 'isGzipEnabled' => 'nullable|boolean', + 'isStripprefixEnabled' => 'nullable|boolean', ]; public function instantSave() @@ -56,11 +74,12 @@ public function instantSaveAdvanced() try { $this->authorize('update', $this->application); if (! $this->application->service->destination->server->isLogDrainEnabled()) { - $this->application->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } + $this->syncToModel(); $this->application->save(); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (\Throwable $e) { @@ -95,11 +114,26 @@ public function mount() try { $this->parameters = get_route_parameters(); $this->authorize('view', $this->application); + $this->syncFromModel(); } catch (\Throwable $e) { return handleError($e, $this); } } + protected function getModelBindings(): array + { + return [ + 'humanName' => 'application.human_name', + 'description' => 'application.description', + 'fqdn' => 'application.fqdn', + 'image' => 'application.image', + 'excludeFromStatus' => 'application.exclude_from_status', + 'isLogDrainEnabled' => 'application.is_log_drain_enabled', + 'isGzipEnabled' => 'application.is_gzip_enabled', + 'isStripprefixEnabled' => 'application.is_stripprefix_enabled', + ]; + } + public function convertToDatabase() { try { @@ -146,19 +180,21 @@ public function submit() { try { $this->authorize('update', $this->application); - $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); - $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { + $this->fqdn = str($this->fqdn)->replaceEnd(',', '')->trim()->toString(); + $this->fqdn = str($this->fqdn)->replaceStart(',', '')->trim()->toString(); + $domains = str($this->fqdn)->trim()->explode(',')->map(function ($domain) { $domain = trim($domain); Url::fromString($domain, ['http', 'https']); return str($domain)->lower(); }); - $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - $warning = sslipDomainWarning($this->application->fqdn); + $this->fqdn = $domains->unique()->implode(','); + $warning = sslipDomainWarning($this->fqdn); if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } + // Sync to model for domain conflict check + $this->syncToModel(); // Check for domain conflicts if not forcing save if (! $this->forceSaveDomains) { $result = checkDomainUsage(resource: $this->application); @@ -175,6 +211,8 @@ public function submit() $this->validate(); $this->application->save(); + $this->application->refresh(); + $this->syncFromModel(); updateCompose($this->application); if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); @@ -186,6 +224,7 @@ public function submit() $originalFqdn = $this->application->getOriginal('fqdn'); if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; + $this->syncFromModel(); } return handleError($e, $this); diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 1961a7985..85cd21a7f 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -15,14 +15,25 @@ class StackForm extends Component protected $listeners = ['saveCompose']; + // Explicit properties + public string $name; + + public ?string $description = null; + + public string $dockerComposeRaw; + + public string $dockerCompose; + + public ?bool $connectToDockerNetwork = null; + protected function rules(): array { $baseRules = [ - 'service.docker_compose_raw' => 'required', - 'service.docker_compose' => 'required', - 'service.name' => ValidationPatterns::nameRules(), - 'service.description' => ValidationPatterns::descriptionRules(), - 'service.connect_to_docker_network' => 'nullable', + 'dockerComposeRaw' => 'required', + 'dockerCompose' => 'required', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'connectToDockerNetwork' => 'nullable', ]; // Add dynamic field rules @@ -39,19 +50,44 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'service.name.required' => 'The Name field is required.', - 'service.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'service.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'service.docker_compose_raw.required' => 'The Docker Compose Raw field is required.', - 'service.docker_compose.required' => 'The Docker Compose field is required.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'dockerComposeRaw.required' => 'The Docker Compose Raw field is required.', + 'dockerCompose.required' => 'The Docker Compose field is required.', ] ); } public $validationAttributes = []; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->service->name = $this->name; + $this->service->description = $this->description; + $this->service->docker_compose_raw = $this->dockerComposeRaw; + $this->service->docker_compose = $this->dockerCompose; + $this->service->connect_to_docker_network = $this->connectToDockerNetwork; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->service->name; + $this->description = $this->service->description; + $this->dockerComposeRaw = $this->service->docker_compose_raw; + $this->dockerCompose = $this->service->docker_compose; + $this->connectToDockerNetwork = $this->service->connect_to_docker_network; + } + } + public function mount() { + $this->syncData(false); $this->fields = collect([]); $extraFields = $this->service->extraFields(); foreach ($extraFields as $serviceName => $fields) { @@ -87,12 +123,13 @@ public function mount() public function saveCompose($raw) { - $this->service->docker_compose_raw = $raw; + $this->dockerComposeRaw = $raw; $this->submit(notify: true); } public function instantSave() { + $this->syncData(true); $this->service->save(); $this->dispatch('success', 'Service settings saved.'); } @@ -101,6 +138,11 @@ public function submit($notify = true) { try { $this->validate(); + $this->syncData(true); + + // Validate for command injection BEFORE saving to database + validateDockerComposeForInjection($this->service->docker_compose_raw); + $this->service->save(); $this->service->saveExtraFields($this->fields); $this->service->parse(); diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index c0714fe03..c8029761d 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -2,35 +2,90 @@ namespace App\Livewire\Project\Shared; +use App\Livewire\Concerns\SynchronizesModelData; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class HealthChecks extends Component { use AuthorizesRequests; + use SynchronizesModelData; public $resource; - protected $rules = [ - 'resource.health_check_enabled' => 'boolean', - 'resource.health_check_path' => 'string', - 'resource.health_check_port' => 'nullable|string', - 'resource.health_check_host' => 'string', - 'resource.health_check_method' => 'string', - 'resource.health_check_return_code' => 'integer', - 'resource.health_check_scheme' => 'string', - 'resource.health_check_response_text' => 'nullable|string', - 'resource.health_check_interval' => 'integer|min:1', - 'resource.health_check_timeout' => 'integer|min:1', - 'resource.health_check_retries' => 'integer|min:1', - 'resource.health_check_start_period' => 'integer', - 'resource.custom_healthcheck_found' => 'boolean', + // Explicit properties + public bool $healthCheckEnabled = false; + public string $healthCheckMethod; + + public string $healthCheckScheme; + + public string $healthCheckHost; + + public ?string $healthCheckPort = null; + + public string $healthCheckPath; + + public int $healthCheckReturnCode; + + public ?string $healthCheckResponseText = null; + + public int $healthCheckInterval; + + public int $healthCheckTimeout; + + public int $healthCheckRetries; + + public int $healthCheckStartPeriod; + + public bool $customHealthcheckFound = false; + + protected $rules = [ + 'healthCheckEnabled' => 'boolean', + 'healthCheckPath' => 'string', + 'healthCheckPort' => 'nullable|string', + 'healthCheckHost' => 'string', + 'healthCheckMethod' => 'string', + 'healthCheckReturnCode' => 'integer', + 'healthCheckScheme' => 'string', + 'healthCheckResponseText' => 'nullable|string', + 'healthCheckInterval' => 'integer|min:1', + 'healthCheckTimeout' => 'integer|min:1', + 'healthCheckRetries' => 'integer|min:1', + 'healthCheckStartPeriod' => 'integer', + 'customHealthcheckFound' => 'boolean', ]; + protected function getModelBindings(): array + { + return [ + 'healthCheckEnabled' => 'resource.health_check_enabled', + 'healthCheckMethod' => 'resource.health_check_method', + 'healthCheckScheme' => 'resource.health_check_scheme', + 'healthCheckHost' => 'resource.health_check_host', + 'healthCheckPort' => 'resource.health_check_port', + 'healthCheckPath' => 'resource.health_check_path', + 'healthCheckReturnCode' => 'resource.health_check_return_code', + 'healthCheckResponseText' => 'resource.health_check_response_text', + 'healthCheckInterval' => 'resource.health_check_interval', + 'healthCheckTimeout' => 'resource.health_check_timeout', + 'healthCheckRetries' => 'resource.health_check_retries', + 'healthCheckStartPeriod' => 'resource.health_check_start_period', + 'customHealthcheckFound' => 'resource.custom_healthcheck_found', + ]; + } + + public function mount() + { + $this->authorize('view', $this->resource); + $this->syncFromModel(); + } + public function instantSave() { $this->authorize('update', $this->resource); + + $this->syncToModel(); $this->resource->save(); $this->dispatch('success', 'Health check updated.'); } @@ -40,6 +95,8 @@ public function submit() try { $this->authorize('update', $this->resource); $this->validate(); + + $this->syncToModel(); $this->resource->save(); $this->dispatch('success', 'Health check updated.'); } catch (\Throwable $e) { @@ -51,14 +108,16 @@ public function toggleHealthcheck() { try { $this->authorize('update', $this->resource); - $wasEnabled = $this->resource->health_check_enabled; - $this->resource->health_check_enabled = ! $this->resource->health_check_enabled; + $wasEnabled = $this->healthCheckEnabled; + $this->healthCheckEnabled = ! $this->healthCheckEnabled; + + $this->syncToModel(); $this->resource->save(); - if ($this->resource->health_check_enabled && ! $wasEnabled && $this->resource->isRunning()) { + if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) { $this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.'); } else { - $this->dispatch('success', 'Health check '.($this->resource->health_check_enabled ? 'enabled' : 'disabled').'.'); + $this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'.'); } } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Shared/ResourceLimits.php b/app/Livewire/Project/Shared/ResourceLimits.php index 196badec8..0b3840289 100644 --- a/app/Livewire/Project/Shared/ResourceLimits.php +++ b/app/Livewire/Project/Shared/ResourceLimits.php @@ -11,52 +11,105 @@ class ResourceLimits extends Component public $resource; + // Explicit properties + public ?string $limitsCpus = null; + + public ?string $limitsCpuset = null; + + public ?int $limitsCpuShares = null; + + public string $limitsMemory; + + public string $limitsMemorySwap; + + public int $limitsMemorySwappiness; + + public string $limitsMemoryReservation; + protected $rules = [ - 'resource.limits_memory' => 'required|string', - 'resource.limits_memory_swap' => 'required|string', - 'resource.limits_memory_swappiness' => 'required|integer|min:0|max:100', - 'resource.limits_memory_reservation' => 'required|string', - 'resource.limits_cpus' => 'nullable', - 'resource.limits_cpuset' => 'nullable', - 'resource.limits_cpu_shares' => 'nullable', + 'limitsMemory' => 'required|string', + 'limitsMemorySwap' => 'required|string', + 'limitsMemorySwappiness' => 'required|integer|min:0|max:100', + 'limitsMemoryReservation' => 'required|string', + 'limitsCpus' => 'nullable', + 'limitsCpuset' => 'nullable', + 'limitsCpuShares' => 'nullable', ]; protected $validationAttributes = [ - 'resource.limits_memory' => 'memory', - 'resource.limits_memory_swap' => 'swap', - 'resource.limits_memory_swappiness' => 'swappiness', - 'resource.limits_memory_reservation' => 'reservation', - 'resource.limits_cpus' => 'cpus', - 'resource.limits_cpuset' => 'cpuset', - 'resource.limits_cpu_shares' => 'cpu shares', + 'limitsMemory' => 'memory', + 'limitsMemorySwap' => 'swap', + 'limitsMemorySwappiness' => 'swappiness', + 'limitsMemoryReservation' => 'reservation', + 'limitsCpus' => 'cpus', + 'limitsCpuset' => 'cpuset', + 'limitsCpuShares' => 'cpu shares', ]; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->resource->limits_cpus = $this->limitsCpus; + $this->resource->limits_cpuset = $this->limitsCpuset; + $this->resource->limits_cpu_shares = $this->limitsCpuShares; + $this->resource->limits_memory = $this->limitsMemory; + $this->resource->limits_memory_swap = $this->limitsMemorySwap; + $this->resource->limits_memory_swappiness = $this->limitsMemorySwappiness; + $this->resource->limits_memory_reservation = $this->limitsMemoryReservation; + } else { + // Sync FROM model (on load/refresh) + $this->limitsCpus = $this->resource->limits_cpus; + $this->limitsCpuset = $this->resource->limits_cpuset; + $this->limitsCpuShares = $this->resource->limits_cpu_shares; + $this->limitsMemory = $this->resource->limits_memory; + $this->limitsMemorySwap = $this->resource->limits_memory_swap; + $this->limitsMemorySwappiness = $this->resource->limits_memory_swappiness; + $this->limitsMemoryReservation = $this->resource->limits_memory_reservation; + } + } + + public function mount() + { + $this->syncData(false); + } + public function submit() { try { $this->authorize('update', $this->resource); - if (! $this->resource->limits_memory) { - $this->resource->limits_memory = '0'; + + // Apply default values to properties + if (! $this->limitsMemory) { + $this->limitsMemory = '0'; } - if (! $this->resource->limits_memory_swap) { - $this->resource->limits_memory_swap = '0'; + if (! $this->limitsMemorySwap) { + $this->limitsMemorySwap = '0'; } - if (is_null($this->resource->limits_memory_swappiness)) { - $this->resource->limits_memory_swappiness = '60'; + if (is_null($this->limitsMemorySwappiness)) { + $this->limitsMemorySwappiness = 60; } - if (! $this->resource->limits_memory_reservation) { - $this->resource->limits_memory_reservation = '0'; + if (! $this->limitsMemoryReservation) { + $this->limitsMemoryReservation = '0'; } - if (! $this->resource->limits_cpus) { - $this->resource->limits_cpus = '0'; + if (! $this->limitsCpus) { + $this->limitsCpus = '0'; } - if ($this->resource->limits_cpuset === '') { - $this->resource->limits_cpuset = null; + if ($this->limitsCpuset === '') { + $this->limitsCpuset = null; } - if (is_null($this->resource->limits_cpu_shares)) { - $this->resource->limits_cpu_shares = 1024; + if (is_null($this->limitsCpuShares)) { + $this->limitsCpuShares = 1024; } + $this->validate(); + + $this->syncData(true); $this->resource->save(); $this->dispatch('success', 'Resource limits updated.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index 4f57cbfa6..5970ec904 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -25,20 +25,48 @@ class Show extends Component public ?string $startedAt = null; + // Explicit properties + public string $name; + + public string $mountPath; + + public ?string $hostPath = null; + protected $rules = [ - 'storage.name' => 'required|string', - 'storage.mount_path' => 'required|string', - 'storage.host_path' => 'string|nullable', + 'name' => 'required|string', + 'mountPath' => 'required|string', + 'hostPath' => 'string|nullable', ]; protected $validationAttributes = [ 'name' => 'name', - 'mount_path' => 'mount', - 'host_path' => 'host', + 'mountPath' => 'mount', + 'hostPath' => 'host', ]; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->storage->name = $this->name; + $this->storage->mount_path = $this->mountPath; + $this->storage->host_path = $this->hostPath; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->storage->name; + $this->mountPath = $this->storage->mount_path; + $this->hostPath = $this->storage->host_path; + } + } + public function mount() { + $this->syncData(false); $this->isReadOnly = $this->storage->isReadOnlyVolume(); } @@ -47,6 +75,7 @@ public function submit() $this->authorize('update', $this->resource); $this->validate(); + $this->syncData(true); $this->storage->save(); $this->dispatch('success', 'Storage updated successfully'); } diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php new file mode 100644 index 000000000..33beff334 --- /dev/null +++ b/app/Livewire/Security/CloudInitScriptForm.php @@ -0,0 +1,101 @@ +scriptId = $scriptId; + $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->authorize('update', $cloudInitScript); + + $this->name = $cloudInitScript->name; + $this->script = $cloudInitScript->script; + } else { + $this->authorize('create', CloudInitScript::class); + } + } + + protected function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'script' => ['required', 'string', new \App\Rules\ValidCloudInitYaml], + ]; + } + + protected function messages(): array + { + return [ + 'name.required' => 'Script name is required.', + 'name.max' => 'Script name cannot exceed 255 characters.', + 'script.required' => 'Cloud-init script content is required.', + ]; + } + + public function save() + { + $this->validate(); + + try { + if ($this->scriptId) { + // Update existing script + $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($this->scriptId); + $this->authorize('update', $cloudInitScript); + + $cloudInitScript->update([ + 'name' => $this->name, + 'script' => $this->script, + ]); + + $message = 'Cloud-init script updated successfully.'; + } else { + // Create new script + $this->authorize('create', CloudInitScript::class); + + CloudInitScript::create([ + 'team_id' => currentTeam()->id, + 'name' => $this->name, + 'script' => $this->script, + ]); + + $message = 'Cloud-init script created successfully.'; + } + + // Only reset fields if creating (not editing) + if (! $this->scriptId) { + $this->reset(['name', 'script']); + } + + $this->dispatch('scriptSaved'); + $this->dispatch('success', $message); + + if ($this->modal_mode) { + $this->dispatch('closeModal'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-init-script-form'); + } +} diff --git a/app/Livewire/Security/CloudInitScripts.php b/app/Livewire/Security/CloudInitScripts.php new file mode 100644 index 000000000..13bcf2caa --- /dev/null +++ b/app/Livewire/Security/CloudInitScripts.php @@ -0,0 +1,52 @@ +authorize('viewAny', CloudInitScript::class); + $this->loadScripts(); + } + + public function getListeners() + { + return [ + 'scriptSaved' => 'loadScripts', + ]; + } + + public function loadScripts() + { + $this->scripts = CloudInitScript::ownedByCurrentTeam()->orderBy('created_at', 'desc')->get(); + } + + public function deleteScript(int $scriptId) + { + try { + $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->authorize('delete', $script); + + $script->delete(); + $this->loadScripts(); + + $this->dispatch('success', 'Cloud-init script deleted successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-init-scripts'); + } +} diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php new file mode 100644 index 000000000..7affb1531 --- /dev/null +++ b/app/Livewire/Security/CloudProviderTokenForm.php @@ -0,0 +1,99 @@ +authorize('create', CloudProviderToken::class); + } + + protected function rules(): array + { + return [ + 'provider' => 'required|string|in:hetzner,digitalocean', + 'token' => 'required|string', + 'name' => 'required|string|max:255', + ]; + } + + protected function messages(): array + { + return [ + 'provider.required' => 'Please select a cloud provider.', + 'provider.in' => 'Invalid cloud provider selected.', + 'token.required' => 'API token is required.', + 'name.required' => 'Token name is required.', + ]; + } + + private function validateToken(string $provider, string $token): bool + { + try { + if ($provider === 'hetzner') { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + ray($response); + + return $response->successful(); + } + + // Add other providers here in the future + // if ($provider === 'digitalocean') { ... } + + return false; + } catch (\Throwable $e) { + return false; + } + } + + public function addToken() + { + $this->validate(); + + try { + // Validate the token with the provider's API + if (! $this->validateToken($this->provider, $this->token)) { + return $this->dispatch('error', 'Invalid API token. Please check your token and try again.'); + } + + $savedToken = CloudProviderToken::create([ + 'team_id' => currentTeam()->id, + 'provider' => $this->provider, + 'token' => $this->token, + 'name' => $this->name, + ]); + + $this->reset(['token', 'name']); + + // Dispatch event with token ID so parent components can react + $this->dispatch('tokenAdded', tokenId: $savedToken->id); + + $this->dispatch('success', 'Cloud provider token added successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-provider-token-form'); + } +} diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php new file mode 100644 index 000000000..f05b3c0ca --- /dev/null +++ b/app/Livewire/Security/CloudProviderTokens.php @@ -0,0 +1,60 @@ +authorize('viewAny', CloudProviderToken::class); + $this->loadTokens(); + } + + public function getListeners() + { + return [ + 'tokenAdded' => 'loadTokens', + ]; + } + + public function loadTokens() + { + $this->tokens = CloudProviderToken::ownedByCurrentTeam()->get(); + } + + public function deleteToken(int $tokenId) + { + try { + $token = CloudProviderToken::ownedByCurrentTeam()->findOrFail($tokenId); + $this->authorize('delete', $token); + + // Check if any servers are using this token + if ($token->hasServers()) { + $serverCount = $token->servers()->count(); + $this->dispatch('error', "Cannot delete this token. It is currently used by {$serverCount} server(s). Please reassign those servers to a different token first."); + + return; + } + + $token->delete(); + $this->loadTokens(); + + $this->dispatch('success', 'Cloud provider token deleted successfully.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.security.cloud-provider-tokens'); + } +} diff --git a/app/Livewire/Security/CloudTokens.php b/app/Livewire/Security/CloudTokens.php new file mode 100644 index 000000000..d6d1333f1 --- /dev/null +++ b/app/Livewire/Security/CloudTokens.php @@ -0,0 +1,13 @@ + currentTeam()->id, ]); + // If in modal mode, dispatch event and don't redirect + if ($this->modal_mode) { + $this->dispatch('privateKeyCreated', keyId: $privateKey->id); + $this->dispatch('success', 'Private key created successfully.'); + + return; + } + return $this->redirectAfterCreation($privateKey); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index 2ff06c349..9928cfe97 100644 --- a/app/Livewire/Security/PrivateKey/Show.php +++ b/app/Livewire/Security/PrivateKey/Show.php @@ -13,15 +13,24 @@ class Show extends Component public PrivateKey $private_key; + // Explicit properties + public string $name; + + public ?string $description = null; + + public string $privateKeyValue; + + public bool $isGitRelated = false; + public $public_key = 'Loading...'; protected function rules(): array { return [ - 'private_key.name' => ValidationPatterns::nameRules(), - 'private_key.description' => ValidationPatterns::descriptionRules(), - 'private_key.private_key' => 'required|string', - 'private_key.is_git_related' => 'nullable|boolean', + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), + 'privateKeyValue' => 'required|string', + 'isGitRelated' => 'nullable|boolean', ]; } @@ -30,25 +39,48 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'private_key.name.required' => 'The Name field is required.', - 'private_key.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'private_key.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'private_key.private_key.required' => 'The Private Key field is required.', - 'private_key.private_key.string' => 'The Private Key must be a valid string.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'privateKeyValue.required' => 'The Private Key field is required.', + 'privateKeyValue.string' => 'The Private Key must be a valid string.', ] ); } protected $validationAttributes = [ - 'private_key.name' => 'name', - 'private_key.description' => 'description', - 'private_key.private_key' => 'private key', + 'name' => 'name', + 'description' => 'description', + 'privateKeyValue' => 'private key', ]; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->private_key->name = $this->name; + $this->private_key->description = $this->description; + $this->private_key->private_key = $this->privateKeyValue; + $this->private_key->is_git_related = $this->isGitRelated; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->private_key->name; + $this->description = $this->private_key->description; + $this->privateKeyValue = $this->private_key->private_key; + $this->isGitRelated = $this->private_key->is_git_related; + } + } + public function mount() { try { $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); + $this->syncData(false); } catch (\Throwable) { abort(404); } @@ -81,6 +113,10 @@ public function changePrivateKey() { try { $this->authorize('update', $this->private_key); + + $this->validate(); + + $this->syncData(true); $this->private_key->updatePrivateKey([ 'private_key' => formatPrivateKey($this->private_key->private_key), ]); diff --git a/app/Livewire/Server/CaCertificate/Show.php b/app/Livewire/Server/CaCertificate/Show.php index 039b5f71d..c929d9b3d 100644 --- a/app/Livewire/Server/CaCertificate/Show.php +++ b/app/Livewire/Server/CaCertificate/Show.php @@ -39,7 +39,7 @@ public function mount(string $server_uuid) public function loadCaCertificate() { - $this->caCertificate = SslCertificate::where('server_id', $this->server->id)->where('is_ca_certificate', true)->first(); + $this->caCertificate = $this->server->sslCertificates()->where('is_ca_certificate', true)->first(); if ($this->caCertificate) { $this->certificateContent = $this->caCertificate->ssl_certificate; diff --git a/app/Livewire/Server/CloudProviderToken/Show.php b/app/Livewire/Server/CloudProviderToken/Show.php new file mode 100644 index 000000000..6b22fddc6 --- /dev/null +++ b/app/Livewire/Server/CloudProviderToken/Show.php @@ -0,0 +1,144 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->loadTokens(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function getListeners() + { + return [ + 'tokenAdded' => 'handleTokenAdded', + ]; + } + + public function loadTokens() + { + $this->cloudProviderTokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function handleTokenAdded($tokenId) + { + $this->loadTokens(); + } + + public function setCloudProviderToken($tokenId) + { + $ownedToken = CloudProviderToken::ownedByCurrentTeam()->find($tokenId); + if (is_null($ownedToken)) { + $this->dispatch('error', 'You are not allowed to use this token.'); + + return; + } + try { + $this->authorize('update', $this->server); + + // Validate the token works and can access this specific server + $validationResult = $this->validateTokenForServer($ownedToken); + if (! $validationResult['valid']) { + $this->dispatch('error', $validationResult['error']); + + return; + } + + $this->server->cloudProviderToken()->associate($ownedToken); + $this->server->save(); + $this->dispatch('success', 'Hetzner token updated successfully.'); + $this->dispatch('refreshServerShow'); + } catch (\Exception $e) { + $this->server->refresh(); + $this->dispatch('error', $e->getMessage()); + } + } + + private function validateTokenForServer(CloudProviderToken $token): array + { + try { + // First, validate the token itself + $response = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + if (! $response->successful()) { + return [ + 'valid' => false, + 'error' => 'This token is invalid or has insufficient permissions.', + ]; + } + + // Check if this token can access the specific Hetzner server + if ($this->server->hetzner_server_id) { + $serverResponse = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}"); + + if (! $serverResponse->successful()) { + return [ + 'valid' => false, + 'error' => 'This token cannot access this server. It may belong to a different Hetzner project.', + ]; + } + } + + return ['valid' => true]; + } catch (\Throwable $e) { + return [ + 'valid' => false, + 'error' => 'Failed to validate token: '.$e->getMessage(), + ]; + } + } + + public function validateToken() + { + try { + $token = $this->server->cloudProviderToken; + if (! $token) { + $this->dispatch('error', 'No Hetzner token is associated with this server.'); + + return; + } + + $response = \Illuminate\Support\Facades\Http::withHeaders([ + 'Authorization' => 'Bearer '.$token->token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + if ($response->successful()) { + $this->dispatch('success', 'Hetzner token is valid and working.'); + } else { + $this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.cloud-provider-token.show'); + } +} diff --git a/app/Livewire/Server/Create.php b/app/Livewire/Server/Create.php index 2d4ba4430..cf77664fe 100644 --- a/app/Livewire/Server/Create.php +++ b/app/Livewire/Server/Create.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server; +use App\Models\CloudProviderToken; use App\Models\PrivateKey; use App\Models\Team; use Livewire\Component; @@ -12,6 +13,8 @@ class Create extends Component public bool $limit_reached = false; + public bool $has_hetzner_tokens = false; + public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); @@ -21,6 +24,11 @@ public function mount() return; } $this->limit_reached = Team::serverLimitReached(); + + // Check if user has Hetzner tokens + $this->has_hetzner_tokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->exists(); } public function render() diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index b9e3944b5..8c2c54c99 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -16,6 +16,8 @@ class Delete extends Component public Server $server; + public bool $delete_from_hetzner = false; + public function mount(string $server_uuid) { try { @@ -41,8 +43,15 @@ public function delete($password) return; } + $this->server->delete(); - DeleteServer::dispatch($this->server); + DeleteServer::dispatch( + $this->server->id, + $this->delete_from_hetzner, + $this->server->hetzner_server_id, + $this->server->cloud_provider_token_id, + $this->server->team_id + ); return redirect()->route('server.index'); } catch (\Throwable $e) { @@ -52,6 +61,18 @@ public function delete($password) public function render() { - return view('livewire.server.delete'); + $checkboxes = []; + + if ($this->server->hetzner_server_id) { + $checkboxes[] = [ + 'id' => 'delete_from_hetzner', + 'label' => 'Also delete server from Hetzner Cloud', + 'default_warning' => 'The actual server on Hetzner Cloud will NOT be deleted.', + ]; + } + + return view('livewire.server.delete', [ + 'checkboxes' => $checkboxes, + ]); } } diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index beefed12a..6baa54672 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -118,17 +118,31 @@ public function checkProxyStatus() public function showNotification() { + $this->server->refresh(); $this->proxyStatus = $this->server->proxy->status ?? 'unknown'; - $forceStop = $this->server->proxy->force_stop ?? false; switch ($this->proxyStatus) { case 'running': $this->loadProxyConfiguration(); + $this->dispatch('success', 'Proxy is running.'); break; case 'restarting': $this->dispatch('info', 'Initiating proxy restart.'); break; + case 'exited': + $this->dispatch('info', 'Proxy has exited.'); + break; + case 'stopping': + $this->dispatch('info', 'Proxy is stopping.'); + break; + case 'starting': + $this->dispatch('info', 'Proxy is starting.'); + break; + case 'unknown': + $this->dispatch('info', 'Proxy status is unknown.'); + break; default: + $this->dispatch('info', 'Proxy status updated.'); break; } diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php new file mode 100644 index 000000000..ca5c588f8 --- /dev/null +++ b/app/Livewire/Server/New/ByHetzner.php @@ -0,0 +1,587 @@ +authorize('viewAny', CloudProviderToken::class); + $this->loadTokens(); + $this->loadSavedCloudInitScripts(); + $this->server_name = generate_random_name(); + if ($this->private_keys->count() > 0) { + $this->private_key_id = $this->private_keys->first()->id; + } + } + + public function loadSavedCloudInitScripts() + { + $this->saved_cloud_init_scripts = CloudInitScript::ownedByCurrentTeam()->get(); + } + + public function getListeners() + { + return [ + 'tokenAdded' => 'handleTokenAdded', + 'privateKeyCreated' => 'handlePrivateKeyCreated', + 'modalClosed' => 'resetSelection', + ]; + } + + public function resetSelection() + { + $this->selected_token_id = null; + $this->current_step = 1; + $this->cloud_init_script = null; + $this->save_cloud_init_script = false; + $this->cloud_init_script_name = null; + $this->selected_cloud_init_script_id = null; + } + + public function loadTokens() + { + $this->available_tokens = CloudProviderToken::ownedByCurrentTeam() + ->where('provider', 'hetzner') + ->get(); + } + + public function handleTokenAdded($tokenId) + { + // Refresh token list + $this->loadTokens(); + + // Auto-select the new token + $this->selected_token_id = $tokenId; + + // Automatically proceed to next step + $this->nextStep(); + } + + public function handlePrivateKeyCreated($keyId) + { + // Refresh private keys list + $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); + + // Auto-select the new key + $this->private_key_id = $keyId; + + // Clear validation errors for private_key_id + $this->resetErrorBag('private_key_id'); + } + + protected function rules(): array + { + $rules = [ + 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', + ]; + + if ($this->current_step === 2) { + $rules = array_merge($rules, [ + 'server_name' => ['required', 'string', 'max:253', new ValidHostname], + 'selected_location' => 'required|string', + 'selected_image' => 'required|integer', + 'selected_server_type' => 'required|string', + 'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id, + 'selectedHetznerSshKeyIds' => 'nullable|array', + 'selectedHetznerSshKeyIds.*' => 'integer', + 'enable_ipv4' => 'required|boolean', + 'enable_ipv6' => 'required|boolean', + 'cloud_init_script' => ['nullable', 'string', new \App\Rules\ValidCloudInitYaml], + 'save_cloud_init_script' => 'boolean', + 'cloud_init_script_name' => 'nullable|string|max:255', + 'selected_cloud_init_script_id' => 'nullable|integer|exists:cloud_init_scripts,id', + ]); + } + + return $rules; + } + + protected function messages(): array + { + return [ + 'selected_token_id.required' => 'Please select a Hetzner token.', + 'selected_token_id.exists' => 'Selected token not found.', + ]; + } + + public function selectToken(int $tokenId) + { + $this->selected_token_id = $tokenId; + } + + private function validateHetznerToken(string $token): bool + { + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->timeout(10)->get('https://api.hetzner.cloud/v1/servers'); + + return $response->successful(); + } catch (\Throwable $e) { + return false; + } + } + + private function getHetznerToken(): string + { + if ($this->selected_token_id) { + $token = $this->available_tokens->firstWhere('id', $this->selected_token_id); + + return $token ? $token->token : ''; + } + + return ''; + } + + public function nextStep() + { + // Validate step 1 - just need a token selected + $this->validate([ + 'selected_token_id' => 'required|integer|exists:cloud_provider_tokens,id', + ]); + + try { + $hetznerToken = $this->getHetznerToken(); + + if (! $hetznerToken) { + return $this->dispatch('error', 'Please select a valid Hetzner token.'); + } + + // Load Hetzner data + $this->loadHetznerData($hetznerToken); + + // Move to step 2 + $this->current_step = 2; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function previousStep() + { + $this->current_step = 1; + } + + private function loadHetznerData(string $token) + { + $this->loading_data = true; + + try { + $hetznerService = new HetznerService($token); + + $this->locations = $hetznerService->getLocations(); + $this->serverTypes = $hetznerService->getServerTypes(); + + // Get images and sort by name + $images = $hetznerService->getImages(); + + ray('Raw images from Hetzner API', [ + 'total_count' => count($images), + 'types' => collect($images)->pluck('type')->unique()->values(), + 'sample' => array_slice($images, 0, 3), + ]); + + $this->images = collect($images) + ->filter(function ($image) { + // Only system images + if (! isset($image['type']) || $image['type'] !== 'system') { + return false; + } + + // Filter out deprecated images + if (isset($image['deprecated']) && $image['deprecated'] === true) { + return false; + } + + return true; + }) + ->sortBy('name') + ->values() + ->toArray(); + + ray('Filtered images', [ + 'filtered_count' => count($this->images), + 'debian_images' => collect($this->images)->filter(fn ($img) => str_contains($img['name'] ?? '', 'debian'))->values(), + ]); + + // Load SSH keys from Hetzner + $this->hetznerSshKeys = $hetznerService->getSshKeys(); + + ray('Hetzner SSH Keys', [ + 'total_count' => count($this->hetznerSshKeys), + 'keys' => $this->hetznerSshKeys, + ]); + + $this->loading_data = false; + } catch (\Throwable $e) { + $this->loading_data = false; + throw $e; + } + } + + private function getCpuVendorInfo(array $serverType): ?string + { + $name = strtolower($serverType['name'] ?? ''); + + if (str_starts_with($name, 'ccx')) { + return 'AMD Milan EPYC™'; + } elseif (str_starts_with($name, 'cpx')) { + return 'AMD EPYC™'; + } elseif (str_starts_with($name, 'cx')) { + return 'Intel® Xeon®'; + } elseif (str_starts_with($name, 'cax')) { + return 'Ampere® Altra®'; + } + + return null; + } + + public function getAvailableServerTypesProperty() + { + ray('Getting available server types', [ + 'selected_location' => $this->selected_location, + 'total_server_types' => count($this->serverTypes), + ]); + + if (! $this->selected_location) { + return $this->serverTypes; + } + + $filtered = collect($this->serverTypes) + ->filter(function ($type) { + if (! isset($type['locations'])) { + return false; + } + + $locationNames = collect($type['locations'])->pluck('name')->toArray(); + + return in_array($this->selected_location, $locationNames); + }) + ->map(function ($serverType) { + $serverType['cpu_vendor_info'] = $this->getCpuVendorInfo($serverType); + + return $serverType; + }) + ->values() + ->toArray(); + + ray('Filtered server types', [ + 'selected_location' => $this->selected_location, + 'filtered_count' => count($filtered), + ]); + + return $filtered; + } + + public function getAvailableImagesProperty() + { + ray('Getting available images', [ + 'selected_server_type' => $this->selected_server_type, + 'total_images' => count($this->images), + 'images' => $this->images, + ]); + + if (! $this->selected_server_type) { + return $this->images; + } + + $serverType = collect($this->serverTypes)->firstWhere('name', $this->selected_server_type); + + ray('Server type data', $serverType); + + if (! $serverType || ! isset($serverType['architecture'])) { + ray('No architecture in server type, returning all'); + + return $this->images; + } + + $architecture = $serverType['architecture']; + + $filtered = collect($this->images) + ->filter(fn ($image) => ($image['architecture'] ?? null) === $architecture) + ->values() + ->toArray(); + + ray('Filtered images', [ + 'architecture' => $architecture, + 'filtered_count' => count($filtered), + ]); + + return $filtered; + } + + public function getSelectedServerPriceProperty(): ?string + { + if (! $this->selected_server_type) { + return null; + } + + $serverType = collect($this->serverTypes)->firstWhere('name', $this->selected_server_type); + + if (! $serverType || ! isset($serverType['prices'][0]['price_monthly']['gross'])) { + return null; + } + + $price = $serverType['prices'][0]['price_monthly']['gross']; + + return '€'.number_format($price, 2); + } + + public function updatedSelectedLocation($value) + { + ray('Location selected', $value); + + // Reset server type and image when location changes + $this->selected_server_type = null; + $this->selected_image = null; + } + + public function updatedSelectedServerType($value) + { + ray('Server type selected', $value); + + // Reset image when server type changes + $this->selected_image = null; + } + + public function updatedSelectedImage($value) + { + ray('Image selected', $value); + } + + public function updatedSelectedCloudInitScriptId($value) + { + if ($value) { + $script = CloudInitScript::ownedByCurrentTeam()->findOrFail($value); + $this->cloud_init_script = $script->script; + $this->cloud_init_script_name = $script->name; + } + } + + public function clearCloudInitScript() + { + $this->selected_cloud_init_script_id = null; + $this->cloud_init_script = ''; + $this->cloud_init_script_name = ''; + $this->save_cloud_init_script = false; + } + + private function createHetznerServer(string $token): array + { + $hetznerService = new HetznerService($token); + + // Get the private key and extract public key + $privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id); + + $publicKey = $privateKey->getPublicKey(); + $md5Fingerprint = PrivateKey::generateMd5Fingerprint($privateKey->private_key); + + ray('Private Key Info', [ + 'private_key_id' => $this->private_key_id, + 'sha256_fingerprint' => $privateKey->fingerprint, + 'md5_fingerprint' => $md5Fingerprint, + ]); + + // Check if SSH key already exists on Hetzner by comparing MD5 fingerprints + $existingSshKeys = $hetznerService->getSshKeys(); + $existingKey = null; + + ray('Existing SSH Keys on Hetzner', $existingSshKeys); + + foreach ($existingSshKeys as $key) { + if ($key['fingerprint'] === $md5Fingerprint) { + $existingKey = $key; + break; + } + } + + // Upload SSH key if it doesn't exist + if ($existingKey) { + $sshKeyId = $existingKey['id']; + ray('Using existing SSH key', ['ssh_key_id' => $sshKeyId]); + } else { + $sshKeyName = $privateKey->name; + $uploadedKey = $hetznerService->uploadSshKey($sshKeyName, $publicKey); + $sshKeyId = $uploadedKey['id']; + ray('Uploaded new SSH key', ['ssh_key_id' => $sshKeyId, 'name' => $sshKeyName]); + } + + // Normalize server name to lowercase for RFC 1123 compliance + $normalizedServerName = strtolower(trim($this->server_name)); + + // Prepare SSH keys array: Coolify key + user-selected Hetzner keys + $sshKeys = array_merge( + [$sshKeyId], // Coolify key (always included) + $this->selectedHetznerSshKeyIds // User-selected Hetzner keys + ); + + // Remove duplicates in case the Coolify key was also selected + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); // Re-index array + + // Prepare server creation parameters + $params = [ + 'name' => $normalizedServerName, + 'server_type' => $this->selected_server_type, + 'image' => $this->selected_image, + 'location' => $this->selected_location, + 'start_after_create' => true, + 'ssh_keys' => $sshKeys, + 'public_net' => [ + 'enable_ipv4' => $this->enable_ipv4, + 'enable_ipv6' => $this->enable_ipv6, + ], + ]; + + // Add cloud-init script if provided + if (! empty($this->cloud_init_script)) { + $params['user_data'] = $this->cloud_init_script; + } + + ray('Server creation parameters', $params); + + // Create server on Hetzner + $hetznerServer = $hetznerService->createServer($params); + + ray('Hetzner server created', $hetznerServer); + + return $hetznerServer; + } + + public function submit() + { + $this->validate(); + + try { + $this->authorize('create', Server::class); + + if (Team::serverLimitReached()) { + return $this->dispatch('error', 'You have reached the server limit for your subscription.'); + } + + // Save cloud-init script if requested + if ($this->save_cloud_init_script && ! empty($this->cloud_init_script) && ! empty($this->cloud_init_script_name)) { + $this->authorize('create', CloudInitScript::class); + + CloudInitScript::create([ + 'team_id' => currentTeam()->id, + 'name' => $this->cloud_init_script_name, + 'script' => $this->cloud_init_script, + ]); + } + + $hetznerToken = $this->getHetznerToken(); + + // Create server on Hetzner + $hetznerServer = $this->createHetznerServer($hetznerToken); + + // Determine IP address to use (prefer IPv4, fallback to IPv6) + $ipAddress = null; + if ($this->enable_ipv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($this->enable_ipv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + if (! $ipAddress) { + throw new \Exception('No public IP address available. Enable at least one of IPv4 or IPv6.'); + } + + // Create server in Coolify database + $server = Server::create([ + 'name' => $this->server_name, + 'ip' => $ipAddress, + 'user' => 'root', + 'port' => 22, + 'team_id' => currentTeam()->id, + 'private_key_id' => $this->private_key_id, + 'cloud_provider_token_id' => $this->selected_token_id, + 'hetzner_server_id' => $hetznerServer['id'], + ]); + + $server->proxy->set('status', 'exited'); + $server->proxy->set('type', ProxyTypes::TRAEFIK->value); + $server->save(); + + return redirect()->route('server.show', $server->uuid); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.new.by-hetzner'); + } +} diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 5ef559862..bc7e9bde4 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -22,6 +22,8 @@ class Proxy extends Component public ?string $redirectUrl = null; + public bool $generateExactLabels = false; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -33,7 +35,7 @@ public function getListeners() } protected $rules = [ - 'server.settings.generate_exact_labels' => 'required|boolean', + 'generateExactLabels' => 'required|boolean', ]; public function mount() @@ -41,6 +43,16 @@ public function mount() $this->selectedProxy = $this->server->proxyType(); $this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true); $this->redirectUrl = data_get($this->server, 'proxy.redirect_url'); + $this->syncData(false); + } + + private function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->server->settings->generate_exact_labels = $this->generateExactLabels; + } else { + $this->generateExactLabels = $this->server->settings->generate_exact_labels ?? false; + } } public function getConfigurationFilePathProperty() @@ -75,6 +87,7 @@ public function instantSave() try { $this->authorize('update', $this->server); $this->validate(); + $this->syncData(true); $this->server->settings->save(); $this->dispatch('success', 'Settings saved.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index db4dc9b88..4626a9135 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -67,13 +67,21 @@ class Show extends Component public string $serverTimezone; + public ?string $hetznerServerStatus = null; + + public bool $hetznerServerManuallyStarted = false; + + public bool $isValidating = false; + public function getListeners() { $teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id; return [ 'refreshServerShow' => 'refresh', + 'refreshServer' => '$refresh', "echo-private:team.{$teamId},SentinelRestarted" => 'handleSentinelRestarted', + "echo-private:team.{$teamId},ServerValidated" => 'handleServerValidated', ]; } @@ -138,6 +146,10 @@ public function mount(string $server_uuid) if (! $this->server->isEmpty()) { $this->isBuildServerLocked = true; } + // Load saved Hetzner status and validation state + $this->hetznerServerStatus = $this->server->hetzner_server_status; + $this->isValidating = $this->server->is_validating ?? false; + } catch (\Throwable $e) { return handleError($e, $this); } @@ -218,6 +230,7 @@ public function syncData(bool $toModel = false) $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; $this->sentinelUpdatedAt = $this->server->sentinel_updated_at; $this->serverTimezone = $this->server->settings->server_timezone; + $this->isValidating = $this->server->is_validating ?? false; } } @@ -361,6 +374,87 @@ public function instantSave() } } + public function checkHetznerServerStatus(bool $manual = false) + { + try { + if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) { + $this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.'); + + return; + } + + $hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token); + $serverData = $hetznerService->getServer($this->server->hetzner_server_id); + + $this->hetznerServerStatus = $serverData['status'] ?? null; + + // Save status to database without triggering model events + if ($this->server->hetzner_server_status !== $this->hetznerServerStatus) { + $this->server->hetzner_server_status = $this->hetznerServerStatus; + $this->server->update(['hetzner_server_status' => $this->hetznerServerStatus]); + } + if ($manual) { + $this->dispatch('success', 'Server status refreshed: '.ucfirst($this->hetznerServerStatus ?? 'unknown')); + } + + // If Hetzner server is off but Coolify thinks it's still reachable, update Coolify's state + if ($this->hetznerServerStatus === 'off' && $this->server->settings->is_reachable) { + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + $this->server->settings->is_reachable = $this->isReachable = true; + $this->server->settings->is_usable = $this->isUsable = true; + $this->server->settings->save(); + ServerReachabilityChanged::dispatch($this->server); + } else { + $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); + + return; + } + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function handleServerValidated($event = null) + { + // Check if event is for this server + if ($event && isset($event['serverUuid']) && $event['serverUuid'] !== $this->server->uuid) { + return; + } + + // Refresh server data + $this->server->refresh(); + $this->syncData(); + + // Update validation state + $this->isValidating = $this->server->is_validating ?? false; + $this->dispatch('refreshServerShow'); + $this->dispatch('refreshServer'); + } + + public function startHetznerServer() + { + try { + if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) { + $this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.'); + + return; + } + + $hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token); + $hetznerService->powerOnServer($this->server->hetzner_server_id); + + $this->hetznerServerStatus = 'starting'; + $this->server->update(['hetzner_server_status' => 'starting']); + $this->hetznerServerManuallyStarted = true; // Set flag to trigger auto-validation when running + $this->dispatch('success', 'Hetzner server is starting...'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index bf0b7b6a5..bbd7f3dd9 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -4,6 +4,7 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; +use App\Events\ServerValidated; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -63,6 +64,19 @@ public function startValidatingAfterAsking() $this->init(); } + public function retry() + { + $this->authorize('update', $this->server); + $this->uptime = null; + $this->supported_os_type = null; + $this->docker_installed = null; + $this->docker_compose_installed = null; + $this->docker_version = null; + $this->error = null; + $this->number_of_tries = 0; + $this->init(); + } + public function validateConnection() { $this->authorize('update', $this->server); @@ -136,8 +150,12 @@ public function validateDockerVersion() } else { $this->docker_version = $this->server->validateDockerEngineVersion(); if ($this->docker_version) { + // Mark validation as complete + $this->server->update(['is_validating' => false]); + $this->dispatch('refreshServerShow'); $this->dispatch('refreshBoardingIndex'); + ServerValidated::dispatch($this->server->team_id, $this->server->uuid); $this->dispatch('success', 'Server validated, proxy is starting in a moment.'); $proxyShouldRun = CheckProxy::run($this->server, true); if (! $proxyShouldRun) { diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 9ad5444b9..351407dac 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -34,32 +34,60 @@ class Change extends Component public ?GithubApp $github_app = null; + // Explicit properties public string $name; - public bool $is_system_wide; + public ?string $organization = null; + + public string $apiUrl; + + public string $htmlUrl; + + public string $customUser; + + public int $customPort; + + public int $appId; + + public int $installationId; + + public string $clientId; + + public string $clientSecret; + + public string $webhookSecret; + + public bool $isSystemWide; + + public int $privateKeyId; + + public ?string $contents = null; + + public ?string $metadata = null; + + public ?string $pullRequests = null; public $applications; public $privateKeys; protected $rules = [ - 'github_app.name' => 'required|string', - 'github_app.organization' => 'nullable|string', - 'github_app.api_url' => 'required|string', - 'github_app.html_url' => 'required|string', - 'github_app.custom_user' => 'required|string', - 'github_app.custom_port' => 'required|int', - 'github_app.app_id' => 'required|int', - 'github_app.installation_id' => 'required|int', - 'github_app.client_id' => 'required|string', - 'github_app.client_secret' => 'required|string', - 'github_app.webhook_secret' => 'required|string', - 'github_app.is_system_wide' => 'required|bool', - 'github_app.contents' => 'nullable|string', - 'github_app.metadata' => 'nullable|string', - 'github_app.pull_requests' => 'nullable|string', - 'github_app.administration' => 'nullable|string', - 'github_app.private_key_id' => 'required|int', + 'name' => 'required|string', + 'organization' => 'nullable|string', + 'apiUrl' => 'required|string', + 'htmlUrl' => 'required|string', + 'customUser' => 'required|string', + 'customPort' => 'required|int', + 'appId' => 'required|int', + 'installationId' => 'required|int', + 'clientId' => 'required|string', + 'clientSecret' => 'required|string', + 'webhookSecret' => 'required|string', + 'isSystemWide' => 'required|bool', + 'contents' => 'nullable|string', + 'metadata' => 'nullable|string', + 'pullRequests' => 'nullable|string', + 'privateKeyId' => 'required|int', ]; public function boot() @@ -69,6 +97,52 @@ public function boot() } } + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->github_app->name = $this->name; + $this->github_app->organization = $this->organization; + $this->github_app->api_url = $this->apiUrl; + $this->github_app->html_url = $this->htmlUrl; + $this->github_app->custom_user = $this->customUser; + $this->github_app->custom_port = $this->customPort; + $this->github_app->app_id = $this->appId; + $this->github_app->installation_id = $this->installationId; + $this->github_app->client_id = $this->clientId; + $this->github_app->client_secret = $this->clientSecret; + $this->github_app->webhook_secret = $this->webhookSecret; + $this->github_app->is_system_wide = $this->isSystemWide; + $this->github_app->private_key_id = $this->privateKeyId; + $this->github_app->contents = $this->contents; + $this->github_app->metadata = $this->metadata; + $this->github_app->pull_requests = $this->pullRequests; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->github_app->name; + $this->organization = $this->github_app->organization; + $this->apiUrl = $this->github_app->api_url; + $this->htmlUrl = $this->github_app->html_url; + $this->customUser = $this->github_app->custom_user; + $this->customPort = $this->github_app->custom_port; + $this->appId = $this->github_app->app_id; + $this->installationId = $this->github_app->installation_id; + $this->clientId = $this->github_app->client_id; + $this->clientSecret = $this->github_app->client_secret; + $this->webhookSecret = $this->github_app->webhook_secret; + $this->isSystemWide = $this->github_app->is_system_wide; + $this->privateKeyId = $this->github_app->private_key_id; + $this->contents = $this->github_app->contents; + $this->metadata = $this->github_app->metadata; + $this->pullRequests = $this->github_app->pull_requests; + } + } + public function checkPermissions() { try { @@ -126,6 +200,10 @@ public function mount() $this->applications = $this->github_app->applications; $settings = instanceSettings(); + // Sync data from model to properties + $this->syncData(false); + + // Override name with kebab case for display $this->name = str($this->github_app->name)->kebab(); $this->fqdn = $settings->fqdn; @@ -247,21 +325,9 @@ public function submit() $this->authorize('update', $this->github_app); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); - $this->validate([ - 'github_app.name' => 'required|string', - 'github_app.organization' => 'nullable|string', - 'github_app.api_url' => 'required|string', - 'github_app.html_url' => 'required|string', - 'github_app.custom_user' => 'required|string', - 'github_app.custom_port' => 'required|int', - 'github_app.app_id' => 'required|int', - 'github_app.installation_id' => 'required|int', - 'github_app.client_id' => 'required|string', - 'github_app.client_secret' => 'required|string', - 'github_app.webhook_secret' => 'required|string', - 'github_app.is_system_wide' => 'required|bool', - 'github_app.private_key_id' => 'required|int', - ]); + $this->validate(); + + $this->syncData(true); $this->github_app->save(); $this->dispatch('success', 'Github App updated.'); } catch (\Throwable $e) { @@ -286,6 +352,8 @@ public function instantSave() $this->authorize('update', $this->github_app); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + + $this->syncData(true); $this->github_app->save(); $this->dispatch('success', 'Github App updated.'); } catch (\Throwable $e) { diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php index 9438b7727..d97550693 100644 --- a/app/Livewire/Storage/Form.php +++ b/app/Livewire/Storage/Form.php @@ -14,17 +14,34 @@ class Form extends Component public S3Storage $storage; + // Explicit properties + public ?string $name = null; + + public ?string $description = null; + + public string $endpoint; + + public string $bucket; + + public string $region; + + public string $key; + + public string $secret; + + public ?bool $isUsable = null; + protected function rules(): array { return [ - 'storage.is_usable' => 'nullable|boolean', - 'storage.name' => ValidationPatterns::nameRules(required: false), - 'storage.description' => ValidationPatterns::descriptionRules(), - 'storage.region' => 'required|max:255', - 'storage.key' => 'required|max:255', - 'storage.secret' => 'required|max:255', - 'storage.bucket' => 'required|max:255', - 'storage.endpoint' => 'required|url|max:255', + 'isUsable' => 'nullable|boolean', + 'name' => ValidationPatterns::nameRules(required: false), + 'description' => ValidationPatterns::descriptionRules(), + 'region' => 'required|max:255', + 'key' => 'required|max:255', + 'secret' => 'required|max:255', + 'bucket' => 'required|max:255', + 'endpoint' => 'required|url|max:255', ]; } @@ -33,34 +50,69 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'storage.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'storage.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', - 'storage.region.required' => 'The Region field is required.', - 'storage.region.max' => 'The Region may not be greater than 255 characters.', - 'storage.key.required' => 'The Access Key field is required.', - 'storage.key.max' => 'The Access Key may not be greater than 255 characters.', - 'storage.secret.required' => 'The Secret Key field is required.', - 'storage.secret.max' => 'The Secret Key may not be greater than 255 characters.', - 'storage.bucket.required' => 'The Bucket field is required.', - 'storage.bucket.max' => 'The Bucket may not be greater than 255 characters.', - 'storage.endpoint.required' => 'The Endpoint field is required.', - 'storage.endpoint.url' => 'The Endpoint must be a valid URL.', - 'storage.endpoint.max' => 'The Endpoint may not be greater than 255 characters.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'region.required' => 'The Region field is required.', + 'region.max' => 'The Region may not be greater than 255 characters.', + 'key.required' => 'The Access Key field is required.', + 'key.max' => 'The Access Key may not be greater than 255 characters.', + 'secret.required' => 'The Secret Key field is required.', + 'secret.max' => 'The Secret Key may not be greater than 255 characters.', + 'bucket.required' => 'The Bucket field is required.', + 'bucket.max' => 'The Bucket may not be greater than 255 characters.', + 'endpoint.required' => 'The Endpoint field is required.', + 'endpoint.url' => 'The Endpoint must be a valid URL.', + 'endpoint.max' => 'The Endpoint may not be greater than 255 characters.', ] ); } protected $validationAttributes = [ - 'storage.is_usable' => 'Is Usable', - 'storage.name' => 'Name', - 'storage.description' => 'Description', - 'storage.region' => 'Region', - 'storage.key' => 'Key', - 'storage.secret' => 'Secret', - 'storage.bucket' => 'Bucket', - 'storage.endpoint' => 'Endpoint', + 'isUsable' => 'Is Usable', + 'name' => 'Name', + 'description' => 'Description', + 'region' => 'Region', + 'key' => 'Key', + 'secret' => 'Secret', + 'bucket' => 'Bucket', + 'endpoint' => 'Endpoint', ]; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->storage->name = $this->name; + $this->storage->description = $this->description; + $this->storage->endpoint = $this->endpoint; + $this->storage->bucket = $this->bucket; + $this->storage->region = $this->region; + $this->storage->key = $this->key; + $this->storage->secret = $this->secret; + $this->storage->is_usable = $this->isUsable; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->storage->name; + $this->description = $this->storage->description; + $this->endpoint = $this->storage->endpoint; + $this->bucket = $this->storage->bucket; + $this->region = $this->storage->region; + $this->key = $this->storage->key; + $this->secret = $this->storage->secret; + $this->isUsable = $this->storage->is_usable; + } + } + + public function mount() + { + $this->syncData(false); + } + public function testConnection() { try { @@ -94,6 +146,9 @@ public function submit() DB::transaction(function () { $this->validate(); + + // Sync properties to model before saving + $this->syncData(true); $this->storage->save(); // Test connection with new values - if this fails, transaction will rollback @@ -103,12 +158,16 @@ public function submit() $this->storage->is_usable = true; $this->storage->unusable_email_sent = false; $this->storage->save(); + + // Update local property to reflect success + $this->isUsable = true; }); $this->dispatch('success', 'Storage settings updated and connection verified.'); } catch (\Throwable $e) { // Refresh the model to revert UI to database values after rollback $this->storage->refresh(); + $this->syncData(false); return handleError($e, $this); } diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 8b9b70e14..e4daad311 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -18,11 +18,16 @@ class Index extends Component public Team $team; + // Explicit properties + public string $name; + + public ?string $description = null; + protected function rules(): array { return [ - 'team.name' => ValidationPatterns::nameRules(), - 'team.description' => ValidationPatterns::descriptionRules(), + 'name' => ValidationPatterns::nameRules(), + 'description' => ValidationPatterns::descriptionRules(), ]; } @@ -31,21 +36,40 @@ protected function messages(): array return array_merge( ValidationPatterns::combinedMessages(), [ - 'team.name.required' => 'The Name field is required.', - 'team.name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', - 'team.description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', + 'name.required' => 'The Name field is required.', + 'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().', + 'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.', ] ); } protected $validationAttributes = [ - 'team.name' => 'name', - 'team.description' => 'description', + 'name' => 'name', + 'description' => 'description', ]; + /** + * Sync data between component properties and model + * + * @param bool $toModel If true, sync FROM properties TO model. If false, sync FROM model TO properties. + */ + private function syncData(bool $toModel = false): void + { + if ($toModel) { + // Sync TO model (before save) + $this->team->name = $this->name; + $this->team->description = $this->description; + } else { + // Sync FROM model (on load/refresh) + $this->name = $this->team->name; + $this->description = $this->team->description; + } + } + public function mount() { $this->team = currentTeam(); + $this->syncData(false); if (auth()->user()->isAdminFromSession()) { $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); @@ -62,6 +86,7 @@ public function submit() $this->validate(); try { $this->authorize('update', $this->team); + $this->syncData(true); $this->team->save(); refreshSession(); $this->dispatch('success', 'Team updated.'); diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 45f7e467f..45af53950 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -45,9 +45,16 @@ private function generateInviteLink(bool $sendEmail = false) try { $this->authorize('manageInvitations', currentTeam()); $this->validate(); - if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + + // Prevent privilege escalation: users cannot invite someone with higher privileges + $userRole = auth()->user()->role(); + if ($userRole === 'member' && in_array($this->role, ['admin', 'owner'])) { + throw new \Exception('Members cannot invite admins or owners.'); + } + if ($userRole === 'admin' && $this->role === 'owner') { throw new \Exception('Admins cannot invite owners.'); } + $this->email = strtolower($this->email); $member_emails = currentTeam()->members()->get()->pluck('email'); diff --git a/app/Livewire/Terminal/Index.php b/app/Livewire/Terminal/Index.php index 03dbc1d91..6bb4c5e90 100644 --- a/app/Livewire/Terminal/Index.php +++ b/app/Livewire/Terminal/Index.php @@ -61,6 +61,10 @@ private function getAllActiveContainers() public function updatedSelectedUuid() { + if ($this->selected_uuid === 'default') { + // When cleared to default, do nothing (no error message) + return; + } $this->connectToContainer(); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 595ba1cde..9554d71a7 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1003,29 +1003,30 @@ public function dirOnServer() public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false) { $baseDir = $this->generateBaseDir($deployment_uuid); + $escapedBaseDir = escapeshellarg($baseDir); $isShallowCloneEnabled = $this->settings?->is_git_shallow_clone_enabled ?? false; if ($this->git_commit_sha !== 'HEAD') { // If shallow clone is enabled and we need a specific commit, // we need to fetch that specific commit with depth=1 if ($isShallowCloneEnabled) { - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$this->git_commit_sha} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git fetch --depth=1 origin {$this->git_commit_sha} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1"; } else { - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1"; } } if ($this->settings->is_git_submodules_enabled) { // Check if .gitmodules file exists before running submodule commands - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && if [ -f .gitmodules ]; then"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && if [ -f .gitmodules ]; then"; if ($public) { - $git_clone_command = "{$git_clone_command} sed -i \"s#git@\(.*\):#https://\\1/#g\" {$baseDir}/.gitmodules || true &&"; + $git_clone_command = "{$git_clone_command} sed -i \"s#git@\(.*\):#https://\\1/#g\" {$escapedBaseDir}/.gitmodules || true &&"; } // Add shallow submodules flag if shallow clone is enabled $submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : ''; $git_clone_command = "{$git_clone_command} git submodule sync && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git submodule update --init --recursive {$submoduleFlags}; fi"; } if ($this->settings->is_git_lfs_enabled) { - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull"; + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git lfs pull"; } return $git_clone_command; @@ -1063,18 +1064,24 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ $source_html_url_scheme = $url['scheme']; if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + $escapedCustomRepository = escapeshellarg($customRepository); if ($this->source->is_public) { + $escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}"); $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; - $base_command = "{$base_command} {$this->source->html_url}/{$customRepository}"; + $base_command = "{$base_command} {$escapedRepoUrl}"; } else { $github_access_token = generateGithubInstallationToken($this->source); if ($exec_in_docker) { - $base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; - $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; + $repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git"; + $escapedRepoUrl = escapeshellarg($repoUrl); + $base_command = "{$base_command} {$escapedRepoUrl}"; + $fullRepoUrl = $repoUrl; } else { - $base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; - $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; + $repoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}"; + $escapedRepoUrl = escapeshellarg($repoUrl); + $base_command = "{$base_command} {$escapedRepoUrl}"; + $fullRepoUrl = $repoUrl; } } @@ -1099,7 +1106,10 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ throw new RuntimeException('Private key not found. Please add a private key to the application and try again.'); } $private_key = base64_encode($private_key); - $base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}"; + // When used with executeInDocker (which uses bash -c '...'), we need to escape for bash context + // Replace ' with '\'' to safely escape within single-quoted bash strings + $escapedCustomRepository = str_replace("'", "'\\''", $customRepository); + $base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} '{$escapedCustomRepository}'"; if ($exec_in_docker) { $commands = collect([ @@ -1116,9 +1126,9 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ } if ($exec_in_docker) { - $commands->push(executeInDocker($deployment_uuid, $base_comamnd)); + $commands->push(executeInDocker($deployment_uuid, $base_command)); } else { - $commands->push($base_comamnd); + $commands->push($base_command); } return [ @@ -1130,7 +1140,8 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_ if ($this->deploymentType() === 'other') { $fullRepoUrl = $customRepository; - $base_command = "{$base_command} {$customRepository}"; + $escapedCustomRepository = escapeshellarg($customRepository); + $base_command = "{$base_command} {$escapedCustomRepository}"; if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, $base_command)); @@ -1272,7 +1283,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1280,14 +1291,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } @@ -1305,7 +1316,8 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } if ($this->deploymentType() === 'other') { $fullRepoUrl = $customRepository; - $git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}"; + $escapedCustomRepository = escapeshellarg($customRepository); + $git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}"; $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true); if ($pull_request_id !== 0) { @@ -1316,7 +1328,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'github' || $git_type === 'gitea') { $branch = "pull/{$pull_request_id}/head:$pr_branch_name"; if ($exec_in_docker) { @@ -1324,14 +1336,14 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name); } elseif ($git_type === 'bitbucket') { if ($exec_in_docker) { $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")); } else { $commands->push("echo 'Checking out $branch'"); } - $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); + $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit); } } diff --git a/app/Models/CloudInitScript.php b/app/Models/CloudInitScript.php new file mode 100644 index 000000000..2c78cc582 --- /dev/null +++ b/app/Models/CloudInitScript.php @@ -0,0 +1,33 @@ + 'encrypted', + ]; + } + + public function team() + { + return $this->belongsTo(Team::class); + } + + public static function ownedByCurrentTeam(array $select = ['*']) + { + $selectArray = collect($select)->concat(['id']); + + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); + } +} diff --git a/app/Models/CloudProviderToken.php b/app/Models/CloudProviderToken.php new file mode 100644 index 000000000..607040269 --- /dev/null +++ b/app/Models/CloudProviderToken.php @@ -0,0 +1,41 @@ + 'encrypted', + ]; + + public function team() + { + return $this->belongsTo(Team::class); + } + + public function servers() + { + return $this->hasMany(Server::class); + } + + public function hasServers(): bool + { + return $this->servers()->exists(); + } + + public static function ownedByCurrentTeam(array $select = ['*']) + { + $selectArray = collect($select)->concat(['id']); + + return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); + } + + public function scopeForProvider($query, string $provider) + { + return $query->where('provider', $provider); + } +} diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index ac95bb8a9..cd1c05de4 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -35,13 +35,18 @@ class InstanceSettings extends Model protected static function booted(): void { static::updated(function ($settings) { - if ($settings->isDirty('helper_version')) { + if ($settings->wasChanged('helper_version')) { Server::chunkById(100, function ($servers) { foreach ($servers as $server) { PullHelperImageJob::dispatch($server); } }); } + + // Clear trusted hosts cache when FQDN changes + if ($settings->wasChanged('fqdn')) { + \Cache::forget('instance_settings_fqdn_host'); + } }); } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index c210f3c5b..08f3f1ebd 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -289,6 +289,17 @@ public static function generateFingerprint($privateKey) } } + public static function generateMd5Fingerprint($privateKey) + { + try { + $key = PublicKeyLoader::load($privateKey); + + return $key->getPublicKey()->getFingerprint('md5'); + } catch (\Throwable $e) { + return null; + } + } + public static function fingerprintExists($fingerprint, $excludeId = null) { $query = self::query() diff --git a/app/Models/Server.php b/app/Models/Server.php index 829a4b5aa..e39526949 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -136,6 +136,7 @@ protected static function booted() $destination->delete(); }); $server->settings()->delete(); + $server->sslCertificates()->delete(); }); } @@ -161,7 +162,11 @@ protected static function booted() 'user', 'description', 'private_key_id', + 'cloud_provider_token_id', 'team_id', + 'hetzner_server_id', + 'hetzner_server_status', + 'is_validating', ]; protected $guarded = []; @@ -889,6 +894,16 @@ public function privateKey() return $this->belongsTo(PrivateKey::class); } + public function cloudProviderToken() + { + return $this->belongsTo(CloudProviderToken::class); + } + + public function sslCertificates() + { + return $this->hasMany(SslCertificate::class); + } + public function muxFilename() { return 'mux_'.$this->uuid; @@ -1327,7 +1342,7 @@ public function generateCaCertificate() isCaCertificate: true, validityDays: 10 * 365 ); - $caCertificate = SslCertificate::where('server_id', $this->id)->where('is_ca_certificate', true)->first(); + $caCertificate = $this->sslCertificates()->where('is_ca_certificate', true)->first(); ray('CA certificate generated', $caCertificate); if ($caCertificate) { $certificateContent = $caCertificate->ssl_certificate; diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 3abd55e9c..6da4dd4c6 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -79,11 +79,11 @@ protected static function booted() }); static::updated(function ($settings) { if ( - $settings->isDirty('sentinel_token') || - $settings->isDirty('sentinel_custom_url') || - $settings->isDirty('sentinel_metrics_refresh_rate_seconds') || - $settings->isDirty('sentinel_metrics_history_days') || - $settings->isDirty('sentinel_push_interval_seconds') + $settings->wasChanged('sentinel_token') || + $settings->wasChanged('sentinel_custom_url') || + $settings->wasChanged('sentinel_metrics_refresh_rate_seconds') || + $settings->wasChanged('sentinel_metrics_history_days') || + $settings->wasChanged('sentinel_push_interval_seconds') ) { $settings->server->restartSentinel(); } diff --git a/app/Models/Team.php b/app/Models/Team.php index 51fdeffa4..6c30389ee 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -54,6 +54,7 @@ protected static function booted() $team->slackNotificationSettings()->create(); $team->telegramNotificationSettings()->create(); $team->pushoverNotificationSettings()->create(); + $team->webhookNotificationSettings()->create(); }); static::saving(function ($team) { @@ -258,6 +259,11 @@ public function privateKeys() return $this->hasMany(PrivateKey::class); } + public function cloudProviderTokens() + { + return $this->hasMany(CloudProviderToken::class); + } + public function sources() { $sources = collect([]); @@ -307,4 +313,9 @@ public function pushoverNotificationSettings() { return $this->hasOne(PushoverNotificationSettings::class); } + + public function webhookNotificationSettings() + { + return $this->hasOne(WebhookNotificationSettings::class); + } } diff --git a/app/Models/WebhookNotificationSettings.php b/app/Models/WebhookNotificationSettings.php new file mode 100644 index 000000000..4ca89e0d3 --- /dev/null +++ b/app/Models/WebhookNotificationSettings.php @@ -0,0 +1,64 @@ + 'boolean', + 'webhook_url' => 'encrypted', + + 'deployment_success_webhook_notifications' => 'boolean', + 'deployment_failure_webhook_notifications' => 'boolean', + 'status_change_webhook_notifications' => 'boolean', + 'backup_success_webhook_notifications' => 'boolean', + 'backup_failure_webhook_notifications' => 'boolean', + 'scheduled_task_success_webhook_notifications' => 'boolean', + 'scheduled_task_failure_webhook_notifications' => 'boolean', + 'docker_cleanup_webhook_notifications' => 'boolean', + 'server_disk_usage_webhook_notifications' => 'boolean', + 'server_reachable_webhook_notifications' => 'boolean', + 'server_unreachable_webhook_notifications' => 'boolean', + 'server_patch_webhook_notifications' => 'boolean', + ]; + } + + public function team() + { + return $this->belongsTo(Team::class); + } + + public function isEnabled() + { + return $this->webhook_enabled; + } +} diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index dec361e78..8fff7f03b 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -185,4 +185,30 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => false, + 'message' => 'Deployment failed', + 'event' => 'deployment_failed', + 'application_name' => $this->application_name, + 'application_uuid' => $this->application->uuid, + 'deployment_uuid' => $this->deployment_uuid, + 'deployment_url' => $this->deployment_url, + 'project' => data_get($this->application, 'environment.project.name'), + 'environment' => $this->environment_name, + ]; + + if ($this->preview) { + $data['pull_request_id'] = $this->preview->pull_request_id; + $data['preview_fqdn'] = $this->preview->fqdn; + } + + if ($this->fqdn) { + $data['fqdn'] = $this->fqdn; + } + + return $data; + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 9b59d9162..415df5831 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -205,4 +205,30 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => true, + 'message' => 'New version successfully deployed', + 'event' => 'deployment_success', + 'application_name' => $this->application_name, + 'application_uuid' => $this->application->uuid, + 'deployment_uuid' => $this->deployment_uuid, + 'deployment_url' => $this->deployment_url, + 'project' => data_get($this->application, 'environment.project.name'), + 'environment' => $this->environment_name, + ]; + + if ($this->preview) { + $data['pull_request_id'] = $this->preview->pull_request_id; + $data['preview_fqdn'] = $this->preview->fqdn; + } + + if ($this->fqdn) { + $data['fqdn'] = $this->fqdn; + } + + return $data; + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index fab5487ef..ef61b7e6a 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -113,4 +113,19 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + return [ + 'success' => false, + 'message' => 'Application stopped', + 'event' => 'status_changed', + 'application_name' => $this->resource_name, + 'application_uuid' => $this->resource->uuid, + 'url' => $this->resource_url, + 'project' => data_get($this->resource, 'environment.project.name'), + 'environment' => $this->environment_name, + 'fqdn' => $this->fqdn, + ]; + } } diff --git a/app/Notifications/Channels/WebhookChannel.php b/app/Notifications/Channels/WebhookChannel.php new file mode 100644 index 000000000..8c3e74b17 --- /dev/null +++ b/app/Notifications/Channels/WebhookChannel.php @@ -0,0 +1,37 @@ +webhookNotificationSettings; + + if (! $webhookSettings || ! $webhookSettings->isEnabled() || ! $webhookSettings->webhook_url) { + if (isDev()) { + ray('Webhook notification skipped - not enabled or no URL configured'); + } + + return; + } + + $payload = $notification->toWebhook(); + + if (isDev()) { + ray('Dispatching webhook notification', [ + 'notification' => get_class($notification), + 'url' => $webhookSettings->webhook_url, + 'payload' => $payload, + ]); + } + + SendWebhookJob::dispatch($payload, $webhookSettings->webhook_url); + } +} diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index f6ae69481..2d7eb58b5 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -102,4 +102,22 @@ public function toSlack(): SlackMessage color: SlackMessage::warningColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => true, + 'message' => 'Resource restarted automatically', + 'event' => 'container_restarted', + 'container_name' => $this->name, + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + ]; + + if ($this->url) { + $data['url'] = $this->url; + } + + return $data; + } } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index fc9410a85..f518cd2fd 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -102,4 +102,22 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => false, + 'message' => 'Resource stopped unexpectedly', + 'event' => 'container_stopped', + 'container_name' => $this->name, + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + ]; + + if ($this->url) { + $data['url'] = $this->url; + } + + return $data; + } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index a19fb0431..c2b21b1d5 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -88,4 +88,21 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid; + + return [ + 'success' => false, + 'message' => 'Database backup failed', + 'event' => 'backup_failed', + 'database_name' => $this->name, + 'database_uuid' => $this->database->uuid, + 'database_type' => $this->database_name, + 'frequency' => $this->frequency, + 'error_output' => $this->output, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 78bcfafe3..3d2d8ece3 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -85,4 +85,20 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid; + + return [ + 'success' => true, + 'message' => 'Database backup successful', + 'event' => 'backup_success', + 'database_name' => $this->name, + 'database_uuid' => $this->database->uuid, + 'database_type' => $this->database_name, + 'frequency' => $this->frequency, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Database/BackupSuccessWithS3Warning.php b/app/Notifications/Database/BackupSuccessWithS3Warning.php index 75ae2824c..ee24ef17d 100644 --- a/app/Notifications/Database/BackupSuccessWithS3Warning.php +++ b/app/Notifications/Database/BackupSuccessWithS3Warning.php @@ -113,4 +113,27 @@ public function toSlack(): SlackMessage color: SlackMessage::warningColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/project/'.data_get($this->database, 'environment.project.uuid').'/environment/'.data_get($this->database, 'environment.uuid').'/database/'.$this->database->uuid; + + $data = [ + 'success' => true, + 'message' => 'Database backup succeeded locally, S3 upload failed', + 'event' => 'backup_success_with_s3_warning', + 'database_name' => $this->name, + 'database_uuid' => $this->database->uuid, + 'database_type' => $this->database_name, + 'frequency' => $this->frequency, + 's3_error' => $this->s3_error, + 'url' => $url, + ]; + + if ($this->s3_storage_url) { + $data['s3_storage_url'] = $this->s3_storage_url; + } + + return $data; + } } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index eb4fc7e79..bd060112a 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -114,4 +114,28 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => false, + 'message' => 'Scheduled task failed', + 'event' => 'task_failed', + 'task_name' => $this->task->name, + 'task_uuid' => $this->task->uuid, + 'output' => $this->output, + ]; + + if ($this->task->application) { + $data['application_uuid'] = $this->task->application->uuid; + } elseif ($this->task->service) { + $data['service_uuid'] = $this->task->service->uuid; + } + + if ($this->url) { + $data['url'] = $this->url; + } + + return $data; + } } diff --git a/app/Notifications/ScheduledTask/TaskSuccess.php b/app/Notifications/ScheduledTask/TaskSuccess.php index c45784db2..58c959bd8 100644 --- a/app/Notifications/ScheduledTask/TaskSuccess.php +++ b/app/Notifications/ScheduledTask/TaskSuccess.php @@ -105,4 +105,28 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + $data = [ + 'success' => true, + 'message' => 'Scheduled task succeeded', + 'event' => 'task_success', + 'task_name' => $this->task->name, + 'task_uuid' => $this->task->uuid, + 'output' => $this->output, + ]; + + if ($this->task->application) { + $data['application_uuid'] = $this->task->application->uuid; + } elseif ($this->task->service) { + $data['service_uuid'] = $this->task->service->uuid; + } + + if ($this->url) { + $data['url'] = $this->url; + } + + return $data; + } } diff --git a/app/Notifications/Server/DockerCleanupFailed.php b/app/Notifications/Server/DockerCleanupFailed.php index 0291eed19..9cbdeb488 100644 --- a/app/Notifications/Server/DockerCleanupFailed.php +++ b/app/Notifications/Server/DockerCleanupFailed.php @@ -66,4 +66,19 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/server/'.$this->server->uuid; + + return [ + 'success' => false, + 'message' => 'Docker cleanup job failed', + 'event' => 'docker_cleanup_failed', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'error_message' => $this->message, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Server/DockerCleanupSuccess.php b/app/Notifications/Server/DockerCleanupSuccess.php index 1a652d189..d28f25c6c 100644 --- a/app/Notifications/Server/DockerCleanupSuccess.php +++ b/app/Notifications/Server/DockerCleanupSuccess.php @@ -66,4 +66,19 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/server/'.$this->server->uuid; + + return [ + 'success' => true, + 'message' => 'Docker cleanup job succeeded', + 'event' => 'docker_cleanup_success', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'cleanup_message' => $this->message, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Server/HetznerDeletionFailed.php b/app/Notifications/Server/HetznerDeletionFailed.php new file mode 100644 index 000000000..de894331b --- /dev/null +++ b/app/Notifications/Server/HetznerDeletionFailed.php @@ -0,0 +1,71 @@ +onQueue('high'); + } + + public function via(object $notifiable): array + { + ray('hello'); + ray($notifiable); + + return $notifiable->getEnabledChannels('hetzner_deletion_failed'); + } + + public function toMail(): MailMessage + { + $mail = new MailMessage; + $mail->subject("Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId}"); + $mail->view('emails.hetzner-deletion-failed', [ + 'hetznerServerId' => $this->hetznerServerId, + 'errorMessage' => $this->errorMessage, + ]); + + return $mail; + } + + public function toDiscord(): DiscordMessage + { + return new DiscordMessage( + title: ':cross_mark: Coolify: [ACTION REQUIRED] Failed to delete Hetzner server', + description: "Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\n**Error:** {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.", + color: DiscordMessage::errorColor(), + ); + } + + public function toTelegram(): array + { + return [ + 'message' => "Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.", + ]; + } + + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Hetzner Server Deletion Failed', + level: 'error', + message: "[ACTION REQUIRED] Failed to delete Hetzner server #{$this->hetznerServerId}.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check and manually delete if needed.", + ); + } + + public function toSlack(): SlackMessage + { + return new SlackMessage( + title: 'Coolify: [ACTION REQUIRED] Hetzner Server Deletion Failed', + description: "Failed to delete Hetzner server #{$this->hetznerServerId} from Hetzner Cloud.\n\nError: {$this->errorMessage}\n\nThe server has been removed from Coolify, but may still exist in your Hetzner Cloud account. Please check your Hetzner Cloud console and manually delete the server if needed.", + color: SlackMessage::errorColor() + ); + } +} diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 983e6d81e..149d1bbc8 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -88,4 +88,18 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + return [ + 'success' => false, + 'message' => 'High disk usage detected', + 'event' => 'high_disk_usage', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'disk_usage' => $this->disk_usage, + 'threshold' => $this->server_disk_usage_notification_threshold, + 'url' => base_url().'/server/'.$this->server->uuid, + ]; + } } diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index e03aef6b7..e64b0af2a 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -74,4 +74,18 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/server/'.$this->server->uuid; + + return [ + 'success' => true, + 'message' => 'Server revived', + 'event' => 'server_reachable', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php index 1686a6f37..4d3053569 100644 --- a/app/Notifications/Server/ServerPatchCheck.php +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -345,4 +345,47 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + // Handle error case + if (isset($this->patchData['error'])) { + return [ + 'success' => false, + 'message' => 'Failed to check patches', + 'event' => 'server_patch_check_error', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'os_id' => $this->patchData['osId'] ?? 'unknown', + 'package_manager' => $this->patchData['package_manager'] ?? 'unknown', + 'error' => $this->patchData['error'], + 'url' => $this->serverUrl, + ]; + } + + $totalUpdates = $this->patchData['total_updates'] ?? 0; + $updates = $this->patchData['updates'] ?? []; + + // Check for critical packages + $criticalPackages = collect($updates)->filter(function ($update) { + return str_contains(strtolower($update['package']), 'docker') || + str_contains(strtolower($update['package']), 'kernel') || + str_contains(strtolower($update['package']), 'openssh') || + str_contains(strtolower($update['package']), 'ssl'); + }); + + return [ + 'success' => false, + 'message' => 'Server patches available', + 'event' => 'server_patch_check', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'total_updates' => $totalUpdates, + 'os_id' => $this->patchData['osId'] ?? 'unknown', + 'package_manager' => $this->patchData['package_manager'] ?? 'unknown', + 'updates' => $updates, + 'critical_packages_count' => $criticalPackages->count(), + 'url' => $this->serverUrl, + ]; + } } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index fe90cc610..99742f3b7 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -82,4 +82,18 @@ public function toSlack(): SlackMessage color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + $url = base_url().'/server/'.$this->server->uuid; + + return [ + 'success' => false, + 'message' => 'Server unreachable', + 'event' => 'server_unreachable', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'url' => $url, + ]; + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 0b1d8d6b1..60bc8a0ee 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -7,6 +7,7 @@ use App\Notifications\Channels\PushoverChannel; use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\WebhookChannel; use App\Notifications\Dto\DiscordMessage; use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; @@ -36,6 +37,7 @@ public function via(object $notifiable): array 'telegram' => [TelegramChannel::class], 'slack' => [SlackChannel::class], 'pushover' => [PushoverChannel::class], + 'webhook' => [WebhookChannel::class], default => [], }; } else { @@ -110,4 +112,14 @@ public function toSlack(): SlackMessage description: 'This is a test Slack notification from Coolify.' ); } + + public function toWebhook(): array + { + return [ + 'success' => true, + 'message' => 'This is a test webhook notification from Coolify.', + 'event' => 'test', + 'url' => base_url(), + ]; + } } diff --git a/app/Policies/CloudInitScriptPolicy.php b/app/Policies/CloudInitScriptPolicy.php new file mode 100644 index 000000000..0be4f2662 --- /dev/null +++ b/app/Policies/CloudInitScriptPolicy.php @@ -0,0 +1,65 @@ +isAdmin(); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, CloudInitScript $cloudInitScript): bool + { + return $user->isAdmin(); + } +} diff --git a/app/Policies/CloudProviderTokenPolicy.php b/app/Policies/CloudProviderTokenPolicy.php new file mode 100644 index 000000000..b7b108ba8 --- /dev/null +++ b/app/Policies/CloudProviderTokenPolicy.php @@ -0,0 +1,65 @@ +isAdmin(); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, CloudProviderToken $cloudProviderToken): bool + { + return $user->isAdmin(); + } +} diff --git a/app/Policies/TeamPolicy.php b/app/Policies/TeamPolicy.php index b7ef48943..849e23751 100644 --- a/app/Policies/TeamPolicy.php +++ b/app/Policies/TeamPolicy.php @@ -42,8 +42,7 @@ public function update(User $user, Team $team): bool return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -56,8 +55,7 @@ public function delete(User $user, Team $team): bool return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -70,8 +68,7 @@ public function manageMembers(User $user, Team $team): bool return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -84,8 +81,7 @@ public function viewAdmin(User $user, Team $team): bool return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -98,7 +94,6 @@ public function manageInvitations(User $user, Team $team): bool return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index c017a580e..5d3347936 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -45,6 +45,7 @@ class AuthServiceProvider extends ServiceProvider \App\Models\TelegramNotificationSettings::class => \App\Policies\NotificationPolicy::class, \App\Models\SlackNotificationSettings::class => \App\Policies\NotificationPolicy::class, \App\Models\PushoverNotificationSettings::class => \App\Policies\NotificationPolicy::class, + \App\Models\WebhookNotificationSettings::class => \App\Policies\NotificationPolicy::class, // API Token policy \Laravel\Sanctum\PersonalAccessToken::class => \App\Policies\ApiTokenPolicy::class, diff --git a/app/Rules/ValidCloudInitYaml.php b/app/Rules/ValidCloudInitYaml.php new file mode 100644 index 000000000..8116e1161 --- /dev/null +++ b/app/Rules/ValidCloudInitYaml.php @@ -0,0 +1,55 @@ +getMessage()); + } + + return; + } + + // If it doesn't start with #! or #cloud-config, try to parse as YAML + // (some users might omit the #cloud-config header) + try { + Yaml::parse($script); + } catch (ParseException $e) { + $fail('The :attribute must be either a valid bash script (starting with #!) or valid cloud-config YAML. YAML parse error: '.$e->getMessage()); + } + } +} diff --git a/app/Rules/ValidHostname.php b/app/Rules/ValidHostname.php new file mode 100644 index 000000000..b6b2b8d32 --- /dev/null +++ b/app/Rules/ValidHostname.php @@ -0,0 +1,114 @@ + 253) { + $fail('The :attribute must not exceed 253 characters.'); + + return; + } + + // Check for dangerous shell metacharacters + $dangerousChars = [ + ';', '|', '&', '$', '`', '(', ')', '{', '}', + '<', '>', '\n', '\r', '\0', '"', "'", '\\', + '!', '*', '?', '[', ']', '~', '^', ':', '#', + '@', '%', '=', '+', ',', ' ', + ]; + + foreach ($dangerousChars as $char) { + if (str_contains($hostname, $char)) { + try { + $logData = [ + 'hostname' => $hostname, + 'character' => $char, + ]; + + if (function_exists('request') && app()->has('request')) { + $logData['ip'] = request()->ip(); + } + + if (function_exists('auth') && app()->has('auth')) { + $logData['user_id'] = auth()->id(); + } + + Log::warning('Hostname validation failed - dangerous character', $logData); + } catch (\Throwable $e) { + // Ignore errors when facades are not available (e.g., in unit tests) + } + + $fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.'); + + return; + } + } + + // Additional validation: hostname should not start or end with a dot + if (str_starts_with($hostname, '.') || str_ends_with($hostname, '.')) { + $fail('The :attribute cannot start or end with a dot.'); + + return; + } + + // Check for consecutive dots + if (str_contains($hostname, '..')) { + $fail('The :attribute cannot contain consecutive dots.'); + + return; + } + + // Split into labels (segments between dots) + $labels = explode('.', $hostname); + + foreach ($labels as $label) { + // Check label length (RFC 1123: max 63 characters per label) + if (strlen($label) < 1 || strlen($label) > 63) { + $fail('The :attribute contains an invalid label. Each segment must be 1-63 characters.'); + + return; + } + + // Check if label starts or ends with hyphen + if (str_starts_with($label, '-') || str_ends_with($label, '-')) { + $fail('The :attribute contains an invalid label. Labels cannot start or end with a hyphen.'); + + return; + } + + // Check if label contains only valid characters (lowercase letters, digits, hyphens) + if (! preg_match('/^[a-z0-9-]+$/', $label)) { + $fail('The :attribute contains invalid characters. Only lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.) are allowed.'); + + return; + } + + // RFC 1123 allows labels to be all numeric (unlike RFC 952) + // So we don't need to check for all-numeric labels + } + } +} diff --git a/app/Services/HetznerService.php b/app/Services/HetznerService.php new file mode 100644 index 000000000..aa6de3897 --- /dev/null +++ b/app/Services/HetznerService.php @@ -0,0 +1,143 @@ +token = $token; + } + + private function request(string $method, string $endpoint, array $data = []) + { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + ]) + ->timeout(30) + ->retry(3, function (int $attempt, \Exception $exception) { + // Handle rate limiting (429 Too Many Requests) + if ($exception instanceof \Illuminate\Http\Client\RequestException) { + $response = $exception->response; + + if ($response && $response->status() === 429) { + // Get rate limit reset timestamp from headers + $resetTime = $response->header('RateLimit-Reset'); + + if ($resetTime) { + // Calculate wait time until rate limit resets + $waitSeconds = max(0, $resetTime - time()); + + // Cap wait time at 60 seconds for safety + return min($waitSeconds, 60) * 1000; + } + } + } + + // Exponential backoff for other retriable errors: 100ms, 200ms, 400ms + return $attempt * 100; + }) + ->{$method}($this->baseUrl.$endpoint, $data); + + if (! $response->successful()) { + throw new \Exception('Hetzner API error: '.$response->json('error.message', 'Unknown error')); + } + + return $response->json(); + } + + private function requestPaginated(string $method, string $endpoint, string $resourceKey, array $data = []): array + { + $allResults = []; + $page = 1; + + do { + $data['page'] = $page; + $data['per_page'] = 50; + + $response = $this->request($method, $endpoint, $data); + + if (isset($response[$resourceKey])) { + $allResults = array_merge($allResults, $response[$resourceKey]); + } + + $nextPage = $response['meta']['pagination']['next_page'] ?? null; + $page = $nextPage; + } while ($nextPage !== null); + + return $allResults; + } + + public function getLocations(): array + { + return $this->requestPaginated('get', '/locations', 'locations'); + } + + public function getImages(): array + { + return $this->requestPaginated('get', '/images', 'images', [ + 'type' => 'system', + ]); + } + + public function getServerTypes(): array + { + return $this->requestPaginated('get', '/server_types', 'server_types'); + } + + public function getSshKeys(): array + { + return $this->requestPaginated('get', '/ssh_keys', 'ssh_keys'); + } + + public function uploadSshKey(string $name, string $publicKey): array + { + $response = $this->request('post', '/ssh_keys', [ + 'name' => $name, + 'public_key' => $publicKey, + ]); + + return $response['ssh_key'] ?? []; + } + + public function createServer(array $params): array + { + ray('Hetzner createServer request', [ + 'endpoint' => '/servers', + 'params' => $params, + ]); + + $response = $this->request('post', '/servers', $params); + + ray('Hetzner createServer response', [ + 'response' => $response, + ]); + + return $response['server'] ?? []; + } + + public function getServer(int $serverId): array + { + $response = $this->request('get', "/servers/{$serverId}"); + + return $response['server'] ?? []; + } + + public function powerOnServer(int $serverId): array + { + $response = $this->request('post', "/servers/{$serverId}/actions/poweron"); + + return $response['action'] ?? []; + } + + public function deleteServer(int $serverId): void + { + $this->request('delete', "/servers/{$serverId}"); + } +} diff --git a/app/Traits/DeletesUserSessions.php b/app/Traits/DeletesUserSessions.php index a4d3a7cfd..e9ec0d946 100644 --- a/app/Traits/DeletesUserSessions.php +++ b/app/Traits/DeletesUserSessions.php @@ -26,7 +26,7 @@ protected static function bootDeletesUserSessions() { static::updated(function ($user) { // Check if password was changed - if ($user->isDirty('password')) { + if ($user->wasChanged('password')) { $user->deleteAllSessions(); } }); diff --git a/app/Traits/HasNotificationSettings.php b/app/Traits/HasNotificationSettings.php index 236e4d97c..fded435fd 100644 --- a/app/Traits/HasNotificationSettings.php +++ b/app/Traits/HasNotificationSettings.php @@ -7,6 +7,7 @@ use App\Notifications\Channels\PushoverChannel; use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\WebhookChannel; use Illuminate\Database\Eloquent\Model; trait HasNotificationSettings @@ -17,6 +18,7 @@ trait HasNotificationSettings 'general', 'test', 'ssl_certificate_renewal', + 'hetzner_deletion_failure', ]; /** @@ -30,6 +32,7 @@ public function getNotificationSettings(string $channel): ?Model 'telegram' => $this->telegramNotificationSettings, 'slack' => $this->slackNotificationSettings, 'pushover' => $this->pushoverNotificationSettings, + 'webhook' => $this->webhookNotificationSettings, default => null, }; } @@ -77,6 +80,7 @@ public function getEnabledChannels(string $event): array 'telegram' => TelegramChannel::class, 'slack' => SlackChannel::class, 'pushover' => PushoverChannel::class, + 'webhook' => WebhookChannel::class, ]; if ($event === 'general') { diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index ece7f0e35..eb38d84af 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -6,9 +6,14 @@ use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; +use Visus\Cuid2\Cuid2; class Checkbox extends Component { + public ?string $modelBinding = null; + + public ?string $htmlId = null; + /** * Create a new component instance. */ @@ -22,7 +27,7 @@ public function __construct( public string|bool|null $checked = false, public string|bool $instantSave = false, public bool $disabled = false, - public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 focus:ring-warning dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed', + public string $defaultClass = 'dark:border-neutral-700 text-coolgray-400 dark:bg-coolgray-100 rounded-sm cursor-pointer dark:disabled:bg-base dark:disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base', public ?string $canGate = null, public mixed $canResource = null, public bool $autoDisable = true, @@ -47,6 +52,18 @@ public function __construct( */ public function render(): View|Closure|string { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->id) { + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->id.'-'.$uniqueSuffix; + } else { + $this->htmlId = $this->id; + } + return view('components.forms.checkbox'); } } diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index 25643753d..3b7a9ee34 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -4,12 +4,16 @@ use Closure; use Illuminate\Contracts\View\View; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; use Visus\Cuid2\Cuid2; class Datalist extends Component { + public ?string $modelBinding = null; + + public ?string $htmlId = null; + /** * Create a new component instance. */ @@ -19,9 +23,27 @@ public function __construct( public ?string $label = null, public ?string $helper = null, public bool $required = false, - public string $defaultClass = 'input' + public bool $disabled = false, + public bool $readonly = false, + public bool $multiple = false, + public string|bool $instantSave = false, + public ?string $value = null, + public ?string $placeholder = null, + public bool $autofocus = false, + public string $defaultClass = 'input', + public ?string $canGate = null, + public mixed $canResource = null, + public bool $autoDisable = true, ) { - // + // Handle authorization-based disabling + if ($this->canGate && $this->canResource && $this->autoDisable) { + $hasPermission = Gate::allows($this->canGate, $this->canResource); + + if (! $hasPermission) { + $this->disabled = true; + $this->instantSave = false; // Disable instant save for unauthorized users + } + } } /** @@ -29,14 +51,28 @@ public function __construct( */ public function render(): View|Closure|string { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + if (is_null($this->id)) { $this->id = new Cuid2; - } - if (is_null($this->name)) { - $this->name = $this->id; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; } - $this->label = Str::title($this->label); + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + + if (is_null($this->name)) { + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; + } return view('components.forms.datalist'); } diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 83c98c0df..5ed347f42 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -10,6 +10,10 @@ class Input extends Component { + public ?string $modelBinding = null; + + public ?string $htmlId = null; + public function __construct( public ?string $id = null, public ?string $name = null, @@ -43,11 +47,26 @@ public function __construct( public function render(): View|Closure|string { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + if (is_null($this->id)) { $this->id = new Cuid2; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; } + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + if (is_null($this->name)) { - $this->name = $this->id; + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; } if ($this->type === 'password') { $this->defaultClass = $this->defaultClass.' pr-[2.8rem]'; diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 49b69136b..026e3ba8c 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -10,6 +10,10 @@ class Select extends Component { + public ?string $modelBinding = null; + + public ?string $htmlId = null; + /** * Create a new component instance. */ @@ -40,11 +44,27 @@ public function __construct( */ public function render(): View|Closure|string { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + if (is_null($this->id)) { $this->id = new Cuid2; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; } + + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + if (is_null($this->name)) { - $this->name = $this->id; + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; } return view('components.forms.select'); diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 3148d2566..a5303b947 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -10,6 +10,10 @@ class Textarea extends Component { + public ?string $modelBinding = null; + + public ?string $htmlId = null; + /** * Create a new component instance. */ @@ -27,6 +31,7 @@ public function __construct( public bool $readonly = false, public bool $allowTab = false, public bool $spellcheck = false, + public bool $autofocus = false, public ?string $helper = null, public bool $realtimeValidation = false, public bool $allowToPeak = true, @@ -53,11 +58,27 @@ public function __construct( */ public function render(): View|Closure|string { + // Store original ID for wire:model binding (property name) + $this->modelBinding = $this->id; + if (is_null($this->id)) { $this->id = new Cuid2; + // Don't create wire:model binding for auto-generated IDs + $this->modelBinding = 'null'; } + + // Generate unique HTML ID by adding random suffix + // This prevents duplicate IDs when multiple forms are on the same page + if ($this->modelBinding && $this->modelBinding !== 'null') { + // Use original ID with random suffix for uniqueness + $uniqueSuffix = new Cuid2; + $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; + } else { + $this->htmlId = (string) $this->id; + } + if (is_null($this->name)) { - $this->name = $this->id; + $this->name = $this->modelBinding !== 'null' ? $this->modelBinding : (string) $this->id; } // $this->label = Str::title($this->label); diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index b63c3fc3b..d6c9b5bdf 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -378,6 +378,16 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($serviceLabels) { $middlewares_from_labels = $serviceLabels->map(function ($item) { + // Handle array values from YAML parsing (e.g., "traefik.enable: true" becomes an array) + if (is_array($item)) { + // Convert array to string format "key=value" + $key = collect($item)->keys()->first(); + $value = collect($item)->values()->first(); + $item = "$key=$value"; + } + if (! is_string($item)) { + return null; + } if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) { return $matches[1]; } @@ -1120,6 +1130,76 @@ function escapeDollarSign($value) return str_replace($search, $replace, $value); } +/** + * Escape a value for use in a bash .env file that will be sourced with 'source' command + * Wraps the value in single quotes and escapes any single quotes within the value + * + * @param string|null $value The value to escape + * @return string The escaped value wrapped in single quotes + */ +function escapeBashEnvValue(?string $value): string +{ + // Handle null or empty values + if ($value === null || $value === '') { + return "''"; + } + + // Replace single quotes with '\'' (end quote, escaped quote, start quote) + // This is the standard way to escape single quotes in bash single-quoted strings + $escaped = str_replace("'", "'\\''", $value); + + // Wrap in single quotes + return "'{$escaped}'"; +} + +/** + * Escape a value for bash double-quoted strings (allows $VAR expansion) + * + * This function wraps values in double quotes while escaping special characters, + * but preserves valid bash variable references like $VAR and ${VAR}. + * + * @param string|null $value The value to escape + * @return string The escaped value wrapped in double quotes + */ +function escapeBashDoubleQuoted(?string $value): string +{ + // Handle null or empty values + if ($value === null || $value === '') { + return '""'; + } + + // Step 1: Escape backslashes first (must be done before other escaping) + $escaped = str_replace('\\', '\\\\', $value); + + // Step 2: Escape double quotes + $escaped = str_replace('"', '\\"', $escaped); + + // Step 3: Escape backticks (command substitution) + $escaped = str_replace('`', '\\`', $escaped); + + // Step 4: Escape invalid $ patterns while preserving valid variable references + // Valid patterns to keep: + // - $VAR_NAME (alphanumeric + underscore, starting with letter or _) + // - ${VAR_NAME} (brace expansion) + // - $0-$9 (positional parameters) + // Invalid patterns to escape: $&, $#, $$, $*, $@, $!, $(, etc. + + // Match $ followed by anything that's NOT a valid variable start + // Valid variable starts: letter, underscore, digit (for $0-$9), or open brace + $escaped = preg_replace( + '/\$(?![a-zA-Z_0-9{])/', + '\\\$', + $escaped + ); + + // Preserve pre-escaped dollars inside double quotes: turn \\$ back into \$ + // (keeps tests like "path\\to\\file" intact while restoring \$ semantics) + $escaped = preg_replace('/\\\\(?=\$)/', '\\\\', $escaped); + + // Wrap in double quotes + return "\"{$escaped}\""; +} + /** * Generate Docker build arguments from environment variables collection * Returns only keys (no values) since values are sourced from environment via export diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index a588ed882..f2260f0c6 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -16,6 +16,101 @@ use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +/** + * Validates a Docker Compose YAML string for command injection vulnerabilities. + * This should be called BEFORE saving to database to prevent malicious data from being stored. + * + * @param string $composeYaml The raw Docker Compose YAML content + * + * @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); + } + + if (! is_array($parsed) || ! isset($parsed['services']) || ! is_array($parsed['services'])) { + 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( + 'Invalid Docker Compose service name: '.$e->getMessage(). + ' Service names must not contain shell metacharacters.', + 0, + $e + ); + } + + // Validate volumes in this service (both string and array formats) + if (isset($serviceConfig['volumes']) && is_array($serviceConfig['volumes'])) { + foreach ($serviceConfig['volumes'] as $volume) { + if (is_string($volume)) { + // String format: "source:target" or "source:target:mode" + validateVolumeStringForInjection($volume); + } elseif (is_array($volume)) { + // Array format: {type: bind, source: ..., target: ...} + if (isset($volume['source'])) { + $source = $volume['source']; + if (is_string($source)) { + // Allow simple env vars and env vars with defaults (validated in parseDockerVolumeString) + $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source); + $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $source); + + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault) { + try { + validateShellSafePath($source, 'volume source'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.', + 0, + $e + ); + } + } + } + } + if (isset($volume['target'])) { + $target = $volume['target']; + if (is_string($target)) { + try { + validateShellSafePath($target, 'volume target'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.', + 0, + $e + ); + } + } + } + } + } + } + } +} + +/** + * Validates a Docker volume string (format: "source:target" or "source:target:mode") + * + * @param string $volumeString The volume string to validate + * + * @throws \Exception If the volume string contains command injection attempts + */ +function validateVolumeStringForInjection(string $volumeString): void +{ + // Canonical parsing also validates and throws on unsafe input + parseDockerVolumeString($volumeString); +} + function parseDockerVolumeString(string $volumeString): array { $volumeString = trim($volumeString); @@ -212,6 +307,46 @@ function parseDockerVolumeString(string $volumeString): array // Otherwise keep the variable as-is for later expansion (no default value) } + // Validate source path for command injection attempts + // We validate the final source value after environment variable processing + if ($source !== null) { + // Allow simple environment variables like ${VAR_NAME} or ${VAR} + // but validate everything else for shell metacharacters + $sourceStr = is_string($source) ? $source : $source; + + // Skip validation for simple environment variable references + // Pattern: ${WORD_CHARS} with no special characters inside + $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceStr); + + if (! $isSimpleEnvVar) { + try { + validateShellSafePath($sourceStr, 'volume source'); + } catch (\Exception $e) { + // Re-throw with more context about the volume string + throw new \Exception( + 'Invalid Docker volume definition: '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + } + + // Also validate target path + if ($target !== null) { + $targetStr = is_string($target) ? $target : $target; + // Target paths in containers are typically absolute paths, so we validate them too + // but they're less likely to be dangerous since they're not used in host commands + // Still, defense in depth is important + try { + validateShellSafePath($targetStr, 'volume target'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition: '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + return [ 'source' => $source !== null ? str($source) : null, 'target' => $target !== null ? str($target) : null, @@ -265,6 +400,16 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $allMagicEnvironments = collect([]); foreach ($services as $serviceName => $service) { + // Validate service name for command injection + try { + validateShellSafePath($serviceName, 'service name'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker Compose service name: '.$e->getMessage(). + ' Service names must not contain shell metacharacters.' + ); + } + $magicEnvironments = collect([]); $image = data_get_str($service, 'image'); $environment = collect(data_get($service, 'environment', [])); @@ -561,6 +706,33 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $content = data_get($volume, 'content'); $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + // Validate source and target for command injection (array/long syntax) + if ($source !== null && ! empty($source->value())) { + $sourceValue = $source->value(); + // Allow simple environment variable references + $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); + if (! $isSimpleEnvVar) { + try { + validateShellSafePath($sourceValue, 'volume source'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + } + if ($target !== null && ! empty($target->value())) { + try { + validateShellSafePath($target->value(), 'volume target'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + $foundConfig = $fileStorages->whereMountPath($target)->first(); if ($foundConfig) { $contentNotNull_temp = data_get($foundConfig, 'content'); @@ -1178,26 +1350,39 @@ function serviceParser(Service $resource): Collection $allMagicEnvironments = collect([]); // Presave services foreach ($services as $serviceName => $service) { + // Validate service name for command injection + try { + validateShellSafePath($serviceName, 'service name'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker Compose service name: '.$e->getMessage(). + ' Service names must not contain shell metacharacters.' + ); + } + $image = data_get_str($service, 'image'); $isDatabase = isDatabaseImage($image, $service); if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } + // Update image if it changed + if ($savedService->image !== $image) { + $savedService->image = $image; + $savedService->save(); + } } foreach ($services as $serviceName => $service) { $predefinedPort = null; @@ -1514,20 +1699,18 @@ function serviceParser(Service $resource): Collection } if ($isDatabase) { - $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); + $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); if ($applicationFound) { $savedService = $applicationFound; } else { $savedService = ServiceDatabase::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } } else { $savedService = ServiceApplication::firstOrCreate([ 'name' => $serviceName, - 'image' => $image, 'service_id' => $resource->id, ]); } @@ -1574,6 +1757,33 @@ function serviceParser(Service $resource): Collection $content = data_get($volume, 'content'); $isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null); + // Validate source and target for command injection (array/long syntax) + if ($source !== null && ! empty($source->value())) { + $sourceValue = $source->value(); + // Allow simple environment variable references + $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); + if (! $isSimpleEnvVar) { + try { + validateShellSafePath($sourceValue, 'volume source'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + } + if ($target !== null && ! empty($target->value())) { + try { + validateShellSafePath($target->value(), 'volume target'); + } catch (\Exception $e) { + throw new \Exception( + 'Invalid Docker volume definition (array syntax): '.$e->getMessage(). + ' Please use safe path names without shell metacharacters.' + ); + } + } + $foundConfig = $fileStorages->whereMountPath($target)->first(); if ($foundConfig) { $contentNotNull_temp = data_get($foundConfig, 'content'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 656c607bf..0f5b6f553 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -104,6 +104,48 @@ function sanitize_string(?string $input = null): ?string return $sanitized; } +/** + * Validate that a path or identifier is safe for use in shell commands. + * + * This function prevents command injection by rejecting strings that contain + * shell metacharacters or command substitution patterns. + * + * @param string $input The path or identifier to validate + * @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 + */ +function validateShellSafePath(string $input, string $context = 'path'): string +{ + // List of dangerous shell metacharacters that enable command injection + $dangerousChars = [ + '`' => 'backtick (command substitution)', + '$(' => 'command substitution', + '${' => 'variable substitution with potential command injection', + '|' => 'pipe operator', + '&' => 'background/AND operator', + ';' => 'command separator', + "\n" => 'newline (command separator)', + "\r" => 'carriage return', + "\t" => 'tab (token separator)', + '>' => 'output redirection', + '<' => 'input redirection', + ]; + + // Check for dangerous characters + foreach ($dangerousChars as $char => $description) { + if (str_contains($input, $char)) { + throw new \Exception( + "Invalid {$context}: contains forbidden character '{$char}' ({$description}). ". + 'Shell metacharacters are not allowed for security reasons.' + ); + } + } + + return $input; +} + function generate_readme_file(string $name, string $updated_at): string { $name = sanitize_string($name); @@ -1285,6 +1327,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { + // Handle array values from YAML (e.g., "traefik.enable: true" becomes an array) + if (is_array($serviceLabel)) { + $removedLabels->put($serviceLabelName, $serviceLabel); + + return false; + } if (! str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); @@ -1294,6 +1342,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $serviceLabel; }); foreach ($removedLabels as $removedLabelName => $removedLabel) { + // Convert array values to strings + if (is_array($removedLabel)) { + $removedLabel = (string) collect($removedLabel)->first(); + } $serviceLabels->push("$removedLabelName=$removedLabel"); } } @@ -1317,6 +1369,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'name' => $serviceName, 'service_id' => $resource->id, ])->first(); + if (is_null($savedService)) { + $savedService = ServiceDatabase::create([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } } else { if ($isNew) { @@ -1330,21 +1389,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'name' => $serviceName, 'service_id' => $resource->id, ])->first(); - } - } - if (is_null($savedService)) { - if ($isDatabase) { - $savedService = ServiceDatabase::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); - } else { - $savedService = ServiceApplication::create([ - 'name' => $serviceName, - 'image' => $image, - 'service_id' => $resource->id, - ]); + if (is_null($savedService)) { + $savedService = ServiceApplication::create([ + 'name' => $serviceName, + 'image' => $image, + 'service_id' => $resource->id, + ]); + } } } @@ -2006,6 +2057,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal if ($serviceLabels->count() > 0) { $removedLabels = collect([]); $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) { + // Handle array values from YAML (e.g., "traefik.enable: true" becomes an array) + if (is_array($serviceLabel)) { + $removedLabels->put($serviceLabelName, $serviceLabel); + + return false; + } if (! str($serviceLabel)->contains('=')) { $removedLabels->put($serviceLabelName, $serviceLabel); @@ -2015,6 +2072,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal return $serviceLabel; }); foreach ($removedLabels as $removedLabelName => $removedLabel) { + // Convert array values to strings + if (is_array($removedLabel)) { + $removedLabel = (string) collect($removedLabel)->first(); + } $serviceLabels->push("$removedLabelName=$removedLabel"); } } diff --git a/conductor.json b/conductor.json new file mode 100644 index 000000000..851d13ed0 --- /dev/null +++ b/conductor.json @@ -0,0 +1,7 @@ +{ + "scripts": { + "setup": "./scripts/conductor-setup.sh", + "run": "docker rm -f coolify coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite; spin up; spin down" + }, + "runScriptMode": "nonconcurrent" +} diff --git a/config/constants.php b/config/constants.php index 01eaa7fa1..1fc1af3f3 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.435', + 'version' => '4.0.0-beta.436', 'helper_version' => '1.0.11', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/config/livewire.php b/config/livewire.php index 02725e944..bd3733076 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -90,7 +90,7 @@ | */ - 'legacy_model_binding' => true, + 'legacy_model_binding' => false, /* |--------------------------------------------------------------------------- diff --git a/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php b/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php index 6ffe97c07..4d4be1232 100644 --- a/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php +++ b/database/migrations/2025_06_26_131350_optimize_activity_log_indexes.php @@ -6,6 +6,12 @@ return new class extends Migration { + /** + * Disable transactions for this migration because CREATE INDEX CONCURRENTLY + * cannot run inside a transaction block in PostgreSQL. + */ + public $withinTransaction = false; + /** * Run the migrations. */ @@ -13,10 +19,10 @@ public function up(): void { try { // Add specific index for type_uuid queries with ordering - DB::statement('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_type_uuid_created_at ON activity_log ((properties->>\'type_uuid\'), created_at DESC)'); + DB::unprepared('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_type_uuid_created_at ON activity_log ((properties->>\'type_uuid\'), created_at DESC)'); // Add specific index for status queries on properties - DB::statement('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_properties_status ON activity_log ((properties->>\'status\'))'); + DB::unprepared('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_properties_status ON activity_log ((properties->>\'status\'))'); } catch (\Exception $e) { Log::error('Error adding optimized indexes to activity_log: '.$e->getMessage()); @@ -29,8 +35,8 @@ public function up(): void public function down(): void { try { - DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_type_uuid_created_at'); - DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_properties_status'); + DB::unprepared('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_type_uuid_created_at'); + DB::unprepared('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_properties_status'); } catch (\Exception $e) { Log::error('Error dropping optimized indexes from activity_log: '.$e->getMessage()); } diff --git a/database/migrations/2025_10_03_154100_update_clickhouse_image.php b/database/migrations/2025_10_03_154100_update_clickhouse_image.php index e52bbcc16..e57354037 100644 --- a/database/migrations/2025_10_03_154100_update_clickhouse_image.php +++ b/database/migrations/2025_10_03_154100_update_clickhouse_image.php @@ -1,32 +1,32 @@ -string('image')->default('bitnamilegacy/clickhouse')->change(); - }); - // Optionally, update any existing rows with the old default to the new one - DB::table('standalone_clickhouses') - ->where('image', 'bitnami/clickhouse') - ->update(['image' => 'bitnamilegacy/clickhouse']); - } - - public function down() - { - Schema::table('standalone_clickhouses', function (Blueprint $table) { - $table->string('image')->default('bitnami/clickhouse')->change(); - }); - // Optionally, revert any changed values - DB::table('standalone_clickhouses') - ->where('image', 'bitnamilegacy/clickhouse') - ->update(['image' => 'bitnami/clickhouse']); - } -}; \ No newline at end of file +string('image')->default('bitnamilegacy/clickhouse')->change(); + }); + // Optionally, update any existing rows with the old default to the new one + DB::table('standalone_clickhouses') + ->where('image', 'bitnami/clickhouse') + ->update(['image' => 'bitnamilegacy/clickhouse']); + } + + public function down() + { + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->string('image')->default('bitnami/clickhouse')->change(); + }); + // Optionally, revert any changed values + DB::table('standalone_clickhouses') + ->where('image', 'bitnamilegacy/clickhouse') + ->update(['image' => 'bitnami/clickhouse']); + } +}; diff --git a/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php b/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php new file mode 100644 index 000000000..2c92b0e19 --- /dev/null +++ b/database/migrations/2025_10_08_181125_create_cloud_provider_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('team_id')->constrained()->onDelete('cascade'); + $table->string('provider'); + $table->text('token'); + $table->string('name')->nullable(); + $table->timestamps(); + + $table->index(['team_id', 'provider']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cloud_provider_tokens'); + } +}; diff --git a/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php b/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php new file mode 100644 index 000000000..b1c9ec48b --- /dev/null +++ b/database/migrations/2025_10_08_185203_add_hetzner_server_id_to_servers_table.php @@ -0,0 +1,28 @@ +bigInteger('hetzner_server_id')->nullable()->after('id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('hetzner_server_id'); + }); + } +}; diff --git a/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php b/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php new file mode 100644 index 000000000..a25a4ce83 --- /dev/null +++ b/database/migrations/2025_10_09_095905_add_cloud_provider_token_id_to_servers_table.php @@ -0,0 +1,29 @@ +foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropForeign(['cloud_provider_token_id']); + $table->dropColumn('cloud_provider_token_id'); + }); + } +}; diff --git a/database/migrations/2025_10_09_113602_add_hetzner_server_status_to_servers_table.php b/database/migrations/2025_10_09_113602_add_hetzner_server_status_to_servers_table.php new file mode 100644 index 000000000..d94c9c76f --- /dev/null +++ b/database/migrations/2025_10_09_113602_add_hetzner_server_status_to_servers_table.php @@ -0,0 +1,28 @@ +string('hetzner_server_status')->nullable()->after('hetzner_server_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('hetzner_server_status'); + }); + } +}; diff --git a/database/migrations/2025_10_09_125036_add_is_validating_to_servers_table.php b/database/migrations/2025_10_09_125036_add_is_validating_to_servers_table.php new file mode 100644 index 000000000..ddb655d2c --- /dev/null +++ b/database/migrations/2025_10_09_125036_add_is_validating_to_servers_table.php @@ -0,0 +1,28 @@ +boolean('is_validating')->default(false)->after('hetzner_server_status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('is_validating'); + }); + } +}; diff --git a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php new file mode 100644 index 000000000..fe216a57d --- /dev/null +++ b/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('team_id')->constrained()->onDelete('cascade'); + $table->string('name'); + $table->text('script'); // Encrypted in the model + $table->timestamps(); + + $table->index('team_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cloud_init_scripts'); + } +}; diff --git a/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php b/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php new file mode 100644 index 000000000..a3edacbf9 --- /dev/null +++ b/database/migrations/2025_10_10_120000_create_webhook_notification_settings_table.php @@ -0,0 +1,46 @@ +id(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); + + $table->boolean('webhook_enabled')->default(false); + $table->text('webhook_url')->nullable(); + + $table->boolean('deployment_success_webhook_notifications')->default(false); + $table->boolean('deployment_failure_webhook_notifications')->default(true); + $table->boolean('status_change_webhook_notifications')->default(false); + $table->boolean('backup_success_webhook_notifications')->default(false); + $table->boolean('backup_failure_webhook_notifications')->default(true); + $table->boolean('scheduled_task_success_webhook_notifications')->default(false); + $table->boolean('scheduled_task_failure_webhook_notifications')->default(true); + $table->boolean('docker_cleanup_success_webhook_notifications')->default(false); + $table->boolean('docker_cleanup_failure_webhook_notifications')->default(true); + $table->boolean('server_disk_usage_webhook_notifications')->default(true); + $table->boolean('server_reachable_webhook_notifications')->default(false); + $table->boolean('server_unreachable_webhook_notifications')->default(true); + $table->boolean('server_patch_webhook_notifications')->default(false); + + $table->unique(['team_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('webhook_notification_settings'); + } +}; diff --git a/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php b/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php new file mode 100644 index 000000000..de2707557 --- /dev/null +++ b/database/migrations/2025_10_10_120001_populate_webhook_notification_settings_for_existing_teams.php @@ -0,0 +1,47 @@ +get(); + + foreach ($teams as $team) { + DB::table('webhook_notification_settings')->updateOrInsert( + ['team_id' => $team->id], + [ + 'webhook_enabled' => false, + 'webhook_url' => null, + 'deployment_success_webhook_notifications' => false, + 'deployment_failure_webhook_notifications' => true, + 'status_change_webhook_notifications' => false, + 'backup_success_webhook_notifications' => false, + 'backup_failure_webhook_notifications' => true, + 'scheduled_task_success_webhook_notifications' => false, + 'scheduled_task_failure_webhook_notifications' => true, + 'docker_cleanup_success_webhook_notifications' => false, + 'docker_cleanup_failure_webhook_notifications' => true, + 'server_disk_usage_webhook_notifications' => true, + 'server_reachable_webhook_notifications' => false, + 'server_unreachable_webhook_notifications' => true, + 'server_patch_webhook_notifications' => false, + ] + ); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // We don't need to do anything in down() since the webhook_notification_settings + // table will be dropped by the create migration's down() method + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 2d6f52e31..f012c1534 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -14,6 +14,21 @@ class ApplicationSeeder extends Seeder */ public function run(): void { + Application::create([ + 'name' => 'Docker Compose Example', + 'repository_project_id' => 603035348, + 'git_repository' => 'coollabsio/coolify-examples', + 'git_branch' => 'v4.x', + 'base_directory' => '/docker-compose', + 'docker_compose_location' => 'docker-compose-test.yaml', + 'build_pack' => 'dockercompose', + 'ports_exposes' => '80', + 'environment_id' => 1, + 'destination_id' => 0, + 'destination_type' => StandaloneDocker::class, + 'source_id' => 1, + 'source_type' => GithubApp::class, + ]); Application::create([ 'name' => 'NodeJS Fastify Example', 'fqdn' => 'http://nodejs.127.0.0.1.sslip.io', diff --git a/database/seeders/CaSslCertSeeder.php b/database/seeders/CaSslCertSeeder.php index 09f6cc984..1b71a5e43 100644 --- a/database/seeders/CaSslCertSeeder.php +++ b/database/seeders/CaSslCertSeeder.php @@ -4,7 +4,6 @@ use App\Helpers\SslHelper; use App\Models\Server; -use App\Models\SslCertificate; use Illuminate\Database\Seeder; class CaSslCertSeeder extends Seeder @@ -13,7 +12,7 @@ public function run() { Server::chunk(200, function ($servers) { foreach ($servers as $server) { - $existingCaCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first(); + $existingCaCert = $server->sslCertificates()->where('is_ca_certificate', true)->first(); if (! $existingCaCert) { $caCert = SslHelper::generateSslCertificate( diff --git a/database/seeders/InstanceSettingsSeeder.php b/database/seeders/InstanceSettingsSeeder.php index 7f2deb3a6..baa7abffc 100644 --- a/database/seeders/InstanceSettingsSeeder.php +++ b/database/seeders/InstanceSettingsSeeder.php @@ -16,6 +16,7 @@ public function run(): void InstanceSettings::create([ 'id' => 0, 'is_registration_enabled' => true, + 'is_api_enabled' => isDev(), 'smtp_enabled' => true, 'smtp_host' => 'coolify-mail', 'smtp_port' => 1025, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e8402b7af..fee17dad6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,7 @@ services: coolify: + image: coolify:dev + pull_policy: never build: context: . dockerfile: ./docker/development/Dockerfile @@ -41,6 +43,8 @@ services: volumes: - dev_redis_data:/data soketi: + image: coolify-realtime:dev + pull_policy: never build: context: . dockerfile: ./docker/coolify-realtime/Dockerfile @@ -61,6 +65,7 @@ services: vite: image: node:24-alpine pull_policy: always + container_name: coolify-vite working_dir: /var/www/html environment: VITE_HOST: "${VITE_HOST:-localhost}" @@ -73,6 +78,8 @@ services: networks: - coolify testing-host: + image: coolify-testing-host:dev + pull_policy: never build: context: . dockerfile: ./docker/testing-host/Dockerfile diff --git a/lang/en.json b/lang/en.json index af7f2145d..a81e1ee68 100644 --- a/lang/en.json +++ b/lang/en.json @@ -23,7 +23,7 @@ "auth.failed": "These credentials do not match our records.", "auth.failed.callback": "Failed to process callback from login provider.", "auth.failed.password": "The provided password is incorrect.", - "auth.failed.email": "We can't find a user with that e-mail address.", + "auth.failed.email": "If an account exists with this email address, you will receive a password reset link shortly.", "auth.throttle": "Too many login attempts. Please try again in :seconds seconds.", "input.name": "Name", "input.email": "Email", diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100644 index 000000000..1a4611d0d --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,22 @@ + 'Your password has been reset.', + 'sent' => 'If an account exists with this email address, you will receive a password reset link shortly.', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => 'If an account exists with this email address, you will receive a password reset link shortly.', + +]; diff --git a/openapi.json b/openapi.json index 901741dd0..3667cbe87 100644 --- a/openapi.json +++ b/openapi.json @@ -3698,6 +3698,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -3753,7 +3756,7 @@ "application\/json": { "schema": { "properties": { - "": { + "message": { "type": "string", "example": "Backup configuration and all executions deleted." } @@ -3769,7 +3772,7 @@ "application\/json": { "schema": { "properties": { - "": { + "message": { "type": "string", "example": "Backup configuration not found." } @@ -3892,6 +3895,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4033,6 +4039,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4158,6 +4167,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4279,6 +4291,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4404,6 +4419,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4529,6 +4547,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4666,6 +4687,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4803,6 +4827,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4928,6 +4955,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -4993,7 +5023,7 @@ "application\/json": { "schema": { "properties": { - "": { + "message": { "type": "string", "example": "Backup execution deleted." } @@ -5009,7 +5039,7 @@ "application\/json": { "schema": { "properties": { - "": { + "message": { "type": "string", "example": "Backup execution not found." } @@ -5063,7 +5093,7 @@ "application\/json": { "schema": { "properties": { - "": { + "executions": { "type": "array", "items": { "properties": { @@ -5690,7 +5720,7 @@ "application\/json": { "schema": { "properties": { - "": { + "repositories": { "type": "array", "items": { "type": "object" @@ -5763,7 +5793,7 @@ "application\/json": { "schema": { "properties": { - "": { + "branches": { "type": "array", "items": { "type": "object" @@ -5968,7 +5998,7 @@ "description": "GitHub app not found" }, "422": { - "description": "Validation error" + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -5987,7 +6017,7 @@ "200": { "description": "Returns the version of the application", "content": { - "application\/json": { + "text\/html": { "schema": { "type": "string" }, @@ -6122,7 +6152,7 @@ "200": { "description": "Healthcheck endpoint.", "content": { - "application\/json": { + "text\/html": { "schema": { "type": "string" }, @@ -6228,6 +6258,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6327,6 +6360,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6408,6 +6444,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6464,6 +6503,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6514,6 +6556,9 @@ }, "404": { "description": "Project not found." + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6586,6 +6631,9 @@ }, "409": { "description": "Environment with this name already exists." + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6648,6 +6696,9 @@ }, "404": { "description": "Project or environment not found." + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6779,6 +6830,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -6840,6 +6894,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7094,6 +7151,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7193,6 +7253,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7292,6 +7355,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7473,6 +7539,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7702,6 +7771,9 @@ }, "400": { "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -7953,6 +8025,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -8093,6 +8168,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -8186,6 +8264,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -8288,6 +8369,9 @@ }, "404": { "$ref": "#\/components\/responses\/404" + }, + "422": { + "$ref": "#\/components\/responses\/422" } }, "security": [ @@ -9750,6 +9834,40 @@ } } } + }, + "422": { + "description": "Validation error.", + "content": { + "application\/json": { + "schema": { + "properties": { + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "example": { + "name": [ + "The name field is required." + ], + "api_url": [ + "The api url field is required.", + "The api url format is invalid." + ] + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "type": "object" + } + } + } } }, "securitySchemes": { diff --git a/openapi.yaml b/openapi.yaml index 3e39c5d36..b7df65567 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2377,6 +2377,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2418,7 +2420,7 @@ paths: application/json: schema: properties: - '': { type: string, example: 'Backup configuration and all executions deleted.' } + message: { type: string, example: 'Backup configuration and all executions deleted.' } type: object '404': description: 'Backup configuration not found.' @@ -2426,7 +2428,7 @@ paths: application/json: schema: properties: - '': { type: string, example: 'Backup configuration not found.' } + message: { type: string, example: 'Backup configuration not found.' } type: object security: - @@ -2510,6 +2512,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2612,6 +2616,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2702,6 +2708,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2789,6 +2797,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2879,6 +2889,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -2969,6 +2981,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -3068,6 +3082,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -3167,6 +3183,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -3257,6 +3275,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -3306,7 +3326,7 @@ paths: application/json: schema: properties: - '': { type: string, example: 'Backup execution deleted.' } + message: { type: string, example: 'Backup execution deleted.' } type: object '404': description: 'Backup execution not found.' @@ -3314,7 +3334,7 @@ paths: application/json: schema: properties: - '': { type: string, example: 'Backup execution not found.' } + message: { type: string, example: 'Backup execution not found.' } type: object security: - @@ -3349,7 +3369,7 @@ paths: application/json: schema: properties: - '': { type: array, items: { properties: { uuid: { type: string }, filename: { type: string }, size: { type: integer }, created_at: { type: string }, message: { type: string }, status: { type: string } }, type: object } } + executions: { type: array, items: { properties: { uuid: { type: string }, filename: { type: string }, size: { type: integer }, created_at: { type: string }, message: { type: string }, status: { type: string } }, type: object } } type: object '404': description: 'Backup configuration not found.' @@ -3727,7 +3747,7 @@ paths: application/json: schema: properties: - '': { type: array, items: { type: object } } + repositories: { type: array, items: { type: object } } type: object '400': $ref: '#/components/responses/400' @@ -3774,7 +3794,7 @@ paths: application/json: schema: properties: - '': { type: array, items: { type: object } } + branches: { type: array, items: { type: object } } type: object '400': $ref: '#/components/responses/400' @@ -3900,7 +3920,7 @@ paths: '404': description: 'GitHub app not found' '422': - description: 'Validation error' + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -3913,7 +3933,7 @@ paths: '200': description: 'Returns the version of the application' content: - application/json: + text/html: schema: type: string example: v4.0.0 @@ -3991,7 +4011,7 @@ paths: '200': description: 'Healthcheck endpoint.' content: - application/json: + text/html: schema: type: string example: OK @@ -4057,6 +4077,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4121,6 +4143,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4170,6 +4194,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4208,6 +4234,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4241,6 +4269,8 @@ paths: $ref: '#/components/responses/400' '404': description: 'Project not found.' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4286,6 +4316,8 @@ paths: description: 'Project not found.' '409': description: 'Environment with this name already exists.' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4326,6 +4358,8 @@ paths: description: 'Environment has resources, so it cannot be deleted.' '404': description: 'Project or environment not found.' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4409,6 +4443,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4447,6 +4483,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4610,6 +4648,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4674,6 +4714,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4740,6 +4782,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4837,6 +4881,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -4929,6 +4975,8 @@ paths: $ref: '#/components/responses/401' '400': $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -5097,6 +5145,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -5190,6 +5240,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -5252,6 +5304,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -5299,6 +5353,8 @@ paths: $ref: '#/components/responses/400' '404': $ref: '#/components/responses/404' + '422': + $ref: '#/components/responses/422' security: - bearerAuth: [] @@ -6322,6 +6378,24 @@ components: type: string example: 'Resource not found.' type: object + '422': + description: 'Validation error.' + content: + application/json: + schema: + properties: + message: + type: string + example: 'Validation error.' + errors: + type: object + example: + name: ['The name field is required.'] + api_url: ['The api url field is required.', 'The api url format is invalid.'] + additionalProperties: + type: array + items: { type: string } + type: object securitySchemes: bearerAuth: type: http diff --git a/package-lock.json b/package-lock.json index 56e48288c..fa5ac7aae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,13 +74,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -90,9 +90,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -478,9 +478,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -495,9 +495,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -512,9 +512,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -529,9 +529,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -546,9 +546,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", - "integrity": "sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/fs-minipass": { @@ -565,9 +565,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -586,16 +586,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -604,9 +604,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ "arm" ], @@ -618,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", "cpu": [ "arm64" ], @@ -632,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], @@ -646,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], @@ -660,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ "arm64" ], @@ -674,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ "x64" ], @@ -688,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ "arm" ], @@ -702,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ "arm" ], @@ -716,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ "arm64" ], @@ -730,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ "arm64" ], @@ -743,10 +743,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ "loong64" ], @@ -758,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ "ppc64" ], @@ -772,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ "riscv64" ], @@ -786,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ "riscv64" ], @@ -800,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ "s390x" ], @@ -814,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", "cpu": [ "x64" ], @@ -828,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", "cpu": [ "x64" ], @@ -841,10 +841,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", "cpu": [ "arm64" ], @@ -856,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", "cpu": [ "ia32" ], @@ -869,10 +883,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", "cpu": [ "x64" ], @@ -1131,66 +1159,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.10", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", @@ -1501,9 +1469,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1537,9 +1505,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1607,9 +1575,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -1683,9 +1651,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1696,32 +1664,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/estree-walker": { @@ -1732,11 +1700,14 @@ "license": "MIT" }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -1935,9 +1906,9 @@ } }, "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { @@ -2248,13 +2219,13 @@ "license": "MIT" }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -2310,9 +2281,9 @@ } }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -2322,22 +2293,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2457,9 +2412,9 @@ } }, "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "dev": true, "license": "MIT", "peer": true, @@ -2489,9 +2444,9 @@ } }, "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2505,26 +2460,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" } }, @@ -2637,27 +2594,30 @@ "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { @@ -2665,14 +2625,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" diff --git a/public/svgs/cap.svg b/public/svgs/cap.svg new file mode 100644 index 000000000..83d26e15d --- /dev/null +++ b/public/svgs/cap.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/gotify.png b/public/svgs/gotify.png new file mode 100644 index 000000000..5fe38d60a Binary files /dev/null and b/public/svgs/gotify.png differ diff --git a/public/svgs/gramps-web.svg b/public/svgs/gramps-web.svg new file mode 100644 index 000000000..eeef33047 --- /dev/null +++ b/public/svgs/gramps-web.svg @@ -0,0 +1,154 @@ + +image/svg+xml diff --git a/public/svgs/hetzner.svg b/public/svgs/hetzner.svg new file mode 100644 index 000000000..68b1b868d --- /dev/null +++ b/public/svgs/hetzner.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/svgs/lobe-chat.png b/public/svgs/lobe-chat.png new file mode 100644 index 000000000..2d340df88 Binary files /dev/null and b/public/svgs/lobe-chat.png differ diff --git a/public/svgs/newapi.png b/public/svgs/newapi.png new file mode 100644 index 000000000..f62bfd57f Binary files /dev/null and b/public/svgs/newapi.png differ diff --git a/public/svgs/once-campfire.png b/public/svgs/once-campfire.png new file mode 100644 index 000000000..d1158445b Binary files /dev/null and b/public/svgs/once-campfire.png differ diff --git a/public/svgs/rybbit.svg b/public/svgs/rybbit.svg new file mode 100644 index 000000000..5715b29bd --- /dev/null +++ b/public/svgs/rybbit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/signoz.svg b/public/svgs/signoz.svg new file mode 100644 index 000000000..ac47e1c93 --- /dev/null +++ b/public/svgs/signoz.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/swetrix.svg b/public/svgs/swetrix.svg new file mode 100644 index 000000000..8bb5bfdfa --- /dev/null +++ b/public/svgs/swetrix.svg @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index c1dc7e56d..fa1e61cb2 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -18,7 +18,7 @@ @theme { --color-base: #101010; --color-warning: #fcd452; - --color-success: #16a34a; + --color-success: #22C55E; --color-error: #dc2626; --color-coollabs-50: #f5f0ff; --color-coollabs: #6b16ed; diff --git a/resources/css/utilities.css b/resources/css/utilities.css index bedfb51bc..0bced1ece 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -32,7 +32,7 @@ @utility apexcharts-tooltip-custom-title { } @utility input-sticky { - @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; + @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning; } @utility input-sticky-active { @@ -41,29 +41,29 @@ @utility input-sticky-active { /* Focus */ @utility input-focus { - @apply focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300; + @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base; } /* input, select before */ @utility input-select { - @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-1 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 dark:disabled:ring-transparent; + @apply block py-1.5 w-full text-sm text-black rounded-sm border-0 ring-2 ring-inset dark:bg-coolgray-100 dark:text-white ring-neutral-200 dark:ring-coolgray-300 disabled:bg-neutral-200 disabled:text-neutral-500 dark:disabled:bg-coolgray-100/40 dark:disabled:ring-transparent; } /* Readonly */ @utility input { @apply dark:read-only:text-neutral-500 dark:read-only:ring-0 dark:read-only:bg-coolgray-100/40 placeholder:text-neutral-300 dark:placeholder:text-neutral-700 read-only:text-neutral-500 read-only:bg-neutral-200; - @apply input-focus; @apply input-select; + @apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning; } @utility select { @apply w-full; - @apply input-focus; @apply input-select; + @apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning; } @utility button { - @apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-coolgray-100; + @apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base; } @utility alert-success { diff --git a/resources/views/auth/confirm-password.blade.php b/resources/views/auth/confirm-password.blade.php index 287f2f170..ce8f21481 100644 --- a/resources/views/auth/confirm-password.blade.php +++ b/resources/views/auth/confirm-password.blade.php @@ -1,29 +1,51 @@ -
-
-
-
Coolify
- {{-- --}} -
-
-
- @csrf - - {{ __('auth.confirm_password') }} - - @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach +
+
+
+
+

+ Coolify +

+

+ Confirm Your Password +

+
+ +
+ @if (session('status')) +
+

{{ session('status') }}

+
+ @endif + + @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+
+ + + +

+ This is a secure area. Please confirm your password before continuing. +

+
- @endif - @if (session('status')) -
- {{ session('status') }} -
- @endif + +
+ @csrf + + + {{ __('auth.confirm_password') }} + + +
-
+ diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php index 66a924fb8..4952cfabd 100644 --- a/resources/views/auth/forgot-password.blade.php +++ b/resources/views/auth/forgot-password.blade.php @@ -1,42 +1,88 @@
- - Coolify -
- {{ __('auth.forgot_password_heading') }} -
-
-
+
+
+

+ Coolify +

+

+ {{ __('auth.forgot_password_heading') }} +

+
+ +
+ @if (session('status')) +
+
+ + + +

{{ session('status') }}

+
+
+ @endif + + @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + @if (is_transactional_emails_enabled()) -
- @csrf - - {{ __('auth.forgot_password_send_email') }} - - @else -
Transactional emails are not active on this instance.
-
See how to set it in our docs, or how to - manually reset password. +
+ @csrf + + + {{ __('auth.forgot_password_send_email') }} + + + @else +
+
+ + + +
+

Email Not Configured

+

+ Transactional emails are not active on this instance. +

+

+ See how to set it in our documentation, or + learn how to manually reset your password. +

+
+
+
+ @endif + +
+
+
+
+
+ + Remember your password? + +
- @endif - @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach -
- @endif - @if (session('status')) -
- {{ session('status') }} -
- @endif + + + Back to Login +
- -
+ \ No newline at end of file diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 8bd8e81fc..f85dc268e 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,79 +1,102 @@
- - Coolify - -
- @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach -
- @endif -
-
- @csrf - @env('local') - +
+
+

+ Coolify +

+
- - - - {{ __('auth.forgot_password_link') }} - - @else - - - - {{ __('auth.forgot_password_link') }} - - @endenv - - {{ __('auth.login') }} - - @if (session('error')) -
- {{ session('error') }} -
- @endif - @if (!$is_registration_enabled) -
{{ __('auth.registration_disabled') }}
- @endif - @if (session('status')) -
- {{ session('status') }} -
- @endif - - @if ($is_registration_enabled) - - {{ __('auth.register_now') }} - - @endif - @if ($enabled_oauth_providers->isNotEmpty()) -
- -
- or -
+
+ @if (session('status')) +
+

{{ session('status') }}

@endif - @foreach ($enabled_oauth_providers as $provider_setting) - - {{ __("auth.login.$provider_setting->provider") }} + + @if (session('error')) +
+

{{ session('error') }}

+
+ @endif + + @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+ @csrf + @env('local') + + + @else + + + @endenv + + + + + {{ __('auth.login') }} - @endforeach + + + @if ($is_registration_enabled) +
+
+
+
+
+ + Don't have an account? + +
+
+ + {{ __('auth.register_now') }} + + @else +
+ {{ __('auth.registration_disabled') }} +
+ @endif + + @if ($enabled_oauth_providers->isNotEmpty()) +
+
+
+
+
+ or + continue with +
+
+
+ @foreach ($enabled_oauth_providers as $provider_setting) + + {{ __("auth.login.$provider_setting->provider") }} + + @endforeach +
+ @endif
-
+ \ No newline at end of file diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index a54233774..3db943726 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -11,22 +11,43 @@ function getOldOrLocal($key, $localValue)
- - Coolify - -
-
-
-

- Create an account -

- @if ($isFirstUser) -
This user will be the root user (full admin access). +
+
+

+ Coolify +

+

+ Create your account +

+
+ +
+ @if ($isFirstUser) +
+
+ + + +
+

Root User Setup

+

This user will be the root user with full admin access.

+
- @endif -
-
+
+ @endif + + @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + + @csrf @@ -36,15 +57,32 @@ class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl label="{{ __('input.password') }}" /> -
Your password should be min 8 characters long and contain - at least one uppercase letter, one lowercase letter, one number, and one symbol.
-
- Register - - {{ __('auth.already_registered') }} - + +
+

+ Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. +

+ + + Create Account + + +
+
+
+
+
+ + Already have an account? + +
+
+ + + {{ __('auth.already_registered') }} +
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index ae85b11a5..a4a07ebd6 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -1,39 +1,80 @@
- - Coolify - -
- {{ __('auth.reset_password') }} -
-
-
-
+
+
+

+ Coolify +

+

+ {{ __('auth.reset_password') }} +

+
+ +
+ @if (session('status')) +
+
+ + + +

{{ session('status') }}

+
+
+ @endif + + @if ($errors->any()) +
+ @foreach ($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif + +
+

+ Enter your new password below. Make sure it's strong and secure. +

+
+ + @csrf -
- - + + + +
+

+ Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. +

- {{ __('auth.reset_password') }} + + + {{ __('auth.reset_password') }} + - @if ($errors->any()) -
- @foreach ($errors->all() as $error) -

{{ $error }}

- @endforeach + +
+
+
- @endif - @if (session('status')) -
- {{ session('status') }} +
+ + Remember your password? +
- @endif +
+ + + Back to Login +
diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index 238b7ad8d..d4531cbe8 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -1,40 +1,137 @@ -
+
- - Coolify - -
-
-
- @csrf -
- -
Enter - Recovery Code -
+
+
+

+ Coolify +

+

+ Two-Factor Authentication +

+
+ +
+ @if (session('status')) +
+

{{ session('status') }}

-
- -
- {{ __('auth.login') }} - + @endif + @if ($errors->any()) -
+
@foreach ($errors->all() as $error) -

{{ $error }}

+

{{ $error }}

@endforeach
@endif - @if (session('status')) -
- {{ session('status') }} + +
+
+ + + +

+ Enter the verification code from your authenticator app to continue. +

- @endif +
+ +
+ @csrf +
+ +
+ +
+ +
+
+ + +
+ + {{ __('auth.login') }} + +
+ +
+
+
+
+
+ + Need help? + +
+
+ + + Back to Login +
- + \ No newline at end of file diff --git a/resources/views/components/boarding-progress.blade.php b/resources/views/components/boarding-progress.blade.php new file mode 100644 index 000000000..dec34abac --- /dev/null +++ b/resources/views/components/boarding-progress.blade.php @@ -0,0 +1,47 @@ +@props(['currentStep' => 1, 'totalSteps' => 3]) + +
+
+ @for ($i = 1; $i <= $totalSteps; $i++) +
+
+
+ @if ($i < $currentStep) + + + + @else + + {{ $i }} + + @endif +
+ + @if ($i === 1) + Server + @elseif ($i === 2) + Connection + @elseif ($i === 3) + Complete + @endif + +
+ @if ($i < $totalSteps) +
+
+ @endif +
+ @endfor +
+
diff --git a/resources/views/components/boarding-step.blade.php b/resources/views/components/boarding-step.blade.php index d963e55f0..716987baf 100644 --- a/resources/views/components/boarding-step.blade.php +++ b/resources/views/components/boarding-step.blade.php @@ -1,25 +1,29 @@ -
-
-

{{ $title }}

-
+
+
+
+

{{ $title }}

@isset($question) -

+

{{ $question }} -

+
@endisset + + @if ($actions) +
+ {{ $actions }} +
+ @endif
- @if ($actions) -
- {{ $actions }} + + @isset($explanation) +
+

+ Technical Details +

+
+ {{ $explanation }} +
- @endif + @endisset
- @isset($explanation) -
-

Explanation

-
- {{ $explanation }} -
-
- @endisset
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index 868f657f6..b291759a8 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -32,14 +32,14 @@ merge(['class' => $defaultClass]) }} wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}' - wire:model={{ $id }} @if ($checked) checked @endif /> + wire:model={{ $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif /> @else @if ($domValue) merge(['class' => $defaultClass]) }} - value={{ $domValue }} @if ($checked) checked @endif /> + value={{ $domValue }} id="{{ $htmlId }}" @if ($checked) checked @endif /> @else merge(['class' => $defaultClass]) }} - wire:model={{ $value ?? $id }} @if ($checked) checked @endif /> + wire:model={{ $value ?? $modelBinding }} id="{{ $htmlId }}" @if ($checked) checked @endif /> @endif @endif diff --git a/resources/views/components/forms/datalist.blade.php b/resources/views/components/forms/datalist.blade.php index c9710b728..5bb12aa8d 100644 --- a/resources/views/components/forms/datalist.blade.php +++ b/resources/views/components/forms/datalist.blade.php @@ -1,6 +1,6 @@
-
+@endif + +@error($modelBinding) + +@enderror
diff --git a/resources/views/components/forms/input.blade.php b/resources/views/components/forms/input.blade.php index 858f5ac1c..6b88c7b44 100644 --- a/resources/views/components/forms/input.blade.php +++ b/resources/views/components/forms/input.blade.php @@ -27,10 +27,9 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov @endif merge(['class' => $defaultClass]) }} @required($required) - @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" - type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" + @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif + wire:loading.attr="disabled" + type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> @@ -39,20 +38,19 @@ class="flex absolute inset-y-0 right-0 items-center pr-2 cursor-pointer dark:hov @else merge(['class' => $defaultClass]) }} @required($required) @readonly($readonly) - @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" + @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif + wire:loading.attr="disabled" type="{{ $type }}" @disabled($disabled) min="{{ $attributes->get('min') }}" max="{{ $attributes->get('max') }}" minlength="{{ $attributes->get('minlength') }}" maxlength="{{ $attributes->get('maxlength') }}" - @if ($id !== 'null') id={{ $id }} @endif name="{{ $name }}" + @if ($htmlId !== 'null') id={{ $htmlId }} @endif name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" @if ($autofocus) x-ref="autofocusInput" @endif> @endif @if (!$label && $helper) @endif - @error($id) + @error($modelBinding) diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php index 811953153..36da2ebcb 100644 --- a/resources/views/components/forms/monaco-editor.blade.php +++ b/resources/views/components/forms/monaco-editor.blade.php @@ -81,8 +81,13 @@ document.getElementById(monacoId).addEventListener('monaco-editor-focused', (event) => { editor.focus(); }); - + updatePlaceholder(editor.getValue()); + + @if ($autofocus) + // Auto-focus the editor + setTimeout(() => editor.focus(), 100); + @endif $watch('monacoContent', value => { if (editor.getValue() !== value) { @@ -99,7 +104,7 @@ }, 5);" :id="monacoId">
-
+
whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} @else wire:model={{ $id }} @endif> + wire:loading.attr="disabled" name={{ $modelBinding }} id="{{ $htmlId }}" + @if ($attributes->whereStartsWith('wire:model')->first()) {{ $attributes->whereStartsWith('wire:model')->first() }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @else wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif> {{ $slot }} - @error($id) + @error($modelBinding) diff --git a/resources/views/components/forms/textarea.blade.php b/resources/views/components/forms/textarea.blade.php index b4dec192a..cee5faeda 100644 --- a/resources/views/components/forms/textarea.blade.php +++ b/resources/views/components/forms/textarea.blade.php @@ -25,9 +25,9 @@ function handleKeydown(e) { @endif @if ($useMonacoEditor) - + @else @if ($type === 'password')
@@ -45,35 +45,34 @@ class="absolute inset-y-0 right-0 flex items-center h-6 pt-2 pr-2 cursor-pointer @endif merge(['class' => $defaultClassInput]) }} @required($required) - @if ($id !== 'null') wire:model={{ $id }} @endif - wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' - wire:dirty.class="dark:focus:ring-warning dark:ring-warning" wire:loading.attr="disabled" - type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $id }}" + @if ($modelBinding !== 'null') wire:model={{ $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif + wire:loading.attr="disabled" + type="{{ $type }}" @readonly($readonly) @disabled($disabled) id="{{ $htmlId }}" name="{{ $name }}" placeholder="{{ $attributes->get('placeholder') }}" aria-placeholder="{{ $attributes->get('placeholder') }}"> + wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif + @disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}" + name="{{ $name }}" name={{ $modelBinding }} + @if ($autofocus) x-ref="autofocusInput" @endif>
@else + wire:model={{ $value ?? $modelBinding }} wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4" @endif + @disabled($disabled) @readonly($readonly) @required($required) id="{{ $htmlId }}" + name="{{ $name }}" name={{ $modelBinding }} + @if ($autofocus) x-ref="autofocusInput" @endif> @endif @endif - @error($id) + @error($modelBinding) diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 103f18316..46164840d 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -250,6 +250,18 @@ class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300"> {{ $checkbox['label'] }} + @if (isset($checkbox['default_warning'])) + + @endif @endforeach @if (!$disableTwoStepConfirmation) diff --git a/resources/views/components/modal-input.blade.php b/resources/views/components/modal-input.blade.php index c15985d03..6291e8774 100644 --- a/resources/views/components/modal-input.blade.php +++ b/resources/views/components/modal-input.blade.php @@ -8,8 +8,11 @@ 'content' => null, 'closeOutside' => true, 'minWidth' => '36rem', + 'isFullWidth' => false, ]) -
@if ($content)
@@ -17,13 +20,13 @@ class="relative w-auto h-auto" wire:ignore>
@else @if ($disabled) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @elseif ($isErrorButton) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @elseif ($isHighlightedButton) - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @else - {{ $buttonTitle }} + $isFullWidth])>{{ $buttonTitle }} @endif @endif