diff --git a/packages/nocodb-nest/src/modules/audits/audits.controller.spec.ts b/packages/nocodb-nest/src/modules/audits/audits.controller.spec.ts new file mode 100644 index 0000000000..b6f59cd606 --- /dev/null +++ b/packages/nocodb-nest/src/modules/audits/audits.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuditsController } from './audits.controller'; +import { AuditsService } from './audits.service'; + +describe('AuditsController', () => { + let controller: AuditsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuditsController], + providers: [AuditsService], + }).compile(); + + controller = module.get(AuditsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/nocodb-nest/src/modules/audits/audits.controller.ts b/packages/nocodb-nest/src/modules/audits/audits.controller.ts new file mode 100644 index 0000000000..1005379650 --- /dev/null +++ b/packages/nocodb-nest/src/modules/audits/audits.controller.ts @@ -0,0 +1,88 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, + Request, +} from '@nestjs/common'; +import { PagedResponseImpl } from '../../helpers/PagedResponse'; +import { Acl } from '../../middlewares/extract-project-id/extract-project-id.middleware'; +import { Audit } from '../../models'; +import { AuditsService } from './audits.service'; + +@Controller('audits') +export class AuditsController { + constructor(private readonly auditsService: AuditsService) {} + + @Post('/api/v1/db/meta/audits/comments') + @Acl('commentRow') + async commentRow(@Request() req) { + return await this.auditsService.commentRow({ + // todo: correct this + rowId: req.params.rowId ?? req.query.rowId, + user: (req as any).user, + body: req.body, + }); + } + + @Post('/api/v1/db/meta/audits/rows/:rowId/update') + @Acl('auditRowUpdate') + async auditRowUpdate(@Param('rowId') rowId: string, @Body() body: any) { + return await this.auditsService.auditRowUpdate({ + rowId, + body, + }); + } + + @Get('/api/v1/db/meta/audits/comments') + @Acl('commentList') + async commentList(@Request() req) { + return new PagedResponseImpl( + await this.auditsService.commentList({ query: req.query }), + ); + } + + @Patch('/api/v1/db/meta/audits/:auditId/comment') + @Acl('commentUpdate') + async commentUpdate( + @Param('auditId') auditId: string, + @Request() req, + @Body() body: any, + ) { + return await this.auditsService.commentUpdate({ + auditId, + userEmail: req.user?.email, + body: body, + }); + } + + @Get('/api/v1/db/meta/projects/:projectId/audits') + @Acl('auditList') + async auditList(@Request() req, @Param('projectId') projectId: string) { + return new PagedResponseImpl( + await this.auditsService.auditList({ + query: req.query, + projectId, + }), + { + count: await Audit.projectAuditCount(projectId), + ...req.query, + }, + ); + } + + @Get('/api/v1/db/meta/audits/comments/count') + @Acl('commentsCount') + async commentsCount( + @Query('fk_model_id') fk_model_id: string, + @Query('ids') ids: string[], + ) { + return await this.auditsService.commentsCount({ + fk_model_id, + ids, + }); + } +} diff --git a/packages/nocodb-nest/src/modules/audits/audits.module.ts b/packages/nocodb-nest/src/modules/audits/audits.module.ts new file mode 100644 index 0000000000..a7e98b79c9 --- /dev/null +++ b/packages/nocodb-nest/src/modules/audits/audits.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AuditsService } from './audits.service'; +import { AuditsController } from './audits.controller'; + +@Module({ + controllers: [AuditsController], + providers: [AuditsService] +}) +export class AuditsModule {} diff --git a/packages/nocodb-nest/src/modules/audits/audits.service.spec.ts b/packages/nocodb-nest/src/modules/audits/audits.service.spec.ts new file mode 100644 index 0000000000..a6cd52d749 --- /dev/null +++ b/packages/nocodb-nest/src/modules/audits/audits.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuditsService } from './audits.service'; + +describe('AuditsService', () => { + let service: AuditsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuditsService], + }).compile(); + + service = module.get(AuditsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/nocodb-nest/src/modules/audits/audits.service.ts b/packages/nocodb-nest/src/modules/audits/audits.service.ts new file mode 100644 index 0000000000..588195e732 --- /dev/null +++ b/packages/nocodb-nest/src/modules/audits/audits.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import DOMPurify from 'isomorphic-dompurify'; +import { + AuditOperationSubTypes, + AuditOperationTypes, + AuditRowUpdateReqType, + CommentUpdateReqType, +} from 'nocodb-sdk'; +import { validatePayload } from '../../helpers'; +import { NcError } from '../../helpers/catchError'; +import { Audit, Model } from '../../models'; + +@Injectable() +export class AuditsService { + async commentRow(param: { + rowId: string; + body: AuditRowUpdateReqType; + user: any; + }) { + validatePayload('swagger.json#/components/schemas/CommentReq', param.body); + + return await Audit.insert({ + ...param.body, + user: param.user?.email, + op_type: AuditOperationTypes.COMMENT, + }); + } + + async auditRowUpdate(param: { rowId: string; body: AuditRowUpdateReqType }) { + validatePayload( + 'swagger.json#/components/schemas/AuditRowUpdateReq', + param.body, + ); + + const model = await Model.getByIdOrName({ id: param.body.fk_model_id }); + return await Audit.insert({ + fk_model_id: param.body.fk_model_id, + row_id: param.rowId, + op_type: AuditOperationTypes.DATA, + op_sub_type: AuditOperationSubTypes.UPDATE, + description: DOMPurify.sanitize( + `Table ${model.table_name} : field ${param.body.column_name} got changed from ${param.body.prev_value} to ${param.body.value}`, + ), + details: + DOMPurify.sanitize(`${param.body.column_name} + : ${param.body.prev_value} + ${param.body.value}`), + ip: (param as any).clientIp, + user: (param as any).user?.email, + }); + } + + async commentList(param: { query: any }) { + return await Audit.commentsList(param.query); + } + + async auditList(param: { query: any; projectId: string }) { + return await Audit.projectAuditList(param.projectId, param.query); + } + + async commentsCount(param: { fk_model_id: string; ids: string[] }) { + return await Audit.commentsCount({ + fk_model_id: param.fk_model_id as string, + ids: param.ids as string[], + }); + } + + async commentUpdate(param: { + auditId: string; + userEmail: string; + body: CommentUpdateReqType; + }) { + validatePayload( + 'swagger.json#/components/schemas/CommentUpdateReq', + param.body, + ); + + const log = await Audit.get(param.auditId); + + if (log.user !== param.userEmail) { + NcError.unauthorized('Unauthorized access'); + } + return await Audit.commentUpdate(param.auditId, param.body); + } +}