WIp - fix login into central
This commit is contained in:
@@ -18,3 +18,6 @@ JWT_EXPIRES_IN="7d"
|
||||
# Application
|
||||
NODE_ENV="development"
|
||||
PORT="3000"
|
||||
|
||||
# Central Admin Subdomains (comma-separated list of subdomains that access the central database)
|
||||
CENTRAL_SUBDOMAINS="central,admin"
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
# Tenant Migration Scripts
|
||||
# Tenant Migration & Admin Scripts
|
||||
|
||||
This directory contains scripts for managing database migrations across all tenants in the multi-tenant platform.
|
||||
This directory contains scripts for managing database migrations across all tenants and creating admin users in the multi-tenant platform.
|
||||
|
||||
## Available Scripts
|
||||
## Admin User Management
|
||||
|
||||
### Create Central Admin User
|
||||
|
||||
```bash
|
||||
npm run create-central-admin
|
||||
```
|
||||
|
||||
Creates an administrator user in the **central database**. Central admins can:
|
||||
- Manage tenants (create, update, delete)
|
||||
- Access platform-wide administration features
|
||||
- View all tenant information
|
||||
- Manage tenant provisioning
|
||||
|
||||
**Interactive Mode:**
|
||||
```bash
|
||||
npm run create-central-admin
|
||||
# You will be prompted for:
|
||||
# - Email
|
||||
# - Password
|
||||
# - First Name (optional)
|
||||
# - Last Name (optional)
|
||||
# - Role (admin or superadmin)
|
||||
```
|
||||
|
||||
**Non-Interactive Mode (using environment variables):**
|
||||
```bash
|
||||
EMAIL=admin@example.com PASSWORD=securepass123 FIRST_NAME=John LAST_NAME=Doe ROLE=superadmin npm run create-central-admin
|
||||
```
|
||||
|
||||
**Logging In as Central Admin:**
|
||||
1. Access the application using a central subdomain (e.g., `central.yourdomain.com` or `admin.yourdomain.com`)
|
||||
2. Enter your central admin credentials
|
||||
3. You'll be authenticated against the central database (not a tenant database)
|
||||
|
||||
**Note:** The system automatically detects if you're logging in from a central subdomain based on the `CENTRAL_SUBDOMAINS` environment variable (defaults to `central,admin`). No special UI or configuration is needed on the frontend.
|
||||
|
||||
### Create Tenant User
|
||||
|
||||
For creating users within a specific tenant database, use:
|
||||
```bash
|
||||
npm run create-tenant-user <tenant-slug>
|
||||
# (Note: This script may need to be created or already exists)
|
||||
```
|
||||
|
||||
## Migration Scripts
|
||||
|
||||
### 1. Create a New Migration
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
UnauthorizedException,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||
import { AuthService } from './auth.service';
|
||||
@@ -40,17 +41,36 @@ class RegisterDto {
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
private isCentralSubdomain(subdomain: string): boolean {
|
||||
const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(',');
|
||||
return centralSubdomains.includes(subdomain);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('login')
|
||||
async login(@TenantId() tenantId: string, @Body() loginDto: LoginDto) {
|
||||
if (!tenantId) {
|
||||
throw new UnauthorizedException('Tenant ID is required');
|
||||
async login(
|
||||
@TenantId() tenantId: string,
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: any,
|
||||
) {
|
||||
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) {
|
||||
throw new UnauthorizedException('Tenant ID is required');
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.authService.validateUser(
|
||||
tenantId,
|
||||
loginDto.email,
|
||||
loginDto.password,
|
||||
subdomain,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
@@ -64,9 +84,15 @@ export class AuthController {
|
||||
async register(
|
||||
@TenantId() tenantId: string,
|
||||
@Body() registerDto: RegisterDto,
|
||||
@Req() req: any,
|
||||
) {
|
||||
if (!tenantId) {
|
||||
throw new UnauthorizedException('Tenant ID is required');
|
||||
const subdomain = req.raw?.subdomain;
|
||||
|
||||
// If it's a central subdomain, tenantId is not required
|
||||
if (!subdomain || !this.isCentralSubdomain(subdomain)) {
|
||||
if (!tenantId) {
|
||||
throw new UnauthorizedException('Tenant ID is required');
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.authService.register(
|
||||
@@ -75,6 +101,7 @@ export class AuthController {
|
||||
registerDto.password,
|
||||
registerDto.firstName,
|
||||
registerDto.lastName,
|
||||
subdomain,
|
||||
);
|
||||
|
||||
return user;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
||||
import { getCentralPrisma } from '../prisma/central-prisma.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
@@ -10,11 +11,24 @@ export class AuthService {
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
private isCentralSubdomain(subdomain: string): boolean {
|
||||
const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(',');
|
||||
return centralSubdomains.includes(subdomain);
|
||||
}
|
||||
|
||||
async validateUser(
|
||||
tenantId: string,
|
||||
email: string,
|
||||
password: string,
|
||||
subdomain?: string,
|
||||
): Promise<any> {
|
||||
|
||||
// Check if this is a central subdomain
|
||||
if (subdomain && this.isCentralSubdomain(subdomain)) {
|
||||
return this.validateCentralUser(email, password);
|
||||
}
|
||||
|
||||
// Otherwise, validate as tenant user
|
||||
const tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
|
||||
|
||||
const user = await tenantDb('users')
|
||||
@@ -43,6 +57,31 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async validateCentralUser(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<any> {
|
||||
const centralPrisma = getCentralPrisma();
|
||||
|
||||
const user = await centralPrisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await bcrypt.compare(password, user.password)) {
|
||||
const { password: _, ...result } = user;
|
||||
return {
|
||||
...result,
|
||||
isCentralAdmin: true,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: any) {
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
@@ -66,7 +105,14 @@ export class AuthService {
|
||||
password: string,
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
subdomain?: string,
|
||||
) {
|
||||
// Check if this is a central subdomain
|
||||
if (subdomain && this.isCentralSubdomain(subdomain)) {
|
||||
return this.registerCentralUser(email, password, firstName, lastName);
|
||||
}
|
||||
|
||||
// Otherwise, register as tenant user
|
||||
const tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
@@ -88,4 +134,28 @@ export class AuthService {
|
||||
const { password: _, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async registerCentralUser(
|
||||
email: string,
|
||||
password: string,
|
||||
firstName?: string,
|
||||
lastName?: string,
|
||||
) {
|
||||
const centralPrisma = getCentralPrisma();
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await centralPrisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
firstName: firstName || null,
|
||||
lastName: lastName || null,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { password: _, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,14 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
// Extract subdomain from hostname
|
||||
const host = req.headers.host || '';
|
||||
const hostname = host.split(':')[0]; // Remove port if present
|
||||
const parts = hostname.split('.');
|
||||
|
||||
// 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}, parts: ${JSON.stringify(parts)}`);
|
||||
this.logger.log(`Host header: ${host}, hostname: ${hostname}, origin: ${origin}, referer: ${referer}, parts: ${JSON.stringify(parts)}`);
|
||||
|
||||
// For local development, accept x-tenant-id header
|
||||
let tenantId = req.headers['x-tenant-id'] as string;
|
||||
@@ -27,12 +32,26 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
|
||||
this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}, x-tenant-id: ${tenantId}`);
|
||||
|
||||
// If x-tenant-id is explicitly provided, use it directly
|
||||
if (tenantId) {
|
||||
this.logger.log(`Using explicit x-tenant-id: ${tenantId}`);
|
||||
(req as any).tenantId = tenantId;
|
||||
next();
|
||||
return;
|
||||
// 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 && !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")
|
||||
@@ -51,6 +70,36 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
|
||||
this.logger.log(`Extracted subdomain: ${subdomain}`);
|
||||
|
||||
// Always attach subdomain to request if present
|
||||
if (subdomain) {
|
||||
(req as any).subdomain = subdomain;
|
||||
}
|
||||
|
||||
// If x-tenant-id is explicitly provided, use it directly but still keep subdomain
|
||||
if (tenantId) {
|
||||
this.logger.log(`Using explicit x-tenant-id: ${tenantId}`);
|
||||
(req as any).tenantId = tenantId;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Always attach subdomain to request if present
|
||||
if (subdomain) {
|
||||
(req as any).subdomain = subdomain;
|
||||
}
|
||||
|
||||
// Check if this is a central subdomain
|
||||
const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(',');
|
||||
const isCentral = subdomain && centralSubdomains.includes(subdomain);
|
||||
|
||||
// If it's a central subdomain, skip tenant resolution
|
||||
if (isCentral) {
|
||||
this.logger.log(`Central subdomain detected: ${subdomain}, skipping tenant resolution`);
|
||||
(req as any).subdomain = subdomain;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tenant by subdomain if available
|
||||
if (subdomain) {
|
||||
try {
|
||||
@@ -72,9 +121,6 @@ export class TenantMiddleware implements NestMiddleware {
|
||||
if (tenantId) {
|
||||
// Attach tenant info to request object
|
||||
(req as any).tenantId = tenantId;
|
||||
if (subdomain) {
|
||||
(req as any).subdomain = subdomain;
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`No tenant identified from host: ${hostname}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user