WIp - fix login into central
This commit is contained in:
3
.env.api
3
.env.api
@@ -8,3 +8,6 @@ REDIS_URL="redis://redis:6379"
|
|||||||
# JWT, multi-tenant hints, etc.
|
# JWT, multi-tenant hints, etc.
|
||||||
JWT_SECRET="devsecret"
|
JWT_SECRET="devsecret"
|
||||||
TENANCY_STRATEGY="single-db"
|
TENANCY_STRATEGY="single-db"
|
||||||
|
|
||||||
|
|
||||||
|
CENTRAL_SUBDOMAINS="central,admin"
|
||||||
|
|||||||
231
CENTRAL_ADMIN_AUTH_GUIDE.md
Normal file
231
CENTRAL_ADMIN_AUTH_GUIDE.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# Central Admin Authentication Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The platform now supports **two types of authentication**:
|
||||||
|
|
||||||
|
1. **Tenant Login** - Authenticates users against a specific tenant's database
|
||||||
|
2. **Central Admin Login** - Authenticates administrators against the central platform database
|
||||||
|
|
||||||
|
## Central vs Tenant Authentication
|
||||||
|
|
||||||
|
### Tenant Authentication (Default)
|
||||||
|
- Users login to their specific tenant database
|
||||||
|
- Each tenant has isolated user tables
|
||||||
|
- Access is scoped to the tenant's data
|
||||||
|
- API Endpoint: `/api/auth/login`
|
||||||
|
- Requires `x-tenant-id` header or subdomain detection
|
||||||
|
|
||||||
|
### Central Admin Authentication
|
||||||
|
- Administrators login to the central platform database
|
||||||
|
- Can manage all tenants and platform-wide features
|
||||||
|
- Users stored in the central database `users` table
|
||||||
|
- API Endpoint: `/api/central/auth/login`
|
||||||
|
- No tenant ID required
|
||||||
|
|
||||||
|
## Creating a Central Admin User
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run create-central-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the interactive prompts to create your admin user.
|
||||||
|
|
||||||
|
### Environment Variable Method
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EMAIL=admin@platform.com \
|
||||||
|
PASSWORD=SecureP@ssw0rd \
|
||||||
|
FIRST_NAME=Admin \
|
||||||
|
LAST_NAME=User \
|
||||||
|
ROLE=superadmin \
|
||||||
|
npm run create-central-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role Types
|
||||||
|
|
||||||
|
- **admin** - Standard administrator with platform management access
|
||||||
|
- **superadmin** - Super administrator with full platform access
|
||||||
|
|
||||||
|
## Logging In as Central Admin
|
||||||
|
|
||||||
|
### Frontend Login
|
||||||
|
|
||||||
|
1. Navigate to the login page (`/login`)
|
||||||
|
2. **Check the "Login as Central Admin" checkbox**
|
||||||
|
3. Enter your central admin email and password
|
||||||
|
4. Click "Login to Central"
|
||||||
|
|
||||||
|
The checkbox toggles between:
|
||||||
|
- ✅ **Checked** - Authenticates against central database
|
||||||
|
- ⬜ **Unchecked** - Authenticates against tenant database (default)
|
||||||
|
|
||||||
|
### API Login (Direct)
|
||||||
|
|
||||||
|
**Central Admin Login:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/central/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"email": "admin@platform.com",
|
||||||
|
"password": "SecureP@ssw0rd"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"user": {
|
||||||
|
"id": "cm5a1b2c3d4e5f6g7h8i9j0k",
|
||||||
|
"email": "admin@platform.com",
|
||||||
|
"firstName": "Admin",
|
||||||
|
"lastName": "User",
|
||||||
|
"role": "superadmin",
|
||||||
|
"isCentralAdmin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tenant Login (for comparison):**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-tenant-id: tenant1" \
|
||||||
|
-d '{
|
||||||
|
"email": "user@tenant1.com",
|
||||||
|
"password": "password123"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## JWT Token Differences
|
||||||
|
|
||||||
|
### Central Admin Token Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "user-id",
|
||||||
|
"email": "admin@platform.com",
|
||||||
|
"isCentralAdmin": true,
|
||||||
|
"iat": 1234567890,
|
||||||
|
"exp": 1234654290
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tenant User Token Payload
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "user-id",
|
||||||
|
"email": "user@tenant1.com",
|
||||||
|
"iat": 1234567890,
|
||||||
|
"exp": 1234654290
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `isCentralAdmin` flag in the JWT can be used to determine if the user is a central admin.
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Central Database - `users` Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE users (
|
||||||
|
id VARCHAR(30) PRIMARY KEY,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
firstName VARCHAR(100),
|
||||||
|
lastName VARCHAR(100),
|
||||||
|
role VARCHAR(50) DEFAULT 'admin',
|
||||||
|
isActive BOOLEAN DEFAULT true,
|
||||||
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tenant Database - `users` Table
|
||||||
|
|
||||||
|
Tenant databases have their own separate `users` table with similar structure but tenant-specific users.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Separate Password Storage** - Central admin passwords are stored separately from tenant user passwords
|
||||||
|
2. **Role-Based Access** - Central admins have different permissions than tenant users
|
||||||
|
3. **JWT Identification** - The `isCentralAdmin` flag helps identify admin users
|
||||||
|
4. **Encryption** - All passwords are hashed using bcrypt with salt rounds
|
||||||
|
|
||||||
|
## Common Use Cases
|
||||||
|
|
||||||
|
### Platform Administration
|
||||||
|
- **Login as:** Central Admin
|
||||||
|
- **Can do:**
|
||||||
|
- Create/manage tenants
|
||||||
|
- View all tenant information
|
||||||
|
- Manage platform-wide settings
|
||||||
|
- Access tenant provisioning APIs
|
||||||
|
|
||||||
|
### Tenant Management
|
||||||
|
- **Login as:** Tenant User
|
||||||
|
- **Can do:**
|
||||||
|
- Access tenant-specific data
|
||||||
|
- Manage records within the tenant
|
||||||
|
- Use tenant applications
|
||||||
|
- Limited to tenant scope
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Tenant ID is required" Error
|
||||||
|
- You're trying to login to tenant endpoint without tenant ID
|
||||||
|
- Solution: Either provide `x-tenant-id` header or use central admin login
|
||||||
|
|
||||||
|
### "Invalid credentials" with Central Login
|
||||||
|
- Check that you're using the "Login as Central Admin" checkbox
|
||||||
|
- Verify the user exists in the central database
|
||||||
|
- Use the script to create a central admin if needed
|
||||||
|
|
||||||
|
### "User already exists"
|
||||||
|
- A central admin with that email already exists
|
||||||
|
- Use a different email or reset the existing user's password
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ Frontend Login Form │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ ☑ Login as Central Admin │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
└──────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────┴────────┐
|
||||||
|
│ Checked? │
|
||||||
|
└───────┬────────┘
|
||||||
|
│
|
||||||
|
┌──────────┴──────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
/api/central/auth/login /api/auth/login
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
Central Database Tenant Database
|
||||||
|
(users table) (users table)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints Summary
|
||||||
|
|
||||||
|
| Endpoint | Purpose | Requires Tenant ID | Database |
|
||||||
|
|----------|---------|-------------------|----------|
|
||||||
|
| `POST /api/central/auth/login` | Central admin login | ❌ No | Central |
|
||||||
|
| `POST /api/central/auth/register` | Create central admin | ❌ No | Central |
|
||||||
|
| `POST /api/auth/login` | Tenant user login | ✅ Yes | Tenant |
|
||||||
|
| `POST /api/auth/register` | Create tenant user | ✅ Yes | Tenant |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Create your first central admin user
|
||||||
|
2. Login with the central admin checkbox enabled
|
||||||
|
3. Access platform administration features
|
||||||
|
4. Manage tenants and platform settings
|
||||||
|
|
||||||
|
For tenant management and provisioning, see [TENANT_MIGRATION_GUIDE.md](../TENANT_MIGRATION_GUIDE.md).
|
||||||
130
CENTRAL_LOGIN.md
Normal file
130
CENTRAL_LOGIN.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Central Admin Login
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The platform supports seamless authentication for both **tenant users** and **central administrators** using the same login endpoint. The system automatically determines which database to authenticate against based on the subdomain.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Subdomain-Based Routing
|
||||||
|
|
||||||
|
The authentication flow uses subdomain detection to determine the authentication context:
|
||||||
|
|
||||||
|
1. **Central Subdomains** (e.g., `central.yourdomain.com`, `admin.yourdomain.com`)
|
||||||
|
- Authenticates against the **central database**
|
||||||
|
- Used for platform administrators
|
||||||
|
- Configured via `CENTRAL_SUBDOMAINS` environment variable
|
||||||
|
|
||||||
|
2. **Tenant Subdomains** (e.g., `acme.yourdomain.com`, `client1.yourdomain.com`)
|
||||||
|
- Authenticates against the **tenant's database**
|
||||||
|
- Used for regular tenant users
|
||||||
|
- Each tenant has its own isolated database
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Set the central subdomains in your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comma-separated list of subdomains that access the central database
|
||||||
|
CENTRAL_SUBDOMAINS="central,admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
#### 1. Tenant Middleware (`tenant.middleware.ts`)
|
||||||
|
|
||||||
|
The middleware extracts the subdomain from the request and:
|
||||||
|
- Checks if it matches a central subdomain
|
||||||
|
- If yes: Skips tenant resolution and attaches subdomain to request
|
||||||
|
- If no: Resolves the tenant ID from the subdomain and attaches it to request
|
||||||
|
|
||||||
|
#### 2. Auth Service (`auth.service.ts`)
|
||||||
|
|
||||||
|
The auth service has branching logic in `validateUser()` and `register()`:
|
||||||
|
- Checks if the subdomain is in the central list
|
||||||
|
- Routes to `validateCentralUser()` or normal tenant user validation
|
||||||
|
- Central users are authenticated against the `central` database
|
||||||
|
- Tenant users are authenticated against their tenant's database
|
||||||
|
|
||||||
|
#### 3. Auth Controller (`auth.controller.ts`)
|
||||||
|
|
||||||
|
The controller:
|
||||||
|
- Extracts subdomain from the request
|
||||||
|
- Validates tenant ID requirement (not needed for central subdomains)
|
||||||
|
- Passes subdomain to auth service for proper routing
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a Central Admin User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run create-central-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the prompts to enter:
|
||||||
|
- Email
|
||||||
|
- Password
|
||||||
|
- First Name (optional)
|
||||||
|
- Last Name (optional)
|
||||||
|
|
||||||
|
### Logging In as Central Admin
|
||||||
|
|
||||||
|
1. Navigate to `central.yourdomain.com` (or whatever central subdomain you configured)
|
||||||
|
2. Enter your central admin email and password
|
||||||
|
3. You'll be authenticated against the central database
|
||||||
|
|
||||||
|
**No special UI elements needed** - the system automatically detects the subdomain!
|
||||||
|
|
||||||
|
### Logging In as Tenant User
|
||||||
|
|
||||||
|
1. Navigate to `yourtenantslug.yourdomain.com`
|
||||||
|
2. Enter your tenant user credentials
|
||||||
|
3. You'll be authenticated against that tenant's database
|
||||||
|
|
||||||
|
## Architecture Benefits
|
||||||
|
|
||||||
|
✅ **Transparent to Frontend** - No need for special "login as admin" checkboxes or UI elements
|
||||||
|
✅ **Secure** - Central and tenant authentication are completely separated
|
||||||
|
✅ **Scalable** - Easy to add more central subdomains by updating environment variable
|
||||||
|
✅ **Clean Code** - Single auth controller/service with clear branching logic
|
||||||
|
✅ **Flexible** - Can be used for both development (localhost) and production
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
For local development, you can:
|
||||||
|
|
||||||
|
1. **Use subdomain on localhost:**
|
||||||
|
```
|
||||||
|
central.localhost:3000
|
||||||
|
acme.localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use x-tenant-id header** (for tenant-specific requests):
|
||||||
|
```bash
|
||||||
|
curl -H "x-tenant-id: acme-corp" http://localhost:3000/api/auth/login
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **For central admin, use central subdomain:**
|
||||||
|
```bash
|
||||||
|
curl http://central.localhost:3000/api/auth/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Central Database (`User` model)
|
||||||
|
- Stores platform administrators
|
||||||
|
- Prisma schema: `schema-central.prisma`
|
||||||
|
- Fields: id, email, password, firstName, lastName, isActive, createdAt, updatedAt
|
||||||
|
|
||||||
|
### Tenant Database (`users` table)
|
||||||
|
- Stores tenant-specific users
|
||||||
|
- Knex migrations: `migrations/tenant/`
|
||||||
|
- Fields: id, email, password, firstName, lastName, isActive, created_at, updated_at
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Central admin credentials are never stored in tenant databases
|
||||||
|
- Tenant user credentials are never stored in the central database
|
||||||
|
- JWT tokens include user context (tenant ID or central admin flag)
|
||||||
|
- Subdomain validation prevents unauthorized access
|
||||||
@@ -18,3 +18,6 @@ JWT_EXPIRES_IN="7d"
|
|||||||
# Application
|
# Application
|
||||||
NODE_ENV="development"
|
NODE_ENV="development"
|
||||||
PORT="3000"
|
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
|
### 1. Create a New Migration
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
Req,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
@@ -40,17 +41,36 @@ class RegisterDto {
|
|||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private authService: AuthService) {}
|
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)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('login')
|
@Post('login')
|
||||||
async login(@TenantId() tenantId: string, @Body() loginDto: LoginDto) {
|
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) {
|
if (!tenantId) {
|
||||||
throw new UnauthorizedException('Tenant ID is required');
|
throw new UnauthorizedException('Tenant ID is required');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.authService.validateUser(
|
const user = await this.authService.validateUser(
|
||||||
tenantId,
|
tenantId,
|
||||||
loginDto.email,
|
loginDto.email,
|
||||||
loginDto.password,
|
loginDto.password,
|
||||||
|
subdomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -64,10 +84,16 @@ export class AuthController {
|
|||||||
async register(
|
async register(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
@Body() registerDto: RegisterDto,
|
@Body() registerDto: RegisterDto,
|
||||||
|
@Req() req: any,
|
||||||
) {
|
) {
|
||||||
|
const subdomain = req.raw?.subdomain;
|
||||||
|
|
||||||
|
// If it's a central subdomain, tenantId is not required
|
||||||
|
if (!subdomain || !this.isCentralSubdomain(subdomain)) {
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
throw new UnauthorizedException('Tenant ID is required');
|
throw new UnauthorizedException('Tenant ID is required');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.authService.register(
|
const user = await this.authService.register(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -75,6 +101,7 @@ export class AuthController {
|
|||||||
registerDto.password,
|
registerDto.password,
|
||||||
registerDto.firstName,
|
registerDto.firstName,
|
||||||
registerDto.lastName,
|
registerDto.lastName,
|
||||||
|
subdomain,
|
||||||
);
|
);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
||||||
|
import { getCentralPrisma } from '../prisma/central-prisma.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -10,11 +11,24 @@ export class AuthService {
|
|||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private isCentralSubdomain(subdomain: string): boolean {
|
||||||
|
const centralSubdomains = (process.env.CENTRAL_SUBDOMAINS || 'central,admin').split(',');
|
||||||
|
return centralSubdomains.includes(subdomain);
|
||||||
|
}
|
||||||
|
|
||||||
async validateUser(
|
async validateUser(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
|
subdomain?: string,
|
||||||
): Promise<any> {
|
): 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 tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
|
||||||
|
|
||||||
const user = await tenantDb('users')
|
const user = await tenantDb('users')
|
||||||
@@ -43,6 +57,31 @@ export class AuthService {
|
|||||||
return null;
|
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) {
|
async login(user: any) {
|
||||||
const payload = {
|
const payload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
@@ -66,7 +105,14 @@ export class AuthService {
|
|||||||
password: string,
|
password: string,
|
||||||
firstName?: string,
|
firstName?: string,
|
||||||
lastName?: 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 tenantDb = await this.tenantDbService.getTenantKnex(tenantId);
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
@@ -88,4 +134,28 @@ export class AuthService {
|
|||||||
const { password: _, ...result } = user;
|
const { password: _, ...result } = user;
|
||||||
return result;
|
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
|
// Extract subdomain from hostname
|
||||||
const host = req.headers.host || '';
|
const host = req.headers.host || '';
|
||||||
const hostname = host.split(':')[0]; // Remove port if present
|
const hostname = host.split(':')[0]; // Remove port if present
|
||||||
const parts = hostname.split('.');
|
|
||||||
|
|
||||||
this.logger.log(`Host header: ${host}, hostname: ${hostname}, parts: ${JSON.stringify(parts)}`);
|
// 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
|
// For local development, accept x-tenant-id header
|
||||||
let tenantId = req.headers['x-tenant-id'] as string;
|
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}`);
|
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
|
// Try to extract subdomain from Origin header first (for API calls from frontend)
|
||||||
if (tenantId) {
|
if (origin) {
|
||||||
this.logger.log(`Using explicit x-tenant-id: ${tenantId}`);
|
try {
|
||||||
(req as any).tenantId = tenantId;
|
const originUrl = new URL(origin);
|
||||||
next();
|
const originHost = originUrl.hostname;
|
||||||
return;
|
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")
|
// Extract subdomain (e.g., "tenant1" from "tenant1.routebox.co")
|
||||||
@@ -51,6 +70,36 @@ export class TenantMiddleware implements NestMiddleware {
|
|||||||
|
|
||||||
this.logger.log(`Extracted subdomain: ${subdomain}`);
|
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
|
// Get tenant by subdomain if available
|
||||||
if (subdomain) {
|
if (subdomain) {
|
||||||
try {
|
try {
|
||||||
@@ -72,9 +121,6 @@ export class TenantMiddleware implements NestMiddleware {
|
|||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
// Attach tenant info to request object
|
// Attach tenant info to request object
|
||||||
(req as any).tenantId = tenantId;
|
(req as any).tenantId = tenantId;
|
||||||
if (subdomain) {
|
|
||||||
(req as any).subdomain = subdomain;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`No tenant identified from host: ${hostname}`);
|
this.logger.warn(`No tenant identified from host: ${hostname}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user