diff --git a/MATRIX_TESTING.md b/MATRIX_TESTING.md new file mode 100644 index 0000000000..fd8bd6c48e --- /dev/null +++ b/MATRIX_TESTING.md @@ -0,0 +1,289 @@ +# Testing Matrix Notifications in Coolify + +This guide explains how to test the new Matrix notification functionality using the Docker image with Matrix support. + +## Docker Image + +The Matrix notification feature is available in this Docker image: +``` +keithah/coolify:matrix-notifications +``` + +## Quick Start Testing + +### 1. Prepare Testing Environment + +```bash +# Create testing directory +mkdir coolify-matrix-test +cd coolify-matrix-test + +# Create database directory and file for SQLite +mkdir -p database +touch database/database.sqlite + +# Pull the test image +docker pull keithah/coolify:matrix-notifications +``` + +### 2. Setup Environment + +Create a `.env` file based on Coolify's requirements. The key environment variables you'll need: + +```env +# Basic Coolify settings +APP_NAME="Coolify" +APP_ENV=local +APP_URL=http://localhost:8000 +APP_ID=local + +# Database (SQLite for quick testing) +DB_CONNECTION=sqlite +DB_DATABASE=/var/www/html/database/database.sqlite + +# Required for Laravel +APP_KEY=base64:YOUR_APP_KEY_HERE + +# Optional: enable detailed logs +LOG_LEVEL=debug +``` + +**Important:** Create the SQLite database file before starting the container: + +```bash +# Create database directory and file +mkdir -p database +touch database/database.sqlite +``` + +### 3. Run with Docker + +```bash +# Simple docker run command +docker run -d \ + --name coolify-matrix-test \ + -p 8000:80 \ + -v $(pwd)/database:/var/www/html/database \ + -v $(pwd)/.env:/var/www/html/.env \ + keithah/coolify:matrix-notifications + +# Or use docker-compose (create docker-compose.yml first) +docker-compose up -d +``` + +### 4. Access Coolify + +1. Open your browser to `http://localhost:8000` +2. Complete the initial setup +3. Navigate to **Settings** → **Notifications** → **Matrix** + +## Matrix Configuration + +### Prerequisites + +You'll need: +1. **Matrix Account**: Create a dedicated bot account (recommended) +2. **Matrix Room**: Create or use an existing room +3. **Access Token**: Get an access token for your bot account + +### Getting Matrix Access Token + +```bash +# Replace with your homeserver URL and credentials +# Note: Use the full Matrix user ID (@username:homeserver.com) +curl -XPOST -H "Content-Type: application/json" -d '{ + "type": "m.login.password", + "identifier": {"user": "@your_bot_username:your-homeserver.com", "type": "m.id.user"}, + "password": "your_bot_password" +}' "https://your-homeserver.com/_matrix/client/v3/login" +``` + +This returns JSON with an `access_token` field. Example response: +```json +{ + "access_token": "syt_...", + "user_id": "@your_bot_username:your-homeserver.com" +} +``` + +### Getting Room ID + +1. In your Matrix client, go to Room Settings +2. Click "Advanced" +3. Copy the "Internal room ID" (format: `!xyz:homeserver.com`) + +### Configuration in Coolify + +1. **Homeserver URL**: Your Matrix homeserver (e.g., `https://matrix.org`) +2. **Room ID**: The internal room ID from above +3. **Access Token**: The access token from the login response +4. **Friendly Name**: Optional descriptive name + +## Test Scenarios + +### 1. Basic Connection Test + +1. Configure Matrix settings in Coolify +2. Click "Send Test Notification" +3. Check your Matrix room for the test message + +### 2. Application Deployment Test + +1. Deploy a simple application in Coolify +2. Monitor Matrix room for deployment notifications +3. Test both success and failure scenarios + +### 3. Server Monitoring Test + +1. Add a server to Coolify +2. Test server unreachable notifications by temporarily blocking connectivity +3. Verify Matrix notifications are received + +### 4. Backup Notifications Test + +1. Configure a database backup +2. Run the backup job +3. Check Matrix room for backup status notifications + +## Notification Types Supported + +✅ **Deployment Success** - When applications deploy successfully +✅ **Deployment Failure** - When deployments fail +✅ **Container Status Changes** - When containers stop/restart +✅ **Backup Success** - When backups complete successfully +✅ **Backup Failure** - When backups fail +✅ **Scheduled Task Success** - When scheduled tasks complete +✅ **Scheduled Task Failure** - When scheduled tasks fail +✅ **Docker Cleanup** - When cleanup operations run +✅ **Server Disk Usage** - When disk usage is high +✅ **Server Reachable** - When servers come back online +✅ **Server Unreachable** - When servers go offline +✅ **Server Patching** - When server updates are available + +## Testing with Docker Compose + +### Sample docker-compose.yml + +```yaml +version: '3.8' +services: + coolify: + image: keithah/coolify:matrix-notifications + container_name: coolify-matrix-test + ports: + - "8000:8080" + volumes: + - ./data:/var/www/html/storage + - ./.env:/var/www/html/.env + environment: + - APP_ENV=local + - APP_DEBUG=true + - PHP_MEMORY_LIMIT=512M + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + interval: 30s + timeout: 10s + retries: 3 + + database: + image: postgres:15 + container_name: coolify-matrix-db + environment: + POSTGRES_DB: coolify + POSTGRES_USER: coolify + POSTGRES_PASSWORD: password + volumes: + - db_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + redis: + image: redis:7-alpine + container_name: coolify-matrix-redis + ports: + - "6379:6379" + +volumes: + db_data: +``` + +### Environment File (.env) + +```env +APP_NAME="Coolify Matrix Test" +APP_ENV=local +APP_KEY=base64:GENERATE_32_CHAR_KEY_HERE +APP_DEBUG=true +APP_URL=http://localhost:8000 + +DB_CONNECTION=pgsql +DB_HOST=database +DB_PORT=5432 +DB_DATABASE=coolify +DB_USERNAME=coolify +DB_PASSWORD=password + +CACHE_DRIVER=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis + +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +LOG_CHANNEL=stack +LOG_LEVEL=debug +``` + +## Troubleshooting + +### Matrix Messages Not Sending + +1. **Check Access Token**: Ensure the token is valid and has proper permissions +2. **Verify Room ID**: Confirm the room ID format is correct (`!xyz:homeserver.com`) +3. **Bot Permissions**: Make sure your bot user is invited to the room +4. **Homeserver URL**: Verify the URL includes the correct protocol and port + +### Docker Issues + +1. **Port Conflicts**: Change port mapping if 8000 is in use +2. **Memory Issues**: Increase Docker memory limits for large applications +3. **Permission Issues**: Check file permissions in mounted volumes + +### Common Error Messages + +- **"Room not found"**: Bot user needs to be invited to the room +- **"Invalid access token"**: Token expired or incorrect +- **"Connection refused"**: Homeserver URL incorrect or unreachable + +## Matrix API Details + +The implementation uses: +- **Matrix Client-Server API v1.0** +- **PUT requests** with transaction IDs for message deduplication +- **HTML formatted messages** with plain text fallback +- **Proper authentication** via Bearer tokens + +## Security Notes + +- ✅ All sensitive data (homeserver URL, room ID, access token) is **encrypted at rest** +- ✅ Uses dedicated bot accounts (recommended) +- ✅ Follows Matrix API best practices +- ✅ Implements proper authorization checks + +## Support + +If you encounter issues: + +1. Check Docker container logs: `docker logs coolify-matrix-test` +2. Verify Matrix room permissions and bot setup +3. Test Matrix API connectivity manually with curl +4. Check the [PR discussion](https://github.com/coollabsio/coolify/pull/6717) for updates + +## Production Deployment + +Once testing is complete, this feature will be available in the main Coolify release. The Docker image `keithah/coolify:matrix-notifications` is for testing purposes only. + +--- + +**Built with Matrix ❤️ for the Coolify community** \ No newline at end of file diff --git a/app/Jobs/SendMessageToMatrixJob.php b/app/Jobs/SendMessageToMatrixJob.php new file mode 100644 index 0000000000..2b39c01ec3 --- /dev/null +++ b/app/Jobs/SendMessageToMatrixJob.php @@ -0,0 +1,85 @@ +onQueue('high'); + } + + public function handle(): void + { + $transactionId = 'coolify_' . time() . '_' . rand(1000, 9999); + $url = rtrim($this->homeserverUrl, '/') . "/_matrix/client/r0/rooms/{$this->roomId}/send/m.room.message/{$transactionId}"; + + $body = [ + 'msgtype' => 'm.text', + 'body' => "{$this->message->title}\n\n{$this->message->description}", + 'format' => 'org.matrix.custom.html', + 'formatted_body' => "

message->color}\">{$this->message->title}

{$this->message->description}

", + ]; + + try { + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . $this->accessToken, + 'Content-Type' => 'application/json', + ])->timeout(15)->put($url, $body); + + $response->throw(); + + // Log successful delivery for monitoring + logger()->info('Matrix notification sent successfully', [ + 'room_id' => $this->roomId, + 'homeserver' => $this->homeserverUrl, + 'transaction_id' => $transactionId, + ]); + } catch (\Throwable $e) { + // Enhanced error logging for Matrix API failures + logger()->error('Matrix notification failed', [ + 'room_id' => $this->roomId, + 'homeserver' => $this->homeserverUrl, + 'transaction_id' => $transactionId, + 'error' => $e->getMessage(), + 'status_code' => $e->getCode(), + 'attempt' => $this->attempts(), + ]); + + throw $e; + } + } + + public function failed(Throwable $exception): void + { + report($exception); + } +} \ No newline at end of file diff --git a/app/Livewire/Notifications/Matrix.php b/app/Livewire/Notifications/Matrix.php new file mode 100644 index 0000000000..f643ae8b2a --- /dev/null +++ b/app/Livewire/Notifications/Matrix.php @@ -0,0 +1,211 @@ + '$refresh']; + + #[Locked] + public Team $team; + + #[Locked] + public MatrixNotificationSettings $settings; + + #[Validate(['boolean'])] + public bool $matrixEnabled = false; + + #[Validate(['url', 'nullable', 'regex:/^https?:\/\/.+/'])] + public ?string $matrixHomeserverUrl = null; + + #[Validate(['string', 'nullable', 'regex:/^![a-zA-Z0-9]+:.+/'])] + public ?string $matrixRoomId = null; + + #[Validate(['string', 'nullable'])] + public ?string $matrixAccessToken = null; + + #[Validate(['string', 'nullable'])] + public ?string $matrixFriendlyName = null; + + #[Validate(['boolean'])] + public bool $deploymentSuccessMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $deploymentFailureMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $statusChangeMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $backupSuccessMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $backupFailureMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $scheduledTaskSuccessMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $scheduledTaskFailureMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $dockerCleanupSuccessMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $dockerCleanupFailureMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $serverDiskUsageMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $serverReachableMatrixNotifications = false; + + #[Validate(['boolean'])] + public bool $serverUnreachableMatrixNotifications = true; + + #[Validate(['boolean'])] + public bool $serverPatchMatrixNotifications = false; + + public function mount() + { + try { + $this->team = auth()->user()->currentTeam(); + $this->settings = $this->team->matrixNotificationSettings() + ->firstOrCreate(['team_id' => $this->team->id]); + $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->matrix_enabled = $this->matrixEnabled; + $this->settings->matrix_homeserver_url = $this->matrixHomeserverUrl; + $this->settings->matrix_room_id = $this->matrixRoomId; + $this->settings->matrix_access_token = $this->matrixAccessToken; + $this->settings->matrix_friendly_name = $this->matrixFriendlyName; + + $this->settings->deployment_success_matrix_notifications = $this->deploymentSuccessMatrixNotifications; + $this->settings->deployment_failure_matrix_notifications = $this->deploymentFailureMatrixNotifications; + $this->settings->status_change_matrix_notifications = $this->statusChangeMatrixNotifications; + $this->settings->backup_success_matrix_notifications = $this->backupSuccessMatrixNotifications; + $this->settings->backup_failure_matrix_notifications = $this->backupFailureMatrixNotifications; + $this->settings->scheduled_task_success_matrix_notifications = $this->scheduledTaskSuccessMatrixNotifications; + $this->settings->scheduled_task_failure_matrix_notifications = $this->scheduledTaskFailureMatrixNotifications; + $this->settings->docker_cleanup_success_matrix_notifications = $this->dockerCleanupSuccessMatrixNotifications; + $this->settings->docker_cleanup_failure_matrix_notifications = $this->dockerCleanupFailureMatrixNotifications; + $this->settings->server_disk_usage_matrix_notifications = $this->serverDiskUsageMatrixNotifications; + $this->settings->server_reachable_matrix_notifications = $this->serverReachableMatrixNotifications; + $this->settings->server_unreachable_matrix_notifications = $this->serverUnreachableMatrixNotifications; + $this->settings->server_patch_matrix_notifications = $this->serverPatchMatrixNotifications; + + $this->settings->save(); + refreshSession(); + } else { + $this->matrixEnabled = $this->settings->matrix_enabled; + $this->matrixHomeserverUrl = $this->settings->matrix_homeserver_url; + $this->matrixRoomId = $this->settings->matrix_room_id; + $this->matrixAccessToken = $this->settings->matrix_access_token; + $this->matrixFriendlyName = $this->settings->matrix_friendly_name; + + $this->deploymentSuccessMatrixNotifications = $this->settings->deployment_success_matrix_notifications; + $this->deploymentFailureMatrixNotifications = $this->settings->deployment_failure_matrix_notifications; + $this->statusChangeMatrixNotifications = $this->settings->status_change_matrix_notifications; + $this->backupSuccessMatrixNotifications = $this->settings->backup_success_matrix_notifications; + $this->backupFailureMatrixNotifications = $this->settings->backup_failure_matrix_notifications; + $this->scheduledTaskSuccessMatrixNotifications = $this->settings->scheduled_task_success_matrix_notifications; + $this->scheduledTaskFailureMatrixNotifications = $this->settings->scheduled_task_failure_matrix_notifications; + $this->dockerCleanupSuccessMatrixNotifications = $this->settings->docker_cleanup_success_matrix_notifications; + $this->dockerCleanupFailureMatrixNotifications = $this->settings->docker_cleanup_failure_matrix_notifications; + $this->serverDiskUsageMatrixNotifications = $this->settings->server_disk_usage_matrix_notifications; + $this->serverReachableMatrixNotifications = $this->settings->server_reachable_matrix_notifications; + $this->serverUnreachableMatrixNotifications = $this->settings->server_unreachable_matrix_notifications; + $this->serverPatchMatrixNotifications = $this->settings->server_patch_matrix_notifications; + } + } + + public function instantSaveMatrixEnabled() + { + try { + $this->validate([ + 'matrixHomeserverUrl' => 'required|url|regex:/^https?:\/\/.+/', + 'matrixRoomId' => 'required|string|regex:/^![a-zA-Z0-9]+:.+/', + 'matrixAccessToken' => 'required|string', + ], [ + 'matrixHomeserverUrl.required' => 'Matrix Homeserver URL is required.', + 'matrixHomeserverUrl.url' => 'Matrix Homeserver URL must be a valid URL.', + 'matrixHomeserverUrl.regex' => 'Matrix Homeserver URL must start with http:// or https://.', + 'matrixRoomId.required' => 'Matrix Room ID is required.', + 'matrixRoomId.regex' => 'Matrix Room ID must be in the format !roomname:homeserver.domain (e.g., !example:matrix.org).', + 'matrixAccessToken.required' => 'Matrix Access Token is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->matrixEnabled = false; + + return handleError($e, $this); + } finally { + $this->dispatch('refresh'); + } + } + + public function instantSave() + { + try { + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->dispatch('refresh'); + } + } + + 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(); + $this->dispatch('success', 'Settings saved.'); + } + + public function sendTestNotification() + { + try { + $this->authorize('sendTest', $this->settings); + $this->team->notify(new Test(channel: 'matrix')); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.notifications.matrix'); + } +} \ No newline at end of file diff --git a/app/Models/MatrixNotificationSettings.php b/app/Models/MatrixNotificationSettings.php new file mode 100644 index 0000000000..8cabfc75c6 --- /dev/null +++ b/app/Models/MatrixNotificationSettings.php @@ -0,0 +1,72 @@ + 'boolean', + 'matrix_homeserver_url' => 'encrypted', + 'matrix_room_id' => 'encrypted', + 'matrix_access_token' => 'encrypted', + 'deployment_success_matrix_notifications' => 'boolean', + 'deployment_failure_matrix_notifications' => 'boolean', + 'status_change_matrix_notifications' => 'boolean', + 'backup_success_matrix_notifications' => 'boolean', + 'backup_failure_matrix_notifications' => 'boolean', + 'scheduled_task_success_matrix_notifications' => 'boolean', + 'scheduled_task_failure_matrix_notifications' => 'boolean', + 'docker_cleanup_success_matrix_notifications' => 'boolean', + 'docker_cleanup_failure_matrix_notifications' => 'boolean', + 'server_disk_usage_matrix_notifications' => 'boolean', + 'server_reachable_matrix_notifications' => 'boolean', + 'server_unreachable_matrix_notifications' => 'boolean', + 'server_patch_matrix_notifications' => 'boolean', + ]; + } + + public function team(): BelongsTo + { + return $this->belongsTo(Team::class); + } + + public function isEnabled(): bool + { + return $this->matrix_enabled; + } +} \ No newline at end of file diff --git a/app/Models/Team.php b/app/Models/Team.php index 97a7d89d70..f14ea5c216 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -5,6 +5,7 @@ use App\Events\ServerReachabilityChanged; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use App\Notifications\Channels\SendsMatrix; use App\Notifications\Channels\SendsPushover; use App\Notifications\Channels\SendsSlack; use App\Traits\HasNotificationSettings; @@ -35,7 +36,7 @@ ] )] -class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, SendsSlack +class Team extends Model implements SendsDiscord, SendsEmail, SendsMatrix, SendsPushover, SendsSlack { use HasNotificationSettings, HasSafeStringAttribute, Notifiable; @@ -53,6 +54,7 @@ protected static function booted() $team->slackNotificationSettings()->create(); $team->telegramNotificationSettings()->create(); $team->pushoverNotificationSettings()->create(); + $team->matrixNotificationSettings()->create(); }); static::saving(function ($team) { @@ -187,7 +189,8 @@ public function isAnyNotificationEnabled() $this->getNotificationSettings('discord')?->isEnabled() || $this->getNotificationSettings('slack')?->isEnabled() || $this->getNotificationSettings('telegram')?->isEnabled() || - $this->getNotificationSettings('pushover')?->isEnabled(); + $this->getNotificationSettings('pushover')?->isEnabled() || + $this->getNotificationSettings('matrix')?->isEnabled(); } public function subscriptionEnded() @@ -306,4 +309,14 @@ public function pushoverNotificationSettings() { return $this->hasOne(PushoverNotificationSettings::class); } + + public function matrixNotificationSettings() + { + return $this->hasOne(MatrixNotificationSettings::class); + } + + public function routeNotificationForMatrix() + { + return $this->matrixNotificationSettings; + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 9b59d9162a..788d846e61 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -6,6 +6,7 @@ use App\Models\ApplicationPreview; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\MatrixMessage; use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -205,4 +206,31 @@ public function toSlack(): SlackMessage color: SlackMessage::successColor() ); } + + public function toMatrix(): MatrixMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} successfully deployed"; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->preview->fqdn) { + $description .= "\nPreview URL: {$this->preview->fqdn}"; + } + } else { + $title = 'New version successfully deployed'; + $description = "New version successfully deployed for {$this->application_name}"; + if ($this->fqdn) { + $description .= "\nApplication URL: {$this->fqdn}"; + } + } + + $description .= "\n\nProject: ".data_get($this->application, 'environment.project.name'); + $description .= "\nEnvironment: {$this->environment_name}"; + $description .= "\nDeployment Logs: {$this->deployment_url}"; + + return new MatrixMessage( + title: $title, + description: $description, + color: MatrixMessage::successColor() + ); + } } diff --git a/app/Notifications/Channels/MatrixChannel.php b/app/Notifications/Channels/MatrixChannel.php new file mode 100644 index 0000000000..0716708eaa --- /dev/null +++ b/app/Notifications/Channels/MatrixChannel.php @@ -0,0 +1,29 @@ +toMatrix(); + $matrixSettings = $notifiable->matrixNotificationSettings; + + if (! $matrixSettings || ! $matrixSettings->isEnabled() || ! $matrixSettings->matrix_homeserver_url || ! $matrixSettings->matrix_room_id || ! $matrixSettings->matrix_access_token) { + return; + } + + SendMessageToMatrixJob::dispatch( + $message, + $matrixSettings->matrix_homeserver_url, + $matrixSettings->matrix_room_id, + $matrixSettings->matrix_access_token + ); + } +} \ No newline at end of file diff --git a/app/Notifications/Channels/SendsMatrix.php b/app/Notifications/Channels/SendsMatrix.php new file mode 100644 index 0000000000..5baae65442 --- /dev/null +++ b/app/Notifications/Channels/SendsMatrix.php @@ -0,0 +1,8 @@ + [TelegramChannel::class], 'slack' => [SlackChannel::class], 'pushover' => [PushoverChannel::class], + 'matrix' => [MatrixChannel::class], default => [], }; } else { @@ -110,4 +113,13 @@ public function toSlack(): SlackMessage description: 'This is a test Slack notification from Coolify.' ); } + + public function toMatrix(): MatrixMessage + { + return new MatrixMessage( + title: 'Test Matrix Notification', + description: 'This is a test Matrix notification from Coolify.', + color: MatrixMessage::successColor() + ); + } } diff --git a/app/Traits/HasNotificationSettings.php b/app/Traits/HasNotificationSettings.php index 236e4d97cf..afde9e9be1 100644 --- a/app/Traits/HasNotificationSettings.php +++ b/app/Traits/HasNotificationSettings.php @@ -4,6 +4,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\MatrixChannel; use App\Notifications\Channels\PushoverChannel; use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; @@ -30,6 +31,7 @@ public function getNotificationSettings(string $channel): ?Model 'telegram' => $this->telegramNotificationSettings, 'slack' => $this->slackNotificationSettings, 'pushover' => $this->pushoverNotificationSettings, + 'matrix' => $this->matrixNotificationSettings, default => null, }; } @@ -77,6 +79,7 @@ public function getEnabledChannels(string $event): array 'telegram' => TelegramChannel::class, 'slack' => SlackChannel::class, 'pushover' => PushoverChannel::class, + 'matrix' => MatrixChannel::class, ]; if ($event === 'general') { diff --git a/database/factories/MatrixNotificationSettingsFactory.php b/database/factories/MatrixNotificationSettingsFactory.php new file mode 100644 index 0000000000..d24b631f3d --- /dev/null +++ b/database/factories/MatrixNotificationSettingsFactory.php @@ -0,0 +1,65 @@ + + */ +class MatrixNotificationSettingsFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = MatrixNotificationSettings::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'team_id' => Team::factory(), + 'matrix_enabled' => false, + 'matrix_homeserver_url' => null, + 'matrix_room_id' => null, + 'matrix_access_token' => null, + 'matrix_friendly_name' => null, + + 'deployment_success_matrix_notifications' => false, + 'deployment_failure_matrix_notifications' => true, + 'status_change_matrix_notifications' => false, + 'backup_success_matrix_notifications' => false, + 'backup_failure_matrix_notifications' => true, + 'scheduled_task_success_matrix_notifications' => false, + 'scheduled_task_failure_matrix_notifications' => true, + 'docker_cleanup_success_matrix_notifications' => false, + 'docker_cleanup_failure_matrix_notifications' => true, + 'server_disk_usage_matrix_notifications' => true, + 'server_reachable_matrix_notifications' => false, + 'server_unreachable_matrix_notifications' => true, + 'server_patch_matrix_notifications' => false, + ]; + } + + /** + * Indicate that the Matrix notifications are enabled. + */ + public function enabled(): static + { + return $this->state(fn (array $attributes) => [ + 'matrix_enabled' => true, + 'matrix_homeserver_url' => 'https://matrix.org', + 'matrix_room_id' => '!test:matrix.org', + 'matrix_access_token' => 'test_access_token', + 'matrix_friendly_name' => 'Test Matrix Setup', + ]); + } +} \ No newline at end of file diff --git a/database/migrations/2025_09_27_174514_create_matrix_notification_settings_table.php b/database/migrations/2025_09_27_174514_create_matrix_notification_settings_table.php new file mode 100644 index 0000000000..276c0a5e57 --- /dev/null +++ b/database/migrations/2025_09_27_174514_create_matrix_notification_settings_table.php @@ -0,0 +1,68 @@ +id(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); + + $table->boolean('matrix_enabled')->default(false); + $table->text('matrix_homeserver_url')->nullable(); + $table->text('matrix_room_id')->nullable(); + $table->text('matrix_access_token')->nullable(); + $table->string('matrix_friendly_name')->nullable(); + + $table->boolean('deployment_success_matrix_notifications')->default(false); + $table->boolean('deployment_failure_matrix_notifications')->default(true); + $table->boolean('status_change_matrix_notifications')->default(false); + $table->boolean('backup_success_matrix_notifications')->default(false); + $table->boolean('backup_failure_matrix_notifications')->default(true); + $table->boolean('scheduled_task_success_matrix_notifications')->default(false); + $table->boolean('scheduled_task_failure_matrix_notifications')->default(true); + $table->boolean('docker_cleanup_success_matrix_notifications')->default(false); + $table->boolean('docker_cleanup_failure_matrix_notifications')->default(true); + $table->boolean('server_disk_usage_matrix_notifications')->default(true); + $table->boolean('server_reachable_matrix_notifications')->default(false); + $table->boolean('server_unreachable_matrix_notifications')->default(true); + $table->boolean('server_patch_matrix_notifications')->default(false); + + $table->unique(['team_id']); + }); + + DB::table('teams') + ->orderBy('id') + ->chunkById(500, function ($teams): void { + foreach ($teams as $team) { + try { + DB::table('matrix_notification_settings')->insert([ + 'team_id' => $team->id, + ]); + } catch (\Throwable $e) { + Log::error( + 'Error creating matrix notification settings for existing teams: '.$e->getMessage(), + ['team_id' => $team->id] + ); + } + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('matrix_notification_settings'); + } +}; \ No newline at end of file diff --git a/resources/views/components/notification/navbar.blade.php b/resources/views/components/notification/navbar.blade.php index c42ec28ec6..69c8822422 100644 --- a/resources/views/components/notification/navbar.blade.php +++ b/resources/views/components/notification/navbar.blade.php @@ -23,6 +23,10 @@ href="{{ route('notifications.pushover') }}"> + + + diff --git a/resources/views/livewire/notifications/matrix.blade.php b/resources/views/livewire/notifications/matrix.blade.php new file mode 100644 index 0000000000..cee4e6e878 --- /dev/null +++ b/resources/views/livewire/notifications/matrix.blade.php @@ -0,0 +1,89 @@ +
+ + Notifications | Coolify + + +
+
+

Matrix

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

Notification Settings

+

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

+
+
+

Deployments

+
+ + + +
+
+
+

Backups

+
+ + +
+
+
+

Scheduled Tasks

+
+ + +
+
+
+

Server

+
+ + + + + + +
+
+
+
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index e6567daadf..e0ec7f7d7b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,6 +11,7 @@ use App\Livewire\ForcePasswordReset; use App\Livewire\Notifications\Discord as NotificationDiscord; use App\Livewire\Notifications\Email as NotificationEmail; +use App\Livewire\Notifications\Matrix as NotificationMatrix; use App\Livewire\Notifications\Pushover as NotificationPushover; use App\Livewire\Notifications\Slack as NotificationSlack; use App\Livewire\Notifications\Telegram as NotificationTelegram; @@ -124,6 +125,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('/matrix', NotificationMatrix::class)->name('notifications.matrix'); }); Route::prefix('storages')->group(function () { diff --git a/tests/Feature/MatrixNotificationTest.php b/tests/Feature/MatrixNotificationTest.php new file mode 100644 index 0000000000..a24f7d72fe --- /dev/null +++ b/tests/Feature/MatrixNotificationTest.php @@ -0,0 +1,108 @@ +user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + $this->actingAs($this->user); +}); + +it('can create matrix notification settings for a team', function () { + $settings = MatrixNotificationSettings::factory()->create([ + 'team_id' => $this->team->id, + 'matrix_enabled' => true, + 'matrix_homeserver_url' => 'https://matrix.org', + 'matrix_room_id' => '!test:matrix.org', + 'matrix_access_token' => 'test_token', + 'matrix_friendly_name' => 'Test Matrix', + ]); + + expect($settings->team_id)->toBe($this->team->id); + expect($settings->matrix_enabled)->toBeTrue(); + expect($settings->matrix_homeserver_url)->toBe('https://matrix.org'); + expect($settings->matrix_room_id)->toBe('!test:matrix.org'); + expect($settings->matrix_access_token)->toBe('test_token'); + expect($settings->matrix_friendly_name)->toBe('Test Matrix'); +}); + +it('has correct default notification settings', function () { + $settings = MatrixNotificationSettings::factory()->create(['team_id' => $this->team->id]); + + expect($settings->deployment_success_matrix_notifications)->toBeFalse(); + expect($settings->deployment_failure_matrix_notifications)->toBeTrue(); + expect($settings->backup_failure_matrix_notifications)->toBeTrue(); + expect($settings->server_unreachable_matrix_notifications)->toBeTrue(); +}); + +it('belongs to a team', function () { + $settings = MatrixNotificationSettings::factory()->create(['team_id' => $this->team->id]); + + expect($settings->team->id)->toBe($this->team->id); +}); + +it('can check if matrix notifications are enabled', function () { + $disabledTeam = Team::factory()->create(); + $enabledTeam = Team::factory()->create(); + + $disabledSettings = MatrixNotificationSettings::factory()->create([ + 'team_id' => $disabledTeam->id, + 'matrix_enabled' => false, + ]); + + $enabledSettings = MatrixNotificationSettings::factory()->create([ + 'team_id' => $enabledTeam->id, + 'matrix_enabled' => true, + ]); + + expect($disabledSettings->isEnabled())->toBeFalse(); + expect($enabledSettings->isEnabled())->toBeTrue(); +}); + +it('encrypts sensitive matrix fields', function () { + $settings = MatrixNotificationSettings::factory()->create([ + 'team_id' => $this->team->id, + 'matrix_homeserver_url' => 'https://matrix.org', + 'matrix_room_id' => '!test:matrix.org', + 'matrix_access_token' => 'test_token', + ]); + + // Fields should be encrypted in the database + $rawData = $settings->getAttributes(); + expect($rawData['matrix_homeserver_url'])->not->toBe('https://matrix.org'); + expect($rawData['matrix_room_id'])->not->toBe('!test:matrix.org'); + expect($rawData['matrix_access_token'])->not->toBe('test_token'); + + // But accessible normally through the model + expect($settings->matrix_homeserver_url)->toBe('https://matrix.org'); + expect($settings->matrix_room_id)->toBe('!test:matrix.org'); + expect($settings->matrix_access_token)->toBe('test_token'); +}); + +it('can render matrix notification livewire component', function () { + $response = $this->get(route('notifications.matrix')); + + $response->assertStatus(200); + $response->assertSeeLivewire('notifications.matrix'); +}); + +it('can send test matrix notification', function () { + Notification::fake(); + + $settings = MatrixNotificationSettings::factory()->create([ + 'team_id' => $this->team->id, + 'matrix_enabled' => true, + 'matrix_homeserver_url' => 'https://matrix.org', + 'matrix_room_id' => '!test:matrix.org', + 'matrix_access_token' => 'test_token', + ]); + + $this->team->notify(new Test(channel: 'matrix')); + + Notification::assertSentTo($this->team, Test::class); +}); \ No newline at end of file