[TASK-028] security: Add tenant_id validation to uniqueness checks

- warehouses.service.ts: Add code uniqueness check with tenantId
- products.service.ts: Add SKU/barcode uniqueness checks with tenantId
- accounts.service.ts: Add tenantId to code and parent validation

Fixes 5 RLS gaps in backend services for multi-tenant isolation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-24 22:22:59 -06:00
parent 7ae745e509
commit 2d2a562274
3 changed files with 52 additions and 2 deletions

View File

@ -209,9 +209,10 @@ class AccountsService {
userId: string userId: string
): Promise<Account> { ): Promise<Account> {
try { try {
// Validate unique code within company // Validate unique code within company and tenant (RLS compliance)
const existing = await this.accountRepository.findOne({ const existing = await this.accountRepository.findOne({
where: { where: {
tenantId,
companyId: dto.companyId, companyId: dto.companyId,
code: dto.code, code: dto.code,
deletedAt: IsNull(), deletedAt: IsNull(),
@ -225,11 +226,12 @@ class AccountsService {
// Validate account type exists // Validate account type exists
await this.findAccountTypeById(dto.accountTypeId); await this.findAccountTypeById(dto.accountTypeId);
// Validate parent account if specified // Validate parent account if specified (RLS compliance)
if (dto.parentId) { if (dto.parentId) {
const parent = await this.accountRepository.findOne({ const parent = await this.accountRepository.findOne({
where: { where: {
id: dto.parentId, id: dto.parentId,
tenantId,
companyId: dto.companyId, companyId: dto.companyId,
deletedAt: IsNull(), deletedAt: IsNull(),
}, },
@ -295,6 +297,7 @@ class AccountsService {
const parent = await this.accountRepository.findOne({ const parent = await this.accountRepository.findOne({
where: { where: {
id: dto.parentId, id: dto.parentId,
tenantId,
companyId: existing.companyId, companyId: existing.companyId,
deletedAt: IsNull(), deletedAt: IsNull(),
}, },

View File

@ -164,6 +164,24 @@ class ProductsServiceClass {
} }
async create(tenantId: string, dto: CreateProductDto, createdBy?: string): Promise<Product> { async create(tenantId: string, dto: CreateProductDto, createdBy?: string): Promise<Product> {
// Validate unique SKU within tenant (RLS compliance)
const existingSku = await this.productRepository.findOne({
where: { sku: dto.sku, tenantId, deletedAt: IsNull() },
});
if (existingSku) {
throw new Error(`Product with SKU '${dto.sku}' already exists`);
}
// Validate unique barcode within tenant if provided (RLS compliance)
if (dto.barcode) {
const existingBarcode = await this.productRepository.findOne({
where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() },
});
if (existingBarcode) {
throw new Error(`Product with barcode '${dto.barcode}' already exists`);
}
}
const product = this.productRepository.create({ const product = this.productRepository.create({
...dto, ...dto,
tenantId, tenantId,
@ -175,6 +193,27 @@ class ProductsServiceClass {
async update(id: string, tenantId: string, dto: UpdateProductDto, updatedBy?: string): Promise<Product | null> { async update(id: string, tenantId: string, dto: UpdateProductDto, updatedBy?: string): Promise<Product | null> {
const product = await this.findOne(id, tenantId); const product = await this.findOne(id, tenantId);
if (!product) return null; if (!product) return null;
// Validate unique SKU within tenant if changing (RLS compliance)
if (dto.sku && dto.sku !== product.sku) {
const existingSku = await this.productRepository.findOne({
where: { sku: dto.sku, tenantId, deletedAt: IsNull() },
});
if (existingSku) {
throw new Error(`Product with SKU '${dto.sku}' already exists`);
}
}
// Validate unique barcode within tenant if changing (RLS compliance)
if (dto.barcode && dto.barcode !== product.barcode) {
const existingBarcode = await this.productRepository.findOne({
where: { barcode: dto.barcode, tenantId, deletedAt: IsNull() },
});
if (existingBarcode) {
throw new Error(`Product with barcode '${dto.barcode}' already exists`);
}
}
Object.assign(product, { ...dto, updatedBy }); Object.assign(product, { ...dto, updatedBy });
return this.productRepository.save(product); return this.productRepository.save(product);
} }

View File

@ -136,6 +136,14 @@ class WarehousesServiceClass {
} }
async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise<Warehouse> { async create(tenantId: string, dto: CreateWarehouseDto, _createdBy?: string): Promise<Warehouse> {
// Validate unique code within tenant (RLS compliance)
const existingCode = await this.warehouseRepository.findOne({
where: { code: dto.code, tenantId, deletedAt: IsNull() },
});
if (existingCode) {
throw new Error(`Warehouse with code '${dto.code}' already exists`);
}
// If this is set as default, unset other defaults // If this is set as default, unset other defaults
if (dto.isDefault) { if (dto.isDefault) {
await this.warehouseRepository.update( await this.warehouseRepository.update(