mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 16:23:30 +00:00
feat(api): add tags to resource creation
Normalize tag names before attaching them, reject names that are too short after sanitization, and return 404 when removing tags not attached to the resource. Adds a per-team unique tag-name index and migrates duplicate tags onto the kept record before creating the constraint.
This commit is contained in:
@@ -969,6 +969,13 @@ class ApplicationsController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$return = $this->validateTagsParameter($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$tagNames = $request->input('tags') ?? [];
|
||||
|
||||
$environmentUuid = $request->environment_uuid;
|
||||
$environmentName = $request->environment_name;
|
||||
if (blank($environmentUuid) && blank($environmentName)) {
|
||||
@@ -1226,8 +1233,8 @@ class ApplicationsController extends Controller
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($application, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
@@ -1472,8 +1479,8 @@ class ApplicationsController extends Controller
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($application, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
@@ -1688,8 +1695,8 @@ class ApplicationsController extends Controller
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($application, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
@@ -1815,8 +1822,8 @@ class ApplicationsController extends Controller
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($application, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
@@ -1941,8 +1948,8 @@ class ApplicationsController extends Controller
|
||||
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
|
||||
$application->save();
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($application, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($application, $tagNames, $teamId);
|
||||
}
|
||||
$application->isConfigurationChanged(true);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Http\Controllers\Api\TagsController;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
trait HandlesTagsApi
|
||||
@@ -46,7 +45,7 @@ trait HandlesTagsApi
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -65,9 +64,9 @@ trait HandlesTagsApi
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'tag_name' => 'required_without:tag_names|string|min:2',
|
||||
'tag_name' => 'required_without:tag_names|string',
|
||||
'tag_names' => 'required_without:tag_name|array|min:1',
|
||||
'tag_names.*' => 'string|min:2',
|
||||
'tag_names.*' => 'string',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), ['tag_name', 'tag_names']);
|
||||
@@ -85,7 +84,14 @@ trait HandlesTagsApi
|
||||
], 422);
|
||||
}
|
||||
|
||||
$tagNames = $request->has('tag_names') ? $request->tag_names : [$request->tag_name];
|
||||
$tagNames = $this->normalizeTagNames($request->has('tag_names') ? $request->tag_names : [$request->tag_name]);
|
||||
$invalidTags = array_filter($tagNames, fn (string $tagName): bool => mb_strlen($tagName) < 2);
|
||||
if (! empty($invalidTags)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['tag_name' => ['Each tag name must be at least 2 characters after sanitization.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->attachTagsToResource($resource, $tagNames, $teamId);
|
||||
|
||||
@@ -111,32 +117,58 @@ trait HandlesTagsApi
|
||||
return response()->json(['message' => 'Tag not found.'], 404);
|
||||
}
|
||||
|
||||
$resource->tags()->detach($tag->id);
|
||||
|
||||
if (DB::table('taggables')->where('tag_id', $tag->id)->count() === 0) {
|
||||
$tag->delete();
|
||||
if (! $resource->tags()->whereKey($tag->id)->exists()) {
|
||||
return response()->json(['message' => 'Tag not found on resource.'], 404);
|
||||
}
|
||||
|
||||
$resource->tags()->detach($tag->id);
|
||||
$tag->deleteIfOrphaned();
|
||||
|
||||
return response()->json(['message' => 'Tag removed.']);
|
||||
}
|
||||
|
||||
protected function attachTagsToResource($resource, array $tagNames, int|string $teamId): void
|
||||
{
|
||||
foreach ($tagNames as $tagName) {
|
||||
$tagName = strtolower(strip_tags($tagName));
|
||||
if (strlen($tagName) < 2) {
|
||||
foreach ($this->normalizeTagNames($tagNames) as $tagName) {
|
||||
if (mb_strlen($tagName) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = Tag::where('team_id', $teamId)->where('name', $tagName)->first();
|
||||
if (! $tag) {
|
||||
$tag = Tag::create([
|
||||
'name' => $tagName,
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
}
|
||||
$tag = Tag::query()->createOrFirst([
|
||||
'team_id' => $teamId,
|
||||
'name' => $tagName,
|
||||
]);
|
||||
|
||||
$resource->tags()->syncWithoutDetaching([$tag->id]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateTagsParameter(Request $request): ?JsonResponse
|
||||
{
|
||||
if (! $request->has('tags')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tagNames = $this->normalizeTagNames($request->input('tags', []));
|
||||
$invalidTags = array_filter($tagNames, fn (string $tagName): bool => mb_strlen($tagName) < 2);
|
||||
if (! empty($invalidTags)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['tags' => ['Each tag name must be at least 2 characters after sanitization.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$request->merge(['tags' => $tagNames]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function normalizeTagNames(array $tagNames): array
|
||||
{
|
||||
return collect($tagNames)
|
||||
->map(fn ($tagName): string => strtolower(trim(strip_tags((string) $tagName))))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1663,7 +1663,7 @@ class DatabasesController extends Controller
|
||||
|
||||
public function create_database(Request $request, NewDatabaseTypes $type)
|
||||
{
|
||||
$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', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
|
||||
$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', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'tags'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
@@ -1771,6 +1771,13 @@ class DatabasesController extends Controller
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
$return = $this->validateTagsParameter($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$tagNames = $request->input('tags') ?? [];
|
||||
|
||||
if ($request->public_port) {
|
||||
if ($request->public_port < 1024 || $request->public_port > 65535) {
|
||||
return response()->json([
|
||||
@@ -1830,8 +1837,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
$database->refresh();
|
||||
$payload = [
|
||||
@@ -1901,8 +1908,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -1973,8 +1980,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -2042,8 +2049,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -2092,8 +2099,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse([
|
||||
@@ -2144,8 +2151,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -2193,8 +2200,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
@@ -2264,8 +2271,8 @@ class DatabasesController extends Controller
|
||||
if ($instantDeploy) {
|
||||
StartDatabase::dispatch($database);
|
||||
}
|
||||
if ($request->has('tags')) {
|
||||
$this->attachTagsToResource($database, $request->tags, $teamId);
|
||||
if ($tagNames !== []) {
|
||||
$this->attachTagsToResource($database, $tagNames, $teamId);
|
||||
}
|
||||
|
||||
$database->refresh();
|
||||
|
||||
@@ -352,6 +352,11 @@ class ServicesController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$return = $this->validateTagsParameter($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if (filled($request->type) && filled($request->docker_compose_raw)) {
|
||||
return response()->json([
|
||||
'message' => 'You cannot provide both service type and docker_compose_raw. Use one or the other.',
|
||||
@@ -533,6 +538,8 @@ class ServicesController extends Controller
|
||||
'urls.*.url' => 'string|nullable',
|
||||
'force_domain_override' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
'tags' => 'array|nullable',
|
||||
'tags.*' => 'string|min:2',
|
||||
];
|
||||
$validationMessages = [
|
||||
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
|
||||
|
||||
Reference in New Issue
Block a user