feat: create audit trail, show audit trail from DB, partial proof webservice

This commit is contained in:
yosaphatprs 2025-11-13 12:31:48 +07:00
parent 37c3676c59
commit 43eb0e464d
16 changed files with 534 additions and 203 deletions

View File

@ -0,0 +1,91 @@
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "public";
-- CreateTable
CREATE TABLE "public"."blockchain_log_queue" (
"id" BIGSERIAL NOT NULL,
"status" VARCHAR(20) NOT NULL DEFAULT 'PENDING',
"event" VARCHAR(50) NOT NULL,
"user_id" BIGINT NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"payload" JSONB NOT NULL,
"processed_at" TIMESTAMPTZ(6),
"transactionid" VARCHAR(64),
CONSTRAINT "blockchain_log_queue_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."pemberian_obat" (
"id" SERIAL NOT NULL,
"id_visit" VARCHAR(25) NOT NULL,
"obat" VARCHAR(100) NOT NULL,
"jumlah_obat" INTEGER NOT NULL,
"aturan_pakai" TEXT,
CONSTRAINT "pemberian_obat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."pemberian_tindakan" (
"id" SERIAL NOT NULL,
"id_visit" VARCHAR(25) NOT NULL,
"tindakan" VARCHAR(100) NOT NULL,
"kategori_tindakan" VARCHAR(50),
"kelompok_tindakan" VARCHAR(50),
CONSTRAINT "pemberian_tindakan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "public"."rekam_medis" (
"id_visit" VARCHAR(25) NOT NULL,
"waktu_visit" TIMESTAMP(6) NOT NULL,
"no_rm" VARCHAR(20) NOT NULL,
"nama_pasien" VARCHAR(100) NOT NULL,
"umur" INTEGER,
"jenis_kelamin" VARCHAR(10),
"gol_darah" VARCHAR(3),
"pekerjaan" VARCHAR(100),
"suku" VARCHAR(100),
"kode_diagnosa" VARCHAR(20),
"diagnosa" TEXT,
"anamnese" TEXT,
"sistolik" INTEGER,
"diastolik" INTEGER,
"nadi" INTEGER,
"suhu" DECIMAL(4,1),
"nafas" INTEGER,
"tinggi_badan" DECIMAL(10,5),
"berat_badan" DECIMAL(10,5),
"jenis_kasus" VARCHAR(50),
"tindak_lanjut" TEXT,
CONSTRAINT "rekam_medis_pkey" PRIMARY KEY ("id_visit")
);
-- CreateTable
CREATE TABLE "public"."users" (
"id" BIGSERIAL NOT NULL,
"nama_lengkap" VARCHAR(255) NOT NULL,
"username" VARCHAR(255) NOT NULL,
"password_hash" VARCHAR(255) NOT NULL,
"role" VARCHAR(50) DEFAULT 'user',
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "public"."users"("username" ASC);
-- AddForeignKey
ALTER TABLE "public"."blockchain_log_queue" ADD CONSTRAINT "fk_log_user" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "public"."pemberian_obat" ADD CONSTRAINT "fk_pemberian_obat_visit" FOREIGN KEY ("id_visit") REFERENCES "public"."rekam_medis"("id_visit") ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE "public"."pemberian_tindakan" ADD CONSTRAINT "fk_tindakan_visit" FOREIGN KEY ("id_visit") REFERENCES "public"."rekam_medis"("id_visit") ON DELETE CASCADE ON UPDATE NO ACTION;

View File

@ -0,0 +1,18 @@
-- CreateEnum
CREATE TYPE "AuditEvent" AS ENUM ('tindakan_dokter_created', 'obat_created', 'rekam_medis_created', 'tindakan_dokter_updated', 'obat_updated', 'rekam_medis_updated', 'tindakan_dokter_deleted', 'obat_deleted', 'rekam_medis_deleted');
-- CreateEnum
CREATE TYPE "resultStatus" AS ENUM ('tampered', 'non_tampered');
-- CreateTable
CREATE TABLE "audit" (
"id" VARCHAR(50) NOT NULL,
"event" "AuditEvent" NOT NULL,
"payload" TEXT NOT NULL,
"timestamp" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" BIGINT NOT NULL,
"last_sync" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"result" "resultStatus" NOT NULL,
CONSTRAINT "audit_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -75,3 +75,30 @@ model users {
updated_at DateTime? @default(now()) @db.Timestamptz(6)
blockchain_log_queue blockchain_log_queue[]
}
enum AuditEvent {
tindakan_dokter_created
obat_created
rekam_medis_created
tindakan_dokter_updated
obat_updated
rekam_medis_updated
tindakan_dokter_deleted
obat_deleted
rekam_medis_deleted
}
enum resultStatus {
tampered
non_tampered
}
model audit {
id String @id @db.VarChar(50)
event AuditEvent
payload String
timestamp DateTime @default(now()) @db.Timestamptz(6)
user_id BigInt
last_sync DateTime @default(now()) @db.Timestamptz(6)
result resultStatus
}

View File

@ -11,6 +11,7 @@ import { PrismaModule } from './modules/prisma/prisma.module';
import { AuthModule } from './modules/auth/auth.module';
import { FabricModule } from './modules/fabric/fabric.module';
import { AuditModule } from './modules/audit/audit.module';
import { ProofModule } from './modules/proof/proof.module';
@Module({
imports: [
@ -26,6 +27,7 @@ import { AuditModule } from './modules/audit/audit.module';
PrismaModule,
FabricModule,
AuditModule,
ProofModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/guard/auth.guard';
import { AuditService } from './audit.service';
@ -12,8 +12,14 @@ export class AuditController {
@Query('pageSize') pageSize: number,
@Query('bookmark') bookmark: string,
) {
console.log('Audit trail accessed');
const result = await this.auditService.getAuditTrails(pageSize, bookmark);
return result;
}
@Post('/trail')
@UseGuards(AuthGuard)
async createAuditTrail() {
const result = await this.auditService.storeAuditTrail();
return result;
}
}

View File

@ -5,11 +5,25 @@ 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;
};
@Injectable()
export class AuditService {
constructor(
// private readonly prisma: PrismaService,
private readonly prisma: PrismaService,
private readonly logService: LogService,
private readonly obatService: ObatService,
private readonly rekamMedisService: RekammedisService,
@ -17,76 +31,195 @@ export class AuditService {
) {}
async getAuditTrails(pageSize: number, bookmark: string) {
if (!pageSize || pageSize <= 0) {
pageSize = 10;
}
if (!bookmark) {
bookmark = '';
}
const logs = await this.logService.getLogsWithPagination(
pageSize,
bookmark,
);
console.log('Fetched logs:', logs);
const flattenLogs = logs.logs.map((log: { value: any }) => {
return {
...log.value,
bookmark: logs.bookmark,
};
const auditLogs = await this.prisma.audit.findMany({
take: pageSize,
skip: bookmark ? 1 : 0,
cursor: bookmark ? { id: bookmark } : undefined,
orderBy: { timestamp: 'asc' },
});
const formattedLogs = await Promise.all(
flattenLogs.map(async (log: any) => {
let relatedData = null;
let payloadData = null;
let payloadHash = null;
const id = log.id.split('_')[1];
return auditLogs;
}
if (log.event === 'obat_created' || log.event === 'obat_updated') {
relatedData = await this.obatService.getObatById(parseInt(id));
payloadData = {
id: relatedData?.id_visit,
obat: relatedData?.obat,
jumlah_obat: relatedData?.jumlah_obat,
aturan_pakai: relatedData?.aturan_pakai,
};
payloadHash = sha256(JSON.stringify(payloadData));
} else if (
log.event === 'rekam_medis_created' ||
log.event === 'rekam_medis_updated'
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)
) {
relatedData = await this.rekamMedisService.getRekamMedisById(id);
payloadData = {
dokter_id: 123,
visit_id: relatedData?.id_visit,
anamnese: relatedData?.anamnese,
jenis_kasus: relatedData?.jenis_kasus,
tindak_lanjut: relatedData?.tindak_lanjut,
};
payloadHash = sha256(JSON.stringify(payloadData));
} else if (
log.event === 'tindakan_dokter_created' ||
log.event === 'tindakan_dokter_updated'
) {
relatedData = await this.tindakanDokterService.getTindakanDokterById(
parseInt(id),
);
payloadData = {
id_visit: relatedData?.id_visit,
tindakan: relatedData?.tindakan,
kategori_tindakan: relatedData?.kategori_tindakan,
kelompok_tindakan: relatedData?.kelompok_tindakan,
};
payloadHash = sha256(JSON.stringify(payloadData));
break;
}
return {
...log,
isTampered: payloadHash === log.payload,
};
}),
);
const records = (
await Promise.all(
logs.map((logEntry) => this.buildAuditRecord(logEntry)),
)
).filter((record): record is AuditRecordPayload => record !== null);
return formattedLogs;
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) {
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,
});
}
}
} 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';
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;
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProofController } from './proof.controller';
describe('ProofController', () => {
let controller: ProofController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProofController],
}).compile();
controller = module.get<ProofController>(ProofController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@ -0,0 +1,12 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { ProofService } from './proof.service';
@Controller('proof')
export class ProofController {
constructor(private proofService: ProofService) {}
@Post('/request')
requestProof(@Body('id_visit') id_visit: string) {
return this.proofService.getProof(id_visit);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ProofController } from './proof.controller';
import { ProofService } from './proof.service';
import { RekamMedisModule } from '../rekammedis/rekammedis.module';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [RekamMedisModule, PrismaModule],
providers: [ProofService],
controllers: [ProofController],
})
export class ProofModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProofService } from './proof.service';
describe('ProofService', () => {
let service: ProofService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ProofService],
}).compile();
service = module.get<ProofService>(ProofService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,13 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { RekammedisService } from '../rekammedis/rekammedis.service';
@Injectable()
export class ProofService {
constructor(
private prismaService: PrismaService,
private rekamMedisService: RekammedisService,
) {}
async getProof(id_visit: any) {}
}

View File

@ -394,6 +394,18 @@ export class RekammedisService {
};
}
async getAgeByIdVisit(id_visit: string) {
const age = await this.prisma.rekam_medis.findUnique({
select: {
umur: true,
},
where: {
id_visit: id_visit,
},
});
return age;
}
async countRekamMedis() {
return this.prisma.rekam_medis.count();
}

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import ButtonDark from "./dashboard/ButtonDark.vue";
interface Props {
title?: string;
@ -52,12 +53,10 @@ defineExpose({
<h3 class="text-lg font-bold">{{ title }}</h3>
<p class="py-4">{{ message }}</p>
<div class="modal-action">
<button class="btn btn-ghost" @click="handleCancel">
<ButtonDark @click="handleConfirm" :text="confirmText" />
<button class="btn btn-error btn-sm" @click="handleCancel">
{{ cancelText }}
</button>
<button class="btn btn-error" @click="handleConfirm">
{{ confirmText }}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">

View File

@ -15,6 +15,9 @@ import {
ITEMS_PER_PAGE_OPTIONS,
} from "../../constants/pagination";
import type { BlockchainLog } from "../../constants/interfaces";
import ButtonDark from "../../components/dashboard/ButtonDark.vue";
import DialogConfirm from "../../components/DialogConfirm.vue";
import PaginationControls from "../../components/dashboard/PaginationControls.vue";
type AuditLogType = "obat" | "rekam_medis" | "tindakan" | "unknown";
@ -31,8 +34,6 @@ interface AuditLogResponse {
totalCount: number;
}
const bookmark = ref("");
const prevBookmark = ref("");
const router = useRouter();
const route = useRoute();
const api = useApi();
@ -102,6 +103,17 @@ const formatTimestamp = (rawValue?: string) => {
return date.toLocaleString("id-ID", options).replace(/\./g, ":");
};
const runAuditTrail = async () => {
console.log("Running audit trail...");
try {
const result = await api.post("/audit/trail", {});
console.log("Audit trail run result:", result);
} catch (error) {
// console.error("Error running audit trail:", error);
}
};
const deriveType = (entry: Partial<AuditLogEntry>): AuditLogType => {
const { type, event = "", id = "" } = entry;
if (type && type !== "unknown") {
@ -154,11 +166,7 @@ const normalizeEntry = (entry: any): AuditLogEntry => {
id: flattened.id,
});
const isTampered = Boolean(
flattened.isTampered ||
(typeof flattened.status === "string" &&
flattened.status.toLowerCase().includes("tamper"))
);
const isTampered = Boolean(flattened.result.toLowerCase() === "tampered");
const statusLabel = flattened.status
? flattened.status
@ -185,10 +193,9 @@ const normalizeEntry = (entry: any): AuditLogEntry => {
};
};
const handleNextBookmark = () => {
if (bookmark.value) {
fetchData(bookmark.value);
}
const auditDialog = ref<InstanceType<typeof DialogConfirm> | null>(null);
const showAuditModal = () => {
auditDialog.value?.show();
};
const updateQueryParams = () => {
@ -246,24 +253,11 @@ const fetchData = async (bookmarkParam?: string, isInitial?: boolean) => {
? { data: response, totalCount: response.length }
: response;
const bookmarkValue = Array.isArray(response)
? (response[0] as any)?.bookmark
: (response as any).bookmark;
// console.log(
// "Bookmark:",
// Array.isArray(response)
// ? (response[0] as any)?.bookmark
// : (response as any).bookmark
// );
const normalized = Array.isArray(payload.data)
? payload.data.map((item) => normalizeEntry(item))
: [];
bookmark.value = bookmarkValue;
localStorage.setItem("bookmark-page-audit", bookmark.value || "");
logs.value = normalized;
console.log(logs.value);
pagination.totalCount.value = payload.totalCount ?? normalized.length;
if (!isInitial) {
@ -283,13 +277,6 @@ const handleSearch = () => {
debouncedFetch();
};
const handlePageSizeClick = (size: number) => {
handlePageSizeChange(size); // your existing logic
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
const handlePageSizeChange = (size: number) => {
pageSize.value = size;
fetchData();
@ -421,6 +408,16 @@ onMounted(async () => {
placeholder="Cari berdasarkan ID Log"
@search="handleSearch"
/>
<DialogConfirm
ref="auditDialog"
title="Konfirmasi"
message="Apakah anda yakin ingin menjalankan audit? (Proses ini mungkin
memakan waktu lama)"
confirm-text="Iya, Jalankan"
cancel-text="Batal"
@confirm="runAuditTrail"
/>
<ButtonDark text="Lakukan Audit" @click="showAuditModal" />
</div>
<DataTable
@ -430,65 +427,22 @@ onMounted(async () => {
empty-message="Tidak ada log audit"
:is-aksi="false"
/>
<div>
<!-- Pagination Info -->
<div class="text-sm text-gray-600 px-4 py-4">
Menampilkan data {{ 1 }} - {{ logs.length }}
</div>
<!-- Pagination Controls -->
<div class="flex justify-between items-center px-4 pb-4">
<!-- Items per page selector -->
<div class="flex items-center gap-2">
<span class="text-xs text-gray-600">Data per halaman:</span>
<div class="dropdown dropdown-top">
<div
tabindex="0"
role="button"
class="btn btn-xs shadow-none rounded-full bg-white text-dark font-bold border border-gray-200 hover:bg-gray-50"
>
{{ pageSize }}
</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-white rounded-box z-10 w-16 p-2 shadow-lg border border-gray-100"
>
<li v-for="size in ITEMS_PER_PAGE_OPTIONS" :key="size">
<a
@click="(event) => handlePageSizeClick(size)"
:class="{
'bg-gray-100': pageSize === size,
}"
>
{{ size }}
</a>
</li>
</ul>
</div>
</div>
<!-- Page navigation -->
<div class="join text-sm space-x-1">
<button
@click=""
class="join-item btn btn-sm bg-white text-dark border-none shadow-none hover:bg-gray-100"
>
«
</button>
{{ console.log("") }}
<button
@click="handleNextBookmark"
:disabled="bookmark === ''"
:class="
bookmark === '' ? 'opacity-50 cursor-not-allowed' : ''
"
class="join-item btn btn-sm bg-white text-dark border-none shadow-none hover:bg-gray-100"
>
»
</button>
</div>
</div>
</div>
<PaginationControls
v-if="!api.isLoading.value && logs.length > 0"
:page="pagination.page"
:page-size="pagination.pageSize"
:total-count="pagination.totalCount"
:start-index="pagination.startIndex"
:end-index="pagination.endIndex"
:can-go-next="pagination.canGoNext"
:can-go-previous="pagination.canGoPrevious"
:page-size-options="[...ITEMS_PER_PAGE_OPTIONS]"
:get-page-numbers="pagination.getPageNumbers"
@page-change="pagination.goToPage"
@page-size-change="handlePageSizeChange"
@next="pagination.nextPage"
@previous="pagination.previousPage"
/>
</div>
</Sidebar>
</div>

View File

@ -12,7 +12,6 @@ import { LOG_TABLE_COLUMNS } from "../../constants/pagination";
const data = ref<Obat>();
const dataLog = ref<BlockchainLog[]>([]);
const dataLogRaw = ref<any[]>([]);
const currentHash = ref<string>("");
const route = useRoute();
const api = useApi();
@ -31,49 +30,63 @@ const fetchLogData = async () => {
const result = await api.get<{
currentDataHash: string;
data: Record<string, any>;
isTampered: boolean;
}>(`/obat/${route.params.id}/log`);
currentHash.value = result.currentDataHash || "";
if ("data" in result && Array.isArray(result.data)) {
dataLogRaw.value = result.data;
} else {
const apiResponse = result as any;
const dataArray: any[] = [];
Object.keys(apiResponse).forEach((key) => {
if (key !== "totalCount") {
const item = apiResponse[Number(key)];
if (item) {
dataArray.push(item);
}
}
});
const flattenedData = dataArray.map((item) => {
const date = new Date(item.value.timestamp);
const options: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Jakarta", // This sets the time to WIB
day: "numeric",
month: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false, // Use 24-hour format
};
const indonesianTime = date.toLocaleString("id-ID", options);
const formattedTime = indonesianTime.replace(/\./g, ":");
return {
id: item.value.id,
event: item.value.event,
hash: item.value.payload,
userId: item.value.user_id,
timestamp: formattedTime,
txId: item.txId,
status:
item.value.payload === currentHash.value ? "Valid" : "Changed",
};
});
dataLog.value = flattenedData;
}
currentHash.value = result.currentDataHash || "";
const apiResponse = result as any;
const dataArray: any[] = apiResponse.logs || [];
const flattenedData = dataArray.map((item, index) => {
const date = new Date(item.timestamp);
const options: Intl.DateTimeFormatOptions = {
timeZone: "Asia/Jakarta",
day: "numeric",
month: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
};
const indonesianTime = date.toLocaleString("id-ID", options);
const formattedTime = indonesianTime.replace(/\./g, ":");
const statusLabel = (() => {
if (item.status === "ORIGINAL") {
return "ORIGINAL DATA";
}
if (index === 0) {
if (result.isTampered) {
return `TAMPERED after ${item.status}`;
}
if (dataArray[dataArray.length - 1].payload === currentHash.value) {
return "DATA SAME WITH ORIGINAL";
}
return `VALID ${item.status}`;
}
if (index === dataArray.length - 1) {
return item.status;
}
return `OLD ${item.status}`;
})();
return {
id: item.id,
event: item.event,
hash: item.payload,
userId: item.user_id,
timestamp: formattedTime,
txId: item.txId,
status: statusLabel,
};
});
dataLog.value = flattenedData;
} catch (error) {
dataLog.value = [];
}