mirror of
https://github.com/tiennm99/coolify.git
synced 2026-09-02 12:19:57 +00:00
Merge remote-tracking branch 'origin/next' into audit-policies
This commit is contained in:
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Actions\Application\LoadComposeFile;
|
||||
use App\Actions\Application\StopApplication;
|
||||
use App\Actions\Service\StartService;
|
||||
use App\Enums\BuildPackTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
@@ -18,7 +17,6 @@ use App\Models\LocalPersistentVolume;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Services\DockerImageParser;
|
||||
@@ -159,7 +157,7 @@ class ApplicationsController extends Controller
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
@@ -328,7 +326,7 @@ class ApplicationsController extends Controller
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
|
||||
@@ -494,7 +492,7 @@ class ApplicationsController extends Controller
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
|
||||
@@ -656,7 +654,7 @@ class ApplicationsController extends Controller
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['dockerfile'], 'description' => 'The build pack type.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
@@ -903,105 +901,6 @@ class ApplicationsController extends Controller
|
||||
return $this->create_application($request, 'dockerimage');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is an unstable duplicate of POST /api/v1/services.
|
||||
*/
|
||||
#[OA\Post(
|
||||
summary: 'Create (Docker Compose)',
|
||||
description: 'Deprecated: Use POST /api/v1/services instead.',
|
||||
path: '/applications/dockercompose',
|
||||
operationId: 'create-dockercompose-application',
|
||||
deprecated: true,
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Application object that needs to be created.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
|
||||
properties: [
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Application created successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Domain conflicts detected.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'],
|
||||
'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'],
|
||||
'conflicts' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'domain' => ['type' => 'string', 'example' => 'example.com'],
|
||||
'resource_name' => ['type' => 'string', 'example' => 'My Application'],
|
||||
'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'],
|
||||
'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'],
|
||||
'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_dockercompose_application(Request $request)
|
||||
{
|
||||
return $this->create_application($request, 'dockercompose');
|
||||
}
|
||||
|
||||
private function create_application(Request $request, $type)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
@@ -1313,6 +1212,15 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'application_name' => data_get($application, 'name'),
|
||||
'application_type' => $type,
|
||||
'build_pack' => data_get($application, 'build_pack'),
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
@@ -1543,6 +1451,15 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'application_name' => data_get($application, 'name'),
|
||||
'application_type' => $type,
|
||||
'build_pack' => data_get($application, 'build_pack'),
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
@@ -1743,6 +1660,15 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'application_name' => data_get($application, 'name'),
|
||||
'application_type' => $type,
|
||||
'build_pack' => data_get($application, 'build_pack'),
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
@@ -1850,6 +1776,15 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'application_name' => data_get($application, 'name'),
|
||||
'application_type' => $type,
|
||||
'build_pack' => data_get($application, 'build_pack'),
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
@@ -1960,93 +1895,19 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.application.created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => data_get($application, 'uuid'),
|
||||
'application_name' => data_get($application, 'name'),
|
||||
'application_type' => $type,
|
||||
'build_pack' => data_get($application, 'build_pack'),
|
||||
'instant_deploy' => (bool) ($instantDeploy ?? false),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
]))->setStatusCode(201);
|
||||
} elseif ($type === 'dockercompose') {
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$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);
|
||||
}
|
||||
if (! $request->has('name')) {
|
||||
$request->offsetSet('name', 'service'.new Cuid2);
|
||||
}
|
||||
$validationRules = [
|
||||
'docker_compose_raw' => 'string|required',
|
||||
];
|
||||
$validationRules = array_merge(sharedDataApplications(), $validationRules);
|
||||
$validator = customApiValidator($request->all(), $validationRules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
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, 'UTF-8', 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;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
$service->fill($request->only($allowedFields));
|
||||
|
||||
$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();
|
||||
if (isset($isContainerLabelEscapeEnabled)) {
|
||||
$service->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
|
||||
}
|
||||
$service->save();
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
// Apply service-specific application prerequisites
|
||||
applyServiceApplicationPrerequisites($service);
|
||||
|
||||
if ($instantDeploy) {
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => data_get($service, 'uuid'),
|
||||
'domains' => data_get($service, 'domains'),
|
||||
]))->setStatusCode(201);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Invalid type.'], 400);
|
||||
@@ -2301,6 +2162,12 @@ class ApplicationsController extends Controller
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
auditLog('api.application.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Application deletion request queued.',
|
||||
]);
|
||||
@@ -2343,7 +2210,7 @@ class ApplicationsController extends Controller
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
|
||||
@@ -2800,6 +2667,13 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$application->save();
|
||||
|
||||
auditLog('api.application.updated', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
@@ -3052,6 +2926,14 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$env->save();
|
||||
|
||||
auditLog('api.application.env_updated', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
'is_preview' => (bool) $is_preview,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
} else {
|
||||
return response()->json([
|
||||
@@ -3085,6 +2967,14 @@ class ApplicationsController extends Controller
|
||||
}
|
||||
$env->save();
|
||||
|
||||
auditLog('api.application.env_updated', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
'is_preview' => (bool) $is_preview,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
} else {
|
||||
return response()->json([
|
||||
@@ -3311,6 +3201,12 @@ class ApplicationsController extends Controller
|
||||
$returnedEnvs->push($this->removeSensitiveData($env));
|
||||
}
|
||||
|
||||
auditLog('api.application.env_bulk_upserted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_count' => $returnedEnvs->count(),
|
||||
]);
|
||||
|
||||
return response()->json($returnedEnvs)->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -3450,6 +3346,14 @@ class ApplicationsController extends Controller
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
|
||||
auditLog('api.application.env_created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
'is_preview' => (bool) $is_preview,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $env->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -3475,6 +3379,14 @@ class ApplicationsController extends Controller
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
|
||||
auditLog('api.application.env_created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
'is_preview' => (bool) $is_preview,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $env->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -3566,8 +3478,17 @@ class ApplicationsController extends Controller
|
||||
'message' => 'Environment variable not found.',
|
||||
], 404);
|
||||
}
|
||||
$envKey = $found_env->key;
|
||||
$envUuid = $found_env->uuid;
|
||||
$found_env->forceDelete();
|
||||
|
||||
auditLog('api.application.env_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'env_uuid' => $envUuid,
|
||||
'env_key' => $envKey,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Environment variable deleted.',
|
||||
]);
|
||||
@@ -3679,6 +3600,15 @@ class ApplicationsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
auditLog('api.application.deployed', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'force_rebuild' => $force,
|
||||
'instant_deploy' => $instant_deploy,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Deployment request queued.',
|
||||
@@ -3767,6 +3697,13 @@ class ApplicationsController extends Controller
|
||||
$dockerCleanup = $request->boolean('docker_cleanup', true);
|
||||
StopApplication::dispatch($application, false, $dockerCleanup);
|
||||
|
||||
auditLog('api.application.stopped', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'docker_cleanup' => $dockerCleanup,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Application stopping request queued.',
|
||||
@@ -3857,6 +3794,13 @@ class ApplicationsController extends Controller
|
||||
], 200);
|
||||
}
|
||||
|
||||
auditLog('api.application.restarted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Restart request queued.',
|
||||
@@ -4125,7 +4069,7 @@ class ApplicationsController extends Controller
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
@@ -4225,6 +4169,15 @@ class ApplicationsController extends Controller
|
||||
|
||||
$storage->save();
|
||||
|
||||
auditLog('api.application.storage_updated', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
@@ -4303,7 +4256,7 @@ class ApplicationsController extends Controller
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
@@ -4403,6 +4356,15 @@ class ApplicationsController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
auditLog('api.application.storage_created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path,
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
@@ -4476,8 +4438,18 @@ class ApplicationsController extends Controller
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storageType = $storage instanceof LocalFileVolume ? 'file' : 'persistent';
|
||||
$storageMountPath = $storage->mount_path ?? null;
|
||||
$storage->delete();
|
||||
|
||||
auditLog('api.application.storage_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'storage_uuid' => $storageUuid,
|
||||
'storage_type' => $storageType,
|
||||
'mount_path' => $storageMountPath,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
|
||||
@@ -4547,6 +4519,12 @@ class ApplicationsController extends Controller
|
||||
$preview->delete();
|
||||
CleanupPreviewDeployment::run($application, $pullRequestId, $preview);
|
||||
|
||||
auditLog('api.application.preview_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'pull_request_id' => $pullRequestId,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Preview deletion request queued.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CloudProviderToken;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -246,7 +247,7 @@ class CloudProviderTokensController extends Controller
|
||||
$this->authorize('create', [CloudProviderToken::class]);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -288,6 +289,13 @@ class CloudProviderTokensController extends Controller
|
||||
'name' => $body['name'],
|
||||
]);
|
||||
|
||||
auditLog('api.cloud_token.created', [
|
||||
'team_id' => $teamId,
|
||||
'cloud_token_uuid' => $cloudProviderToken->uuid,
|
||||
'cloud_token_name' => $cloudProviderToken->name,
|
||||
'provider' => $cloudProviderToken->provider,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $cloudProviderToken->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -357,7 +365,7 @@ class CloudProviderTokensController extends Controller
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -392,6 +400,14 @@ class CloudProviderTokensController extends Controller
|
||||
|
||||
$token->update(array_intersect_key($body, array_flip($allowedFields)));
|
||||
|
||||
auditLog('api.cloud_token.updated', [
|
||||
'team_id' => $teamId,
|
||||
'cloud_token_uuid' => $token->uuid,
|
||||
'cloud_token_name' => $token->name,
|
||||
'provider' => $token->provider,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($body))),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $token->uuid,
|
||||
]);
|
||||
@@ -468,8 +484,18 @@ class CloudProviderTokensController extends Controller
|
||||
return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400);
|
||||
}
|
||||
|
||||
$tokenUuid = $token->uuid;
|
||||
$tokenName = $token->name;
|
||||
$tokenProvider = $token->provider;
|
||||
$token->delete();
|
||||
|
||||
auditLog('api.cloud_token.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'cloud_token_uuid' => $tokenUuid,
|
||||
'cloud_token_name' => $tokenName,
|
||||
'provider' => $tokenProvider,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Cloud provider token deleted.']);
|
||||
}
|
||||
|
||||
|
||||
@@ -379,9 +379,9 @@ class DatabasesController extends Controller
|
||||
case 'standalone-postgresql':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'postgres_user' => 'string',
|
||||
'postgres_password' => 'string',
|
||||
'postgres_db' => 'string',
|
||||
'postgres_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'postgres_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'postgres_db' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'postgres_initdb_args' => 'string',
|
||||
'postgres_host_auth_method' => 'string',
|
||||
'postgres_conf' => 'string',
|
||||
@@ -410,20 +410,20 @@ class DatabasesController extends Controller
|
||||
case 'standalone-clickhouse':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'clickhouse_admin_user' => 'string',
|
||||
'clickhouse_admin_password' => 'string',
|
||||
'clickhouse_admin_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'clickhouse_admin_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
]);
|
||||
break;
|
||||
case 'standalone-dragonfly':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'dragonfly_password' => 'string',
|
||||
'dragonfly_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
]);
|
||||
break;
|
||||
case 'standalone-redis':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'redis_password' => 'string',
|
||||
'redis_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'redis_conf' => 'string',
|
||||
]);
|
||||
if ($request->has('redis_conf')) {
|
||||
@@ -450,7 +450,7 @@ class DatabasesController extends Controller
|
||||
case 'standalone-keydb':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'keydb_password' => 'string',
|
||||
'keydb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'keydb_conf' => 'string',
|
||||
]);
|
||||
if ($request->has('keydb_conf')) {
|
||||
@@ -478,10 +478,10 @@ class DatabasesController extends Controller
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mariadb_conf' => 'string',
|
||||
'mariadb_root_password' => 'string',
|
||||
'mariadb_user' => 'string',
|
||||
'mariadb_password' => 'string',
|
||||
'mariadb_database' => 'string',
|
||||
'mariadb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mariadb_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mariadb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mariadb_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
]);
|
||||
if ($request->has('mariadb_conf')) {
|
||||
if (! isBase64Encoded($request->mariadb_conf)) {
|
||||
@@ -508,9 +508,9 @@ class DatabasesController extends Controller
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mongo_conf' => 'string',
|
||||
'mongo_initdb_root_username' => 'string',
|
||||
'mongo_initdb_root_password' => 'string',
|
||||
'mongo_initdb_database' => 'string',
|
||||
'mongo_initdb_root_username' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mongo_initdb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mongo_initdb_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
]);
|
||||
if ($request->has('mongo_conf')) {
|
||||
if (! isBase64Encoded($request->mongo_conf)) {
|
||||
@@ -537,10 +537,10 @@ class DatabasesController extends Controller
|
||||
case 'standalone-mysql':
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mysql_root_password' => 'string',
|
||||
'mysql_password' => 'string',
|
||||
'mysql_user' => 'string',
|
||||
'mysql_database' => 'string',
|
||||
'mysql_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mysql_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mysql_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mysql_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mysql_conf' => 'string',
|
||||
]);
|
||||
if ($request->has('mysql_conf')) {
|
||||
@@ -596,6 +596,14 @@ class DatabasesController extends Controller
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
|
||||
auditLog('api.database.updated', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $database->type(),
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Database updated.',
|
||||
]);
|
||||
@@ -639,10 +647,10 @@ class DatabasesController extends Controller
|
||||
'backup_now' => ['type' => 'boolean', 'description' => 'Whether to trigger backup immediately after creation'],
|
||||
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Number of backups to retain locally'],
|
||||
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Number of days to retain backups locally'],
|
||||
'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage (MB) for local backups'],
|
||||
'database_backup_retention_max_storage_locally' => ['type' => 'number', 'description' => 'Max storage (GB) for local backups'],
|
||||
'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Number of backups to retain in S3'],
|
||||
'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage (MB) for S3 backups'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'number', 'description' => 'Max storage (GB) for S3 backups'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600],
|
||||
],
|
||||
),
|
||||
@@ -703,10 +711,10 @@ class DatabasesController extends Controller
|
||||
'databases_to_backup' => 'string|nullable',
|
||||
'database_backup_retention_amount_locally' => 'integer|min:0',
|
||||
'database_backup_retention_days_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'numeric|min:0',
|
||||
'database_backup_retention_amount_s3' => 'integer|min:0',
|
||||
'database_backup_retention_days_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'numeric|min:0',
|
||||
'timeout' => 'integer|min:60|max:36000',
|
||||
]);
|
||||
|
||||
@@ -826,6 +834,15 @@ class DatabasesController extends Controller
|
||||
dispatch(new DatabaseBackupJob($backupConfig));
|
||||
}
|
||||
|
||||
auditLog('api.database.backup_created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'backup_uuid' => $backupConfig->uuid,
|
||||
'frequency' => $backupConfig->frequency,
|
||||
'save_s3' => (bool) $backupConfig->save_s3,
|
||||
'backup_now' => (bool) $request->backup_now,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $backupConfig->uuid,
|
||||
'message' => 'Backup configuration created successfully.',
|
||||
@@ -878,10 +895,10 @@ class DatabasesController extends Controller
|
||||
'frequency' => ['type' => 'string', 'description' => 'Frequency of the backup'],
|
||||
'database_backup_retention_amount_locally' => ['type' => 'integer', 'description' => 'Retention amount of the backup locally'],
|
||||
'database_backup_retention_days_locally' => ['type' => 'integer', 'description' => 'Retention days of the backup locally'],
|
||||
'database_backup_retention_max_storage_locally' => ['type' => 'integer', 'description' => 'Max storage of the backup locally'],
|
||||
'database_backup_retention_max_storage_locally' => ['type' => 'number', 'description' => 'Max storage of the backup locally'],
|
||||
'database_backup_retention_amount_s3' => ['type' => 'integer', 'description' => 'Retention amount of the backup in s3'],
|
||||
'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'integer', 'description' => 'Max storage of the backup in S3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'number', 'description' => 'Max storage of the backup in S3'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600],
|
||||
],
|
||||
),
|
||||
@@ -933,10 +950,10 @@ class DatabasesController extends Controller
|
||||
'frequency' => 'string',
|
||||
'database_backup_retention_amount_locally' => 'integer|min:0',
|
||||
'database_backup_retention_days_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_locally' => 'numeric|min:0',
|
||||
'database_backup_retention_amount_s3' => 'integer|min:0',
|
||||
'database_backup_retention_days_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'numeric|min:0',
|
||||
'timeout' => 'integer|min:60|max:36000',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
@@ -1045,6 +1062,14 @@ class DatabasesController extends Controller
|
||||
dispatch(new DatabaseBackupJob($backupConfig));
|
||||
}
|
||||
|
||||
auditLog('api.database.backup_updated', [
|
||||
'team_id' => $teamId,
|
||||
'backup_uuid' => $backupConfig->uuid,
|
||||
'database_id' => $backupConfig->database_id,
|
||||
'changed_fields' => array_values(array_intersect($backupConfigFields, array_keys($request->all()))),
|
||||
'backup_now' => (bool) $request->backup_now,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Database backup configuration updated',
|
||||
]);
|
||||
@@ -1724,9 +1749,9 @@ class DatabasesController extends Controller
|
||||
if ($type === NewDatabaseTypes::POSTGRESQL) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'postgres_user' => 'string',
|
||||
'postgres_password' => 'string',
|
||||
'postgres_db' => 'string',
|
||||
'postgres_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'postgres_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'postgres_db' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'postgres_initdb_args' => 'string',
|
||||
'postgres_host_auth_method' => 'string',
|
||||
'postgres_conf' => 'string',
|
||||
@@ -1779,12 +1804,25 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::MARIADB) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'clickhouse_admin_user' => 'string',
|
||||
'clickhouse_admin_password' => 'string',
|
||||
'mariadb_conf' => 'string',
|
||||
'mariadb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mariadb_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mariadb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mariadb_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -1835,14 +1873,24 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::MYSQL) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mysql_root_password' => 'string',
|
||||
'mysql_password' => 'string',
|
||||
'mysql_user' => 'string',
|
||||
'mysql_database' => 'string',
|
||||
'mysql_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mysql_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mysql_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mysql_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mysql_conf' => 'string',
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -1894,11 +1942,21 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::REDIS) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'redis_password' => 'string',
|
||||
'redis_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'redis_conf' => 'string',
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -1950,11 +2008,21 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::DRAGONFLY) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'dragonfly_password' => 'string',
|
||||
'dragonfly_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -1984,7 +2052,7 @@ class DatabasesController extends Controller
|
||||
} elseif ($type === NewDatabaseTypes::KEYDB) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'keydb_password' => 'string',
|
||||
'keydb_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'keydb_conf' => 'string',
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -2036,12 +2104,22 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'clickhouse_admin_user' => 'string',
|
||||
'clickhouse_admin_password' => 'string',
|
||||
'clickhouse_admin_user' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'clickhouse_admin_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -2072,14 +2150,24 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
} elseif ($type === NewDatabaseTypes::MONGODB) {
|
||||
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'mongo_conf' => 'string',
|
||||
'mongo_initdb_root_username' => 'string',
|
||||
'mongo_initdb_root_password' => 'string',
|
||||
'mongo_initdb_database' => 'string',
|
||||
'mongo_initdb_root_username' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
'mongo_initdb_root_password' => ValidationPatterns::databasePasswordRules(required: false),
|
||||
'mongo_initdb_database' => ValidationPatterns::databaseIdentifierRules(required: false),
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -2130,6 +2218,16 @@ class DatabasesController extends Controller
|
||||
$payload['external_db_url'] = $database->external_db_url;
|
||||
}
|
||||
|
||||
auditLog('api.database.created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $type->value,
|
||||
'server_uuid' => $serverUuid,
|
||||
'is_public' => (bool) $database->is_public,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -2214,6 +2312,13 @@ class DatabasesController extends Controller
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
auditLog('api.database.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $database->type(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Database deletion request queued.',
|
||||
]);
|
||||
@@ -2326,13 +2431,21 @@ class DatabasesController extends Controller
|
||||
$backup->delete();
|
||||
DB::commit();
|
||||
|
||||
auditLog('api.database.backup_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'backup_uuid' => $request->scheduled_backup_uuid,
|
||||
'delete_s3' => $deleteS3,
|
||||
'executions_deleted' => $executions->count(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Backup configuration and all executions deleted.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json(['message' => 'Failed to delete backup: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to delete backup.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2448,11 +2561,19 @@ class DatabasesController extends Controller
|
||||
|
||||
$execution->delete();
|
||||
|
||||
auditLog('api.database.backup_execution_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'backup_uuid' => $request->scheduled_backup_uuid,
|
||||
'execution_uuid' => $request->execution_uuid,
|
||||
'delete_s3' => $deleteS3,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Backup execution deleted.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => 'Failed to delete backup execution: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to delete backup execution.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2630,6 +2751,13 @@ class DatabasesController extends Controller
|
||||
}
|
||||
StartDatabase::dispatch($database);
|
||||
|
||||
auditLog('api.database.started', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $database->type(),
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Database starting request queued.',
|
||||
@@ -2721,6 +2849,14 @@ class DatabasesController extends Controller
|
||||
$dockerCleanup = $request->boolean('docker_cleanup', true);
|
||||
StopDatabase::dispatch($database, $dockerCleanup);
|
||||
|
||||
auditLog('api.database.stopped', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $database->type(),
|
||||
'docker_cleanup' => $dockerCleanup,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Database stopping request queued.',
|
||||
@@ -2798,6 +2934,13 @@ class DatabasesController extends Controller
|
||||
|
||||
RestartDatabase::dispatch($database);
|
||||
|
||||
auditLog('api.database.restarted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'database_name' => $database->name,
|
||||
'database_type' => $database->type(),
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Database restarting request queued.',
|
||||
@@ -3014,6 +3157,13 @@ class DatabasesController extends Controller
|
||||
}
|
||||
$env->save();
|
||||
|
||||
auditLog('api.database.env_updated', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveEnvData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -3142,6 +3292,12 @@ class DatabasesController extends Controller
|
||||
$updatedEnvs->push($this->removeSensitiveEnvData($env));
|
||||
}
|
||||
|
||||
auditLog('api.database.env_bulk_upserted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'env_count' => $updatedEnvs->count(),
|
||||
]);
|
||||
|
||||
return response()->json($updatedEnvs)->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -3263,6 +3419,13 @@ class DatabasesController extends Controller
|
||||
'comment' => $request->comment ?? null,
|
||||
]);
|
||||
|
||||
auditLog('api.database.env_created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveEnvData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -3348,8 +3511,17 @@ class DatabasesController extends Controller
|
||||
return response()->json(['message' => 'Environment variable not found.'], 404);
|
||||
}
|
||||
|
||||
$envKey = $env->key;
|
||||
$envUuid = $env->uuid;
|
||||
$env->forceDelete();
|
||||
|
||||
auditLog('api.database.env_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'env_uuid' => $envUuid,
|
||||
'env_key' => $envKey,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Environment variable deleted.']);
|
||||
}
|
||||
|
||||
@@ -3496,7 +3668,7 @@ class DatabasesController extends Controller
|
||||
'type' => 'required|string|in:persistent,file',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
@@ -3596,6 +3768,15 @@ class DatabasesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
auditLog('api.database.storage_created', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path,
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
@@ -3694,7 +3875,7 @@ class DatabasesController extends Controller
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
@@ -3794,6 +3975,15 @@ class DatabasesController extends Controller
|
||||
|
||||
$storage->save();
|
||||
|
||||
auditLog('api.database.storage_updated', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
@@ -3867,8 +4057,18 @@ class DatabasesController extends Controller
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storageType = $storage instanceof LocalFileVolume ? 'file' : 'persistent';
|
||||
$storageMountPath = $storage->mount_path ?? null;
|
||||
$storage->delete();
|
||||
|
||||
auditLog('api.database.storage_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'database_uuid' => $database->uuid,
|
||||
'storage_uuid' => $storageUuid,
|
||||
'storage_type' => $storageType,
|
||||
'mount_path' => $storageMountPath,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +281,14 @@ class DeployController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
auditLog('api.deployment.cancelled', [
|
||||
'team_id' => $teamId,
|
||||
'deployment_uuid' => $deployment->deployment_uuid,
|
||||
'application_id' => $application?->id,
|
||||
'application_uuid' => $application?->uuid,
|
||||
'server_id' => $deployment->server_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Deployment cancelled successfully.',
|
||||
'deployment_uuid' => $deployment->deployment_uuid,
|
||||
@@ -518,6 +526,14 @@ class DeployController extends Controller
|
||||
$message = $result['message'];
|
||||
} else {
|
||||
$message = "Application {$resource->name} deployment queued.";
|
||||
auditLog('api.deployment.triggered', [
|
||||
'resource_type' => 'application',
|
||||
'application_uuid' => $resource->uuid,
|
||||
'application_name' => $resource->name,
|
||||
'deployment_uuid' => $deployment_uuid?->toString(),
|
||||
'force_rebuild' => $force,
|
||||
'pull_request_id' => $pr,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case Service::class:
|
||||
@@ -529,6 +545,10 @@ class DeployController extends Controller
|
||||
}
|
||||
StartService::run($resource);
|
||||
$message = "Service {$resource->name} started. It could take a while, be patient.";
|
||||
auditLog('api.service.deployed', [
|
||||
'service_uuid' => $resource->uuid,
|
||||
'service_name' => $resource->name,
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
// Database resource - check authorization
|
||||
@@ -543,6 +563,11 @@ class DeployController extends Controller
|
||||
$resource->save();
|
||||
|
||||
$message = "Database {$resource->name} started.";
|
||||
auditLog('api.database.started', [
|
||||
'database_uuid' => $resource->uuid,
|
||||
'database_name' => $resource->name,
|
||||
'database_type' => $resource->getMorphClass(),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,12 @@ class GithubController extends Controller
|
||||
|
||||
$githubApp = GithubApp::create($payload);
|
||||
|
||||
auditLog('api.github_app.created', [
|
||||
'team_id' => $teamId,
|
||||
'github_app_uuid' => $githubApp->uuid,
|
||||
'github_app_name' => $githubApp->name,
|
||||
]);
|
||||
|
||||
return response()->json($githubApp, 201);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
@@ -652,6 +658,13 @@ class GithubController extends Controller
|
||||
// Update the GitHub app
|
||||
$githubApp->update($payload);
|
||||
|
||||
auditLog('api.github_app.updated', [
|
||||
'team_id' => $teamId,
|
||||
'github_app_uuid' => $githubApp->uuid,
|
||||
'github_app_name' => $githubApp->name,
|
||||
'changed_fields' => array_values(array_diff($allowedFields, ['client_secret', 'webhook_secret', 'private_key_uuid'])),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitHub app updated successfully',
|
||||
'data' => $githubApp,
|
||||
@@ -737,8 +750,16 @@ class GithubController extends Controller
|
||||
], 409);
|
||||
}
|
||||
|
||||
$deletedUuid = $githubApp->uuid;
|
||||
$deletedName = $githubApp->name;
|
||||
$githubApp->delete();
|
||||
|
||||
auditLog('api.github_app.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'github_app_uuid' => $deletedUuid,
|
||||
'github_app_name' => $deletedName,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'GitHub app deleted successfully',
|
||||
]);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Exceptions\RateLimitException;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -12,6 +13,7 @@ use App\Models\Team;
|
||||
use App\Rules\ValidCloudInitYaml;
|
||||
use App\Rules\ValidHostname;
|
||||
use App\Services\HetznerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
@@ -121,7 +123,7 @@ class HetznerController extends Controller
|
||||
|
||||
return response()->json($locations);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch locations: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner locations.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +244,7 @@ class HetznerController extends Controller
|
||||
|
||||
return response()->json($serverTypes);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch server types: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner server types.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +356,7 @@ class HetznerController extends Controller
|
||||
|
||||
return response()->json(array_values($filtered));
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch images: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner images.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +452,7 @@ class HetznerController extends Controller
|
||||
|
||||
return response()->json($sshKeys);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch SSH keys: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner SSH keys.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +553,7 @@ class HetznerController extends Controller
|
||||
$this->authorize('create', [Server::class]);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -718,9 +720,17 @@ class HetznerController extends Controller
|
||||
|
||||
// Validate server if requested
|
||||
if ($request->instant_validate) {
|
||||
\App\Actions\Server\ValidateServer::dispatch($server);
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
auditLog('api.hetzner_server.created', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'hetzner_server_id' => $hetznerServer['id'],
|
||||
'ip' => $ipAddress,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $server->uuid,
|
||||
'hetzner_server_id' => $hetznerServer['id'],
|
||||
@@ -734,7 +744,7 @@ class HetznerController extends Controller
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to create server: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to create Hetzner server.'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +85,15 @@ class OtherController extends Controller
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
auditLog('api.instance.enable_denied', ['team_id' => $teamId], 'warning');
|
||||
|
||||
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$settings->update(['is_api_enabled' => true]);
|
||||
|
||||
auditLog('api.instance.enabled', ['team_id' => $teamId]);
|
||||
|
||||
return response()->json(['message' => 'API enabled.'], 200);
|
||||
}
|
||||
|
||||
@@ -137,14 +141,130 @@ class OtherController extends Controller
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
auditLog('api.instance.disable_denied', ['team_id' => $teamId], 'warning');
|
||||
|
||||
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$settings->update(['is_api_enabled' => false]);
|
||||
|
||||
auditLog('api.instance.disabled', ['team_id' => $teamId]);
|
||||
|
||||
return response()->json(['message' => 'API disabled.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Enable MCP Server',
|
||||
description: 'Enable the MCP server endpoint at /mcp (only with root permissions).',
|
||||
path: '/mcp/enable',
|
||||
operationId: 'enable-mcp',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'MCP server enabled.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'MCP server enabled.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'You are not allowed to enable the MCP server.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the MCP server.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function enable_mcp(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
auditLog('api.mcp.enable_denied', ['team_id' => $teamId], 'warning');
|
||||
|
||||
return response()->json(['message' => 'You are not allowed to enable the MCP server.'], 403);
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$settings->update(['is_mcp_server_enabled' => true]);
|
||||
|
||||
auditLog('api.mcp.enabled', ['team_id' => $teamId]);
|
||||
|
||||
return response()->json(['message' => 'MCP server enabled.'], 200);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Disable MCP Server',
|
||||
description: 'Disable the MCP server endpoint at /mcp (only with root permissions).',
|
||||
path: '/mcp/disable',
|
||||
operationId: 'disable-mcp',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'MCP server disabled.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'MCP server disabled.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'You are not allowed to disable the MCP server.',
|
||||
content: new OA\JsonContent(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the MCP server.'),
|
||||
]
|
||||
)),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function disable_mcp(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
if ($teamId !== '0') {
|
||||
auditLog('api.mcp.disable_denied', ['team_id' => $teamId], 'warning');
|
||||
|
||||
return response()->json(['message' => 'You are not allowed to disable the MCP server.'], 403);
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$settings->update(['is_mcp_server_enabled' => false]);
|
||||
|
||||
auditLog('api.mcp.disabled', ['team_id' => $teamId]);
|
||||
|
||||
return response()->json(['message' => 'MCP server disabled.'], 200);
|
||||
}
|
||||
|
||||
public function feedback(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
|
||||
@@ -266,6 +266,12 @@ class ProjectController extends Controller
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
|
||||
auditLog('api.project.created', [
|
||||
'team_id' => $teamId,
|
||||
'project_uuid' => $project->uuid,
|
||||
'project_name' => $project->name,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $project->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -385,6 +391,13 @@ class ProjectController extends Controller
|
||||
|
||||
$project->update($request->only($allowedFields));
|
||||
|
||||
auditLog('api.project.updated', [
|
||||
'team_id' => $teamId,
|
||||
'project_uuid' => $project->uuid,
|
||||
'project_name' => $project->name,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $project->uuid,
|
||||
'name' => $project->name,
|
||||
@@ -464,8 +477,16 @@ class ProjectController extends Controller
|
||||
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
$projectUuid = $project->uuid;
|
||||
$projectName = $project->name;
|
||||
$project->delete();
|
||||
|
||||
auditLog('api.project.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'project_uuid' => $projectUuid,
|
||||
'project_name' => $projectName,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Project deleted.']);
|
||||
}
|
||||
|
||||
@@ -646,6 +667,13 @@ class ProjectController extends Controller
|
||||
'name' => $request->name,
|
||||
]);
|
||||
|
||||
auditLog('api.project.environment_created', [
|
||||
'team_id' => $teamId,
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $environment->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -729,8 +757,17 @@ class ProjectController extends Controller
|
||||
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
|
||||
}
|
||||
|
||||
$envUuid = $environment->uuid;
|
||||
$envName = $environment->name;
|
||||
$environment->delete();
|
||||
|
||||
auditLog('api.project.environment_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $envUuid,
|
||||
'environment_name' => $envName,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Environment deleted.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
@@ -33,7 +34,7 @@ class ScheduledTasksController extends Controller
|
||||
return Service::whereRelation('environment.project.team', 'id', $teamId)->where('uuid', $request->uuid)->first();
|
||||
}
|
||||
|
||||
private function listTasks(Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
private function listTasks(Application|Service $resource): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
@@ -44,12 +45,12 @@ class ScheduledTasksController extends Controller
|
||||
return response()->json($tasks);
|
||||
}
|
||||
|
||||
private function createTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
private function createTask(Request $request, Application|Service $resource): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -105,15 +106,23 @@ class ScheduledTasksController extends Controller
|
||||
|
||||
$task->save();
|
||||
|
||||
auditLog('api.scheduled_task.created', [
|
||||
'team_id' => $teamId,
|
||||
'task_uuid' => $task->uuid,
|
||||
'task_name' => $task->name,
|
||||
'resource_type' => $resource instanceof Application ? 'application' : 'service',
|
||||
'resource_uuid' => $resource->uuid,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 201);
|
||||
}
|
||||
|
||||
private function updateTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
private function updateTask(Request $request, Application|Service $resource): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -161,22 +170,43 @@ class ScheduledTasksController extends Controller
|
||||
|
||||
$task->update($request->only($allowedFields));
|
||||
|
||||
auditLog('api.scheduled_task.updated', [
|
||||
'team_id' => getTeamIdFromToken(),
|
||||
'task_uuid' => $task->uuid,
|
||||
'task_name' => $task->name,
|
||||
'resource_type' => $resource instanceof Application ? 'application' : 'service',
|
||||
'resource_uuid' => $resource->uuid,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 200);
|
||||
}
|
||||
|
||||
private function deleteTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
private function deleteTask(Request $request, Application|Service $resource): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$deleted = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->delete();
|
||||
if (! $deleted) {
|
||||
$task = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->first();
|
||||
if (! $task) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
$taskUuid = $task->uuid;
|
||||
$taskName = $task->name;
|
||||
$task->delete();
|
||||
|
||||
auditLog('api.scheduled_task.deleted', [
|
||||
'team_id' => getTeamIdFromToken(),
|
||||
'task_uuid' => $taskUuid,
|
||||
'task_name' => $taskName,
|
||||
'resource_type' => $resource instanceof Application ? 'application' : 'service',
|
||||
'resource_uuid' => $resource->uuid,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Scheduled task deleted.']);
|
||||
}
|
||||
|
||||
private function getExecutions(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
private function getExecutions(Request $request, Application|Service $resource): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
@@ -238,7 +268,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function scheduled_tasks_by_application_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -317,7 +347,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function create_scheduled_task_by_application_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -404,7 +434,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function update_scheduled_task_by_application_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -474,7 +504,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function delete_scheduled_task_by_application_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -542,7 +572,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function executions_by_application_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -601,7 +631,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function scheduled_tasks_by_service_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -680,7 +710,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function create_scheduled_task_by_service_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -767,7 +797,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function update_scheduled_task_by_service_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -837,7 +867,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function delete_scheduled_task_by_service_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -905,7 +935,7 @@ class ScheduledTasksController extends Controller
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
public function executions_by_service_uuid(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
|
||||
@@ -234,6 +234,13 @@ class SecurityController extends Controller
|
||||
'private_key' => $request->private_key,
|
||||
]);
|
||||
|
||||
auditLog('api.private_key.created', [
|
||||
'team_id' => $teamId,
|
||||
'private_key_uuid' => $key->uuid,
|
||||
'private_key_name' => $key->name,
|
||||
'fingerprint' => $fingerPrint,
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => $key->uuid,
|
||||
]))->setStatusCode(201);
|
||||
@@ -336,6 +343,13 @@ class SecurityController extends Controller
|
||||
$this->authorize('update', $foundKey);
|
||||
$foundKey->update($request->only($allowedFields));
|
||||
|
||||
auditLog('api.private_key.updated', [
|
||||
'team_id' => $teamId,
|
||||
'private_key_uuid' => $foundKey->uuid,
|
||||
'private_key_name' => $foundKey->name,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
'uuid' => $foundKey->uuid,
|
||||
]))->setStatusCode(201);
|
||||
@@ -419,8 +433,16 @@ class SecurityController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$keyUuid = $key->uuid;
|
||||
$keyName = $key->name;
|
||||
$key->forceDelete();
|
||||
|
||||
auditLog('api.private_key.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'private_key_uuid' => $keyUuid,
|
||||
'private_key_name' => $keyName,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Private Key deleted.',
|
||||
]);
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server as ModelsServer;
|
||||
use App\Rules\ValidServerIp;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Stringable;
|
||||
@@ -479,7 +480,7 @@ class ServersController extends Controller
|
||||
$this->authorize('create', [ModelsServer::class]);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
@@ -566,6 +567,14 @@ class ServersController extends Controller
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
auditLog('api.server.created', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'ip' => $server->ip,
|
||||
'is_build_server' => (bool) $request->is_build_server,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $server->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -605,6 +614,7 @@ class ServersController extends Controller
|
||||
'deployment_queue_limit' => ['type' => 'integer', 'description' => 'Maximum number of queued deployments.'],
|
||||
'server_disk_usage_notification_threshold' => ['type' => 'integer', 'description' => 'Server disk usage notification threshold (%).'],
|
||||
'server_disk_usage_check_frequency' => ['type' => 'string', 'description' => 'Cron expression for disk usage check frequency.'],
|
||||
'connection_timeout' => ['type' => 'integer', 'description' => 'SSH connection timeout in seconds (1-300). Default: 10.'],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -641,7 +651,7 @@ class ServersController extends Controller
|
||||
)]
|
||||
public function update_server(Request $request)
|
||||
{
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency'];
|
||||
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate', 'proxy_type', 'concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'connection_timeout'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -649,7 +659,7 @@ class ServersController extends Controller
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$validator = customApiValidator($request->all(), [
|
||||
@@ -667,6 +677,7 @@ class ServersController extends Controller
|
||||
'deployment_queue_limit' => 'integer|min:1',
|
||||
'server_disk_usage_notification_threshold' => 'integer|min:1|max:100',
|
||||
'server_disk_usage_check_frequency' => 'string',
|
||||
'connection_timeout' => 'integer|min:1|max:300',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
@@ -712,7 +723,7 @@ class ServersController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency']);
|
||||
$advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'connection_timeout']);
|
||||
if (! empty($advancedSettings)) {
|
||||
$server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value)));
|
||||
}
|
||||
@@ -721,6 +732,13 @@ class ServersController extends Controller
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
auditLog('api.server.updated', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $server->uuid,
|
||||
])->setStatusCode(201);
|
||||
@@ -811,6 +829,9 @@ class ServersController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$deletedUuid = $server->uuid;
|
||||
$deletedName = $server->name;
|
||||
$deletedIp = $server->ip;
|
||||
$server->delete();
|
||||
DeleteServer::dispatch(
|
||||
$server->id,
|
||||
@@ -820,6 +841,14 @@ class ServersController extends Controller
|
||||
$server->team_id
|
||||
);
|
||||
|
||||
auditLog('api.server.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $deletedUuid,
|
||||
'server_name' => $deletedName,
|
||||
'ip' => $deletedIp,
|
||||
'force' => $force,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
||||
@@ -886,6 +915,12 @@ class ServersController extends Controller
|
||||
$this->authorize('update', $server);
|
||||
ValidateServer::dispatch($server);
|
||||
|
||||
auditLog('api.server.validated', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Validation started.'], 201);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,14 @@ class ServicesController extends Controller
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
auditLog('api.service.created', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'service_type' => $oneClickServiceName ?? null,
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
@@ -654,6 +662,14 @@ class ServicesController extends Controller
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
auditLog('api.service.created', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'service_type' => 'docker_compose',
|
||||
'instant_deploy' => (bool) $instantDeploy,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
@@ -796,6 +812,12 @@ class ServicesController extends Controller
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
auditLog('api.service.deleted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Service deletion request queued.',
|
||||
]);
|
||||
@@ -1050,6 +1072,13 @@ class ServicesController extends Controller
|
||||
StartService::dispatch($service);
|
||||
}
|
||||
|
||||
auditLog('api.service.updated', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $service->uuid,
|
||||
'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(),
|
||||
@@ -1259,6 +1288,13 @@ class ServicesController extends Controller
|
||||
}
|
||||
$env->save();
|
||||
|
||||
auditLog('api.service.env_updated', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -1388,6 +1424,12 @@ class ServicesController extends Controller
|
||||
$updatedEnvs->push($this->removeSensitiveData($env));
|
||||
}
|
||||
|
||||
auditLog('api.service.env_bulk_upserted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'env_count' => $updatedEnvs->count(),
|
||||
]);
|
||||
|
||||
return response()->json($updatedEnvs)->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -1510,6 +1552,13 @@ class ServicesController extends Controller
|
||||
'comment' => $request->comment ?? null,
|
||||
]);
|
||||
|
||||
auditLog('api.service.env_created', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'env_uuid' => $env->uuid,
|
||||
'env_key' => $env->key,
|
||||
]);
|
||||
|
||||
return response()->json($this->removeSensitiveData($env))->setStatusCode(201);
|
||||
}
|
||||
|
||||
@@ -1595,8 +1644,17 @@ class ServicesController extends Controller
|
||||
return response()->json(['message' => 'Environment variable not found.'], 404);
|
||||
}
|
||||
|
||||
$envKey = $env->key;
|
||||
$envUuid = $env->uuid;
|
||||
$env->forceDelete();
|
||||
|
||||
auditLog('api.service.env_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'env_uuid' => $envUuid,
|
||||
'env_key' => $envKey,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Environment variable deleted.']);
|
||||
}
|
||||
|
||||
@@ -1672,6 +1730,12 @@ class ServicesController extends Controller
|
||||
}
|
||||
StartService::dispatch($service);
|
||||
|
||||
auditLog('api.service.deployed', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Service starting request queued.',
|
||||
@@ -1763,6 +1827,13 @@ class ServicesController extends Controller
|
||||
$dockerCleanup = $request->boolean('docker_cleanup', true);
|
||||
StopService::dispatch($service, false, $dockerCleanup);
|
||||
|
||||
auditLog('api.service.stopped', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'docker_cleanup' => $dockerCleanup,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Service stopping request queued.',
|
||||
@@ -1850,6 +1921,13 @@ class ServicesController extends Controller
|
||||
$pullLatest = $request->boolean('latest');
|
||||
RestartService::dispatch($service, $pullLatest);
|
||||
|
||||
auditLog('api.service.restarted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'service_name' => $service->name,
|
||||
'pull_latest' => $pullLatest,
|
||||
]);
|
||||
|
||||
return response()->json(
|
||||
[
|
||||
'message' => 'Service restarting request queued.',
|
||||
@@ -2022,7 +2100,7 @@ class ServicesController extends Controller
|
||||
'resource_uuid' => 'required|string',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'required|string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
'is_directory' => 'boolean',
|
||||
'fs_path' => 'string',
|
||||
@@ -2130,6 +2208,15 @@ class ServicesController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
auditLog('api.service.storage_created', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path,
|
||||
]);
|
||||
|
||||
return response()->json($storage, 201);
|
||||
}
|
||||
|
||||
@@ -2231,7 +2318,7 @@ class ServicesController extends Controller
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
'name' => ['string', 'regex:'.ValidationPatterns::VOLUME_NAME_PATTERN],
|
||||
'mount_path' => 'string',
|
||||
'host_path' => 'string|nullable',
|
||||
'host_path' => ['string', 'nullable', 'regex:'.ValidationPatterns::DIRECTORY_PATH_PATTERN],
|
||||
'content' => 'string|nullable',
|
||||
]);
|
||||
|
||||
@@ -2358,6 +2445,15 @@ class ServicesController extends Controller
|
||||
|
||||
$storage->save();
|
||||
|
||||
auditLog('api.service.storage_updated', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'storage_uuid' => $storage->uuid ?? null,
|
||||
'storage_id' => $storage->id,
|
||||
'storage_type' => $request->type,
|
||||
'mount_path' => $storage->mount_path ?? null,
|
||||
]);
|
||||
|
||||
return response()->json($storage);
|
||||
}
|
||||
|
||||
@@ -2458,8 +2554,18 @@ class ServicesController extends Controller
|
||||
$storage->deleteStorageOnServer();
|
||||
}
|
||||
|
||||
$storageType = $storage instanceof LocalFileVolume ? 'file' : 'persistent';
|
||||
$storageMountPath = $storage->mount_path ?? null;
|
||||
$storage->delete();
|
||||
|
||||
auditLog('api.service.storage_deleted', [
|
||||
'team_id' => $teamId,
|
||||
'service_uuid' => $service->uuid,
|
||||
'storage_uuid' => $storageUuid,
|
||||
'storage_type' => $storageType,
|
||||
'mount_path' => $storageMountPath,
|
||||
]);
|
||||
|
||||
return response()->json(['message' => 'Storage deleted.']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use App\Events\TestEvent;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
@@ -39,9 +39,29 @@ class Controller extends BaseController
|
||||
return view('auth.verify-email');
|
||||
}
|
||||
|
||||
public function email_verify(EmailVerificationRequest $request)
|
||||
public function email_verify(Request $request)
|
||||
{
|
||||
$request->fulfill();
|
||||
if (! $request->hasValidSignature()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$user = auth()->user();
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (! hash_equals((string) $request->route('hash'), hash('sha256', $user->getEmailForVerification()))) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (! $user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
event(new Verified($user));
|
||||
}
|
||||
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
@@ -94,10 +114,6 @@ class Controller extends BaseController
|
||||
} else {
|
||||
$team = $user->teams()->first();
|
||||
}
|
||||
if (is_null(data_get($user, 'email_verified_at'))) {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
}
|
||||
Auth::login($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ class OauthController extends Controller
|
||||
{
|
||||
try {
|
||||
$oauthUser = get_socialite_provider($provider)->user();
|
||||
$user = User::whereEmail($oauthUser->email)->first();
|
||||
$email = trim((string) $oauthUser->email);
|
||||
if ($email === '') {
|
||||
abort(403, 'OAuth provider did not return an email address');
|
||||
}
|
||||
$email = strtolower($email);
|
||||
$user = User::whereEmail($email)->first();
|
||||
if (! $user) {
|
||||
$settings = instanceSettings();
|
||||
if (! $settings->is_registration_enabled) {
|
||||
@@ -28,7 +33,7 @@ class OauthController extends Controller
|
||||
|
||||
$user = User::create([
|
||||
'name' => $oauthUser->name,
|
||||
'email' => $oauthUser->email,
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
Auth::login($user);
|
||||
|
||||
@@ -14,6 +14,27 @@ class UploadController extends BaseController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
|
||||
|
||||
private const ALLOWED_EXTENSIONS = [
|
||||
'sql',
|
||||
'sql.gz',
|
||||
'gz',
|
||||
'zip',
|
||||
'tar',
|
||||
'tar.gz',
|
||||
'tgz',
|
||||
'dump',
|
||||
'bak',
|
||||
'bson',
|
||||
'bson.gz',
|
||||
'archive',
|
||||
'archive.gz',
|
||||
'bz2',
|
||||
'xz',
|
||||
'dmp',
|
||||
];
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$databaseIdentifier = request()->route('databaseUuid');
|
||||
@@ -24,6 +45,21 @@ class UploadController extends BaseController
|
||||
|
||||
$this->authorize('uploadBackup', $resource);
|
||||
|
||||
$chunk = $request->file('file');
|
||||
$originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null;
|
||||
if (blank($originalName) || ! self::hasAllowedExtension($originalName)) {
|
||||
return response()->json([
|
||||
'error' => 'Unsupported file type. Allowed extensions: '.implode(', ', self::ALLOWED_EXTENSIONS),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$declaredTotalSize = (int) $request->input('dzTotalFilesize', 0);
|
||||
if ($declaredTotalSize > self::MAX_BYTES) {
|
||||
return response()->json([
|
||||
'error' => 'File exceeds maximum allowed size of '.self::formatMaxSize().'.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
|
||||
|
||||
if ($receiver->isUploaded() === false) {
|
||||
@@ -46,29 +82,20 @@ class UploadController extends BaseController
|
||||
'status' => true,
|
||||
]);
|
||||
}
|
||||
// protected function saveFileToS3($file)
|
||||
// {
|
||||
// $fileName = $this->createFilename($file);
|
||||
|
||||
// $disk = Storage::disk('s3');
|
||||
// // It's better to use streaming Streaming (laravel 5.4+)
|
||||
// $disk->putFileAs('photos', $file, $fileName);
|
||||
|
||||
// // for older laravel
|
||||
// // $disk->put($fileName, file_get_contents($file), 'public');
|
||||
// $mime = str_replace('/', '-', $file->getMimeType());
|
||||
|
||||
// // We need to delete the file when uploaded to s3
|
||||
// unlink($file->getPathname());
|
||||
|
||||
// return response()->json([
|
||||
// 'path' => $disk->url($fileName),
|
||||
// 'name' => $fileName,
|
||||
// 'mime_type' => $mime
|
||||
// ]);
|
||||
// }
|
||||
protected function saveFile(UploadedFile $file, string $resourceIdentifier)
|
||||
{
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$size = $file->getSize();
|
||||
|
||||
if (! self::hasAllowedExtension($originalName) || $size === false || $size > self::MAX_BYTES) {
|
||||
@unlink($file->getPathname());
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Uploaded file failed validation.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$mime = str_replace('/', '-', $file->getMimeType());
|
||||
$filePath = "upload/{$resourceIdentifier}";
|
||||
$finalPath = storage_path('app/'.$filePath);
|
||||
@@ -79,13 +106,30 @@ class UploadController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
protected function createFilename(UploadedFile $file)
|
||||
private static function hasAllowedExtension(string $name): bool
|
||||
{
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$filename = str_replace('.'.$extension, '', $file->getClientOriginalName()); // Filename without extension
|
||||
$lower = strtolower($name);
|
||||
$suffixes = array_map(fn ($ext) => '.'.$ext, self::ALLOWED_EXTENSIONS);
|
||||
usort($suffixes, fn ($a, $b) => strlen($b) <=> strlen($a));
|
||||
|
||||
$filename .= '_'.md5(time()).'.'.$extension;
|
||||
foreach ($suffixes as $suffix) {
|
||||
if (! str_ends_with($lower, $suffix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
$stem = substr($lower, 0, -strlen($suffix));
|
||||
if ($stem !== '' && ! str_ends_with($stem, '.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function formatMaxSize(): string
|
||||
{
|
||||
return (self::MAX_BYTES / (1024 * 1024 * 1024)).' GiB';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -12,6 +13,8 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Bitbucket extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -31,6 +34,16 @@ class Bitbucket extends Controller
|
||||
$branch = data_get($payload, 'push.changes.0.new.name');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$commit = data_get($payload, 'push.changes.0.new.target.hash');
|
||||
// Bitbucket webhooks ship up to 5 commits per change. Larger pushes
|
||||
// are evaluated only on the visible 5.
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(
|
||||
collect(data_get($payload, 'push.changes', []))
|
||||
->flatMap(fn ($change) => data_get($change, 'commits', []))
|
||||
->pluck('message')
|
||||
->filter()
|
||||
->values()
|
||||
->all()
|
||||
);
|
||||
|
||||
if (! $branch) {
|
||||
return response([
|
||||
@@ -45,6 +58,8 @@ class Bitbucket extends Controller
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$pull_request_id = data_get($payload, 'pullrequest.id');
|
||||
$pull_request_html_url = data_get($payload, 'pullrequest.links.html.href');
|
||||
$pull_request_title = data_get($payload, 'pullrequest.title');
|
||||
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title]);
|
||||
$commit = data_get($payload, 'pullrequest.source.commit.hash');
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
@@ -58,6 +73,12 @@ class Bitbucket extends Controller
|
||||
foreach ($applications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket');
|
||||
if (empty($webhook_secret)) {
|
||||
auditLogWebhookFailure('bitbucket', 'webhook_secret_missing', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -70,6 +91,12 @@ class Bitbucket extends Controller
|
||||
|
||||
$parts = explode('=', $x_bitbucket_token, 2);
|
||||
if (count($parts) !== 2 || $parts[0] !== 'sha256') {
|
||||
auditLogWebhookFailure('bitbucket', 'malformed_signature', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -81,6 +108,12 @@ class Bitbucket extends Controller
|
||||
$hash = $parts[1];
|
||||
$payloadHash = hash_hmac('sha256', $payload, $webhook_secret);
|
||||
if (! hash_equals($hash, $payloadHash) && ! isDev()) {
|
||||
auditLogWebhookFailure('bitbucket', 'invalid_signature', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_bitbucket_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -101,6 +134,17 @@ class Bitbucket extends Controller
|
||||
}
|
||||
if ($x_bitbucket_event === 'repo:push') {
|
||||
if ($application->isDeployable()) {
|
||||
if ($skip_deploy_commits ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -118,6 +162,15 @@ class Bitbucket extends Controller
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'bitbucket',
|
||||
'mode' => 'manual',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'commit' => $commit,
|
||||
'repository' => $full_name ?? null,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
@@ -134,6 +187,15 @@ class Bitbucket extends Controller
|
||||
}
|
||||
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
|
||||
if ($application->isPRDeployable()) {
|
||||
if ($skip_deploy_pr ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'PR title contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Webhook\Concerns;
|
||||
|
||||
trait DetectsSkipDeployCommits
|
||||
{
|
||||
/**
|
||||
* Returns true if there is at least one non-empty message and every message
|
||||
* contains [skip cd] or [skip ci] (case-insensitive).
|
||||
*
|
||||
* Accepts commit messages from a push payload. Null/empty entries are
|
||||
* filtered before evaluation.
|
||||
*
|
||||
* @param array<int, string|null> $messages
|
||||
*/
|
||||
public static function shouldSkipDeploy(array $messages): bool
|
||||
{
|
||||
$messages = array_values(array_filter($messages, fn ($m) => filled($m)));
|
||||
|
||||
if (empty($messages)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$lower = strtolower((string) $message);
|
||||
if (! str_contains($lower, '[skip cd]') && ! str_contains($lower, '[skip ci]')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least one non-empty message contains [skip cd] or
|
||||
* [skip ci]. Used for PR/MR title + latest-commit signals where any one
|
||||
* marker should trigger the skip.
|
||||
*
|
||||
* @param array<int, string|null> $messages
|
||||
*/
|
||||
public static function shouldSkipDeployAny(array $messages): bool
|
||||
{
|
||||
foreach ($messages as $message) {
|
||||
if (! filled($message)) {
|
||||
continue;
|
||||
}
|
||||
$lower = strtolower((string) $message);
|
||||
if (str_contains($lower, '[skip cd]') || str_contains($lower, '[skip ci]')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -13,6 +14,8 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Gitea extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -40,12 +43,15 @@ class Gitea extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||
}
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$pull_request_id = data_get($payload, 'number');
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title]);
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
}
|
||||
@@ -68,6 +74,12 @@ class Gitea extends Controller
|
||||
foreach ($applications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_gitea');
|
||||
if (empty($webhook_secret)) {
|
||||
auditLogWebhookFailure('gitea', 'webhook_secret_missing', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitea_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -78,6 +90,12 @@ class Gitea extends Controller
|
||||
}
|
||||
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
|
||||
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
|
||||
auditLogWebhookFailure('gitea', 'invalid_signature', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitea_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -100,6 +118,17 @@ class Gitea extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
if ($skip_deploy_commits ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -117,6 +146,15 @@ class Gitea extends Controller
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'gitea',
|
||||
'mode' => 'manual',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'commit' => data_get($payload, 'after'),
|
||||
'repository' => $full_name ?? null,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
@@ -149,6 +187,15 @@ class Gitea extends Controller
|
||||
if ($x_gitea_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
if ($skip_deploy_pr ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'PR title contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Jobs\GithubAppPermissionJob;
|
||||
use App\Jobs\ProcessGithubPullRequestWebhook;
|
||||
use App\Models\Application;
|
||||
@@ -16,6 +17,8 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Github extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -43,12 +46,14 @@ class Github extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$pull_request_id = data_get($payload, 'number');
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
$before_sha = data_get($payload, 'before');
|
||||
@@ -82,6 +87,12 @@ class Github extends Controller
|
||||
foreach ($serverApplications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
|
||||
if (empty($webhook_secret)) {
|
||||
auditLogWebhookFailure('github', 'webhook_secret_missing', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'mode' => 'manual',
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -92,6 +103,12 @@ class Github extends Controller
|
||||
}
|
||||
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
|
||||
if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) {
|
||||
auditLogWebhookFailure('github', 'invalid_signature', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'mode' => 'manual',
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -114,6 +131,17 @@ class Github extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
if ($skip_deploy_commits ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -131,6 +159,15 @@ class Github extends Controller
|
||||
'message' => $result['message'],
|
||||
]);
|
||||
} else {
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'github',
|
||||
'mode' => 'manual',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $result['deployment_uuid'],
|
||||
'commit' => data_get($payload, 'after'),
|
||||
'repository' => $full_name ?? null,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
@@ -180,6 +217,7 @@ class Github extends Controller
|
||||
action: $action,
|
||||
pullRequestId: $pull_request_id,
|
||||
pullRequestHtmlUrl: $pull_request_html_url,
|
||||
pullRequestTitle: $pull_request_title ?? null,
|
||||
beforeSha: $before_sha,
|
||||
afterSha: $after_sha,
|
||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||
@@ -224,6 +262,13 @@ class Github extends Controller
|
||||
$hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret);
|
||||
if (config('app.env') !== 'local') {
|
||||
if (! hash_equals($x_hub_signature_256, $hmac)) {
|
||||
auditLogWebhookFailure('github', 'invalid_signature', [
|
||||
'mode' => 'app',
|
||||
'github_app_id' => $github_app->id,
|
||||
'github_app_name' => $github_app->name,
|
||||
'installation_target_id' => $x_github_hook_installation_target_id,
|
||||
]);
|
||||
|
||||
return response('Invalid signature.');
|
||||
}
|
||||
}
|
||||
@@ -246,12 +291,14 @@ class Github extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
$id = data_get($payload, 'repository.id');
|
||||
$pull_request_id = data_get($payload, 'number');
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$pull_request_title = data_get($payload, 'pull_request.title');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
$before_sha = data_get($payload, 'before');
|
||||
@@ -300,6 +347,17 @@ class Github extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
if ($skip_deploy_commits ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -311,6 +369,17 @@ class Github extends Controller
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response($result['message'], 429)->header('Retry-After', 60);
|
||||
}
|
||||
if ($result['status'] !== 'skipped' && ! empty($result['deployment_uuid'])) {
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'github',
|
||||
'mode' => 'app',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $result['deployment_uuid'],
|
||||
'commit' => data_get($payload, 'after'),
|
||||
'github_app_id' => $github_app->id,
|
||||
]);
|
||||
}
|
||||
$return_payloads->push([
|
||||
'status' => $result['status'],
|
||||
'message' => $result['message'],
|
||||
@@ -360,6 +429,7 @@ class Github extends Controller
|
||||
action: $action,
|
||||
pullRequestId: $pull_request_id,
|
||||
pullRequestHtmlUrl: $pull_request_html_url,
|
||||
pullRequestTitle: $pull_request_title ?? null,
|
||||
beforeSha: $before_sha,
|
||||
afterSha: $after_sha,
|
||||
commitSha: data_get($payload, 'pull_request.head.sha', 'HEAD'),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Webhook;
|
||||
|
||||
use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
@@ -13,6 +14,8 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Gitlab extends Controller
|
||||
{
|
||||
use DetectsSkipDeployCommits;
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -32,6 +35,9 @@ class Gitlab extends Controller
|
||||
}
|
||||
|
||||
if (empty($x_gitlab_token)) {
|
||||
auditLogWebhookFailure('gitlab', 'webhook_token_missing', [
|
||||
'event' => $x_gitlab_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid signature.',
|
||||
@@ -58,6 +64,7 @@ class Gitlab extends Controller
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
$action = data_get($payload, 'object_attributes.action');
|
||||
@@ -66,6 +73,9 @@ class Gitlab extends Controller
|
||||
$full_name = data_get($payload, 'project.path_with_namespace');
|
||||
$pull_request_id = data_get($payload, 'object_attributes.iid');
|
||||
$pull_request_html_url = data_get($payload, 'object_attributes.url');
|
||||
$pull_request_title = data_get($payload, 'object_attributes.title');
|
||||
$latest_commit_message = data_get($payload, 'object_attributes.last_commit.message');
|
||||
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title, $latest_commit_message]);
|
||||
if (! $branch) {
|
||||
$return_payloads->push([
|
||||
'status' => 'failed',
|
||||
@@ -101,6 +111,12 @@ class Gitlab extends Controller
|
||||
foreach ($applications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
|
||||
if (empty($webhook_secret)) {
|
||||
auditLogWebhookFailure('gitlab', 'webhook_secret_missing', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitlab_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -110,6 +126,12 @@ class Gitlab extends Controller
|
||||
continue;
|
||||
}
|
||||
if (! hash_equals($webhook_secret, $x_gitlab_token ?? '')) {
|
||||
auditLogWebhookFailure('gitlab', 'invalid_signature', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'repository' => $full_name ?? null,
|
||||
'event' => $x_gitlab_event,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
@@ -132,6 +154,17 @@ class Gitlab extends Controller
|
||||
if ($application->isDeployable()) {
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if ($is_watch_path_triggered || blank($application->watch_paths)) {
|
||||
if ($skip_deploy_commits ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci]. Skipping deployment.',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
@@ -150,6 +183,15 @@ class Gitlab extends Controller
|
||||
'application_name' => $application->name,
|
||||
]);
|
||||
} else {
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'gitlab',
|
||||
'mode' => 'manual',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'commit' => data_get($payload, 'after'),
|
||||
'repository' => $full_name ?? null,
|
||||
]);
|
||||
$return_payloads->push([
|
||||
'status' => 'success',
|
||||
'message' => 'Deployment queued.',
|
||||
@@ -182,6 +224,15 @@ class Gitlab extends Controller
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
|
||||
if ($application->isPRDeployable()) {
|
||||
if ($skip_deploy_pr ?? false) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'PR title or latest commit contains [skip cd] or [skip ci]. Skipping preview deployment.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
|
||||
use App\Jobs\StripeProcessJob;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Stripe\Exception\SignatureVerificationException;
|
||||
use Stripe\Webhook;
|
||||
|
||||
class Stripe extends Controller
|
||||
{
|
||||
@@ -14,7 +16,7 @@ class Stripe extends Controller
|
||||
try {
|
||||
$webhookSecret = config('subscription.stripe_webhook_secret');
|
||||
$signature = $request->header('Stripe-Signature');
|
||||
$event = \Stripe\Webhook::constructEvent(
|
||||
$event = Webhook::constructEvent(
|
||||
$request->getContent(),
|
||||
$signature,
|
||||
$webhookSecret
|
||||
@@ -22,6 +24,12 @@ class Stripe extends Controller
|
||||
StripeProcessJob::dispatch($event);
|
||||
|
||||
return response('Webhook received. Cool cool cool cool cool.', 200);
|
||||
} catch (SignatureVerificationException $e) {
|
||||
auditLogWebhookFailure('stripe', 'invalid_signature', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return response($e->getMessage(), 400);
|
||||
} catch (Exception $e) {
|
||||
return response($e->getMessage(), 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user