From b9fa3bd0087a60f6be961bf4d438f3d3d29bea2f Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Wed, 24 Dec 2025 11:42:44 +0100 Subject: [PATCH] WIP - improve login to tenants by domains --- backend/src/auth/auth.controller.ts | 5 +- .../src/tenant/central-admin.controller.ts | 13 ++++ backend/src/tenant/tenant-database.service.ts | 59 +++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 7f496a1..59876f0 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -55,10 +55,7 @@ export class AuthController { ) { const subdomain = req.raw?.subdomain; - console.log('subdomain:' + subdomain); - - console.log('CENTRAL_SUBDOMAINS:', process.env.CENTRAL_SUBDOMAINS); - + // If it's a central subdomain, tenantId is not required if (!subdomain || !this.isCentralSubdomain(subdomain)) { if (!tenantId) { diff --git a/backend/src/tenant/central-admin.controller.ts b/backend/src/tenant/central-admin.controller.ts index 2ad9a67..c11bb24 100644 --- a/backend/src/tenant/central-admin.controller.ts +++ b/backend/src/tenant/central-admin.controller.ts @@ -15,6 +15,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { CentralTenant, CentralDomain, CentralUser } from '../models/central.model'; import { getCentralKnex, initCentralModels } from './central-database.service'; import { TenantProvisioningService } from './tenant-provisioning.service'; +import { TenantDatabaseService } from './tenant-database.service'; import * as bcrypt from 'bcrypt'; /** @@ -26,6 +27,7 @@ import * as bcrypt from 'bcrypt'; export class CentralAdminController { constructor( private readonly provisioningService: TenantProvisioningService, + private readonly tenantDbService: TenantDatabaseService, ) { // Initialize central models on controller creation initCentralModels(); @@ -173,7 +175,18 @@ export class CentralAdminController { @Delete('domains/:id') async deleteDomain(@Req() req: any, @Param('id') id: string) { this.checkCentralAdmin(req); + + // Get domain info before deleting to invalidate cache + const domain = await CentralDomain.query().findById(id); + + // Delete the domain await CentralDomain.query().deleteById(id); + + // Invalidate tenant connection cache for this domain + if (domain) { + this.tenantDbService.removeTenantConnection(domain.domain); + } + return { success: true }; } diff --git a/backend/src/tenant/tenant-database.service.ts b/backend/src/tenant/tenant-database.service.ts index 3bb3db2..725e41d 100644 --- a/backend/src/tenant/tenant-database.service.ts +++ b/backend/src/tenant/tenant-database.service.ts @@ -9,17 +9,68 @@ export class TenantDatabaseService { private tenantConnections: Map = new Map(); async getTenantKnex(tenantIdOrSlug: string): Promise { + // Check if we have a cached connection if (this.tenantConnections.has(tenantIdOrSlug)) { + // For domain-based lookups, validate the domain still exists before returning cached connection + const centralPrisma = getCentralPrisma(); + + // Check if this looks like a domain (not a UUID) + const isDomain = !tenantIdOrSlug.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + + if (isDomain) { + try { + const domainRecord = await centralPrisma.domain.findUnique({ + where: { domain: tenantIdOrSlug }, + }); + + // If domain no longer exists, remove cached connection and continue to error + if (!domainRecord) { + this.logger.warn(`Domain ${tenantIdOrSlug} no longer exists, removing cached connection`); + await this.disconnectTenant(tenantIdOrSlug); + throw new Error(`Domain ${tenantIdOrSlug} not found`); + } + } catch (error) { + // If domain doesn't exist, remove from cache and re-throw + if (error.message.includes('not found')) { + throw error; + } + // For other errors, log but continue with cached connection + this.logger.warn(`Error validating domain ${tenantIdOrSlug}:`, error.message); + } + } + return this.tenantConnections.get(tenantIdOrSlug); } const centralPrisma = getCentralPrisma(); - // Try to find tenant by ID first, then by slug - let tenant = await centralPrisma.tenant.findUnique({ - where: { id: tenantIdOrSlug }, - }); + let tenant = null; + // First, try to find by domain (most common case - subdomain lookup) + try { + const domainRecord = await centralPrisma.domain.findUnique({ + where: { domain: tenantIdOrSlug }, + include: { tenant: true }, + }); + + console.log('here:' + JSON.stringify(domainRecord)); + + if (domainRecord) { + tenant = domainRecord.tenant; + this.logger.log(`Found tenant by domain: ${tenantIdOrSlug} -> ${tenant.name}`); + } + } catch (error) { + this.logger.debug(`No domain found for: ${tenantIdOrSlug}, trying ID/slug lookup`); + } + + // Fallback: Try to find tenant by ID + if (!tenant) { + tenant = await centralPrisma.tenant.findUnique({ + where: { id: tenantIdOrSlug }, + }); + } + + // Fallback: Try to find by slug if (!tenant) { tenant = await centralPrisma.tenant.findUnique({ where: { slug: tenantIdOrSlug },