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
34 changes: 0 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/achievements/__tests__/achievements.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ describe('AchievementsService', () => {
id: mockAchievement.id,
name: mockAchievement.name,
description: mockAchievement.description,
longDescription: mockAchievement.longDescription,
iconUrl: mockAchievement.iconUrl,
type: mockAchievement.type,
difficulty: mockAchievement.difficulty,
Expand Down
3 changes: 2 additions & 1 deletion src/assessment/dto/submit-assessment.dto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('SubmitAssessmentDto', () => {
}),
);
expect(errors).toHaveLength(1);
expect(errors[0].children?.some((child) => child.property === 'questionId')).toBe(true);
const itemChildren = errors[0].children?.[0]?.children ?? [];
expect(itemChildren.some((child) => child.property === 'questionId')).toBe(true);
});
});
2 changes: 2 additions & 0 deletions src/auth/dto/login.dto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ describe('RegisterDto', () => {
username: 'technocrat42',
email: '[email protected]',
password: 'Secure@123',
firstName: 'John',
lastName: 'Doe',
};

it('accepts a minimal valid registration', async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class LoginDto {
@Transform(({ value }) => value?.toLowerCase?.() ?? value)
@IsEmail()
email: string;

Expand Down
2 changes: 1 addition & 1 deletion src/courses/courses.service.bulk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('CoursesService - Bulk Operations', () => {

const admin = {
id: 'admin-1',
roles: ['admin'],
roles: [{ name: 'admin' }],
} as unknown as User;

const createCourse = (overrides: Partial<Course> = {}): Course =>
Expand Down
27 changes: 27 additions & 0 deletions src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ export class CoursesService {
prerequisite,
});
const saved = await this.courseRepo.save(course);
const version = this.versionRepo.create({
courseId: saved.id,
versionNumber: 1,
eventType: CourseVersionEventType.CREATED,
title: saved.title,
description: saved.description,
price: saved.price,
thumbnailUrl: saved.thumbnailUrl,
status: saved.status,
});
await this.versionRepo.save(version);
this.eventEmitter.emit(CACHE_EVENTS.COURSE_CREATED, { id: saved.id });
return saved;
}
Expand Down Expand Up @@ -137,6 +148,22 @@ export class CoursesService {

Object.assign(course, dto, { prerequisite: course.prerequisite });
const saved = await this.courseRepo.save(course);
const previousVersion = await this.versionRepo.findOne({
where: { courseId: saved.id },
order: { versionNumber: 'DESC' },
});
const nextVersionNumber = previousVersion ? previousVersion.versionNumber + 1 : 1;
const version = this.versionRepo.create({
courseId: saved.id,
versionNumber: nextVersionNumber,
eventType: CourseVersionEventType.UPDATED,
title: saved.title,
description: saved.description,
price: saved.price,
thumbnailUrl: saved.thumbnailUrl,
status: saved.status,
});
await this.versionRepo.save(version);
this.eventEmitter.emit(CACHE_EVENTS.COURSE_UPDATED, { id: saved.id });
return saved;
}
Expand Down
5 changes: 4 additions & 1 deletion src/gamification/points/points.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ export class PointsService {
const newLevel = Math.floor(progress.xp / 1000) + 1;
progress.level = newLevel;

const previousTier = progress.tier;
const saved = await this.userProgressRepository.save(progress);
const tierPromoted = false; // Simplified for now
const newTier = this.tiersService.getTierForPoints(saved.totalPoints);
saved.tier = newTier;
const tierPromoted = newTier !== previousTier;

// Emit event so BadgesService can react
this.eventEmitter.emit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const INCIDENT_DETECTION_RULES: IncidentDetectionRule[] = [
incidentTitle: 'High HTTP Error Rate Detected',
incidentDescription: 'HTTP error rate (5xx) has increased significantly',
runbookId: 'error-rate-investigation',
requiredConsecutiveAlerts: 1,
requiredConsecutiveAlerts: 2,
},
{
name: 'cache_hit_rate_degradation',
Expand Down
10 changes: 9 additions & 1 deletion src/incident-management/services/runbook-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,15 @@ export class RunbookExecutionService {
/**
* Get default runbook definition
*/
private getDefaultRunbookDefinition(runbookName: string): RunbookDefinition {
private getDefaultRunbookDefinition(runbookName: string): RunbookDefinition | null {
const knownRunbooks = Object.keys({
'database-failure': true,
'region-outage': true,
'data-corruption': true,
});
if (!knownRunbooks.includes(runbookName)) {
return null;
}
return {
name: runbookName,
title: `${runbookName.replace(/-/g, ' ')} Runbook`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ describe('AutoRemediationService', () => {
);

expect(suggestions.length).toBeGreaterThan(0);
expect(suggestions[0].actionType).toMatch(/database_operation|restart_service/);
expect(suggestions[0].actionType).toMatch(
/database_operation|restart_service|run_database_query/,
);
});

it('should suggest actions for Cache incident', () => {
Expand Down
6 changes: 6 additions & 0 deletions src/modules/email-template.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import sanitizeHtml from 'sanitize-html';
import { EmailTemplate } from './email-template/email-template.entity';
import { CreateEmailTemplateDto } from './email-template/dto/create-email-template.dto';
import { UpdateEmailTemplateDto } from './email-template/dto/update-email-template.dto';

@Injectable()
export class EmailTemplateService {
Expand Down
15 changes: 13 additions & 2 deletions src/modules/gdpr/gdpr.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../../users/entities/user.entity';
import { Enrollment } from '../../courses/entities/enrollment.entity';
import { Payment } from '../../payments/entities/payment.entity';
import { Notification } from '../../notifications/entities/notification.entity';
import { UserConsent } from './entities/user-consent.entity';
import { SessionModule } from '../../session/session.module';
import { GdprService } from './gdpr.service';
import { GdprController } from './gdpr.controller';

@Module({
imports: [SessionModule],
controllers: [GdprController],
imports: [
TypeOrmModule.forFeature([User, Enrollment, Payment, Notification, UserConsent]),
SessionModule,
],
providers: [GdprService],
controllers: [GdprController],
})
export class GdprModule {}
62 changes: 55 additions & 7 deletions src/modules/gdpr/gdpr.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { plainToInstance, instanceToPlain } from 'class-transformer';
import { UserConsent } from './entities/user-consent.entity';
import { ConsentDto } from './dto/consent.dto';
import { GdprExportDto } from './dto/gdpr-export.dto';
import { User } from '../../users/entities/user.entity';
import { Enrollment } from '../../courses/entities/enrollment.entity';
import { Payment } from '../../payments/entities/payment.entity';
import { Notification } from '../../notifications/entities/notification.entity';
import { SessionService } from '../../session/session.service';

@Injectable()
Expand All @@ -19,11 +23,25 @@ export class GdprService {
@InjectRepository(UserConsent)
private readonly consentRepository: Repository<UserConsent>,

@InjectRepository(User)
private readonly userRepository: Repository<User>,

@InjectRepository(Enrollment)
private readonly enrollmentRepository: Repository<Enrollment>,

@InjectRepository(Payment)
private readonly paymentRepository: Repository<Payment>,

@InjectRepository(Notification)
private readonly notificationRepository: Repository<Notification>,
private readonly sessionService: SessionService,
) {}

async exportUserData(userId: string) {
const user = await this.usersService.findById(userId);
async exportUserData(userId: string): Promise<any> {
const user = await this.userRepository.findOne({
where: { id: userId },
withDeleted: true,
});

if (!user) {
throw new NotFoundException('User not found');
Expand All @@ -33,21 +51,53 @@ export class GdprService {
where: {
userId,
},
withDeleted: true,
});

const enrollments = await this.enrollmentRepository.find({
where: { userId },
withDeleted: true,
});

const payments = await this.paymentRepository.find({
where: { userId },
withDeleted: true,
});

const notifications = await this.notificationRepository.find({
where: { userId },
withDeleted: true,
});

await this.auditService.log('GDPR_EXPORT', userId);

const gdprExportUserInstance = plainToInstance(GdprExportDto, user);
const cleanProfile = instanceToPlain(gdprExportUserInstance);

const addDeletedAtField = <T extends object>(records: T[]): T[] => {
return records.map((record) => ({
...record,
_deletedAt: (record as any).deletedAt || null,
}));
};

return {
profile: cleanProfile,
consents,
profile: {
...cleanProfile,
_deletedAt: user.deletedAt || null,
},
consents: addDeletedAtField(consents as any[]),
enrollments: addDeletedAtField(enrollments),
payments: addDeletedAtField(payments),
notifications: addDeletedAtField(notifications),
};
}

async eraseUserData(userId: string) {
const user = await this.usersService.findById(userId);
const user = await this.userRepository.findOne({
where: { id: userId },
withDeleted: true,
});

if (!user) {
throw new NotFoundException('User not found');
Expand All @@ -59,8 +109,6 @@ export class GdprService {
email: null,
firstName: '[DELETED]',
lastName: '[DELETED]',
phone: null,
address: null,
deletedAt: new Date(),
refreshToken: null,
});
Expand Down
Loading