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

226 lines
6.3 KiB
TypeScript
Raw Normal View History

2025-11-10 07:28:08 +00:00
import { Injectable } 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';
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 {
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,
) {}
async getAuditTrails(pageSize: number, bookmark: string) {
const auditLogs = await this.prisma.audit.findMany({
take: pageSize,
skip: bookmark ? 1 : 0,
cursor: bookmark ? { id: bookmark } : undefined,
orderBy: { timestamp: 'asc' },
2025-11-10 07:28:08 +00:00
});
return auditLogs;
}
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)
2025-11-10 07:28:08 +00:00
) {
break;
}
const records = (
await Promise.all(
logs.map((logEntry) => this.buildAuditRecord(logEntry)),
)
).filter((record): record is AuditRecordPayload => record !== null);
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) {
break;
}
bookmark = nextBookmark;
}
} catch (error) {
console.error('Error storing audit trail:', error);
throw error;
}
}
private async buildAuditRecord(
logEntry: any,
): 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
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
}
}