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 { sha256 } from '@api/common/crypto/hash'; 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; } console.log(type, tampered); 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, }, }); console.log(auditLogs); 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 { // const intervalId = setInterval(() => { // processedCount++; // const progressData = { // status: 'RUNNING', // progress_count: processedCount, // }; // this.logger.log('Mengirim progres via WebSocket:', progressData); // // PANGGIL FUNGSI GATEWAY // this.auditGateway.sendProgress(progressData); // if (processedCount >= BATCH_SIZE) { // clearInterval(intervalId); // const completeData = { status: 'COMPLETED' }; // this.logger.log('Mengirim selesai via WebSocket:', completeData); // // PANGGIL FUNGSI GATEWAY // this.auditGateway.sendComplete(completeData); // } // }, 500); // } catch (error) { // console.error('Tes streaming GAGAL', error); // } 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); // const records: AuditRecordPayload[] = []; // for (let index = 0; index < logs.length; index++) { // const record = await this.buildAuditRecord(logs[index], index); // if (record !== null) { // records.push(record); // } // await new Promise((resolve) => setTimeout(resolve, 250)); // } if (records.length > 0) { 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: record.user_id, last_sync: record.last_sync, result: record.result, }, }), ), ); processedCount += records.length; } if (nextBookmark === '' || nextBookmark === bookmark) { const completeData = { status: 'COMPLETED' }; this.logger.log('Mengirim selesai via WebSocket:', completeData); this.auditGateway.sendComplete(completeData); break; } bookmark = nextBookmark; } } catch (error) { console.error('Error storing audit trail:', error); throw error; } } 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; 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); 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); 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); 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); } const isNotTampered = dbHash ? await this.compareData(blockchainHash, dbHash) : false; 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; } }