WIP - BFF

This commit is contained in:
Francisco Gaona
2026-02-04 00:21:06 +01:00
parent f68321c802
commit 0e2f3dddbc
17 changed files with 645 additions and 254 deletions

View File

@@ -10,14 +10,14 @@ export class AppBuilderService {
// Runtime endpoints
async getApps(tenantId: string, userId: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
// For now, return all apps
// In production, you'd filter by user permissions
return App.query(knex).withGraphFetched('pages').orderBy('label', 'asc');
}
async getApp(tenantId: string, slug: string, userId: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await App.query(knex)
.findOne({ slug })
.withGraphFetched('pages');
@@ -35,7 +35,7 @@ export class AppBuilderService {
pageSlug: string,
userId: string,
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await this.getApp(tenantId, appSlug, userId);
const page = await AppPage.query(knex).findOne({
@@ -52,12 +52,12 @@ export class AppBuilderService {
// Setup endpoints
async getAllApps(tenantId: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
return App.query(knex).withGraphFetched('pages').orderBy('label', 'asc');
}
async getAppForSetup(tenantId: string, slug: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await App.query(knex)
.findOne({ slug })
.withGraphFetched('pages');
@@ -77,7 +77,7 @@ export class AppBuilderService {
description?: string;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
return App.query(knex).insert({
...data,
displayOrder: 0,
@@ -92,7 +92,7 @@ export class AppBuilderService {
description?: string;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await this.getAppForSetup(tenantId, slug);
return App.query(knex).patchAndFetchById(app.id, data);
@@ -109,7 +109,7 @@ export class AppBuilderService {
sortOrder?: number;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await this.getAppForSetup(tenantId, appSlug);
return AppPage.query(knex).insert({
@@ -133,7 +133,7 @@ export class AppBuilderService {
sortOrder?: number;
},
) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const app = await this.getAppForSetup(tenantId, appSlug);
const page = await AppPage.query(knex).findOne({

View File

@@ -29,8 +29,8 @@ export class AuthService {
}
// Otherwise, validate as tenant user
const tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
const tenantDb = await this.tenantDbService.getTenantKnexById(tenantId);
const user = await tenantDb('users')
.where({ email })
.first();
@@ -113,7 +113,7 @@ export class AuthService {
}
// Otherwise, register as tenant user
const tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
const tenantDb = await this.tenantDbService.getTenantKnexById(tenantId);
const hashedPassword = await bcrypt.hash(password, 10);

View File

@@ -7,7 +7,7 @@ export class PageLayoutService {
constructor(private tenantDbService: TenantDatabaseService) {}
async create(tenantId: string, createDto: CreatePageLayoutDto) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const layoutType = createDto.layoutType || 'detail';
// If this layout is set as default, unset other defaults for the same object and layout type
@@ -32,7 +32,7 @@ export class PageLayoutService {
}
async findAll(tenantId: string, objectId?: string, layoutType?: PageLayoutType) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
let query = knex('page_layouts');
@@ -49,7 +49,7 @@ export class PageLayoutService {
}
async findOne(tenantId: string, id: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const layout = await knex('page_layouts').where({ id }).first();
@@ -61,7 +61,7 @@ export class PageLayoutService {
}
async findDefaultByObject(tenantId: string, objectId: string, layoutType: PageLayoutType = 'detail') {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
const layout = await knex('page_layouts')
.where({ object_id: objectId, is_default: true, layout_type: layoutType })
@@ -71,7 +71,7 @@ export class PageLayoutService {
}
async update(tenantId: string, id: string, updateDto: UpdatePageLayoutDto) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
// Check if layout exists
const layout = await this.findOne(tenantId, id);
@@ -112,7 +112,7 @@ export class PageLayoutService {
}
async remove(tenantId: string, id: string) {
const knex = await this.tenantDbService.getTenantKnex(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(tenantId);
await this.findOne(tenantId, id);

View File

@@ -14,61 +14,61 @@ export class TenantMiddleware implements NestMiddleware {
next: () => void,
) {
try {
// Extract subdomain from hostname
const host = req.headers.host || '';
const hostname = host.split(':')[0]; // Remove port if present
// Check Origin header to get frontend subdomain (for API calls)
const origin = req.headers.origin as string;
const referer = req.headers.referer as string;
let parts = hostname.split('.');
this.logger.log(`Host header: ${host}, hostname: ${hostname}, origin: ${origin}, referer: ${referer}, parts: ${JSON.stringify(parts)}`);
// For local development, accept x-tenant-id header
// Priority 1: Check x-tenant-subdomain header from Nitro BFF proxy
// This is the primary method when using the BFF architecture
let subdomain = req.headers['x-tenant-subdomain'] as string | null;
let tenantId = req.headers['x-tenant-id'] as string;
let subdomain: string | null = null;
this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}, x-tenant-id: ${tenantId}`);
if (subdomain) {
this.logger.log(`Using x-tenant-subdomain header: ${subdomain}`);
}
// Try to extract subdomain from Origin header first (for API calls from frontend)
if (origin) {
try {
const originUrl = new URL(origin);
const originHost = originUrl.hostname;
parts = originHost.split('.');
this.logger.log(`Using Origin header hostname: ${originHost}, parts: ${JSON.stringify(parts)}`);
} catch (error) {
this.logger.warn(`Failed to parse origin: ${origin}`);
// Priority 2: Fall back to extracting subdomain from Origin/Host headers
// This supports direct backend access for development/testing
if (!subdomain && !tenantId) {
const host = req.headers.host || '';
const hostname = host.split(':')[0];
const origin = req.headers.origin as string;
const referer = req.headers.referer as string;
let parts = hostname.split('.');
this.logger.log(`Host header: ${host}, hostname: ${hostname}, origin: ${origin}, referer: ${referer}`);
// Try to extract subdomain from Origin header first (for API calls from frontend)
if (origin) {
try {
const originUrl = new URL(origin);
const originHost = originUrl.hostname;
parts = originHost.split('.');
this.logger.log(`Using Origin header hostname: ${originHost}, parts: ${JSON.stringify(parts)}`);
} catch (error) {
this.logger.warn(`Failed to parse origin: ${origin}`);
}
} else if (referer) {
// Fallback to Referer if no Origin
try {
const refererUrl = new URL(referer);
const refererHost = refererUrl.hostname;
parts = refererHost.split('.');
this.logger.log(`Using Referer header hostname: ${refererHost}, parts: ${JSON.stringify(parts)}`);
} catch (error) {
this.logger.warn(`Failed to parse referer: ${referer}`);
}
}
} else if (referer && !tenantId) {
// Fallback to Referer if no Origin
try {
const refererUrl = new URL(referer);
const refererHost = refererUrl.hostname;
parts = refererHost.split('.');
this.logger.log(`Using Referer header hostname: ${refererHost}, parts: ${JSON.stringify(parts)}`);
} catch (error) {
this.logger.warn(`Failed to parse referer: ${referer}`);
// Extract subdomain (e.g., "tenant1" from "tenant1.routebox.co")
if (parts.length >= 3) {
subdomain = parts[0];
if (subdomain === 'www') {
subdomain = null;
}
} else if (parts.length === 2 && parts[1] === 'localhost') {
subdomain = parts[0];
}
}
// Extract subdomain (e.g., "tenant1" from "tenant1.routebox.co")
// For production domains with 3+ parts, extract first part as subdomain
if (parts.length >= 3) {
subdomain = parts[0];
// Ignore www subdomain
if (subdomain === 'www') {
subdomain = null;
}
}
// For development (e.g., tenant1.localhost), also check 2 parts
else if (parts.length === 2 && parts[1] === 'localhost') {
subdomain = parts[0];
}
this.logger.log(`Extracted subdomain: ${subdomain}`);
this.logger.log(`Extracted subdomain: ${subdomain}, x-tenant-id: ${tenantId}`);
// Always attach subdomain to request if present
if (subdomain) {
@@ -122,7 +122,7 @@ export class TenantMiddleware implements NestMiddleware {
// Attach tenant info to request object
(req as any).tenantId = tenantId;
} else {
this.logger.warn(`No tenant identified from host: ${hostname}`);
this.logger.warn(`No tenant identified from host: ${subdomain}`);
}
next();