import { BadRequestException, ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Tenant } from './tenant.entity'; import { TenantSetting } from './tenant-setting.entity'; import { TenantMember } from './tenant-member.entity'; @Injectable() export class TenantService { public static readonly DEFAULT_TENANT_NAME = 'Default'; constructor( @InjectRepository(Tenant) private readonly tenantRepository: Repository, @InjectRepository(TenantSetting) private readonly tenantSettingRepository: Repository, @InjectRepository(TenantMember) private readonly tenantMemberRepository: Repository, ) { } async findAll(): Promise { return this.tenantRepository.find({ relations: ['members', 'members.user'], order: { createdAt: 'ASC' } }); } async findById(id: string): Promise { const tenant = await this.tenantRepository.findOneBy({ id }); if (!tenant) throw new NotFoundException(`Tenant ${id} not found`); return tenant; } async findByName(name: string): Promise { return this.tenantRepository.findOneBy({ name }); } async create(name: string, domain?: string, parentId?: string, isSystem: boolean = false): Promise { const existing = await this.findByName(name); if (existing) throw new BadRequestException(`Tenant name "${name}" already exists`); const tenant = this.tenantRepository.create({ name, domain, parentId, isSystem }); const saved = await this.tenantRepository.save(tenant); // Auto-create default TenantSettings const setting = this.tenantSettingRepository.create({ tenantId: saved.id }); await this.tenantSettingRepository.save(setting); return saved; } async update(id: string, data: Partial): Promise { const tenant = await this.findById(id); if (tenant.isSystem) { throw new ForbiddenException(`Cannot modify a system organization`); } await this.tenantRepository.save({ ...tenant, ...data }); return this.findById(id); } async remove(id: string): Promise { const tenant = await this.findById(id); if (tenant.isSystem) { throw new ForbiddenException(`Cannot delete a system organization`); } await this.tenantRepository.delete(id); } async getSettings(tenantId: string): Promise { let setting = await this.tenantSettingRepository.findOneBy({ tenantId }); if (!setting) { // Defensive: Check if tenant actually exists before creating settings // to avoid FOREIGN KEY constraint failure if tenantId is invalid/legacy const tenantExists = await this.tenantRepository.findOneBy({ id: tenantId }); if (!tenantExists) { console.warn(`[TenantService] Attempted to get settings for non-existent tenant: ${tenantId}`); // Return a transient default object without saving to DB return this.tenantSettingRepository.create({ tenantId }); } setting = this.tenantSettingRepository.create({ tenantId }); setting = await this.tenantSettingRepository.save(setting); } return setting; } async updateSettings(tenantId: string, data: Partial): Promise { let setting = await this.tenantSettingRepository.findOneBy({ tenantId }); if (!setting) { setting = this.tenantSettingRepository.create({ tenantId, ...data }); } else { if (data.enabledModelIds) { if (setting.selectedLLMId && !data.enabledModelIds.includes(setting.selectedLLMId)) { data.selectedLLMId = null as any; } if (setting.selectedEmbeddingId && !data.enabledModelIds.includes(setting.selectedEmbeddingId)) { data.selectedEmbeddingId = null as any; } if (setting.selectedRerankId && !data.enabledModelIds.includes(setting.selectedRerankId)) { data.selectedRerankId = null as any; } } Object.assign(setting, data); } return this.tenantSettingRepository.save(setting); } async updateMemberRole(tenantId: string, userId: string, role: string): Promise { const existing = await this.tenantMemberRepository.findOneBy({ tenantId, userId }); if (!existing) { throw new ForbiddenException(`Member not found in this organization`); } existing.role = role as any; return this.tenantMemberRepository.save(existing); } async addMember(tenantId: string, userId: string, role: string = 'USER'): Promise { const tenant = await this.findById(tenantId); if (tenant.isSystem) { throw new ForbiddenException(`Cannot manually bind members to a system organization`); } const existing = await this.tenantMemberRepository.findOneBy({ tenantId, userId }); if (existing) { existing.role = role as any; return this.tenantMemberRepository.save(existing); } const member = this.tenantMemberRepository.create({ tenantId, userId, role: role as any }); return this.tenantMemberRepository.save(member); } async removeMember(tenantId: string, userId: string): Promise { await this.tenantMemberRepository.delete({ tenantId, userId }); } async getMembers(tenantId: string, page?: number, limit?: number): Promise<{ data: TenantMember[]; total: number }> { const queryBuilder = this.tenantMemberRepository.createQueryBuilder('member') .leftJoinAndSelect('member.user', 'user') .where('member.tenantId = :tenantId', { tenantId }) .select(['member', 'user.id', 'user.username', 'user.displayName', 'user.isAdmin']); if (page && limit) { const [data, total] = await queryBuilder .skip((page - 1) * limit) .take(limit) .getManyAndCount(); return { data, total }; } const [data, total] = await queryBuilder.getManyAndCount(); return { data, total }; } /** * Ensure a "Default" tenant exists for data migration purposes. * Called during app bootstrap. */ async ensureDefaultTenant(): Promise { let defaultTenant = await this.findByName(TenantService.DEFAULT_TENANT_NAME); if (!defaultTenant) { defaultTenant = await this.create(TenantService.DEFAULT_TENANT_NAME, 'default.localhost', undefined, true); } else if (!defaultTenant.isSystem) { defaultTenant.isSystem = true; await this.tenantRepository.save(defaultTenant); } return defaultTenant; } }