Skip to content
Open
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
40 changes: 40 additions & 0 deletions src/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdminAuditLog } from './entities/admin-audit-log.entity';
import { AdminAuditLogService } from './services/admin-audit-log.service';
import { AdminUsersService } from './services/admin-users.service';
import { AdminPuzzlesController } from './controllers/admin-puzzles.controller';
import { AdminUsersController } from './controllers/admin-users.controller';
import { AdminAnalyticsController } from './controllers/admin-analytics.controller';
import { AdminModerationController } from './controllers/admin-moderation.controller';
import { AdminMonitoringController } from './controllers/admin-monitoring.controller';
import { PuzzlesModule } from '../puzzles/puzzles.module';
import { AuthModule } from '../auth/auth.module';
import { AnalyticsModule } from '../analytics/analytics.module';
import { User } from '../auth/entities/user.entity';
import { Role } from '../auth/entities/role.entity';

@Module({
imports: [
TypeOrmModule.forFeature([AdminAuditLog, User, Role]),
PuzzlesModule,
AuthModule,
AnalyticsModule,
],
controllers: [
AdminPuzzlesController,
AdminUsersController,
AdminAnalyticsController,
AdminModerationController,
AdminMonitoringController,
],
providers: [
AdminAuditLogService,
AdminUsersService,
],
exports: [
AdminAuditLogService,
AdminUsersService,
],
})
export class AdminModule { }
71 changes: 71 additions & 0 deletions src/admin/controllers/__tests__/admin-puzzles.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminPuzzlesController } from '../admin-puzzles.controller';
import { PuzzlesService } from '../../../puzzles/puzzles.service';
import { AdminAuditLogService } from '../../services/admin-audit-log.service';
import { UserRole } from '../../../auth/constants';

describe('AdminPuzzlesController', () => {
let controller: AdminPuzzlesController;
let puzzlesService: PuzzlesService;
let auditLogService: AdminAuditLogService;

const mockUser = { id: 'admin-id', role: { name: UserRole.ADMIN } };

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminPuzzlesController],
providers: [
{
provide: PuzzlesService,
useValue: {
findAll: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
},
},
{
provide: AdminAuditLogService,
useValue: {
log: jest.fn(),
},
},
],
}).compile();

controller = module.get<AdminPuzzlesController>(AdminPuzzlesController);
puzzlesService = module.get<PuzzlesService>(PuzzlesService);
auditLogService = module.get<AdminAuditLogService>(AdminAuditLogService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('findAll', () => {
it('should call puzzlesService.findAll', async () => {
const searchDto = {};
await controller.findAll(searchDto);
expect(puzzlesService.findAll).toHaveBeenCalledWith(searchDto);
});
});

describe('create', () => {
it('should call puzzlesService.create and auditLogService.log', async () => {
const createDto = { title: 'New Puzzle' } as any;
const mockPuzzle = { id: 'puzzle-id', title: 'New Puzzle' };
(puzzlesService.create as jest.Mock).mockResolvedValue(mockPuzzle);

await controller.create(createDto, mockUser);

expect(puzzlesService.create).toHaveBeenCalledWith(createDto, mockUser.id);
expect(auditLogService.log).toHaveBeenCalledWith({
adminId: mockUser.id,
action: 'CREATE_PUZZLE',
targetType: 'PUZZLE',
targetId: mockPuzzle.id,
details: { title: mockPuzzle.title },
});
});
});
});
62 changes: 62 additions & 0 deletions src/admin/controllers/__tests__/admin-users.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AdminUsersController } from '../admin-users.controller';
import { AdminUsersService } from '../../services/admin-users.service';
import { AdminAuditLogService } from '../../services/admin-audit-log.service';
import { UserRole } from '../../../auth/constants';

describe('AdminUsersController', () => {
let controller: AdminUsersController;
let adminUsersService: AdminUsersService;
let auditLogService: AdminAuditLogService;

const mockAdmin = { id: 'admin-id', email: 'admin@test.com' };

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminUsersController],
providers: [
{
provide: AdminUsersService,
useValue: {
findAll: jest.fn(),
updateRole: jest.fn(),
updateStatus: jest.fn(),
},
},
{
provide: AdminAuditLogService,
useValue: {
log: jest.fn(),
},
},
],
}).compile();

controller = module.get<AdminUsersController>(AdminUsersController);
adminUsersService = module.get<AdminUsersService>(AdminUsersService);
auditLogService = module.get<AdminAuditLogService>(AdminAuditLogService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('updateRole', () => {
it('should call adminUsersService.updateRole and auditLogService.log', async () => {
const userId = 'user-uuid';
const newRole = UserRole.MODERATOR;
(adminUsersService.updateRole as jest.Mock).mockResolvedValue({ id: userId, role: { name: newRole } });

await controller.updateRole(userId, newRole, mockAdmin);

expect(adminUsersService.updateRole).toHaveBeenCalledWith(userId, newRole);
expect(auditLogService.log).toHaveBeenCalledWith({
adminId: mockAdmin.id,
action: 'UPDATE_USER_ROLE',
targetType: 'USER',
targetId: userId,
details: { role: newRole },
});
});
});
});
30 changes: 30 additions & 0 deletions src/admin/controllers/admin-analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Controller,
Get,
Query,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../auth/constants';
import { AnalyticsService } from '../../analytics/analytics.service';
import { AnalyticsFilterDto } from '../../analytics/dto/analytics-filter.dto';

@Controller('admin/analytics')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
export class AdminAnalyticsController {
constructor(private readonly analyticsService: AnalyticsService) { }

@Get('overview')
async getOverview(@Query() filter: AnalyticsFilterDto) {
return await this.analyticsService.getPlayersOverview(filter);
}

@Get('active-players')
async getActivePlayers(@Query() filter: AnalyticsFilterDto) {
// Reusing getPlayersOverview as it gives total distinct players in period
return await this.analyticsService.getPlayersOverview(filter);
}
}
60 changes: 60 additions & 0 deletions src/admin/controllers/admin-moderation.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
ParseUUIDPipe,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../auth/constants';
import { PuzzleModerationService } from '../../puzzles/services/puzzle-moderation.service';
import { PuzzleSubmissionStatus } from '../../puzzles/entities/user-puzzle-submission.entity';
import { ModerationDecisionDto } from '../../puzzles/dto/user-puzzle-submission.dto';
import { AdminAuditLogService } from '../services/admin-audit-log.service';
import { ActiveUser } from '../../auth/decorators/active-user.decorator';

@Controller('admin/moderation')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
export class AdminModerationController {
constructor(
private readonly moderationService: PuzzleModerationService,
private readonly auditLogService: AdminAuditLogService,
) { }

@Get('queue')
async getQueue(
@Query('status') status?: PuzzleSubmissionStatus,
@Query('page') page?: number,
@Query('limit') limit?: number,
) {
return await this.moderationService.getModerationQueue(status, page, limit);
}

@Post(':id/moderate')
async moderate(
@Param('id', ParseUUIDPipe) id: string,
@Body() decision: ModerationDecisionDto,
@ActiveUser() admin: any,
) {
const result = await this.moderationService.moderatePuzzle(id, admin.id, decision);
await this.auditLogService.log({
adminId: admin.id,
action: 'MODERATE_PUZZLE',
targetType: 'PUZZLE_SUBMISSION',
targetId: id,
details: decision,
});
return result;
}

@Get('stats')
async getStats(@Query('timeframe') timeframe?: 'day' | 'week' | 'month') {
return await this.moderationService.getModerationStats(timeframe);
}
}
40 changes: 40 additions & 0 deletions src/admin/controllers/admin-monitoring.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../auth/constants';
import { DatabaseService } from '../../config/database-service';
import { PerformanceMonitoringService } from '../../monitoring/performance.service';

@Controller('admin/monitoring')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
export class AdminMonitoringController {
private databaseService = DatabaseService.getInstance();
private performanceService: PerformanceMonitoringService;

constructor() {
this.performanceService = new PerformanceMonitoringService(
this.databaseService.getDataSource(),
);
}

@Get('health')
async checkHealth() {
return await this.databaseService.checkHealth();
}

@Get('metrics')
async getMetrics() {
return await this.performanceService.getMetrics();
}

@Get('db-stats')
async getDbStats() {
return await this.databaseService.getConnectionStats();
}
}
79 changes: 79 additions & 0 deletions src/admin/controllers/admin-puzzles.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ParseUUIDPipe,
HttpStatus,
HttpCode,
Req,
} from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../../auth/guards/roles.guard';
import { Roles } from '../../auth/decorators/roles.decorator';
import { UserRole } from '../../auth/constants';
import { PuzzlesService } from '../../puzzles/puzzles.service';
import { CreatePuzzleDto, UpdatePuzzleDto, SearchPuzzleDto } from '../../puzzles/dto';
import { AdminAuditLogService } from '../services/admin-audit-log.service';
import { ActiveUser } from '../../auth/decorators/active-user.decorator';

@Controller('admin/puzzles')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
export class AdminPuzzlesController {
constructor(
private readonly puzzlesService: PuzzlesService,
private readonly auditLogService: AdminAuditLogService,
) { }

@Get()
async findAll(@Body() searchDto: SearchPuzzleDto) {
return await this.puzzlesService.findAll(searchDto);
}

@Post()
async create(@Body() createPuzzleDto: CreatePuzzleDto, @ActiveUser() user: any) {
const puzzle = await this.puzzlesService.create(createPuzzleDto, user.id);
await this.auditLogService.log({
adminId: user.id,
action: 'CREATE_PUZZLE',
targetType: 'PUZZLE',
targetId: puzzle.id,
details: { title: puzzle.title },
});
return puzzle;
}

@Patch(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() updatePuzzleDto: UpdatePuzzleDto,
@ActiveUser() user: any,
) {
const puzzle = await this.puzzlesService.update(id, updatePuzzleDto, user.id);
await this.auditLogService.log({
adminId: user.id,
action: 'UPDATE_PUZZLE',
targetType: 'PUZZLE',
targetId: id,
details: updatePuzzleDto,
});
return puzzle;
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseUUIDPipe) id: string, @ActiveUser() user: any) {
await this.puzzlesService.remove(id, user.id);
await this.auditLogService.log({
adminId: user.id,
action: 'DELETE_PUZZLE',
targetType: 'PUZZLE',
targetId: id,
});
}
}
Loading