Add admin enhancements: impersonation, plan management, org status enforcement

Enhancement 1 - Block suspended/archived org access:
- Add org status check in switchOrganization() (auth.service.ts)
- Filter suspended/archived orgs from login response (generateTokenResponse)
- Add org status guard with 60s cache in TenantMiddleware
- Frontend: filter orgs in SelectOrgPage, add 403 handler in api.ts

Enhancement 2 - Change tenant plan level:
- Add updatePlanLevel() to organizations.service.ts
- Add PUT /admin/organizations/:id/plan endpoint
- Frontend: clickable plan dropdown in Organizations table + confirmation modal
- Plan level Select in tenant detail drawer

Enhancement 3 - User impersonation:
- Add impersonateUser() to auth.service.ts with impersonatedBy JWT claim
- Add POST /admin/impersonate/:userId endpoint
- Frontend: Impersonate button in Users tab (disabled for admins)
- Impersonation state management in authStore (start/stop/persist)
- Orange impersonation banner in AppLayout header with stop button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 13:21:59 -05:00
parent e156cf7c87
commit d9bb9363dd
10 changed files with 345 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
import { Controller, Get, Post, Put, Body, Param, UseGuards, Req, ForbiddenException, BadRequestException } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { OrganizationsService } from '../organizations/organizations.service';
import { AdminAnalyticsService } from './admin-analytics.service';
@@ -12,6 +13,7 @@ import * as bcrypt from 'bcryptjs';
@UseGuards(JwtAuthGuard)
export class AdminController {
constructor(
private authService: AuthService,
private usersService: UsersService,
private orgService: OrganizationsService,
private analyticsService: AdminAnalyticsService,
@@ -92,6 +94,23 @@ export class AdminController {
return { success: true, organization: org };
}
// ── Plan Level ──
@Put('organizations/:id/plan')
async updateOrgPlan(
@Req() req: any,
@Param('id') id: string,
@Body() body: { planLevel: string },
) {
await this.requireSuperadmin(req);
const validPlans = ['standard', 'premium', 'enterprise'];
if (!validPlans.includes(body.planLevel)) {
throw new BadRequestException(`Invalid plan. Must be one of: ${validPlans.join(', ')}`);
}
const org = await this.orgService.updatePlanLevel(id, body.planLevel);
return { success: true, organization: org };
}
// ── Superadmin Toggle ──
@Post('users/:id/superadmin')
@@ -101,6 +120,15 @@ export class AdminController {
return { success: true };
}
// ── User Impersonation ──
@Post('impersonate/:userId')
async impersonateUser(@Req() req: any, @Param('userId') userId: string) {
await this.requireSuperadmin(req);
const adminUserId = req.user.userId || req.user.sub;
return this.authService.impersonateUser(adminUserId, userId);
}
// ── Tenant Health ──
@Get('tenants-health')