Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, ApiQuery } from '@nestjs/swagger';
import { AdminService } from './admin.service';
import { AdminGuard } from './guards/admin.guard';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './decorators/roles.decorator';
import { CurrentAdmin } from './decorators/current-admin.decorator';
import { Admin, AdminRole } from './entities/admin.entity';
import { CreateAdminDto, UpdateAdminRoleDto, UpdateAdminStatusDto, AdminLoginDto, AdminResponseDto } from './dto/admin.dto';

@ApiTags('admin')
@ApiBearerAuth()
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}

@Post('auth/login')
@ApiOperation({ summary: 'Authenticate as admin' })
@ApiResponse({ status: 200, description: 'Admin authenticated successfully' })
@ApiResponse({ status: 403, description: 'Not an admin' })
async login(@Body() loginDto: AdminLoginDto) {
const admin = await this.adminService.findByWallet(loginDto.address);
if (!admin) {
return { authenticated: false, message: 'Admin not found' };
}
await this.adminService.recordLogin(admin.walletAddress);
return {
authenticated: true,
admin: {
id: admin.id,
walletAddress: admin.walletAddress,
role: admin.role,
isActive: admin.isActive,
},
};
}

@Get('auth/profile')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.MODERATOR, AdminRole.SECURITY_ANALYST, AdminRole.GOVERNANCE_OPERATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get current admin profile' })
@ApiResponse({ status: 200, description: 'Admin profile' })
async getProfile(@CurrentAdmin() admin: Admin) {
return {
id: admin.id,
walletAddress: admin.walletAddress,
role: admin.role,
isActive: admin.isActive,
lastLoginAt: admin.lastLoginAt,
createdAt: admin.createdAt,
};
}

@Post('admins')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR)
@ApiOperation({ summary: 'Create a new admin (super_admin or admin only)' })
@ApiResponse({ status: 201, description: 'Admin created' })
async createAdmin(@Body() createAdminDto: CreateAdminDto, @CurrentAdmin() admin: Admin) {
return this.adminService.create(createAdminDto, admin);
}

@Get('admins')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'List all admins' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'limit', required: false })
@ApiResponse({ status: 200, description: 'List of admins' })
async listAdmins(
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.adminService.findAll(
page ? parseInt(page, 10) : 1,
limit ? Math.min(parseInt(limit, 10), 100) : 20,
);
}

@Get('admins/:id')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get admin by ID' })
@ApiParam({ name: 'id', description: 'Admin ID' })
async getAdmin(@Param('id') id: string) {
return this.adminService.findById(id);
}

@Patch('admins/:id/role')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR)
@ApiOperation({ summary: 'Update admin role' })
@ApiParam({ name: 'id', description: 'Admin ID' })
async updateAdminRole(
@Param('id') id: string,
@Body() updateRoleDto: UpdateAdminRoleDto,
@CurrentAdmin() admin: Admin,
) {
return this.adminService.updateRole(id, updateRoleDto, admin);
}

@Patch('admins/:id/status')
@UseGuards(AdminGuard, RolesGuard)
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR)
@ApiOperation({ summary: 'Activate or deactivate an admin' })
@ApiParam({ name: 'id', description: 'Admin ID' })
async updateAdminStatus(
@Param('id') id: string,
@Body() updateStatusDto: UpdateAdminStatusDto,
@CurrentAdmin() admin: Admin,
) {
return this.adminService.updateStatus(id, updateStatusDto, admin);
}
}
44 changes: 44 additions & 0 deletions src/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Admin } from './entities/admin.entity';
import { Incident } from './entities/incident.entity';
import { ModerationReport } from './entities/moderation-report.entity';
import { AuditLog } from '../audit/entities/audit-log.entity';
import { AdminService } from './admin.service';
import { AdminController } from './admin.controller';
import { ModerationService } from './moderation/moderation.service';
import { ModerationController } from './moderation/moderation.controller';
import { IncidentService } from './incidents/incident.service';
import { IncidentController } from './incidents/incident.controller';
import { DashboardService } from './dashboard/dashboard.service';
import { DashboardController } from './dashboard/dashboard.controller';
import { RolesGuard } from './guards/roles.guard';
import { AdminGuard } from './guards/admin.guard';
@Module({
imports: [
TypeOrmModule.forFeature([Admin, Incident, ModerationReport, AuditLog]),
],
controllers: [
AdminController,
ModerationController,
IncidentController,
DashboardController,
],
providers: [
AdminService,
ModerationService,
IncidentService,
DashboardService,
RolesGuard,
AdminGuard,
],
exports: [
AdminService,
ModerationService,
IncidentService,
DashboardService,
RolesGuard,
AdminGuard,
],
})
export class AdminModule {}
114 changes: 114 additions & 0 deletions src/admin/admin.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Injectable, ConflictException, NotFoundException, Logger, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Admin, AdminRole, AdminRoleHierarchy } from './entities/admin.entity';
import { CreateAdminDto, UpdateAdminRoleDto, UpdateAdminStatusDto } from './dto/admin.dto';

@Injectable()
export class AdminService {
private readonly logger = new Logger(AdminService.name);

constructor(
@InjectRepository(Admin)
private readonly adminRepo: Repository<Admin>,
) {}

async create(createAdminDto: CreateAdminDto, requestedBy: Admin): Promise<Admin> {
if (!this.canManageAdmins(requestedBy.role)) {
throw new ForbiddenException('Insufficient permissions to create admins');
}

const existing = await this.adminRepo.findOne({
where: { walletAddress: createAdminDto.walletAddress.toLowerCase() },
});

if (existing) {
throw new ConflictException('Admin with this wallet address already exists');
}

const admin = this.adminRepo.create({
walletAddress: createAdminDto.walletAddress.toLowerCase(),
role: createAdminDto.role,
isActive: true,
});

const saved = await this.adminRepo.save(admin);
this.logger.log(`Admin created: ${saved.id} with role ${saved.role}`);
return saved;
}

async findAll(page = 1, limit = 20): Promise<{ data: Admin[]; total: number; page: number; limit: number }> {
const [data, total] = await this.adminRepo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});

return { data, total, page, limit };
}

async findById(id: string): Promise<Admin> {
const admin = await this.adminRepo.findOneBy({ id });
if (!admin) {
throw new NotFoundException(`Admin with ID ${id} not found`);
}
return admin;
}

async findByWallet(walletAddress: string): Promise<Admin | null> {
return this.adminRepo.findOne({
where: { walletAddress: walletAddress.toLowerCase(), isActive: true },
});
}

async updateRole(id: string, updateRoleDto: UpdateAdminRoleDto, requestedBy: Admin): Promise<Admin> {
if (!this.canManageAdmins(requestedBy.role)) {
throw new ForbiddenException('Insufficient permissions to update admin roles');
}

const admin = await this.findById(id);

if (admin.role === AdminRole.SUPER_ADMIN && requestedBy.role !== AdminRole.SUPER_ADMIN) {
throw new ForbiddenException('Cannot modify a super admin');
}

admin.role = updateRoleDto.role;
const saved = await this.adminRepo.save(admin);
this.logger.log(`Admin ${id} role updated to ${updateRoleDto.role}`);
return saved;
}

async updateStatus(id: string, updateStatusDto: UpdateAdminStatusDto, requestedBy: Admin): Promise<Admin> {
if (!this.canManageAdmins(requestedBy.role)) {
throw new ForbiddenException('Insufficient permissions to update admin status');
}

const admin = await this.findById(id);

if (admin.role === AdminRole.SUPER_ADMIN && requestedBy.role !== AdminRole.SUPER_ADMIN) {
throw new ForbiddenException('Cannot modify a super admin');
}

admin.isActive = updateStatusDto.isActive;
const saved = await this.adminRepo.save(admin);
this.logger.log(`Admin ${id} status updated to ${updateStatusDto.isActive}`);
return saved;
}

async recordLogin(walletAddress: string): Promise<Admin> {
const admin = await this.adminRepo.findOne({
where: { walletAddress: walletAddress.toLowerCase() },
});

if (!admin) {
throw new NotFoundException('Admin not found');
}

admin.lastLoginAt = new Date();
return this.adminRepo.save(admin);
}

private canManageAdmins(role: AdminRole): boolean {
return role === AdminRole.SUPER_ADMIN || role === AdminRole.ADMINISTRATOR;
}
}
59 changes: 59 additions & 0 deletions src/admin/dashboard/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { DashboardService } from './dashboard.service';
import { AdminGuard } from '../guards/admin.guard';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
import { AdminRole } from '../entities/admin.entity';

@ApiTags('admin / dashboard')
@ApiBearerAuth()
@Controller('admin/dashboard')
@UseGuards(AdminGuard, RolesGuard)
export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {}

@Get('overview')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get main dashboard overview' })
@ApiResponse({ status: 200, description: 'Dashboard overview data' })
async getOverview() {
return this.dashboardService.getOverview();
}

@Get('health')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.SECURITY_ANALYST, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get protocol health status' })
async getHealth() {
return this.dashboardService.getHealth();
}

@Get('moderation')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.MODERATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get moderation workload statistics' })
async getModerationStats() {
return this.dashboardService.getOverview();
}

@Get('incidents')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.SECURITY_ANALYST, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get incident statistics' })
async getIncidentStats() {
return this.dashboardService.getOverview();
}

@Get('audit')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get audit summary' })
@ApiQuery({ name: 'days', required: false, description: 'Number of days' })
async getAuditSummary(@Query('days') days?: string) {
return this.dashboardService.getAuditSummary(days ? parseInt(days, 10) : 7);
}

@Get('monitoring')
@Roles(AdminRole.SUPER_ADMIN, AdminRole.ADMINISTRATOR, AdminRole.SECURITY_ANALYST, AdminRole.AUDITOR)
@ApiOperation({ summary: 'Get real-time monitoring metrics' })
async getMonitoring() {
return this.dashboardService.getMonitoring();
}
}
Loading