import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { LogService } from '../log/log.service'; import { ObatService } from '../obat/obat.service'; import { RekammedisService } from '../rekammedis/rekammedis.service'; import { TindakanDokterService } from '../tindakandokter/tindakandokter.service'; import type { AuditEvent, resultStatus as ResultStatus, } from '@dist/generated/prisma'; import { AuditGateway } from './audit.gateway'; type AuditRecordPayload = { id: string; event: AuditEvent; payload: string; timestamp: Date; user_id: bigint; last_sync: Date; result: ResultStatus; }; @Injectable() export class AuditService { private readonly logger = new Logger(AuditService.name); constructor( private readonly prisma: PrismaService, private readonly logService: LogService, private readonly obatService: ObatService, private readonly rekamMedisService: RekammedisService, private readonly tindakanDokterService: TindakanDokterService, private auditGateway: AuditGateway, ) {} async getAuditTrails( search: string, page: number, pageSize: number, type?: string, tampered?: string, ) { if (type === 'all' || type === 'initial') { type = undefined; } else if (type === 'rekam_medis') { type = 'REKAM'; } else if (type === 'tindakan') { type = 'TINDAKAN'; } else if (type === 'obat') { type = 'OBAT'; } if (tampered === 'all' || tampered === 'initial' || !tampered) { tampered = undefined; } const auditLogs = await this.prisma.audit.findMany({ take: pageSize, skip: (page - 1) * pageSize, orderBy: { timestamp: 'asc' }, where: { id: type && type !== 'all' ? { startsWith: type } : undefined, result: tampered ? (tampered as ResultStatus) : undefined, OR: search ? [{ id: { contains: search } }] : undefined, }, }); const count = await this.prisma.audit.count({ where: { id: type && type !== 'all' ? { startsWith: type } : undefined, result: tampered ? (tampered as ResultStatus) : undefined, }, }); return { ...auditLogs, totalCount: count, }; } async storeAuditTrail() { console.log('Storing audit trail...'); const BATCH_SIZE = 25; let bookmark = ''; let processedCount = 0; try { while (true) { const pageResults = await this.logService.getLogsWithPagination( BATCH_SIZE, bookmark, ); const logs: any[] = Array.isArray(pageResults?.logs) ? pageResults.logs : []; const nextBookmark: string = pageResults?.bookmark ?? ''; if ( logs.length === 0 && (nextBookmark === '' || nextBookmark === bookmark) ) { break; } const records = ( await Promise.all( logs.map((logEntry, index) => this.buildAuditRecord(logEntry, index), ), ) ).filter((record): record is AuditRecordPayload => record !== null); if (records.length > 0) { console.log(records); await this.prisma.$transaction( records.map((record) => this.prisma.audit.upsert({ where: { id: record.id }, create: record, update: { event: record.event, payload: record.payload, timestamp: record.timestamp, user_id: BigInt(record.user_id), last_sync: record.last_sync, result: record.result, }, }), ), ); processedCount += records.length; } if (nextBookmark === '' || nextBookmark === bookmark) { const completeData = { status: 'COMPLETED' }; this.auditGateway.sendComplete(completeData); break; } bookmark = nextBookmark; } } catch (error) { console.error('Error storing audit trail:', error); throw error; } } async getCountAuditTamperedData() { const auditTamperedCount = await this.prisma.audit.count({ where: { result: 'tampered', }, }); const auditNonTamperedCount = await this.prisma.audit.count({ where: { result: 'non_tampered', }, }); const rekamMedisTamperedCount = await this.prisma.audit.count({ where: { result: 'tampered', id: { startsWith: 'REKAM', }, }, }); const tindakanDokterTamperedCount = await this.prisma.audit.count({ where: { result: 'tampered', id: { startsWith: 'TINDAKAN', }, }, }); const obatTamperedCount = await this.prisma.audit.count({ where: { result: 'tampered', id: { startsWith: 'OBAT', }, }, }); return { auditTamperedCount, auditNonTamperedCount, rekamMedisTamperedCount, tindakanDokterTamperedCount, obatTamperedCount, }; } private async buildAuditRecord( logEntry: any, index?: number, ): Promise { if (!logEntry?.value) { return null; } const { value } = logEntry; const logId: string | undefined = value.id; if (!logId) { return null; } const now = new Date(); const timestamp = this.parseTimestamp(value.timestamp) ?? now; const userId = value.user_id; const blockchainHash: string | undefined = value.payload; let data: any = null; if (!blockchainHash) { return null; } let dbHash: string | null = null; try { if (logId.startsWith('OBAT')) { const obatId = this.extractNumericId(logId); if (obatId !== null) { const obat = await this.obatService.getObatById(obatId); data = obat; if (obat) { dbHash = this.obatService.createHashingPayload({ obat: obat.obat, jumlah_obat: obat.jumlah_obat, aturan_pakai: obat.aturan_pakai, }); } } } else if (logId.startsWith('REKAM')) { const rekamMedisId = this.extractStringId(logId); if (rekamMedisId) { const rekamMedis = await this.rekamMedisService.getRekamMedisById(rekamMedisId); data = rekamMedis; if (rekamMedis) { dbHash = this.rekamMedisService.createHashingPayload({ dokter_id: 123, visit_id: rekamMedis.id_visit ?? '', anamnese: rekamMedis.anamnese ?? '', jenis_kasus: rekamMedis.jenis_kasus ?? '', tindak_lanjut: rekamMedis.tindak_lanjut ?? '', }); } } } else if (logId.startsWith('TINDAKAN')) { const tindakanId = this.extractNumericId(logId); if (tindakanId !== null) { const tindakanDokter = await this.tindakanDokterService.getTindakanDokterById(tindakanId); data = tindakanDokter; if (tindakanDokter) { dbHash = this.tindakanDokterService.createHashingPayload({ id_visit: tindakanDokter.id_visit, tindakan: tindakanDokter.tindakan, kategori_tindakan: tindakanDokter.kategori_tindakan, kelompok_tindakan: tindakanDokter.kelompok_tindakan, }); } } } else { return null; } } catch (err) { console.warn(`Failed to resolve related data for log ${logId}:`, err); } let isNotTampered = false; const eventType = logEntry.value.event?.split('_').at(-1); const isDeleteEvent = eventType === 'deleted'; const hasRow = Boolean(data); if (!hasRow) { isNotTampered = isDeleteEvent; } else if (isDeleteEvent || data.deleted_status === 'DELETED') { isNotTampered = isDeleteEvent && data.deleted_status === 'DELETED'; } else { const hashesMatch = dbHash && (await this.compareData(blockchainHash, dbHash)); isNotTampered = Boolean(hashesMatch); } const result: ResultStatus = isNotTampered ? 'non_tampered' : 'tampered'; const progressData = { status: 'RUNNING', progress_count: index ?? 0, }; // this.logger.log('Mengirim progres via WebSocket:', progressData); this.auditGateway.sendProgress(progressData); return { id: logId, event: value.event as AuditEvent, payload: blockchainHash, timestamp, user_id: userId, last_sync: now, result, }; } private parseTimestamp(rawTimestamp?: string) { if (!rawTimestamp) { return null; } const parsed = new Date(rawTimestamp); if (Number.isNaN(parsed.getTime())) { return null; } return parsed; } private extractNumericId(rawId: string): number | null { const [, numericPart] = rawId.split('_'); const parsed = parseInt(numericPart, 10); return Number.isNaN(parsed) ? null : parsed; } private extractStringId(rawId: string): string | null { const parts = rawId.split('_'); return parts.length > 1 ? parts[1] : null; } async compareData(blockchainHash: string, postgreHash: string) { return blockchainHash === postgreHash; } }