initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:37:07 +08:00
commit 5382f3b4e5
87 changed files with 5278 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { Controller, Post, Delete, Param, Req } from '@nestjs/common';
import { Request } from 'express';
import { ImpersonateService } from './impersonate.service';
@Controller('impersonate')
export class ImpersonateController {
constructor(private readonly service: ImpersonateService) {}
@Post(':tenantId')
start(@Param('tenantId') tenantId: string, @Req() req: Request & { admin: any }) {
const admin = req.admin;
return this.service.startImpersonation(
tenantId,
admin.sub,
`${admin.firstName || ''} ${admin.lastName || ''}`.trim(),
);
}
@Delete()
end(@Req() req: Request & { admin: any }) {
return this.service.endImpersonation(req.admin.sub);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ImpersonateController } from './impersonate.controller';
import { ImpersonateService } from './impersonate.service';
import { AuditModule } from '../audit/audit.module';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [AuditModule, AuthModule],
controllers: [ImpersonateController],
providers: [ImpersonateService],
})
export class ImpersonateModule {}

View File

@@ -0,0 +1,80 @@
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { TenantPrismaService } from '../prisma/tenant-prisma.service';
import { AuditService } from '../audit/audit.service';
@Injectable()
export class ImpersonateService {
private readonly mainJwtSecret: string;
constructor(
private readonly tenantDb: TenantPrismaService,
private readonly jwt: JwtService,
private readonly config: ConfigService,
private readonly audit: AuditService,
) {
// Use the MAIN API's JWT secret so the token works with the tenant API
this.mainJwtSecret = this.config.get<string>('JWT_SECRET', 'dev-jwt-secret-not-for-production');
}
async startImpersonation(tenantId: string, adminId: string, adminName: string) {
const tenant = await this.tenantDb.tenant.findUnique({
where: { id: tenantId, deletedAt: null, isActive: true },
});
if (!tenant) throw new NotFoundException('Tenant not found or inactive');
// Find the tenant's first admin user
const adminUser = await this.tenantDb.user.findFirst({
where: {
tenantId,
deletedAt: null,
isActive: true,
roles: { some: { role: 'tenant_admin' } },
},
});
if (!adminUser) throw new NotFoundException('No tenant admin found for this tenant');
// Generate a JWT compatible with the main API
const token = this.jwt.sign(
{
sub: adminUser.id,
tenantId,
roles: ['tenant_admin'],
permissions: 'all',
impersonatedBy: adminId,
},
{
secret: this.mainJwtSecret,
expiresIn: '1h',
},
);
// Audit the impersonation
await this.audit.log(adminId, 'impersonate.start', tenantId, {
tenantName: tenant.name,
impersonatedUserId: adminUser.id,
});
return {
accessToken: token,
impersonatedUser: {
id: adminUser.id,
email: adminUser.email,
firstName: adminUser.firstName,
lastName: adminUser.lastName,
},
tenant: {
id: tenant.id,
name: tenant.name,
slug: tenant.slug,
},
expiresAt: new Date(Date.now() + 3600000).toISOString(),
};
}
async endImpersonation(adminId: string) {
await this.audit.log(adminId, 'impersonate.end');
return { message: 'Impersonation ended' };
}
}