Add comprehensive platform administration panel
- Database: Add login_history, ai_recommendation_log tables; is_platform_owner column on users; subscription fields on organizations (payment_date, confirmation_number, renewal_date) - Backend: New AdminAnalyticsService with platform metrics, tenant detail, and health score calculations (0-100 based on activity, budget, transactions, members, AI usage) - Backend: Login/org-switch now records to login_history; AI recommendations logged to ai_recommendation_log; platform owner protected from superadmin toggle - Frontend: 4-tab admin panel (Dashboard, Organizations, Users, Tenant Health) with tenant detail drawer, subscription management, health scoring visualization - Platform owner account (admin@hoaledgeriq.com) auto-redirects to admin panel - Seed data includes platform owner account and sample login history Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { OrganizationsService } from '../organizations/organizations.service';
|
||||
import { AdminAnalyticsService } from './admin-analytics.service';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
@ApiTags('admin')
|
||||
@@ -13,6 +14,7 @@ export class AdminController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private orgService: OrganizationsService,
|
||||
private analyticsService: AdminAnalyticsService,
|
||||
) {}
|
||||
|
||||
private async requireSuperadmin(req: any) {
|
||||
@@ -22,25 +24,76 @@ export class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Platform Metrics ──
|
||||
|
||||
@Get('metrics')
|
||||
async getPlatformMetrics(@Req() req: any) {
|
||||
await this.requireSuperadmin(req);
|
||||
return this.analyticsService.getPlatformMetrics();
|
||||
}
|
||||
|
||||
// ── Users ──
|
||||
|
||||
@Get('users')
|
||||
async listUsers(@Req() req: any) {
|
||||
await this.requireSuperadmin(req);
|
||||
const users = await this.usersService.findAllUsers();
|
||||
return users.map(u => ({
|
||||
id: u.id, email: u.email, firstName: u.firstName, lastName: u.lastName,
|
||||
isSuperadmin: u.isSuperadmin, lastLoginAt: u.lastLoginAt, createdAt: u.createdAt,
|
||||
isSuperadmin: u.isSuperadmin, isPlatformOwner: u.isPlatformOwner || false,
|
||||
lastLoginAt: u.lastLoginAt, createdAt: u.createdAt,
|
||||
organizations: u.userOrganizations?.map(uo => ({
|
||||
id: uo.organizationId, name: uo.organization?.name, role: uo.role,
|
||||
})) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Organizations ──
|
||||
|
||||
@Get('organizations')
|
||||
async listOrganizations(@Req() req: any) {
|
||||
await this.requireSuperadmin(req);
|
||||
return this.usersService.findAllOrganizations();
|
||||
}
|
||||
|
||||
@Get('organizations/:id/detail')
|
||||
async getTenantDetail(@Req() req: any, @Param('id') id: string) {
|
||||
await this.requireSuperadmin(req);
|
||||
const detail = await this.analyticsService.getTenantDetail(id);
|
||||
if (!detail) {
|
||||
throw new BadRequestException('Organization not found');
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Put('organizations/:id/subscription')
|
||||
async updateSubscription(
|
||||
@Req() req: any,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { paymentDate?: string; confirmationNumber?: string; renewalDate?: string },
|
||||
) {
|
||||
await this.requireSuperadmin(req);
|
||||
const org = await this.orgService.updateSubscription(id, body);
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
|
||||
@Put('organizations/:id/status')
|
||||
async updateOrgStatus(
|
||||
@Req() req: any,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { status: string },
|
||||
) {
|
||||
await this.requireSuperadmin(req);
|
||||
const validStatuses = ['active', 'suspended', 'trial', 'archived'];
|
||||
if (!validStatuses.includes(body.status)) {
|
||||
throw new BadRequestException(`Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
||||
}
|
||||
const org = await this.orgService.updateStatus(id, body.status);
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
|
||||
// ── Superadmin Toggle ──
|
||||
|
||||
@Post('users/:id/superadmin')
|
||||
async toggleSuperadmin(@Req() req: any, @Param('id') id: string, @Body() body: { isSuperadmin: boolean }) {
|
||||
await this.requireSuperadmin(req);
|
||||
@@ -48,6 +101,16 @@ export class AdminController {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ── Tenant Health ──
|
||||
|
||||
@Get('tenants-health')
|
||||
async getTenantsHealth(@Req() req: any) {
|
||||
await this.requireSuperadmin(req);
|
||||
return this.analyticsService.getAllTenantsHealth();
|
||||
}
|
||||
|
||||
// ── Create Tenant ──
|
||||
|
||||
@Post('tenants')
|
||||
async createTenant(@Req() req: any, @Body() body: {
|
||||
orgName: string;
|
||||
@@ -105,19 +168,4 @@ export class AdminController {
|
||||
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
|
||||
@Put('organizations/:id/status')
|
||||
async updateOrgStatus(
|
||||
@Req() req: any,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { status: string },
|
||||
) {
|
||||
await this.requireSuperadmin(req);
|
||||
const validStatuses = ['active', 'suspended', 'trial', 'archived'];
|
||||
if (!validStatuses.includes(body.status)) {
|
||||
throw new BadRequestException(`Invalid status. Must be one of: ${validStatuses.join(', ')}`);
|
||||
}
|
||||
const org = await this.orgService.updateStatus(id, body.status);
|
||||
return { success: true, organization: org };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user