@endif
@if (count($filteredTags) > 0)
- Exisiting Tags
+ Existing Tags
Click to add quickly
@foreach ($filteredTags as $tag)
diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php
index 77aebbc04..4ea8e1f16 100644
--- a/resources/views/livewire/server/show.blade.php
+++ b/resources/views/livewire/server/show.blade.php
@@ -12,7 +12,7 @@
@if ($server->id === 0)
@else
diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php
index 8b1c6cfed..6ee9a7deb 100644
--- a/resources/views/livewire/settings/index.blade.php
+++ b/resources/views/livewire/settings/index.blade.php
@@ -141,7 +141,7 @@
Date: Thu, 20 Mar 2025 08:28:28 +0200
Subject: [PATCH 160/180] feat(api): update Services api routes and handlers
---
.../Controllers/Api/ServicesController.php | 311 ++++++++++++------
openapi.yaml | 72 +++-
routes/api.php | 2 +-
3 files changed, 275 insertions(+), 110 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 81b51633f..c2b0a361f 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -406,7 +406,7 @@ class ServicesController extends Controller
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
- 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'The flag to connect the service to the predefined Docker network.'],
+ 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
],
@@ -424,7 +424,7 @@ class ServicesController extends Controller
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
- 'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
+ 'domains' => ['type' => 'array', 'items' => ['type' => 'string', 'nullable' => true], 'description' => 'Service domains.'],
]
)
),
@@ -442,8 +442,6 @@ class ServicesController extends Controller
)]
public function create_service(Request $request)
{
- $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', "docker_compose_raw", "connect_to_docker_network"];
-
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -453,113 +451,11 @@ class ServicesController extends Controller
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
- $validator = customApiValidator($request->all(), [
- 'project_uuid' => 'string|required',
- 'environment_name' => 'string|nullable',
- 'environment_uuid' => 'string|nullable',
- 'server_uuid' => 'string|required',
- 'destination_uuid' => 'string',
- 'name' => 'string|max:255',
- 'description' => 'string|nullable',
- 'instant_deploy' => 'boolean',
- 'connect_to_docker_network' => 'boolean',
- 'docker_compose_raw' => 'string|required',
-
- ]);
-
- $extraFields = array_diff(array_keys($request->all()), $allowedFields);
- if ($validator->fails() || ! empty($extraFields)) {
- $errors = $validator->errors();
- if (! empty($extraFields)) {
- foreach ($extraFields as $field) {
- $errors->add($field, 'This field is not allowed.');
- }
- }
-
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => $errors,
- ], 422);
- }
- $environmentUuid = $request->environment_uuid;
- $environmentName = $request->environment_name;
- if (blank($environmentUuid) && blank($environmentName)) {
- return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
- }
- $serverUuid = $request->server_uuid;
- $instantDeploy = $request->instant_deploy ?? false;
- $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
- if (! $project) {
- return response()->json(['message' => 'Project not found.'], 404);
- }
- $environment = $project->environments()->where('name', $environmentName)->first();
- if (! $environment) {
- $environment = $project->environments()->where('uuid', $environmentUuid)->first();
- }
- if (! $environment) {
- return response()->json(['message' => 'Environment not found.'], 404);
- }
- $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
- if (! $server) {
- return response()->json(['message' => 'Server not found.'], 404);
- }
- $destinations = $server->destinations();
- if ($destinations->count() == 0) {
- return response()->json(['message' => 'Server has no destinations.'], 400);
- }
- if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
- return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
- }
- $destination = $destinations->first();
- if (! isBase64Encoded($request->docker_compose_raw)) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
- ],
- ], 422);
- }
- $dockerComposeRaw = base64_decode($request->docker_compose_raw);
- if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
- return response()->json([
- 'message' => 'Validation failed.',
- 'errors' => [
- 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
- ],
- ], 422);
- }
- $dockerCompose = base64_decode($request->docker_compose_raw);
- $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$service = new Service;
- $service->name = $request->name;
- $service->description = $request->description;
- $service->docker_compose_raw = $dockerComposeRaw;
- $service->environment_id = $environment->id;
- $service->server_id = $server->id;
- $service->destination_id = $destination->id;
- $service->destination_type = $destination->getMorphClass();
- $service->connect_to_docker_network = $request->connect_to_docker_network;
- $service->save();
+ $result = $this->upsert_service($request, $service, $teamId);
-
- $service->parse(isNew: true);
- if ($instantDeploy) {
- StartService::dispatch($service);
- }
-
- $domains = $service->applications()->get()->pluck('fqdn')->sort();
- $domains = $domains->map(function ($domain) {
- if (count(explode(':', $domain)) > 2) {
- return str($domain)->beforeLast(':')->value();
- }
-
- return $domain;
- });
- return response()->json(serializeApiResponse([
- 'uuid' => $service->uuid,
- 'domains' => $domains,
- ]))->setStatusCode(201);
+ return response()->json(serializeApiResponse($result))->setStatusCode(201);
}
#[OA\Get(
@@ -693,6 +589,205 @@ class ServicesController extends Controller
]);
}
+ #[OA\Patch(
+ summary: 'Update',
+ description: 'Update service by UUID.',
+ path: '/services/{uuid}',
+ operationId: 'update-service-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ requestBody: new OA\RequestBody(
+ description: 'Service updated.',
+ required: true,
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
+ properties: [
+ 'name' => ['type' => 'string', 'description' => 'The service name.'],
+ 'description' => ['type' => 'string', 'description' => 'The service description.'],
+ 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
+ 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
+ 'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID.'],
+ 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
+ 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
+ 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'],
+ 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
+ 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
+ ],
+ )
+ ),
+ ]
+ ),
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Service updated.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
+ 'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
+ ]
+ )
+ ),
+ ]
+ ),
+ 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',
+ ),
+ ]
+ )]
+ public function update_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+
+ $return = validateIncomingRequest($request);
+ if ($return instanceof \Illuminate\Http\JsonResponse) {
+ return $return;
+ }
+
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $result = $this->upsert_service($request, $service, $teamId);
+
+ return response()->json(serializeApiResponse($result))->setStatusCode(200);
+ }
+
+ private function upsert_service(Request $request, Service $service, string $teamId)
+ {
+ $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network'];
+ $validator = customApiValidator($request->all(), [
+ 'project_uuid' => 'string|required',
+ 'environment_name' => 'string|nullable',
+ 'environment_uuid' => 'string|nullable',
+ 'server_uuid' => 'string|required',
+ 'destination_uuid' => 'string',
+ 'name' => 'string|max:255',
+ 'description' => 'string|nullable',
+ 'instant_deploy' => 'boolean',
+ 'connect_to_docker_network' => 'boolean',
+ 'docker_compose_raw' => 'string|required',
+ ]);
+
+ $extraFields = array_diff(array_keys($request->all()), $allowedFields);
+ if ($validator->fails() || ! empty($extraFields)) {
+ $errors = $validator->errors();
+ if (! empty($extraFields)) {
+ foreach ($extraFields as $field) {
+ $errors->add($field, 'This field is not allowed.');
+ }
+ }
+
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+
+ $environmentUuid = $request->environment_uuid;
+ $environmentName = $request->environment_name;
+ if (blank($environmentUuid) && blank($environmentName)) {
+ return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
+ }
+ $serverUuid = $request->server_uuid;
+ $instantDeploy = $request->instant_deploy ?? false;
+ $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
+ if (! $project) {
+ return response()->json(['message' => 'Project not found.'], 404);
+ }
+ $environment = $project->environments()->where('name', $environmentName)->first();
+ if (! $environment) {
+ $environment = $project->environments()->where('uuid', $environmentUuid)->first();
+ }
+ if (! $environment) {
+ return response()->json(['message' => 'Environment not found.'], 404);
+ }
+ $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
+ if (! $server) {
+ return response()->json(['message' => 'Server not found.'], 404);
+ }
+ $destinations = $server->destinations();
+ if ($destinations->count() == 0) {
+ return response()->json(['message' => 'Server has no destinations.'], 400);
+ }
+ if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
+ return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
+ }
+ $destination = $destinations->first();
+ if (! isBase64Encoded($request->docker_compose_raw)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerComposeRaw = base64_decode($request->docker_compose_raw);
+ if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.',
+ ],
+ ], 422);
+ }
+ $dockerCompose = base64_decode($request->docker_compose_raw);
+ $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+
+ $service->name = $request->name;
+ $service->description = $request->description;
+ $service->docker_compose_raw = $dockerComposeRaw;
+ $service->environment_id = $environment->id;
+ $service->server_id = $server->id;
+ $service->destination_id = $destination->id;
+ $service->destination_type = $destination->getMorphClass();
+ $service->connect_to_docker_network = $request->connect_to_docker_network;
+ $service->save();
+
+ $service->parse();
+ if ($instantDeploy) {
+ StartService::dispatch($service);
+ }
+
+ $domains = $service->applications()->get()->pluck('fqdn')->sort();
+ $domains = $domains->map(function ($domain) {
+ if (count(explode(':', $domain)) > 2) {
+ return str($domain)->beforeLast(':')->value();
+ }
+
+ return $domain;
+ })->values();
+
+ return [
+ 'uuid' => $service->uuid,
+ 'domains' => $domains,
+ ];
+ }
+
#[OA\Get(
summary: 'List Envs',
description: 'List all envs by service UUID.',
diff --git a/openapi.yaml b/openapi.yaml
index d14f3edf9..c965e9fe2 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -4053,7 +4053,7 @@ paths:
schema:
properties:
uuid: { type: string, description: 'Service UUID.' }
- domains: { type: array, items: { type: string }, description: 'Service domains.' }
+ domains: { type: array, items: { type: string, nullable: true }, description: 'Service domains.' }
type: object
'401':
$ref: '#/components/responses/401'
@@ -4225,6 +4225,76 @@ paths:
security:
-
bearerAuth: []
+ patch:
+ tags:
+ - Services
+ summary: Update
+ description: 'Update service by UUID.'
+ operationId: update-service-by-uuid
+ requestBody:
+ description: 'Service updated.'
+ required: true
+ content:
+ application/json:
+ schema:
+ required:
+ - server_uuid
+ - project_uuid
+ - environment_name
+ - environment_uuid
+ - docker_compose_raw
+ properties:
+ name:
+ type: string
+ description: 'The service name.'
+ description:
+ type: string
+ description: 'The service description.'
+ project_uuid:
+ type: string
+ description: 'The project UUID.'
+ environment_name:
+ type: string
+ description: 'The environment name.'
+ environment_uuid:
+ type: string
+ description: 'The environment UUID.'
+ server_uuid:
+ type: string
+ description: 'The server UUID.'
+ destination_uuid:
+ type: string
+ description: 'The destination UUID.'
+ instant_deploy:
+ type: boolean
+ description: 'The flag to indicate if the service should be deployed instantly.'
+ connect_to_docker_network:
+ type: boolean
+ default: false
+ description: 'The flag to connect the service to the predefined Docker network.'
+ docker_compose_raw:
+ type: string
+ description: 'The Docker Compose raw content.'
+ type: object
+ responses:
+ '200':
+ description: 'Service updated.'
+ content:
+ application/json:
+ schema:
+ properties:
+ uuid: { type: string, description: 'Service UUID.' }
+ domains: { type: array, items: { type: string }, description: 'Service domains.' }
+ type: object
+ '401':
+ $ref: '#/components/responses/401'
+ '400':
+ $ref: '#/components/responses/400'
+ '404':
+ $ref: '#/components/responses/404'
+ security:
+ -
+ bearerAuth: []
'/services/{uuid}/envs':
get:
tags:
diff --git a/routes/api.php b/routes/api.php
index fbd02f926..814033665 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -117,7 +117,7 @@ Route::group([
Route::post('/services/one-click', [ServicesController::class, 'create_one_click_service'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']);
- // Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']);
+ Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']);
Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['api.ability:read']);
From 21938c7f923aa6b86feb6c5169e543ba7dab264b Mon Sep 17 00:00:00 2001
From: Louis Midson LAJEANTY
Date: Thu, 20 Mar 2025 20:56:45 -0400
Subject: [PATCH 161/180] fix(service): replace deprecated credentials env
variables on keycloak service
---
templates/compose/keycloak-with-postgres.yaml | 4 ++--
templates/compose/keycloak.yaml | 4 ++--
templates/service-templates.json | 6 +++---
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/templates/compose/keycloak-with-postgres.yaml b/templates/compose/keycloak-with-postgres.yaml
index 9f9a395a0..731817113 100644
--- a/templates/compose/keycloak-with-postgres.yaml
+++ b/templates/compose/keycloak-with-postgres.yaml
@@ -12,8 +12,8 @@ services:
environment:
- SERVICE_FQDN_KEYCLOAK_8080
- TZ=${TIMEZONE:-UTC}
- - KEYCLOAK_ADMIN=${SERVICE_USER_ADMIN}
- - KEYCLOAK_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
+ - KC_BOOTSTRAP_ADMIN_USERNAME=${SERVICE_USER_ADMIN}
+ - KC_BOOTSTRAP_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
- KC_DB=postgres
- KC_DB_USERNAME=${SERVICE_USER_DATABASE}
- KC_DB_PASSWORD=${SERVICE_PASSWORD_64_DATABASE}
diff --git a/templates/compose/keycloak.yaml b/templates/compose/keycloak.yaml
index b3e7ecf07..76c9d233b 100644
--- a/templates/compose/keycloak.yaml
+++ b/templates/compose/keycloak.yaml
@@ -12,8 +12,8 @@ services:
environment:
- SERVICE_FQDN_KEYCLOAK_8080
- TZ=${TIMEZONE:-UTC}
- - KEYCLOAK_ADMIN=${SERVICE_USER_ADMIN}
- - KEYCLOAK_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
+ - KC_BOOTSTRAP_ADMIN_USERNAME=${SERVICE_USER_ADMIN}
+ - KC_BOOTSTRAP_ADMIN_PASSWORD=${SERVICE_PASSWORD_ADMIN}
- KC_HOSTNAME=${SERVICE_FQDN_KEYCLOAK}
- KC_HTTP_ENABLED=${KC_HTTP_ENABLED:-true}
- KC_HEALTH_ENABLED=${KC_HEALTH_ENABLED:-true}
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 112748c1a..5e8a0e413 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -1511,7 +1511,7 @@
"keycloak-with-postgres": {
"documentation": "https://www.keycloak.org?utm_source=coolify.io",
"slogan": "Keycloak is an open-source Identity and Access Management tool.",
- "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tFWUNMT0FLX0FETUlOPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBLQ19EQj1wb3N0Z3JlcwogICAgICAtICdLQ19EQl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9EQVRBQkFTRX0nCiAgICAgIC0gJ0tDX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0nCiAgICAgIC0gS0NfREJfVVJMX1BPUlQ9NTQzMgogICAgICAtICdLQ19EQl9VUkw9amRiYzpwb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1rZXljbG9ha30nCiAgICAgIC0gJ0tDX0hPU1ROQU1FPSR7U0VSVklDRV9GUUROX0tFWUNMT0FLfScKICAgICAgLSAnS0NfSFRUUF9FTkFCTEVEPSR7S0NfSFRUUF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfSEVBTFRIX0VOQUJMRUQ9JHtLQ19IRUFMVEhfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0tDX1BST1hZX0hFQURFUlM9JHtLQ19QUk9YWV9IRUFERVJTOi14Zm9yd2FyZGVkfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2tleWNsb2FrLWRhdGE6L29wdC9rZXljbG9hay9kYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDsgZWNobyAtZSAnR0VUIC9oZWFsdGgvcmVhZHkgSFRUUC8xLjFcclxuSG9zdDogbG9jYWxob3N0OjkwMDBcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2NhdCA8JjMgfCBncmVwIC1xICdcInN0YXR1c1wiOiBcIlVQXCInICYmIGV4aXQgMCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1rZXljbG9ha30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
+ "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogcXVheS5pby9rZXljbG9hay9rZXljbG9hazoyNi4wCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fS0VZQ0xPQUtfODA4MAogICAgICAtIFRaPSR7VElNRVpPTkU6LVVUQ30KICAgICAgLSBLQ19CT09UU1RSQVBfQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59CiAgICAgIC0gS0NfQk9PVFNUUkFQX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0KICAgICAgLSBLQ19EQj1wb3N0Z3JlcwogICAgICAtIEtDX0RCX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfQogICAgICAtIEtDX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0KICAgICAgLSBLQ19EQl9VUkxfUE9SVD01NDMyCiAgICAgIC0gS0NfREJfVVJMPWpkYmM6cG9zdGdyZXNxbDovL3Bvc3RncmVzLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTota2V5Y2xvYWt9CiAgICAgIC0gS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9CiAgICAgIC0gS0NfSFRUUF9FTkFCTEVEPSR7S0NfSFRUUF9FTkFCTEVEOi10cnVlfQogICAgICAtIEtDX0hFQUxUSF9FTkFCTEVEPSR7S0NfSEVBTFRIX0VOQUJMRUQ6LXRydWV9CiAgICAgIC0gS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9CiAgICB2b2x1bWVzOgogICAgICAtIGtleWNsb2FrLWRhdGE6L29wdC9rZXljbG9hay9kYXRhCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgWwogICAgICAgICAgIkNNRC1TSEVMTCIsCiAgICAgICAgICAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDsgZWNobyAtZSAnR0VUIC9oZWFsdGgvcmVhZHkgSFRUUC8xLjFcclxuSG9zdDogbG9jYWxob3N0OjkwMDBcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2NhdCA8JjMgfCBncmVwIC1xICdcInN0YXR1c1wiOiBcIlVQXCInICYmIGV4aXQgMCB8fCBleGl0IDEiLAogICAgICAgIF0KICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6IHBvc3RncmVzOjE2LWFscGluZQogICAgdm9sdW1lczoKICAgICAgLSBrZXljbG9hay1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfQogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0KICAgICAgLSBQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWtleWNsb2FrfQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtIHBnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9CiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTA=",
"tags": [
"keycloak",
"identity",
@@ -1538,7 +1538,7 @@
"keycloak": {
"documentation": "https://www.keycloak.org?utm_source=coolify.io",
"slogan": "Keycloak is an open-source Identity and Access Management tool.",
- "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tFWUNMT0FLX0FETUlOPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9JwogICAgICAtICdLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19IRUFMVEhfRU5BQkxFRD0ke0tDX0hFQUxUSF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9JwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gImV4ZWMgMzw+L2Rldi90Y3AvMTI3LjAuMC4xLzkwMDA7IGVjaG8gLWUgJ0dFVCAvaGVhbHRoL3JlYWR5IEhUVFAvMS4xXHJcbkhvc3Q6IGxvY2FsaG9zdDo5MDAwXHJcbkNvbm5lY3Rpb246IGNsb3NlXHJcblxyXG4nID4mMztjYXQgPCYzIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjogXCJVUFwiJyAmJiBleGl0IDAgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==",
+ "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogcXVheS5pby9rZXljbG9hay9rZXljbG9hazoyNi4wCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fS0VZQ0xPQUtfODA4MAogICAgICAtIFRaPSR7VElNRVpPTkU6LVVUQ30KICAgICAgLSBLQ19CT09UU1RSQVBfQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59CiAgICAgIC0gS0NfQk9PVFNUUkFQX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0KICAgICAgLSBLQ19IT1NUTkFNRT0ke1NFUlZJQ0VfRlFETl9LRVlDTE9BS30KICAgICAgLSBLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9CiAgICAgIC0gS0NfSEVBTFRIX0VOQUJMRUQ9JHtLQ19IRUFMVEhfRU5BQkxFRDotdHJ1ZX0KICAgICAgLSBLQ19QUk9YWV9IRUFERVJTPSR7S0NfUFJPWFlfSEVBREVSUzoteGZvcndhcmRlZH0KICAgIHZvbHVtZXM6CiAgICAgIC0ga2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIFsKICAgICAgICAgICJDTUQtU0hFTEwiLAogICAgICAgICAgImV4ZWMgMzw+L2Rldi90Y3AvMTI3LjAuMC4xLzkwMDA7IGVjaG8gLWUgJ0dFVCAvaGVhbHRoL3JlYWR5IEhUVFAvMS4xXHJcbkhvc3Q6IGxvY2FsaG9zdDo5MDAwXHJcbkNvbm5lY3Rpb246IGNsb3NlXHJcblxyXG4nID4mMztjYXQgPCYzIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjogXCJVUFwiJyAmJiBleGl0IDAgfHwgZXhpdCAxIiwKICAgICAgICBdCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTA=",
"tags": [
"keycloak",
"identity",
@@ -3343,4 +3343,4 @@
"minversion": "0.0.0",
"port": "3000"
}
-}
+}
\ No newline at end of file
From 120facfca39cb245131a935e938245aa7a9bb925 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 21 Mar 2025 11:31:17 +0100
Subject: [PATCH 162/180] feat(api): unify service creation endpoint and
enhance validation
---
.../Controllers/Api/ServicesController.php | 111 ++++--------------
app/Livewire/Project/New/DockerCompose.php | 4 -
app/Livewire/Project/Resource/Create.php | 1 -
app/Models/Service.php | 5 +
routes/api.php | 1 -
routes/web.php | 9 --
6 files changed, 25 insertions(+), 106 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index c2b0a361f..cbbe4ab34 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -89,10 +89,10 @@ class ServicesController extends Controller
}
#[OA\Post(
- summary: 'Create one-click',
- description: 'Create a one-click service',
- path: '/services/one-click',
- operationId: 'create-one-click-service',
+ summary: 'Create service',
+ description: 'Create a one-click / custom service',
+ path: '/services',
+ operationId: 'create-service',
security: [
['bearerAuth' => []],
],
@@ -236,9 +236,9 @@ class ServicesController extends Controller
),
]
)]
- public function create_one_click_service(Request $request)
+ public function create_service(Request $request)
{
- $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
+ $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -250,7 +250,8 @@ class ServicesController extends Controller
return $return;
}
$validator = customApiValidator($request->all(), [
- 'type' => 'string|required',
+ 'type' => 'string|required_without:docker_compose_raw',
+ 'docker_compose_raw' => 'string|required_without:type',
'project_uuid' => 'string|required',
'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
@@ -373,89 +374,16 @@ class ServicesController extends Controller
]);
}
- return response()->json(['message' => 'Service not found.'], 404);
+ return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
+ } elseif (filled($request->docker_compose_raw)) {
+
+ $service = new Service;
+ $result = $this->upsert_service($request, $service, $teamId);
+
+ return response()->json(serializeApiResponse($result))->setStatusCode(201);
} else {
- return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
+ return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400);
}
-
- return response()->json(['message' => 'Invalid service type.'], 400);
- }
-
- #[OA\Post(
- summary: 'Create',
- description: 'Create a service',
- path: '/services',
- operationId: 'create-service',
- security: [
- ['bearerAuth' => []],
- ],
- tags: ['Services'],
- requestBody: new OA\RequestBody(
- required: true,
- content: new OA\MediaType(
- mediaType: 'application/json',
- schema: new OA\Schema(
- type: 'object',
- required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
- properties: [
- 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
- 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
- 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
- 'environment_name' => ['type' => 'string', 'description' => 'Environment name. You need to provide at least one of environment_name or environment_uuid.'],
- 'environment_uuid' => ['type' => 'string', 'description' => 'Environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
- 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
- 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
- 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
- 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'],
- 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
-
- ],
- ),
- ),
- ),
- responses: [
- new OA\Response(
- response: 201,
- description: 'Service created successfully.',
- content: [
- new OA\MediaType(
- mediaType: 'application/json',
- schema: new OA\Schema(
- type: 'object',
- properties: [
- 'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
- 'domains' => ['type' => 'array', 'items' => ['type' => 'string', 'nullable' => true], 'description' => 'Service domains.'],
- ]
- )
- ),
- ]
- ),
- new OA\Response(
- response: 401,
- ref: '#/components/responses/401',
- ),
- new OA\Response(
- response: 400,
- ref: '#/components/responses/400',
- ),
- ]
- )]
- public function create_service(Request $request)
- {
- $teamId = getTeamIdFromToken();
- if (is_null($teamId)) {
- return invalidTokenResponse();
- }
-
- $return = validateIncomingRequest($request);
- if ($return instanceof \Illuminate\Http\JsonResponse) {
- return $return;
- }
-
- $service = new Service;
- $result = $this->upsert_service($request, $service, $teamId);
-
- return response()->json(serializeApiResponse($result))->setStatusCode(201);
}
#[OA\Get(
@@ -757,15 +685,16 @@ class ServicesController extends Controller
}
$dockerCompose = base64_decode($request->docker_compose_raw);
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $connectToDockerNetwork = $request->connect_to_docker_network ?? false;
- $service->name = $request->name;
- $service->description = $request->description;
+ $service->name = $request->name ?? null;
+ $service->description = $request->description ?? null;
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
$service->server_id = $server->id;
$service->destination_id = $destination->id;
$service->destination_type = $destination->getMorphClass();
- $service->connect_to_docker_network = $request->connect_to_docker_network;
+ $service->connect_to_docker_network = $connectToDockerNetwork;
$service->save();
$service->parse();
diff --git a/app/Livewire/Project/New/DockerCompose.php b/app/Livewire/Project/New/DockerCompose.php
index 2f51094d1..3d47ffae5 100644
--- a/app/Livewire/Project/New/DockerCompose.php
+++ b/app/Livewire/Project/New/DockerCompose.php
@@ -7,7 +7,6 @@ use App\Models\Project;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
-use Illuminate\Support\Str;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@@ -66,7 +65,6 @@ class DockerCompose extends Component
$destination_class = $destination->getMorphClass();
$service = Service::create([
- 'name' => 'service'.Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
@@ -85,8 +83,6 @@ class DockerCompose extends Component
'resourceable_type' => $service->getMorphClass(),
]);
}
- $service->name = "service-$service->uuid";
-
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 0faf0b8da..e7cff4f29 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -73,7 +73,6 @@ class Create extends Component
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
- 'name' => "$oneClickServiceName-".str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 25e6b92ea..23ddb5923 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -50,6 +50,11 @@ class Service extends BaseModel
protected static function booted()
{
+ static::creating(function ($service) {
+ if (blank($service->name)) {
+ $service->name = 'service-'.(new Cuid2);
+ }
+ });
static::created(function ($service) {
$service->compose_parsing_version = self::$parserVersion;
$service->save();
diff --git a/routes/api.php b/routes/api.php
index 814033665..d2aa0a5b7 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -114,7 +114,6 @@ Route::group([
Route::get('/services', [ServicesController::class, 'services'])->middleware(['api.ability:read']);
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
- Route::post('/services/one-click', [ServicesController::class, 'create_one_click_service'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']);
Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']);
diff --git a/routes/web.php b/routes/web.php
index 618e4e090..17fa46c05 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -99,15 +99,6 @@ Route::middleware(['throttle:login'])->group(function () {
Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect');
Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback');
-// Route::prefix('magic')->middleware(['auth'])->group(function () {
-// Route::get('/servers', [MagicController::class, 'servers']);
-// Route::get('/destinations', [MagicController::class, 'destinations']);
-// Route::get('/projects', [MagicController::class, 'projects']);
-// Route::get('/environments', [MagicController::class, 'environments']);
-// Route::get('/project/new', [MagicController::class, 'newProject']);
-// Route::get('/environment/new', [MagicController::class, 'newEnvironment']);
-// });
-
Route::middleware(['auth', 'verified'])->group(function () {
Route::middleware(['throttle:force-password-reset'])->group(function () {
Route::get('/force-password-reset', ForcePasswordReset::class)->name('auth.force-password-reset');
From 26f4d37346b7c9b5833f0f187e5470774ff07298 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 21 Mar 2025 12:16:33 +0100
Subject: [PATCH 163/180] feat(notifications): add discord ping functionality
and settings
---
app/Livewire/Notifications/Discord.php | 25 ++++++++++++++++-
app/Models/DiscordNotificationSettings.php | 7 +++++
app/Notifications/Channels/DiscordChannel.php | 4 +++
app/Notifications/Test.php | 3 +-
...2025_03_21_104103_disable_discord_here.php | 28 +++++++++++++++++++
.../livewire/notifications/discord.blade.php | 9 ++++--
6 files changed, 71 insertions(+), 5 deletions(-)
create mode 100644 database/migrations/2025_03_21_104103_disable_discord_here.php
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index 57007813e..9489eb128 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -56,6 +56,9 @@ class Discord extends Component
#[Validate(['boolean'])]
public bool $serverUnreachableDiscordNotifications = true;
+ #[Validate(['boolean'])]
+ public bool $discordPingEnabled = true;
+
public function mount()
{
try {
@@ -87,6 +90,8 @@ class Discord extends Component
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
+ $this->settings->discord_ping_enabled = $this->discordPingEnabled;
+
$this->settings->save();
refreshSession();
} else {
@@ -105,12 +110,30 @@ class Discord extends Component
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
+
+ $this->discordPingEnabled = $this->settings->discord_ping_enabled;
+ }
+ }
+
+ public function instantSaveDiscordPingEnabled()
+ {
+ try {
+ $original = $this->discordPingEnabled;
+ $this->validate([
+ 'discordPingEnabled' => 'required',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->discordPingEnabled = $original;
+
+ return handleError($e, $this);
}
}
public function instantSaveDiscordEnabled()
{
try {
+ $original = $this->discordEnabled;
$this->validate([
'discordWebhookUrl' => 'required',
], [
@@ -118,7 +141,7 @@ class Discord extends Component
]);
$this->saveModel();
} catch (\Throwable $e) {
- $this->discordEnabled = false;
+ $this->discordEnabled = $original;
return handleError($e, $this);
}
diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php
index 619393ddc..1ba16ccd8 100644
--- a/app/Models/DiscordNotificationSettings.php
+++ b/app/Models/DiscordNotificationSettings.php
@@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications',
'server_unreachable_discord_notifications',
+ 'discord_ping_enabled',
];
protected $casts = [
@@ -45,6 +46,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications' => 'boolean',
'server_reachable_discord_notifications' => 'boolean',
'server_unreachable_discord_notifications' => 'boolean',
+ 'discord_ping_enabled' => 'boolean',
];
public function team()
@@ -56,4 +58,9 @@ class DiscordNotificationSettings extends Model
{
return $this->discord_enabled;
}
+
+ public function isPingEnabled()
+ {
+ return $this->discord_ping_enabled;
+ }
}
diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php
index 362006d8e..b4ba9bf8c 100644
--- a/app/Notifications/Channels/DiscordChannel.php
+++ b/app/Notifications/Channels/DiscordChannel.php
@@ -20,6 +20,10 @@ class DiscordChannel
return;
}
+ if (! $discordSettings->discord_ping_enabled) {
+ $message->isCritical = false;
+ }
+
SendMessageToDiscordJob::dispatch($message, $discordSettings->discord_webhook_url);
}
}
diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php
index ebb8735f5..2a0581bbf 100644
--- a/app/Notifications/Test.php
+++ b/app/Notifications/Test.php
@@ -22,7 +22,7 @@ class Test extends Notification implements ShouldQueue
public $tries = 5;
- public function __construct(public ?string $emails = null, public ?string $channel = null)
+ public function __construct(public ?string $emails = null, public ?string $channel = null, public ?bool $ping = false)
{
$this->onQueue('high');
}
@@ -68,6 +68,7 @@ class Test extends Notification implements ShouldQueue
title: ':white_check_mark: Test Success',
description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:',
color: DiscordMessage::successColor(),
+ isCritical: $this->ping,
);
$message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true);
diff --git a/database/migrations/2025_03_21_104103_disable_discord_here.php b/database/migrations/2025_03_21_104103_disable_discord_here.php
new file mode 100644
index 000000000..6aef45c04
--- /dev/null
+++ b/database/migrations/2025_03_21_104103_disable_discord_here.php
@@ -0,0 +1,28 @@
+boolean('discord_ping_enabled')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('discord_notification_settings', function (Blueprint $table) {
+ $table->dropColumn('discord_ping_enabled');
+ });
+ }
+};
diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php
index 6caf31176..9b48c9793 100644
--- a/resources/views/livewire/notifications/discord.blade.php
+++ b/resources/views/livewire/notifications/discord.blade.php
@@ -20,12 +20,15 @@
@endif
-
+
+
+ helper="Create a Discord Server and generate a Webhook URL.
Webhook Documentation"
+ required id="discordWebhookUrl" label="Webhook" />
Notification Settings
From d7d80e926eee8a31e2fdaab403857dc6b244d641 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 21 Mar 2025 15:45:29 +0100
Subject: [PATCH 164/180] feat(user): implement session deletion on password
reset
---
app/Actions/Fortify/ResetUserPassword.php | 1 +
app/Console/Commands/RootResetPassword.php | 3 +-
app/Livewire/Profile/Index.php | 1 +
app/Models/User.php | 4 ++-
app/Traits/DeletesUserSessions.php | 34 +++++++++++++++++++
.../views/livewire/profile/index.blade.php | 15 ++++----
6 files changed, 48 insertions(+), 10 deletions(-)
create mode 100644 app/Traits/DeletesUserSessions.php
diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php
index d3727a52c..158996c90 100644
--- a/app/Actions/Fortify/ResetUserPassword.php
+++ b/app/Actions/Fortify/ResetUserPassword.php
@@ -24,5 +24,6 @@ class ResetUserPassword implements ResetsUserPasswords
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
+ $user->deleteAllSessions();
}
}
diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php
index f36c11a4f..8d440ebd7 100644
--- a/app/Console/Commands/RootResetPassword.php
+++ b/app/Console/Commands/RootResetPassword.php
@@ -39,7 +39,8 @@ class RootResetPassword extends Command
}
$this->info('Updating root password...');
try {
- User::find(0)->update(['password' => Hash::make($password)]);
+ $user = User::find(0);
+ $user->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root password.');
diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php
index 53314cd5c..788802353 100644
--- a/app/Livewire/Profile/Index.php
+++ b/app/Livewire/Profile/Index.php
@@ -70,6 +70,7 @@ class Index extends Component
$this->current_password = '';
$this->new_password = '';
$this->new_password_confirmation = '';
+ $this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 7c23631c3..f59f506fc 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
+use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -37,7 +38,7 @@ use OpenApi\Attributes as OA;
)]
class User extends Authenticatable implements SendsEmail
{
- use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
+ use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $guarded = [];
@@ -57,6 +58,7 @@ class User extends Authenticatable implements SendsEmail
protected static function boot()
{
parent::boot();
+
static::created(function (User $user) {
$team = [
'name' => $user->name."'s Team",
diff --git a/app/Traits/DeletesUserSessions.php b/app/Traits/DeletesUserSessions.php
new file mode 100644
index 000000000..2581d4203
--- /dev/null
+++ b/app/Traits/DeletesUserSessions.php
@@ -0,0 +1,34 @@
+where('user_id', $this->id)->delete();
+ }
+
+ /**
+ * Boot the trait.
+ */
+ protected static function bootDeletesUserSessions()
+ {
+ static::updated(function ($user) {
+ // Check if password was changed
+ if ($user->isDirty('password')) {
+ $user->deleteAllSessions();
+ }
+ });
+ }
+}
diff --git a/resources/views/livewire/profile/index.blade.php b/resources/views/livewire/profile/index.blade.php
index bc9f19f56..967c71746 100644
--- a/resources/views/livewire/profile/index.blade.php
+++ b/resources/views/livewire/profile/index.blade.php
@@ -19,6 +19,7 @@
Change Password
Save
+ Resetting the password will logout all sessions.
@@ -36,23 +37,21 @@
-
+
{!! request()->user()->twoFactorQrCodeSvg() !!}
-
-
+
+
From fa6d50ae50704a8c6077af4dfa92b3e51d885599 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 09:46:01 +0100
Subject: [PATCH 165/180] fix(keycloak): update keycloak image version to 26.1
---
templates/compose/keycloak-with-postgres.yaml | 2 +-
templates/compose/keycloak.yaml | 2 +-
templates/service-templates.json | 6 +++---
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/templates/compose/keycloak-with-postgres.yaml b/templates/compose/keycloak-with-postgres.yaml
index 731817113..c58f97387 100644
--- a/templates/compose/keycloak-with-postgres.yaml
+++ b/templates/compose/keycloak-with-postgres.yaml
@@ -6,7 +6,7 @@
services:
keycloak:
- image: quay.io/keycloak/keycloak:26.0
+ image: quay.io/keycloak/keycloak:26.1
command:
- start
environment:
diff --git a/templates/compose/keycloak.yaml b/templates/compose/keycloak.yaml
index 76c9d233b..1f5ebb6a3 100644
--- a/templates/compose/keycloak.yaml
+++ b/templates/compose/keycloak.yaml
@@ -6,7 +6,7 @@
services:
keycloak:
- image: quay.io/keycloak/keycloak:26.0
+ image: quay.io/keycloak/keycloak:26.1
command:
- start
environment:
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 615f2dcc0..86b5873e5 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -1556,7 +1556,7 @@
"keycloak-with-postgres": {
"documentation": "https://www.keycloak.org?utm_source=coolify.io",
"slogan": "Keycloak is an open-source Identity and Access Management tool.",
- "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogcXVheS5pby9rZXljbG9hay9rZXljbG9hazoyNi4wCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fS0VZQ0xPQUtfODA4MAogICAgICAtIFRaPSR7VElNRVpPTkU6LVVUQ30KICAgICAgLSBLQ19CT09UU1RSQVBfQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59CiAgICAgIC0gS0NfQk9PVFNUUkFQX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0KICAgICAgLSBLQ19EQj1wb3N0Z3JlcwogICAgICAtIEtDX0RCX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfQogICAgICAtIEtDX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0KICAgICAgLSBLQ19EQl9VUkxfUE9SVD01NDMyCiAgICAgIC0gS0NfREJfVVJMPWpkYmM6cG9zdGdyZXNxbDovL3Bvc3RncmVzLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTota2V5Y2xvYWt9CiAgICAgIC0gS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9CiAgICAgIC0gS0NfSFRUUF9FTkFCTEVEPSR7S0NfSFRUUF9FTkFCTEVEOi10cnVlfQogICAgICAtIEtDX0hFQUxUSF9FTkFCTEVEPSR7S0NfSEVBTFRIX0VOQUJMRUQ6LXRydWV9CiAgICAgIC0gS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9CiAgICB2b2x1bWVzOgogICAgICAtIGtleWNsb2FrLWRhdGE6L29wdC9rZXljbG9hay9kYXRhCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgWwogICAgICAgICAgIkNNRC1TSEVMTCIsCiAgICAgICAgICAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDsgZWNobyAtZSAnR0VUIC9oZWFsdGgvcmVhZHkgSFRUUC8xLjFcclxuSG9zdDogbG9jYWxob3N0OjkwMDBcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2NhdCA8JjMgfCBncmVwIC1xICdcInN0YXR1c1wiOiBcIlVQXCInICYmIGV4aXQgMCB8fCBleGl0IDEiLAogICAgICAgIF0KICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6IHBvc3RncmVzOjE2LWFscGluZQogICAgdm9sdW1lczoKICAgICAgLSBrZXljbG9hay1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfQogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0KICAgICAgLSBQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWtleWNsb2FrfQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtIHBnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9CiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTA=",
+ "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMScKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tDX0JPT1RTVFJBUF9BRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0tDX0JPT1RTVFJBUF9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtIEtDX0RCPXBvc3RncmVzCiAgICAgIC0gJ0tDX0RCX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfScKICAgICAgLSAnS0NfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0RBVEFCQVNFfScKICAgICAgLSBLQ19EQl9VUkxfUE9SVD01NDMyCiAgICAgIC0gJ0tDX0RCX1VSTD1qZGJjOnBvc3RncmVzcWw6Ly9wb3N0Z3Jlcy8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWtleWNsb2FrfScKICAgICAgLSAnS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9JwogICAgICAtICdLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19IRUFMVEhfRU5BQkxFRD0ke0tDX0hFQUxUSF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9JwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJleGVjIDM8Pi9kZXYvdGNwLzEyNy4wLjAuMS85MDAwOyBlY2hvIC1lICdHRVQgL2hlYWx0aC9yZWFkeSBIVFRQLzEuMVxyXG5Ib3N0OiBsb2NhbGhvc3Q6OTAwMFxyXG5Db25uZWN0aW9uOiBjbG9zZVxyXG5cclxuJyA+JjM7Y2F0IDwmMyB8IGdyZXAgLXEgJ1wic3RhdHVzXCI6IFwiVVBcIicgJiYgZXhpdCAwIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdrZXljbG9hay1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfREFUQUJBU0V9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfREFUQUJBU0V9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWtleWNsb2FrfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"tags": [
"keycloak",
"identity",
@@ -1583,7 +1583,7 @@
"keycloak": {
"documentation": "https://www.keycloak.org?utm_source=coolify.io",
"slogan": "Keycloak is an open-source Identity and Access Management tool.",
- "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogcXVheS5pby9rZXljbG9hay9rZXljbG9hazoyNi4wCiAgICBjb21tYW5kOgogICAgICAtIHN0YXJ0CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fS0VZQ0xPQUtfODA4MAogICAgICAtIFRaPSR7VElNRVpPTkU6LVVUQ30KICAgICAgLSBLQ19CT09UU1RSQVBfQURNSU5fVVNFUk5BTUU9JHtTRVJWSUNFX1VTRVJfQURNSU59CiAgICAgIC0gS0NfQk9PVFNUUkFQX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0KICAgICAgLSBLQ19IT1NUTkFNRT0ke1NFUlZJQ0VfRlFETl9LRVlDTE9BS30KICAgICAgLSBLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9CiAgICAgIC0gS0NfSEVBTFRIX0VOQUJMRUQ9JHtLQ19IRUFMVEhfRU5BQkxFRDotdHJ1ZX0KICAgICAgLSBLQ19QUk9YWV9IRUFERVJTPSR7S0NfUFJPWFlfSEVBREVSUzoteGZvcndhcmRlZH0KICAgIHZvbHVtZXM6CiAgICAgIC0ga2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIFsKICAgICAgICAgICJDTUQtU0hFTEwiLAogICAgICAgICAgImV4ZWMgMzw+L2Rldi90Y3AvMTI3LjAuMC4xLzkwMDA7IGVjaG8gLWUgJ0dFVCAvaGVhbHRoL3JlYWR5IEhUVFAvMS4xXHJcbkhvc3Q6IGxvY2FsaG9zdDo5MDAwXHJcbkNvbm5lY3Rpb246IGNsb3NlXHJcblxyXG4nID4mMztjYXQgPCYzIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjogXCJVUFwiJyAmJiBleGl0IDAgfHwgZXhpdCAxIiwKICAgICAgICBdCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTA=",
+ "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMScKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tDX0JPT1RTVFJBUF9BRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0tDX0JPT1RTVFJBUF9BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdLQ19IT1NUTkFNRT0ke1NFUlZJQ0VfRlFETl9LRVlDTE9BS30nCiAgICAgIC0gJ0tDX0hUVFBfRU5BQkxFRD0ke0tDX0hUVFBfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0tDX0hFQUxUSF9FTkFCTEVEPSR7S0NfSEVBTFRIX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19QUk9YWV9IRUFERVJTPSR7S0NfUFJPWFlfSEVBREVSUzoteGZvcndhcmRlZH0nCiAgICB2b2x1bWVzOgogICAgICAtICdrZXljbG9hay1kYXRhOi9vcHQva2V5Y2xvYWsvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDsgZWNobyAtZSAnR0VUIC9oZWFsdGgvcmVhZHkgSFRUUC8xLjFcclxuSG9zdDogbG9jYWxob3N0OjkwMDBcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2NhdCA8JjMgfCBncmVwIC1xICdcInN0YXR1c1wiOiBcIlVQXCInICYmIGV4aXQgMCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"tags": [
"keycloak",
"identity",
@@ -3421,4 +3421,4 @@
"minversion": "0.0.0",
"port": "3000"
}
-}
\ No newline at end of file
+}
From 806d892031b1da544747567d7c6dd7f81b189e08 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 11:43:10 +0100
Subject: [PATCH 166/180] refactor(application): streamline healthcheck parsing
from Dockerfile
---
app/Jobs/ApplicationDeploymentJob.php | 5 ++-
app/Livewire/Project/Application/General.php | 3 ++
app/Livewire/Project/New/SimpleDockerfile.php | 2 +-
app/Models/Application.php | 22 +++++++------
bootstrap/helpers/shared.php | 32 +++++++++++++++++++
5 files changed, 50 insertions(+), 14 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 530136378..92186953b 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1211,7 +1211,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->container_name) {
$counter = 1;
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
- if ($this->full_healthcheck_url) {
+ if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
@@ -1718,8 +1718,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'save' => 'dockerfile_from_repo',
'ignore_errors' => true,
]);
- $dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
- $this->application->parseHealthcheckFromDockerfile($dockerfile);
+ $this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
}
$docker_compose = [
'services' => [
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 08fff38c6..1d58ed33a 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -369,6 +369,9 @@ class General extends Component
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
}
+ if ($this->application->isDirty('dockerfile')) {
+ $this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
+ }
$this->checkFqdns();
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index c3ed6039a..ebc9878dc 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -74,7 +74,7 @@ CMD ["nginx", "-g", "daemon off;"]
'fqdn' => $fqdn,
]);
- $application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true);
+ $application->parseHealthcheckFromDockerfile(dockerfile: $this->dockerfile, isInit: true);
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
diff --git a/app/Models/Application.php b/app/Models/Application.php
index ad688c93f..57a69423d 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1507,6 +1507,7 @@ class Application extends BaseModel
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
{
+ $dockerfile = str($dockerfile)->trim()->explode("\n");
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
$healthcheckCommand = null;
$lines = $dockerfile->toArray();
@@ -1526,23 +1527,24 @@ class Application extends BaseModel
}
}
if (str($healthcheckCommand)->isNotEmpty()) {
- $interval = str($healthcheckCommand)->match('/--interval=(\d+)/');
- $timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/');
- $start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/');
- $start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/');
+ $interval = str($healthcheckCommand)->match('/--interval=([0-9]+[a-zµ]*)/');
+ $timeout = str($healthcheckCommand)->match('/--timeout=([0-9]+[a-zµ]*)/');
+ $start_period = str($healthcheckCommand)->match('/--start-period=([0-9]+[a-zµ]*)/');
+ $start_interval = str($healthcheckCommand)->match('/--start-interval=([0-9]+[a-zµ]*)/');
$retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
+
if ($interval->isNotEmpty()) {
- $this->health_check_interval = $interval->toInteger();
+ $this->health_check_interval = parseDockerfileInterval($interval);
}
if ($timeout->isNotEmpty()) {
- $this->health_check_timeout = $timeout->toInteger();
+ $this->health_check_timeout = parseDockerfileInterval($timeout);
}
if ($start_period->isNotEmpty()) {
- $this->health_check_start_period = $start_period->toInteger();
+ $this->health_check_start_period = parseDockerfileInterval($start_period);
+ }
+ if ($start_interval->isNotEmpty()) {
+ $this->health_check_start_interval = parseDockerfileInterval($start_interval);
}
- // if ($start_interval) {
- // $this->health_check_start_interval = $start_interval->value();
- // }
if ($retries->isNotEmpty()) {
$this->health_check_retries = $retries->toInteger();
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index db3085649..60e71cd9c 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -4137,3 +4137,35 @@ function getJobStatus(?string $jobId = null)
return $jobFound->first()->status;
}
+
+function parseDockerfileInterval(string $something)
+{
+ $value = preg_replace('/[^0-9]/', '', $something);
+ $unit = preg_replace('/[0-9]/', '', $something);
+
+ // Default to seconds if no unit specified
+ $unit = $unit ?: 's';
+
+ // Convert to seconds based on unit
+ $seconds = (int) $value;
+ switch ($unit) {
+ case 'ns':
+ $seconds = (int) ($value / 1000000000);
+ break;
+ case 'us':
+ case 'µs':
+ $seconds = (int) ($value / 1000000);
+ break;
+ case 'ms':
+ $seconds = (int) ($value / 1000);
+ break;
+ case 'm':
+ $seconds = (int) ($value * 60);
+ break;
+ case 'h':
+ $seconds = (int) ($value * 3600);
+ break;
+ }
+
+ return $seconds;
+}
From 5e6c112fccc032822cb558878ffd2b718d495759 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 14:29:17 +0100
Subject: [PATCH 167/180] feat(github): enhance repository loading and
validation in applications
---
.../Api/ApplicationsController.php | 23 ++++++++++++++++++
.../Project/New/GithubPrivateRepository.php | 23 +++++-------------
bootstrap/helpers/github.php | 24 +++++++++++++++++++
3 files changed, 53 insertions(+), 17 deletions(-)
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 2aa656320..cbeb6b55d 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -932,10 +932,31 @@ class ApplicationsController extends Controller
if (! $githubApp) {
return response()->json(['message' => 'Github App not found.'], 404);
}
+ $token = generateGithubInstallationToken($githubApp);
+ if (! $token) {
+ return response()->json(['message' => 'Failed to generate Github App token.'], 400);
+ }
+
+ $repositories = collect();
+ $page = 1;
+ $repositories = loadRepositoryByPage($githubApp, $token, $page);
+ if ($repositories['total_count'] > 0) {
+ while (count($repositories['repositories']) < $repositories['total_count']) {
+ $page++;
+ $repositories = loadRepositoryByPage($githubApp, $token, $page);
+ }
+ }
+
$gitRepository = $request->git_repository;
if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) {
$gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', '');
}
+ $gitRepositoryFound = collect($repositories['repositories'])->firstWhere('full_name', $gitRepository);
+ if (! $gitRepositoryFound) {
+ return response()->json(['message' => 'Repository not found.'], 404);
+ }
+ $repository_project_id = data_get($gitRepositoryFound, 'id');
+
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
@@ -966,6 +987,8 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id;
+ $application->repository_project_id = $repository_project_id;
+
$application->save();
$application->refresh();
if (isset($useBuildServer)) {
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 4a81d841f..b1b0aef15 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -106,11 +106,15 @@ class GithubPrivateRepository extends Component
$this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first();
$this->token = generateGithubInstallationToken($this->github_app);
- $this->loadRepositoryByPage();
+ $repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
+ $this->total_repositories_count = $repositories['total_count'];
+ $this->repositories = $this->repositories->concat(collect($repositories['repositories']));
if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) {
$this->page++;
- $this->loadRepositoryByPage();
+ $repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
+ $this->total_repositories_count = $repositories['total_count'];
+ $this->repositories = $this->repositories->concat(collect($repositories['repositories']));
}
}
$this->repositories = $this->repositories->sortBy('name');
@@ -120,21 +124,6 @@ class GithubPrivateRepository extends Component
$this->current_step = 'repository';
}
- protected function loadRepositoryByPage()
- {
- $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
- $json = $response->json();
- if ($response->status() !== 200) {
- return $this->dispatch('error', $json['message']);
- }
-
- if ($json['total_count'] === 0) {
- return;
- }
- $this->total_repositories_count = $json['total_count'];
- $this->repositories = $this->repositories->concat(collect($json['repositories']));
- }
-
public function loadBranches()
{
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 3a3f6e7b2..81f8ff18a 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -129,3 +129,27 @@ function getPermissionsPath(GithubApp $source)
return "$github->html_url/settings/apps/$name/permissions";
}
+
+function loadRepositoryByPage(GithubApp $source, string $token, int $page)
+{
+ $response = Http::withToken($token)->get("{$source->api_url}/installation/repositories?per_page=100&page={$page}");
+ $json = $response->json();
+ if ($response->status() !== 200) {
+ return [
+ 'total_count' => 0,
+ 'repositories' => [],
+ ];
+ }
+
+ if ($json['total_count'] === 0) {
+ return [
+ 'total_count' => 0,
+ 'repositories' => [],
+ ];
+ }
+
+ return [
+ 'total_count' => $json['total_count'],
+ 'repositories' => $json['repositories'],
+ ];
+}
From e7f32a1c443a5bec4af2fe08dd478d2326dedf9b Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 17:55:10 +0100
Subject: [PATCH 168/180] refactor(notifications): standardize getRecipients
method signatures
---
app/Models/InstanceSettings.php | 22 +++++++++------------
app/Models/Team.php | 13 +++++++-----
app/Models/User.php | 4 ++--
app/Notifications/Channels/EmailChannel.php | 2 +-
app/Notifications/Channels/SendsEmail.php | 2 +-
app/Notifications/Notification.php | 22 +++++++++++++++++++++
6 files changed, 43 insertions(+), 22 deletions(-)
create mode 100644 app/Notifications/Notification.php
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index 5b89bb401..ac95bb8a9 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -3,16 +3,12 @@
namespace App\Models;
use App\Jobs\PullHelperImageJob;
-use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Notifications\Notifiable;
use Spatie\Url\Url;
-class InstanceSettings extends Model implements SendsEmail
+class InstanceSettings extends Model
{
- use Notifiable;
-
protected $guarded = [];
protected $casts = [
@@ -92,15 +88,15 @@ class InstanceSettings extends Model implements SendsEmail
return InstanceSettings::findOrFail(0);
}
- public function getRecipients($notification)
- {
- $recipients = data_get($notification, 'emails', null);
- if (is_null($recipients) || $recipients === '') {
- return [];
- }
+ // public function getRecipients($notification)
+ // {
+ // $recipients = data_get($notification, 'emails', null);
+ // if (is_null($recipients) || $recipients === '') {
+ // return [];
+ // }
- return explode(',', $recipients);
- }
+ // return explode(',', $recipients);
+ // }
public function getTitleDisplayName(): string
{
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 6796b22ad..d36f8c1ab 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -163,14 +163,17 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
];
}
- public function getRecipients($notification)
+ public function getRecipients(): array
{
- $recipients = data_get($notification, 'emails', null);
- if (is_null($recipients)) {
- return $this->members()->pluck('email')->toArray();
+ $recipients = $this->members()->pluck('email')->toArray();
+ $validatedEmails = array_filter($recipients, function ($email) {
+ return filter_var($email, FILTER_VALIDATE_EMAIL);
+ });
+ if (is_null($validatedEmails)) {
+ return [];
}
- return explode(',', $recipients);
+ return array_values($validatedEmails);
}
public function isAnyNotificationEnabled()
diff --git a/app/Models/User.php b/app/Models/User.php
index f59f506fc..f9515ad09 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -116,9 +116,9 @@ class User extends Authenticatable implements SendsEmail
return $this->belongsToMany(Team::class)->withPivot('role');
}
- public function getRecipients($notification)
+ public function getRecipients(): array
{
- return $this->email;
+ return [$this->email];
}
public function sendVerificationEmail()
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index 215fae4ea..2a18a2d34 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -13,7 +13,7 @@ class EmailChannel
{
try {
$this->bootConfigs($notifiable);
- $recipients = $notifiable->getRecipients($notification);
+ $recipients = $notifiable->getRecipients();
if (count($recipients) === 0) {
throw new Exception('No email recipients found');
}
diff --git a/app/Notifications/Channels/SendsEmail.php b/app/Notifications/Channels/SendsEmail.php
index 3adc6d0a2..7039a3066 100644
--- a/app/Notifications/Channels/SendsEmail.php
+++ b/app/Notifications/Channels/SendsEmail.php
@@ -4,5 +4,5 @@ namespace App\Notifications\Channels;
interface SendsEmail
{
- public function getRecipients($notification);
+ public function getRecipients(): array;
}
diff --git a/app/Notifications/Notification.php b/app/Notifications/Notification.php
new file mode 100644
index 000000000..d37716a8b
--- /dev/null
+++ b/app/Notifications/Notification.php
@@ -0,0 +1,22 @@
+
Date: Mon, 24 Mar 2025 18:00:31 +0100
Subject: [PATCH 169/180] fix(console): handle missing root user in password
reset command
---
app/Console/Commands/RootResetPassword.php | 5 +++++
other/nightly/install.sh | 2 +-
scripts/install.sh | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php
index 8d440ebd7..436363d06 100644
--- a/app/Console/Commands/RootResetPassword.php
+++ b/app/Console/Commands/RootResetPassword.php
@@ -40,6 +40,11 @@ class RootResetPassword extends Command
$this->info('Updating root password...');
try {
$user = User::find(0);
+ if (! $user) {
+ $this->error('Root user not found.');
+
+ return;
+ }
$user->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
diff --git a/other/nightly/install.sh b/other/nightly/install.sh
index 944012f86..38f1c3919 100755
--- a/other/nightly/install.sh
+++ b/other/nightly/install.sh
@@ -481,7 +481,7 @@ if ! [ -x "$(command -v docker)" ]; then
dnf install docker -y >/dev/null 2>&1
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
- curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
+ curl -sL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
diff --git a/scripts/install.sh b/scripts/install.sh
index 676e66d8e..4abe98dc7 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -481,7 +481,7 @@ if ! [ -x "$(command -v docker)" ]; then
dnf install docker -y >/dev/null 2>&1
DOCKER_CONFIG=${DOCKER_CONFIG:-/usr/local/lib/docker}
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
- curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
+ curl -sL "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
From 7b8e2e7175c4fef4039eacd34709b223e90d55d5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 20:29:54 +0100
Subject: [PATCH 170/180] fix(ssl): handle missing CA certificate in SSL
regeneration job
---
app/Jobs/RegenerateSslCertJob.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php
index 0570227b6..cf598c75c 100644
--- a/app/Jobs/RegenerateSslCertJob.php
+++ b/app/Jobs/RegenerateSslCertJob.php
@@ -49,6 +49,11 @@ class RegenerateSslCertJob implements ShouldQueue
->where('is_ca_certificate', true)
->first();
+ if (! $caCert) {
+ Log::error("No CA certificate found for server_id: {$certificate->server_id}");
+
+ return;
+ }
SSLHelper::generateSslCertificate(
commonName: $certificate->common_name,
subjectAlternativeNames: $certificate->subject_alternative_names,
From d01889a0c2d20ccd20330d6ecbc20d38a0dd4ae1 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 24 Mar 2025 20:33:25 +0100
Subject: [PATCH 171/180] fix(copy-button): ensure text is safely passed to
clipboard
---
resources/views/components/forms/copy-button.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/components/forms/copy-button.blade.php b/resources/views/components/forms/copy-button.blade.php
index 5ed41fe1a..39be2bc6a 100644
--- a/resources/views/components/forms/copy-button.blade.php
+++ b/resources/views/components/forms/copy-button.blade.php
@@ -3,7 +3,7 @@