feat(02-01): UserModule, TenantModule, admin seed, and app.module wiring
- Create UserService with findByUsername (unscoped), create, update, deactivate, delete - Create AdminSeedService that seeds Super-Admin from Docker ENV on bootstrap (D-05/D-07/D-13) - Create TenantService with findAll, findById, create, update - Create TenantMiddleware extracting tenantId from JWT with Super-Admin tenant switching (D-08/D-10) - Wire PrismaModule, AuthModule, UserModule, TenantModule into AppModule - Register JwtAuthGuard and RolesGuard as global APP_GUARD providers - Apply TenantMiddleware to all routes via NestModule.configure - Add @Public() decorator to HealthController for unauthenticated access
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
} from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
* Extracts tenantId from the authenticated user's JWT claim and creates
|
||||
* a tenant-scoped Prisma client for the request.
|
||||
*
|
||||
* Super-Admin can switch tenant context via x-tenant-id header (D-10).
|
||||
* Per D-08: Tenant context from JWT, no URL-based routing.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TenantMiddleware implements NestMiddleware {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const user = (req as any).user;
|
||||
|
||||
// No user means public route (e.g., login, health) - skip tenant context
|
||||
if (!user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Determine tenant ID
|
||||
let tenantId: string | undefined = user.tenantId;
|
||||
|
||||
// Super-Admin can switch tenant via header
|
||||
if (user.role === 'SUPER_ADMIN' && req.headers['x-tenant-id']) {
|
||||
tenantId = req.headers['x-tenant-id'] as string;
|
||||
}
|
||||
|
||||
// Non-Super-Admin users MUST have a tenant
|
||||
if (!tenantId && user.role !== 'SUPER_ADMIN') {
|
||||
throw new ForbiddenException('No tenant context');
|
||||
}
|
||||
|
||||
// Attach tenant-scoped Prisma client
|
||||
if (tenantId) {
|
||||
(req as any).tenantPrisma = forTenant(this.prisma, tenantId);
|
||||
(req as any).tenantId = tenantId;
|
||||
} else {
|
||||
// Super-Admin without tenant header gets unscoped access
|
||||
(req as any).tenantPrisma = this.prisma;
|
||||
(req as any).tenantId = null;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TenantService } from './tenant.service';
|
||||
|
||||
@Module({
|
||||
providers: [TenantService],
|
||||
exports: [TenantService],
|
||||
})
|
||||
export class TenantModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class TenantService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.tenant.findMany();
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.prisma.tenant.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async create(data: { name: string; slug: string }) {
|
||||
return this.prisma.tenant.create({ data });
|
||||
}
|
||||
|
||||
async update(id: string, data: { name?: string; slug?: string; isActive?: boolean }) {
|
||||
return this.prisma.tenant.update({ where: { id }, data });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user