hospital-log/backend/api/src/modules/audit/audit.service.ts

314 lines
9.1 KiB
TypeScript
Raw Normal View History

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';
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;
};
2025-11-10 07:28:08 +00:00
@Injectable()
export class AuditService {
private readonly logger = new Logger(AuditService.name);
2025-11-10 07:28:08 +00:00
constructor(
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,
private auditGateway: AuditGateway,
2025-11-10 07:28:08 +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);
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,
},
2025-11-10 07:28:08 +00:00
});
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)
2025-11-10 07:28:08 +00:00
) {
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);
}
// Wait 1 second before processing next item
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,
},
}),
),
2025-11-10 07:28:08 +00:00
);
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<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;
//
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
}
} 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
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;
}
2025-11-10 07:28:08 +00:00
async compareData(blockchainHash: string, postgreHash: string) {
return blockchainHash === postgreHash;
2025-11-10 07:28:08 +00:00
}
}