diff --git a/apps/server/src/modules/class/class.module.ts b/apps/server/src/modules/class/class.module.ts index 550b50bd454..b36f8637982 100644 --- a/apps/server/src/modules/class/class.module.ts +++ b/apps/server/src/modules/class/class.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { ClassService } from './service'; import { ClassesRepo } from './repo'; @Module({ + imports: [LoggerModule], providers: [ClassService, ClassesRepo], exports: [ClassService], }) diff --git a/apps/server/src/modules/class/service/class.service.spec.ts b/apps/server/src/modules/class/service/class.service.spec.ts index 08a9452445c..fca6bb9ee0d 100644 --- a/apps/server/src/modules/class/service/class.service.spec.ts +++ b/apps/server/src/modules/class/service/class.service.spec.ts @@ -4,6 +4,7 @@ import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain/types'; import { setupEntities } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { Class } from '../domain'; import { classFactory } from '../domain/testing'; import { classEntityFactory } from '../entity/testing'; @@ -24,6 +25,10 @@ describe(ClassService.name, () => { provide: ClassesRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/class/service/class.service.ts b/apps/server/src/modules/class/service/class.service.ts index 6b56ea86384..81d62253a79 100644 --- a/apps/server/src/modules/class/service/class.service.ts +++ b/apps/server/src/modules/class/service/class.service.ts @@ -1,11 +1,15 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { Class } from '../domain'; import { ClassesRepo } from '../repo'; @Injectable() export class ClassService { - constructor(private readonly classesRepo: ClassesRepo) {} + constructor(private readonly classesRepo: ClassesRepo, private readonly logger: Logger) { + this.logger.setContext(ClassService.name); + } public async findClassesForSchool(schoolId: EntityId): Promise { const classes: Class[] = await this.classesRepo.findAllBySchoolId(schoolId); @@ -19,8 +23,16 @@ export class ClassService { return classes; } - // FIXME There is no usage of this method public async deleteUserDataFromClasses(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting data from Classes', + DomainModel.CLASS, + userId, + StatusModel.PENDING + ) + ); + if (!userId) { throw new InternalServerErrorException('User id is missing'); } @@ -34,8 +46,20 @@ export class ClassService { return domainObject; }); + const numberOfUpdatedClasses = updatedClasses.length; + await this.classesRepo.updateMany(updatedClasses); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully removed user data from Classes', + DomainModel.CLASS, + userId, + StatusModel.FINISHED, + numberOfUpdatedClasses, + 0 + ) + ); - return updatedClasses.length; + return numberOfUpdatedClasses; } } diff --git a/apps/server/src/modules/files/service/files.service.spec.ts b/apps/server/src/modules/files/service/files.service.spec.ts index 4a65adaefc6..702906ddd25 100644 --- a/apps/server/src/modules/files/service/files.service.spec.ts +++ b/apps/server/src/modules/files/service/files.service.spec.ts @@ -2,6 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { setupEntities } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { FilesService } from './files.service'; import { FilesRepo } from '../repo'; import { fileEntityFactory, filePermissionEntityFactory } from '../entity/testing'; @@ -20,6 +21,10 @@ describe(FilesService.name, () => { provide: FilesRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/files/service/files.service.ts b/apps/server/src/modules/files/service/files.service.ts index c665e196f46..1b3941aaffc 100644 --- a/apps/server/src/modules/files/service/files.service.ts +++ b/apps/server/src/modules/files/service/files.service.ts @@ -1,17 +1,29 @@ import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { FileEntity } from '../entity'; import { FilesRepo } from '../repo'; @Injectable() export class FilesService { - constructor(private readonly repo: FilesRepo) {} + constructor(private readonly repo: FilesRepo, private readonly logger: Logger) { + this.logger.setContext(FilesService.name); + } async findFilesAccessibleOrCreatedByUser(userId: EntityId): Promise { return this.repo.findByPermissionRefIdOrCreatorId(userId); } async removeUserPermissionsOrCreatorReferenceToAnyFiles(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Files', + DomainModel.FILE, + userId, + StatusModel.PENDING + ) + ); const entities = await this.repo.findByPermissionRefIdOrCreatorId(userId); if (entities.length === 0) { @@ -25,7 +37,20 @@ export class FilesService { await this.repo.save(entities); - return entities.length; + const numberOfUpdatedFiles = entities.length; + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully removed user data from Files', + DomainModel.FILE, + userId, + StatusModel.FINISHED, + numberOfUpdatedFiles, + 0 + ) + ); + + return numberOfUpdatedFiles; } async findFilesOwnedByUser(userId: EntityId): Promise { @@ -33,6 +58,14 @@ export class FilesService { } async markFilesOwnedByUserForDeletion(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Marking user files to deletion', + DomainModel.FILE, + userId, + StatusModel.PENDING + ) + ); const entities = await this.repo.findByOwnerUserId(userId); if (entities.length === 0) { @@ -43,6 +76,19 @@ export class FilesService { await this.repo.save(entities); - return entities.length; + const numberOfMarkedForDeletionFiles = entities.length; + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully marked user files for deletion', + DomainModel.FILE, + userId, + StatusModel.FINISHED, + numberOfMarkedForDeletionFiles, + 0 + ) + ); + + return numberOfMarkedForDeletionFiles; } } diff --git a/apps/server/src/modules/learnroom/service/course.service.spec.ts b/apps/server/src/modules/learnroom/service/course.service.spec.ts index 41732f8a11d..379c588d518 100644 --- a/apps/server/src/modules/learnroom/service/course.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain/entity'; import { CourseRepo, UserRepo } from '@shared/repo'; import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { CourseService } from './course.service'; describe('CourseService', () => { @@ -24,6 +25,10 @@ describe('CourseService', () => { provide: CourseRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); courseRepo = module.get(CourseRepo); diff --git a/apps/server/src/modules/learnroom/service/course.service.ts b/apps/server/src/modules/learnroom/service/course.service.ts index 82ff1153655..f85526bcc83 100644 --- a/apps/server/src/modules/learnroom/service/course.service.ts +++ b/apps/server/src/modules/learnroom/service/course.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { Course } from '@shared/domain/entity'; -import { Counted, EntityId } from '@shared/domain/types'; +import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; +import { Logger } from '@src/core/logger'; @Injectable() export class CourseService { - constructor(private readonly repo: CourseRepo) {} + constructor(private readonly repo: CourseRepo, private readonly logger: Logger) { + this.logger.setContext(CourseService.name); + } async findById(courseId: EntityId): Promise { return this.repo.findById(courseId); @@ -18,11 +22,29 @@ export class CourseService { } public async deleteUserDataFromCourse(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting data from Courses', + DomainModel.COURSE, + userId, + StatusModel.PENDING + ) + ); const [courses, count] = await this.repo.findAllByUserId(userId); courses.forEach((course: Course) => course.removeUser(userId)); await this.repo.save(courses); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully removed data from Courses', + DomainModel.COURSE, + userId, + StatusModel.FINISHED, + 0, + count + ) + ); return count; } diff --git a/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts b/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts index 48caf02c378..438be147390 100644 --- a/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/coursegroup.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { CourseGroupRepo, UserRepo } from '@shared/repo'; import { courseGroupFactory, setupEntities, userFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { CourseGroupService } from './coursegroup.service'; describe('CourseGroupService', () => { @@ -23,6 +24,10 @@ describe('CourseGroupService', () => { provide: CourseGroupRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); courseGroupRepo = module.get(CourseGroupRepo); diff --git a/apps/server/src/modules/learnroom/service/coursegroup.service.ts b/apps/server/src/modules/learnroom/service/coursegroup.service.ts index f1da14c8728..b9da03616d2 100644 --- a/apps/server/src/modules/learnroom/service/coursegroup.service.ts +++ b/apps/server/src/modules/learnroom/service/coursegroup.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { CourseGroup } from '@shared/domain/entity'; -import { Counted, EntityId } from '@shared/domain/types'; +import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { CourseGroupRepo } from '@shared/repo'; +import { Logger } from '@src/core/logger'; @Injectable() export class CourseGroupService { - constructor(private readonly repo: CourseGroupRepo) {} + constructor(private readonly repo: CourseGroupRepo, private readonly logger: Logger) { + this.logger.setContext(CourseGroupService.name); + } public async findAllCourseGroupsByUserId(userId: EntityId): Promise> { const [courseGroups, count] = await this.repo.findByUserId(userId); @@ -14,11 +18,29 @@ export class CourseGroupService { } public async deleteUserDataFromCourseGroup(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from CourseGroup', + DomainModel.COURSEGROUP, + userId, + StatusModel.PENDING + ) + ); const [courseGroups, count] = await this.repo.findByUserId(userId); courseGroups.forEach((courseGroup) => courseGroup.removeStudent(userId)); await this.repo.save(courseGroups); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from CourseGroup', + DomainModel.COURSEGROUP, + userId, + StatusModel.FINISHED, + count, + 0 + ) + ); return count; } diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts index 00f8207e23c..d87e7b0a48d 100644 --- a/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/dashboard.service.spec.ts @@ -4,6 +4,7 @@ import { DashboardElementRepo, IDashboardRepo, UserRepo } from '@shared/repo'; import { setupEntities, userFactory } from '@shared/testing'; import { LearnroomMetadata, LearnroomTypes } from '@shared/domain/types'; import { DashboardEntity, GridElement } from '@shared/domain/entity'; +import { Logger } from '@src/core/logger'; import { DashboardService } from '.'; const learnroomMock = (id: string, name: string) => { @@ -44,6 +45,10 @@ describe(DashboardService.name, () => { provide: DashboardElementRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); dashboardService = module.get(DashboardService); diff --git a/apps/server/src/modules/learnroom/service/dashboard.service.ts b/apps/server/src/modules/learnroom/service/dashboard.service.ts index 4a7910f0991..b60400de5fa 100644 --- a/apps/server/src/modules/learnroom/service/dashboard.service.ts +++ b/apps/server/src/modules/learnroom/service/dashboard.service.ts @@ -1,18 +1,41 @@ import { Inject, Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { IDashboardRepo, DashboardElementRepo } from '@shared/repo'; +import { Logger } from '@src/core/logger'; @Injectable() export class DashboardService { constructor( @Inject('DASHBOARD_REPO') private readonly dashboardRepo: IDashboardRepo, - private readonly dashboardElementRepo: DashboardElementRepo - ) {} + private readonly dashboardElementRepo: DashboardElementRepo, + private readonly logger: Logger + ) { + this.logger.setContext(DashboardService.name); + } async deleteDashboardByUserId(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Dashboard', + DomainModel.DASHBOARD, + userId, + StatusModel.PENDING + ) + ); const usersDashboard = await this.dashboardRepo.getUsersDashboard(userId); await this.dashboardElementRepo.deleteByDashboardId(usersDashboard.id); const result = await this.dashboardRepo.deleteDashboardByUserId(userId); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from Dashboard', + DomainModel.DASHBOARD, + userId, + StatusModel.FINISHED, + 0, + result + ) + ); return result; } diff --git a/apps/server/src/modules/lesson/service/lesson.service.spec.ts b/apps/server/src/modules/lesson/service/lesson.service.spec.ts index dc1b320e426..9eba963ad8a 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.spec.ts @@ -4,6 +4,7 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client' import { Test, TestingModule } from '@nestjs/testing'; import { ComponentProperties, ComponentType } from '@shared/domain/entity'; import { lessonFactory, setupEntities } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { LessonRepo } from '../repository'; import { LessonService } from './lesson.service'; @@ -26,6 +27,10 @@ describe('LessonService', () => { provide: FilesStorageClientAdapterService, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); lessonService = module.get(LessonService); diff --git a/apps/server/src/modules/lesson/service/lesson.service.ts b/apps/server/src/modules/lesson/service/lesson.service.ts index 75cd3e459cb..af38c26df8d 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.ts @@ -1,16 +1,21 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Injectable } from '@nestjs/common'; import { ComponentProperties, LessonEntity } from '@shared/domain/entity'; -import { Counted, EntityId } from '@shared/domain/types'; +import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { AuthorizationLoaderService } from '@src/modules/authorization'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { LessonRepo } from '../repository'; @Injectable() export class LessonService implements AuthorizationLoaderService { constructor( private readonly lessonRepo: LessonRepo, - private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService - ) {} + private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, + private readonly logger: Logger + ) { + this.logger.setContext(LessonService.name); + } async deleteLesson(lesson: LessonEntity): Promise { await this.filesStorageClientAdapterService.deleteFilesOfParent(lesson.id); @@ -33,6 +38,14 @@ export class LessonService implements AuthorizationLoaderService { } async deleteUserDataFromLessons(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Lessons', + DomainModel.LESSONS, + userId, + StatusModel.PENDING + ) + ); const lessons = await this.lessonRepo.findByUserId(userId); const updatedLessons = lessons.map((lesson: LessonEntity) => { @@ -47,6 +60,19 @@ export class LessonService implements AuthorizationLoaderService { await this.lessonRepo.save(updatedLessons); - return updatedLessons.length; + const numberOfUpdatedLessons = updatedLessons.length; + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully removed user data from Classes', + DomainModel.LESSONS, + userId, + StatusModel.FINISHED, + numberOfUpdatedLessons, + 0 + ) + ); + + return numberOfUpdatedLessons; } } diff --git a/apps/server/src/modules/pseudonym/pseudonym.module.ts b/apps/server/src/modules/pseudonym/pseudonym.module.ts index 3a8bcdacbd1..2b69307c6d2 100644 --- a/apps/server/src/modules/pseudonym/pseudonym.module.ts +++ b/apps/server/src/modules/pseudonym/pseudonym.module.ts @@ -2,12 +2,12 @@ import { LearnroomModule } from '@modules/learnroom'; import { ToolModule } from '@modules/tool'; import { UserModule } from '@modules/user'; import { forwardRef, Module } from '@nestjs/common'; -import { LegacyLogger } from '@src/core/logger'; +import { LegacyLogger, LoggerModule } from '@src/core/logger'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from './repo'; import { FeathersRosterService, PseudonymService } from './service'; @Module({ - imports: [UserModule, LearnroomModule, forwardRef(() => ToolModule)], + imports: [UserModule, LearnroomModule, forwardRef(() => ToolModule), LoggerModule], providers: [PseudonymService, PseudonymsRepo, ExternalToolPseudonymRepo, LegacyLogger, FeathersRosterService], exports: [PseudonymService, FeathersRosterService], }) diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts index b63982b03ee..53c1c58cf3b 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts @@ -7,6 +7,7 @@ import { IFindOptions } from '@shared/domain/interface'; import { LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain/domainobject'; import { externalToolFactory, ltiToolDOFactory, pseudonymFactory, userDoFactory } from '@shared/testing/factory'; +import { Logger } from '@src/core/logger'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; import { PseudonymService } from './pseudonym.service'; @@ -30,6 +31,10 @@ describe('PseudonymService', () => { provide: ExternalToolPseudonymRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts index 4f6580e71ce..7fbfd09464b 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts @@ -5,6 +5,9 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { v4 as uuidv4 } from 'uuid'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainModel, StatusModel } from '@shared/domain/types'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; @@ -12,8 +15,11 @@ import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; export class PseudonymService { constructor( private readonly pseudonymRepo: PseudonymsRepo, - private readonly externalToolPseudonymRepo: ExternalToolPseudonymRepo - ) {} + private readonly externalToolPseudonymRepo: ExternalToolPseudonymRepo, + private readonly logger: Logger + ) { + this.logger.setContext(PseudonymService.name); + } public async findByUserAndToolOrThrow(user: UserDO, tool: ExternalTool | LtiToolDO): Promise { if (!user.id || !tool.id) { @@ -73,6 +79,14 @@ export class PseudonymService { } public async deleteByUserId(userId: string): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Pseudonyms', + DomainModel.PSEUDONYMS, + userId, + StatusModel.PENDING + ) + ); if (!userId) { throw new InternalServerErrorException('User id is missing'); } @@ -82,7 +96,20 @@ export class PseudonymService { this.deleteExternalToolPseudonymsByUserId(userId), ]); - return deletedPseudonyms + deletedExternalToolPseudonyms; + const numberOfDeletedPseudonyms = deletedPseudonyms + deletedExternalToolPseudonyms; + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from Pseudonyms', + DomainModel.PSEUDONYMS, + userId, + StatusModel.FINISHED, + 0, + numberOfDeletedPseudonyms + ) + ); + + return numberOfDeletedPseudonyms; } private async findPseudonymsByUserId(userId: string): Promise { diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts index b5c6a2f3296..c5a7545c32f 100644 --- a/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userDoFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { RegistrationPinService } from '.'; import { RegistrationPinRepo } from '../repo'; @@ -17,6 +18,10 @@ describe(RegistrationPinService.name, () => { provide: RegistrationPinRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/registration-pin/service/registration-pin.service.ts b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts index 4681b08329c..8b750802268 100644 --- a/apps/server/src/modules/registration-pin/service/registration-pin.service.ts +++ b/apps/server/src/modules/registration-pin/service/registration-pin.service.ts @@ -1,11 +1,36 @@ import { Injectable } from '@nestjs/common'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; +import { DomainModel, StatusModel } from '@shared/domain/types'; import { RegistrationPinRepo } from '../repo'; @Injectable() export class RegistrationPinService { - constructor(private readonly registrationPinRepo: RegistrationPinRepo) {} + constructor(private readonly registrationPinRepo: RegistrationPinRepo, private readonly logger: Logger) { + this.logger.setContext(RegistrationPinService.name); + } async deleteRegistrationPinByEmail(email: string): Promise { - return this.registrationPinRepo.deleteRegistrationPinByEmail(email); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from RegistrationPin', + DomainModel.REGISTRATIONPIN, + email, + StatusModel.PENDING + ) + ); + const result = await this.registrationPinRepo.deleteRegistrationPinByEmail(email); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from RegistrationPin', + DomainModel.REGISTRATIONPIN, + email, + StatusModel.FINISHED, + 0, + result + ) + ); + + return result; } } diff --git a/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts index f0d30d76c37..529fa75a471 100644 --- a/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts +++ b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; import { RocketChatUserRepo } from './repo'; import { RocketChatUserService } from './service/rocket-chat-user.service'; @Module({ + imports: [LoggerModule], providers: [RocketChatUserRepo, RocketChatUserService], exports: [RocketChatUserService], }) diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts index 57d7c2da254..578171003fd 100644 --- a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { RocketChatUserService } from './rocket-chat-user.service'; import { RocketChatUserRepo } from '../repo'; import { rocketChatUserFactory } from '../domain/testing/rocket-chat-user.factory'; @@ -20,6 +21,10 @@ describe(RocketChatUserService.name, () => { provide: RocketChatUserRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts index 53eac463399..c0f6e64b18f 100644 --- a/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts +++ b/apps/server/src/modules/rocketchat-user/service/rocket-chat-user.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { RocketChatUser } from '../domain'; import { RocketChatUserRepo } from '../repo'; @Injectable() export class RocketChatUserService { - constructor(private readonly rocketChatUserRepo: RocketChatUserRepo) {} + constructor(private readonly rocketChatUserRepo: RocketChatUserRepo, private readonly logger: Logger) { + this.logger.setContext(RocketChatUserService.name); + } public async findByUserId(userId: EntityId): Promise { const user: RocketChatUser = await this.rocketChatUserRepo.findByUserId(userId); @@ -13,7 +17,28 @@ export class RocketChatUserService { return user; } - public deleteByUserId(userId: EntityId): Promise { - return this.rocketChatUserRepo.deleteByUserId(userId); + public async deleteByUserId(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user from rocket chat', + DomainModel.ROCKETCHATUSER, + userId, + StatusModel.PENDING + ) + ); + const deletedRocketChatUser = await this.rocketChatUserRepo.deleteByUserId(userId); + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user from rocket chat', + DomainModel.ROCKETCHATUSER, + userId, + StatusModel.FINISHED, + 0, + deletedRocketChatUser + ) + ); + + return deletedRocketChatUser; } } diff --git a/apps/server/src/modules/task/service/task.service.spec.ts b/apps/server/src/modules/task/service/task.service.spec.ts index fd77063c934..913ae9745b2 100644 --- a/apps/server/src/modules/task/service/task.service.spec.ts +++ b/apps/server/src/modules/task/service/task.service.spec.ts @@ -5,7 +5,7 @@ import { courseFactory, setupEntities, submissionFactory, taskFactory, userFacto import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { DomainModel } from '@shared/domain/types'; import { DomainOperationBuilder } from '@shared/domain/builder'; -import { LegacyLogger } from '@src/core/logger'; +import { Logger } from '@src/core/logger'; import { SubmissionService } from './submission.service'; import { TaskService } from './task.service'; @@ -33,8 +33,8 @@ describe('TaskService', () => { useValue: createMock(), }, { - provide: LegacyLogger, - useValue: createMock(), + provide: Logger, + useValue: createMock(), }, ], }).compile(); diff --git a/apps/server/src/modules/task/service/task.service.ts b/apps/server/src/modules/task/service/task.service.ts index 0472c23bf42..337358f10fd 100644 --- a/apps/server/src/modules/task/service/task.service.ts +++ b/apps/server/src/modules/task/service/task.service.ts @@ -2,10 +2,11 @@ import { FilesStorageClientAdapterService } from '@modules/files-storage-client' import { Injectable } from '@nestjs/common'; import { Task } from '@shared/domain/entity'; import { DomainOperation, IFindOptions } from '@shared/domain/interface'; -import { Counted, DomainModel, EntityId } from '@shared/domain/types'; +import { Counted, DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { TaskRepo } from '@shared/repo'; import { DomainOperationBuilder } from '@shared/domain/builder'; -import { LegacyLogger } from '@src/core/logger'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { SubmissionService } from './submission.service'; @Injectable() @@ -14,7 +15,7 @@ export class TaskService { private readonly taskRepo: TaskRepo, private readonly submissionService: SubmissionService, private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService, - private readonly logger: LegacyLogger + private readonly logger: Logger ) { this.logger.setContext(TaskService.name); } @@ -48,7 +49,15 @@ export class TaskService { } async deleteTasksByOnlyCreator(creatorId: EntityId): Promise { - this.logger.log(`Deleting Tasks where creatorId ${creatorId} is only parent`); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting data from Task', + DomainModel.TASK, + creatorId, + StatusModel.PENDING + ) + ); + const [tasksByOnlyCreatorId, counterOfTasksOnlyWithCreatorId] = await this.taskRepo.findByOnlyCreatorId(creatorId); if (counterOfTasksOnlyWithCreatorId > 0) { @@ -57,15 +66,29 @@ export class TaskService { } const result = DomainOperationBuilder.build(DomainModel.TASK, 0, counterOfTasksOnlyWithCreatorId); - this.logger.log( - `Successfully deleted ${counterOfTasksOnlyWithCreatorId} where creatorId ${creatorId} is only parent` + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted data from Task', + DomainModel.TASK, + creatorId, + StatusModel.FINISHED, + counterOfTasksOnlyWithCreatorId, + 0 + ) ); return result; } async removeCreatorIdFromTasks(creatorId: EntityId): Promise { - this.logger.log(`Deleting creatorId ${creatorId} from Tasks`); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Task', + DomainModel.TASK, + creatorId, + StatusModel.PENDING + ) + ); const [tasksByCreatorIdWithCoursesAndLessons, counterOfTasksWithCoursesorLessons] = await this.taskRepo.findByCreatorIdWithCourseAndLesson(creatorId); @@ -75,12 +98,28 @@ export class TaskService { } const result = DomainOperationBuilder.build(DomainModel.TASK, counterOfTasksWithCoursesorLessons, 0); - this.logger.log(`Successfully updated ${counterOfTasksWithCoursesorLessons} Tasks without creatorId ${creatorId}`); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from Task', + DomainModel.TASK, + creatorId, + StatusModel.FINISHED, + counterOfTasksWithCoursesorLessons, + 0 + ) + ); return result; } async removeUserFromFinished(userId: EntityId): Promise { - this.logger.log(`Deleting userId ${userId} from Archve collection in Tasks`); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Task archive collection', + DomainModel.TASK, + userId, + StatusModel.PENDING + ) + ); const [tasksWithUserInFinished, counterOfTasksWithUserInFinished] = await this.taskRepo.findByUserIdInFinished( userId ); @@ -92,8 +131,15 @@ export class TaskService { } const result = DomainOperationBuilder.build(DomainModel.TASK, counterOfTasksWithUserInFinished, 0); - this.logger.log( - `Successfully updated ${counterOfTasksWithUserInFinished} Tasks without userId ${userId} in archive collection in Tasks` + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from Task archive collection', + DomainModel.TASK, + userId, + StatusModel.FINISHED, + counterOfTasksWithUserInFinished, + 0 + ) ); return result; diff --git a/apps/server/src/modules/teams/service/team.service.spec.ts b/apps/server/src/modules/teams/service/team.service.spec.ts index 1406f90e0b9..d14f9fbfc19 100644 --- a/apps/server/src/modules/teams/service/team.service.spec.ts +++ b/apps/server/src/modules/teams/service/team.service.spec.ts @@ -2,6 +2,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { TeamsRepo } from '@shared/repo'; import { setupEntities, teamFactory, teamUserFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { TeamService } from './team.service'; describe('TeamService', () => { @@ -18,6 +19,10 @@ describe('TeamService', () => { provide: TeamsRepo, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/teams/service/team.service.ts b/apps/server/src/modules/teams/service/team.service.ts index 09d26f2ed5c..2264af9a56c 100644 --- a/apps/server/src/modules/teams/service/team.service.ts +++ b/apps/server/src/modules/teams/service/team.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { TeamEntity } from '@shared/domain/entity'; -import { EntityId } from '@shared/domain/types'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { TeamsRepo } from '@shared/repo'; +import { Logger } from '@src/core/logger'; @Injectable() export class TeamService { - constructor(private readonly teamsRepo: TeamsRepo) {} + constructor(private readonly teamsRepo: TeamsRepo, private readonly logger: Logger) { + this.logger.setContext(TeamService.name); + } public async findUserDataFromTeams(userId: EntityId): Promise { const teams = await this.teamsRepo.findByUserId(userId); @@ -14,6 +18,14 @@ export class TeamService { } public async deleteUserDataFromTeams(userId: EntityId): Promise { + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Deleting user data from Teams', + DomainModel.TEAMS, + userId, + StatusModel.PENDING + ) + ); const teams = await this.teamsRepo.findByUserId(userId); teams.forEach((team) => { @@ -22,6 +34,19 @@ export class TeamService { await this.teamsRepo.save(teams); - return teams.length; + const numberOfUpdatedTeams = teams.length; + + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user data from Teams', + DomainModel.TEAMS, + userId, + StatusModel.PENDING, + numberOfUpdatedTeams, + 0 + ) + ); + + return numberOfUpdatedTeams; } } diff --git a/apps/server/src/modules/teams/teams.module.ts b/apps/server/src/modules/teams/teams.module.ts index cb83bf00d8e..e8d56860d36 100644 --- a/apps/server/src/modules/teams/teams.module.ts +++ b/apps/server/src/modules/teams/teams.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { TeamsRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { TeamService } from './service'; @Module({ + imports: [LoggerModule], providers: [TeamService, TeamsRepo], exports: [TeamService], }) diff --git a/apps/server/src/modules/user/service/user.service.spec.ts b/apps/server/src/modules/user/service/user.service.spec.ts index 310f5ccdfef..4ef690fe99f 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -13,6 +13,7 @@ import { EntityId } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { UserDto } from '../uc/dto/user.dto'; import { UserQuery } from './user-query.type'; import { UserService } from './user.service'; @@ -55,6 +56,10 @@ describe('UserService', () => { provide: AccountService, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); service = module.get(UserService); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index ce2fe2316fa..796b1d92181 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -10,9 +10,11 @@ import { ConfigService } from '@nestjs/config'; import { Page, RoleReference, UserDO } from '@shared/domain/domainobject'; import { LanguageType, User } from '@shared/domain/entity'; import { IFindOptions } from '@shared/domain/interface'; -import { EntityId } from '@shared/domain/types'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; +import { Logger } from '@src/core/logger'; +import { DataDeletionDomainOperationLoggable } from '@shared/common/loggable'; import { UserConfig } from '../interfaces'; import { UserMapper } from '../mapper/user.mapper'; import { UserDto } from '../uc/dto/user.dto'; @@ -25,8 +27,11 @@ export class UserService { private readonly userDORepo: UserDORepo, private readonly configService: ConfigService, private readonly roleService: RoleService, - private readonly accountService: AccountService - ) {} + private readonly accountService: AccountService, + private readonly logger: Logger + ) { + this.logger.setContext(UserService.name); + } async me(userId: EntityId): Promise<[User, string[]]> { const user = await this.userRepo.findById(userId, true); @@ -124,7 +129,20 @@ export class UserService { } async deleteUser(userId: EntityId): Promise { - const deletedUserNumber: Promise = this.userRepo.deleteUser(userId); + this.logger.info( + new DataDeletionDomainOperationLoggable('Deleting user', DomainModel.USER, userId, StatusModel.PENDING) + ); + const deletedUserNumber = await this.userRepo.deleteUser(userId); + this.logger.info( + new DataDeletionDomainOperationLoggable( + 'Successfully deleted user', + DomainModel.USER, + userId, + StatusModel.FINISHED, + 0, + deletedUserNumber + ) + ); return deletedUserNumber; } diff --git a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts new file mode 100644 index 00000000000..b9ed8b111d8 --- /dev/null +++ b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.spec.ts @@ -0,0 +1,50 @@ +import { ObjectId } from 'bson'; +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { DataDeletionDomainOperationLoggable } from './data-deletion-domain-operation-loggable'; + +describe(DataDeletionDomainOperationLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const user: EntityId = new ObjectId().toHexString(); + const message = 'Test message.'; + const domain = DomainModel.USER; + const status = StatusModel.FINISHED; + const modifiedCount = 0; + const deletedCount = 1; + + const loggable: DataDeletionDomainOperationLoggable = new DataDeletionDomainOperationLoggable( + message, + domain, + user, + status, + modifiedCount, + deletedCount + ); + + return { + loggable, + message, + domain, + user, + status, + modifiedCount, + deletedCount, + }; + }; + + it('should return the correct log message', () => { + const { loggable, message, domain, user, status, modifiedCount, deletedCount } = setup(); + + expect(loggable.getLogMessage()).toEqual({ + message, + data: { + domain, + user, + status, + modifiedCount, + deletedCount, + }, + }); + }); + }); +}); diff --git a/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts new file mode 100644 index 00000000000..1be7727a6cd --- /dev/null +++ b/apps/server/src/shared/common/loggable/data-deletion-domain-operation-loggable.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { DomainModel, EntityId, StatusModel } from '@shared/domain/types'; +import { ErrorLogMessage, LogMessage, Loggable, ValidationErrorLogMessage } from '@src/core/logger'; + +export class DataDeletionDomainOperationLoggable implements Loggable { + constructor( + private readonly message: string, + private readonly domain: DomainModel, + private readonly user: EntityId, + private readonly status: StatusModel, + private readonly modifiedCount?: number, + private readonly deletedCount?: number + ) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: this.message, + data: { + domain: this.domain, + user: this.user, + status: this.status, + modifiedCount: this.modifiedCount, + deletedCount: this.deletedCount, + }, + }; + } +} diff --git a/apps/server/src/shared/common/loggable/index.ts b/apps/server/src/shared/common/loggable/index.ts index 5f21a462625..988b184d393 100644 --- a/apps/server/src/shared/common/loggable/index.ts +++ b/apps/server/src/shared/common/loggable/index.ts @@ -1 +1,2 @@ export { ReferencedEntityNotFoundLoggable } from './referenced-entity-not-found-loggable'; +export { DataDeletionDomainOperationLoggable } from './data-deletion-domain-operation-loggable'; diff --git a/apps/server/src/shared/domain/types/index.ts b/apps/server/src/shared/domain/types/index.ts index e591c94c825..4957fc6de59 100644 --- a/apps/server/src/shared/domain/types/index.ts +++ b/apps/server/src/shared/domain/types/index.ts @@ -11,3 +11,4 @@ export * from './system.type'; export * from './task.types'; export * from './value-of'; export * from './domain'; +export * from './status-model.enum'; diff --git a/apps/server/src/shared/domain/types/status-model.enum.ts b/apps/server/src/shared/domain/types/status-model.enum.ts new file mode 100644 index 00000000000..d4550d60bcd --- /dev/null +++ b/apps/server/src/shared/domain/types/status-model.enum.ts @@ -0,0 +1,4 @@ +export const enum StatusModel { + FINISHED = 'finished', + PENDING = 'pending', +} diff --git a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts index 2ddb7599921..10a01d379e7 100644 --- a/apps/server/src/shared/repo/dashboard/dashboard.repo.ts +++ b/apps/server/src/shared/repo/dashboard/dashboard.repo.ts @@ -54,7 +54,7 @@ export class DashboardRepo implements IDashboardRepo { } async deleteDashboardByUserId(userId: EntityId): Promise { - const promise: Promise = this.em.nativeDelete(DashboardModelEntity, { user: userId }); + const promise = await this.em.nativeDelete(DashboardModelEntity, { user: userId }); return promise; }