From 11ab265d559d00e702cf3dd7840a4be3005728aa Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 26 Mar 2026 21:49:21 +0100 Subject: [PATCH] fix(tags): add FK constraint, token validation, input validation - Add proper FK constraint on tags.groupId -> tag_groups.id (onDelete: set null) - Validate auth token is non-empty before API requests in TagsClient - Add @IsNotEmpty/@MinLength(1) on tag and tag group name DTOs - Add @MaxLength on all query params in tag-links DTOs - Add GetTagsForEntityDto for validated query params on tags-for-entity endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared-tags/src/client.ts | 4 ++++ .../mana-core-auth/src/db/schema/tags.schema.ts | 3 ++- .../src/tag-groups/dto/create-tag-group.dto.ts | 12 +++++++++++- .../src/tag-links/dto/query-tag-links.dto.ts | 15 ++++++++++++++- .../src/tag-links/tag-links.controller.ts | 7 +++---- .../mana-core-auth/src/tags/dto/create-tag.dto.ts | 13 ++++++++++++- 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/packages/shared-tags/src/client.ts b/packages/shared-tags/src/client.ts index 9a8036542..149163a8e 100644 --- a/packages/shared-tags/src/client.ts +++ b/packages/shared-tags/src/client.ts @@ -44,6 +44,10 @@ export class TagsClient { private async request(path: string, options: RequestInit = {}): Promise { const token = await this.getToken(); + if (!token) { + throw new Error('No authentication token available'); + } + const response = await fetch(`${this.authUrl}/api/v1${path}`, { ...options, headers: { diff --git a/services/mana-core-auth/src/db/schema/tags.schema.ts b/services/mana-core-auth/src/db/schema/tags.schema.ts index b30a6a662..29d1dfbb8 100644 --- a/services/mana-core-auth/src/db/schema/tags.schema.ts +++ b/services/mana-core-auth/src/db/schema/tags.schema.ts @@ -8,6 +8,7 @@ import { unique, integer, } from 'drizzle-orm/pg-core'; +import { tagGroups } from './tag-groups.schema'; /** * Central tags table for all Manacore applications. @@ -21,7 +22,7 @@ export const tags = pgTable( name: varchar('name', { length: 100 }).notNull(), color: varchar('color', { length: 7 }).default('#3B82F6'), icon: varchar('icon', { length: 50 }), // Optional: Phosphor Icon name - groupId: uuid('group_id'), // Reference to tag_groups (validated in service layer) + groupId: uuid('group_id').references(() => tagGroups.id, { onDelete: 'set null' }), sortOrder: integer('sort_order').default(0).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), diff --git a/services/mana-core-auth/src/tag-groups/dto/create-tag-group.dto.ts b/services/mana-core-auth/src/tag-groups/dto/create-tag-group.dto.ts index e2e8b3515..b87e7a6e7 100644 --- a/services/mana-core-auth/src/tag-groups/dto/create-tag-group.dto.ts +++ b/services/mana-core-auth/src/tag-groups/dto/create-tag-group.dto.ts @@ -1,7 +1,17 @@ -import { IsString, IsOptional, IsInt, MaxLength, Matches } from 'class-validator'; +import { + IsString, + IsOptional, + IsNotEmpty, + IsInt, + MinLength, + MaxLength, + Matches, +} from 'class-validator'; export class CreateTagGroupDto { @IsString() + @IsNotEmpty({ message: 'Group name must not be empty' }) + @MinLength(1) @MaxLength(100) name: string; diff --git a/services/mana-core-auth/src/tag-links/dto/query-tag-links.dto.ts b/services/mana-core-auth/src/tag-links/dto/query-tag-links.dto.ts index 722e5b97f..3786707c3 100644 --- a/services/mana-core-auth/src/tag-links/dto/query-tag-links.dto.ts +++ b/services/mana-core-auth/src/tag-links/dto/query-tag-links.dto.ts @@ -1,19 +1,32 @@ -import { IsString, IsOptional, IsUUID } from 'class-validator'; +import { IsString, IsOptional, IsUUID, MaxLength } from 'class-validator'; export class QueryTagLinksDto { @IsOptional() @IsString() + @MaxLength(50) appId?: string; @IsOptional() @IsString() + @MaxLength(255) entityId?: string; @IsOptional() @IsString() + @MaxLength(100) entityType?: string; @IsOptional() @IsUUID() tagId?: string; } + +export class GetTagsForEntityDto { + @IsString() + @MaxLength(50) + appId: string; + + @IsString() + @MaxLength(255) + entityId: string; +} diff --git a/services/mana-core-auth/src/tag-links/tag-links.controller.ts b/services/mana-core-auth/src/tag-links/tag-links.controller.ts index 2c9f58887..3c027e3a0 100644 --- a/services/mana-core-auth/src/tag-links/tag-links.controller.ts +++ b/services/mana-core-auth/src/tag-links/tag-links.controller.ts @@ -20,7 +20,7 @@ import { BulkCreateTagLinksDto, SyncTagLinksDto, } from './dto/create-tag-link.dto'; -import { QueryTagLinksDto } from './dto/query-tag-links.dto'; +import { QueryTagLinksDto, GetTagsForEntityDto } from './dto/query-tag-links.dto'; @Controller('tag-links') @UseGuards(JwtAuthGuard) @@ -63,10 +63,9 @@ export class TagLinksController { @Get('tags-for-entity') async getTagsForEntity( @CurrentUser() user: CurrentUserData, - @Query('appId') appId: string, - @Query('entityId') entityId: string + @Query() query: GetTagsForEntityDto ) { - return this.tagLinksService.getTagsForEntity(user.userId, appId, entityId); + return this.tagLinksService.getTagsForEntity(user.userId, query.appId, query.entityId); } /** diff --git a/services/mana-core-auth/src/tags/dto/create-tag.dto.ts b/services/mana-core-auth/src/tags/dto/create-tag.dto.ts index 31c76bc2e..31ed9a647 100644 --- a/services/mana-core-auth/src/tags/dto/create-tag.dto.ts +++ b/services/mana-core-auth/src/tags/dto/create-tag.dto.ts @@ -1,7 +1,18 @@ -import { IsString, IsOptional, IsUUID, IsInt, MaxLength, Matches } from 'class-validator'; +import { + IsString, + IsOptional, + IsNotEmpty, + IsUUID, + IsInt, + MinLength, + MaxLength, + Matches, +} from 'class-validator'; export class CreateTagDto { @IsString() + @IsNotEmpty({ message: 'Tag name must not be empty' }) + @MinLength(1) @MaxLength(100) name: string;