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

353 lines
10 KiB
TypeScript

import {
Injectable,
Logger,
InternalServerErrorException,
} 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,
orderBy?: string,
order?: 'asc' | 'desc',
) {
this.logger.debug(
`Fetching audit trails: page=${page}, pageSize=${pageSize}, type=${type}`,
);
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;
}
try {
const auditLogs = await this.prisma.audit.findMany({
take: pageSize,
skip: (page - 1) * pageSize,
orderBy: orderBy
? { [orderBy]: order || 'asc' }
: { timestamp: 'desc' },
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,
OR: search ? [{ id: { contains: search } }] : undefined,
},
});
return {
...auditLogs,
totalCount: count,
};
} catch (error) {
this.logger.error('Failed to fetch audit trails', error.stack);
throw new InternalServerErrorException('Failed to fetch audit trails');
}
}
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) {
this.logger.debug(`Processing ${records.length} audit 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) {
this.logger.error('Error storing audit trail', error.stack);
this.auditGateway.sendError({
status: 'ERROR',
message: error.message || 'Failed to store audit trail',
});
throw new InternalServerErrorException('Failed to store audit trail');
}
}
async getCountAuditTamperedData() {
try {
const [
auditTamperedCount,
auditNonTamperedCount,
rekamMedisTamperedCount,
tindakanDokterTamperedCount,
obatTamperedCount,
] = await Promise.all([
this.prisma.audit.count({
where: { result: 'tampered' },
}),
this.prisma.audit.count({
where: { result: 'non_tampered' },
}),
this.prisma.audit.count({
where: { result: 'tampered', id: { startsWith: 'REKAM' } },
}),
this.prisma.audit.count({
where: { result: 'tampered', id: { startsWith: 'TINDAKAN' } },
}),
this.prisma.audit.count({
where: { result: 'tampered', id: { startsWith: 'OBAT' } },
}),
]);
return {
auditTamperedCount,
auditNonTamperedCount,
rekamMedisTamperedCount,
tindakanDokterTamperedCount,
obatTamperedCount,
};
} catch (error) {
this.logger.error('Failed to get audit tampered count', error.stack);
throw new InternalServerErrorException('Failed to get audit statistics');
}
}
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;
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) {
this.logger.warn(
`Failed to resolve related data for log ${logId}: ${err.message}`,
);
}
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.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;
}
}