2025-11-17 04:14:13 +00:00
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
2025-11-10 07:28:08 +00:00
|
|
|
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';
|
2025-11-13 05:31:48 +00:00
|
|
|
import type {
|
|
|
|
|
AuditEvent,
|
|
|
|
|
resultStatus as ResultStatus,
|
|
|
|
|
} from '@dist/generated/prisma';
|
2025-11-17 04:14:13 +00:00
|
|
|
import { AuditGateway } from './audit.gateway';
|
2025-11-13 05:31:48 +00:00
|
|
|
|
|
|
|
|
type AuditRecordPayload = {
|
|
|
|
|
id: string;
|
|
|
|
|
event: AuditEvent;
|
|
|
|
|
payload: string;
|
|
|
|
|
timestamp: Date;
|
|
|
|
|
user_id: bigint;
|
|
|
|
|
last_sync: Date;
|
|
|
|
|
result: ResultStatus;
|
|
|
|
|
};
|
2025-11-10 07:28:08 +00:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class AuditService {
|
2025-11-17 04:14:13 +00:00
|
|
|
private readonly logger = new Logger(AuditService.name);
|
2025-11-10 07:28:08 +00:00
|
|
|
constructor(
|
2025-11-13 05:31:48 +00:00
|
|
|
private readonly prisma: PrismaService,
|
2025-11-10 07:28:08 +00:00
|
|
|
private readonly logService: LogService,
|
|
|
|
|
private readonly obatService: ObatService,
|
|
|
|
|
private readonly rekamMedisService: RekammedisService,
|
|
|
|
|
private readonly tindakanDokterService: TindakanDokterService,
|
2025-11-17 04:14:13 +00:00
|
|
|
private auditGateway: AuditGateway,
|
2025-11-10 07:28:08 +00:00
|
|
|
) {}
|
|
|
|
|
|
2025-11-17 04:14:13 +00:00
|
|
|
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);
|
2025-11-13 05:31:48 +00:00
|
|
|
const auditLogs = await this.prisma.audit.findMany({
|
|
|
|
|
take: pageSize,
|
2025-11-17 04:14:13 +00:00
|
|
|
skip: (page - 1) * pageSize,
|
2025-11-13 05:31:48 +00:00
|
|
|
orderBy: { timestamp: 'asc' },
|
2025-11-17 04:14:13 +00:00
|
|
|
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,
|
|
|
|
|
},
|
2025-11-10 07:28:08 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-17 04:14:13 +00:00
|
|
|
return {
|
|
|
|
|
...auditLogs,
|
|
|
|
|
totalCount: count,
|
|
|
|
|
};
|
2025-11-13 05:31:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async storeAuditTrail() {
|
|
|
|
|
console.log('Storing audit trail...');
|
|
|
|
|
const BATCH_SIZE = 25;
|
|
|
|
|
let bookmark = '';
|
|
|
|
|
let processedCount = 0;
|
|
|
|
|
|
2025-11-17 04:14:13 +00:00
|
|
|
// 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);
|
|
|
|
|
// }
|
|
|
|
|
|
2025-11-13 05:31:48 +00:00
|
|
|
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)
|
2025-11-10 07:28:08 +00:00
|
|
|
) {
|
2025-11-13 05:31:48 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 07:51:47 +00:00
|
|
|
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));
|
|
|
|
|
// }
|
2025-11-13 05:31:48 +00:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
),
|
2025-11-10 07:28:08 +00:00
|
|
|
);
|
2025-11-13 05:31:48 +00:00
|
|
|
processedCount += records.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextBookmark === '' || nextBookmark === bookmark) {
|
2025-11-17 04:14:13 +00:00
|
|
|
const completeData = { status: 'COMPLETED' };
|
|
|
|
|
this.logger.log('Mengirim selesai via WebSocket:', completeData);
|
|
|
|
|
this.auditGateway.sendComplete(completeData);
|
2025-11-13 05:31:48 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookmark = nextBookmark;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error storing audit trail:', error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async buildAuditRecord(
|
|
|
|
|
logEntry: any,
|
2025-11-17 04:14:13 +00:00
|
|
|
index?: number,
|
2025-11-13 05:31:48 +00:00
|
|
|
): Promise<AuditRecordPayload | null> {
|
|
|
|
|
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;
|
2025-11-17 04:14:13 +00:00
|
|
|
//
|
2025-11-13 05:31:48 +00:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-10 07:28:08 +00:00
|
|
|
}
|
2025-11-13 05:31:48 +00:00
|
|
|
} 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';
|
2025-11-10 07:28:08 +00:00
|
|
|
|
2025-11-17 04:14:13 +00:00
|
|
|
const progressData = {
|
|
|
|
|
status: 'RUNNING',
|
|
|
|
|
progress_count: index ?? 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.logger.log('Mengirim progres via WebSocket:', progressData);
|
|
|
|
|
this.auditGateway.sendProgress(progressData);
|
|
|
|
|
|
2025-11-13 05:31:48 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-11-10 07:28:08 +00:00
|
|
|
|
2025-11-13 05:31:48 +00:00
|
|
|
async compareData(blockchainHash: string, postgreHash: string) {
|
|
|
|
|
return blockchainHash === postgreHash;
|
2025-11-10 07:28:08 +00:00
|
|
|
}
|
|
|
|
|
}
|