| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- 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<Tenant>,
- @InjectRepository(TenantSetting)
- private readonly tenantSettingRepository: Repository<TenantSetting>,
- @InjectRepository(TenantMember)
- private readonly tenantMemberRepository: Repository<TenantMember>,
- ) { }
- async findAll(): Promise<Tenant[]> {
- return this.tenantRepository.find({
- relations: ['members', 'members.user'],
- order: { createdAt: 'ASC' }
- });
- }
- async findById(id: string): Promise<Tenant> {
- const tenant = await this.tenantRepository.findOneBy({ id });
- if (!tenant) throw new NotFoundException(`Tenant ${id} not found`);
- return tenant;
- }
- async findByName(name: string): Promise<Tenant | null> {
- return this.tenantRepository.findOneBy({ name });
- }
- async create(name: string, domain?: string, parentId?: string, isSystem: boolean = false): Promise<Tenant> {
- 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<Tenant>): Promise<Tenant> {
- 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<void> {
- 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<TenantSetting> {
- 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<TenantSetting>): Promise<TenantSetting> {
- 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<TenantMember> {
- 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<TenantMember> {
- 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<void> {
- 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<Tenant> {
- 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;
- }
- }
|