feat: done filter and search audit, done audit trail function, done webservice for proof
This commit is contained in:
parent
9f6d861ea0
commit
bd7f93e826
28
backend/api/backfill-state.json
Normal file
28
backend/api/backfill-state.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"cursors": {
|
||||||
|
"pemberian_obat": "5",
|
||||||
|
"rekam_medis": "100079",
|
||||||
|
"pemberian_tindakan": "5"
|
||||||
|
},
|
||||||
|
"failures": {},
|
||||||
|
"metadata": {
|
||||||
|
"pemberian_obat": {
|
||||||
|
"lastRunAt": "2025-11-14T03:35:57.788Z",
|
||||||
|
"processed": 5,
|
||||||
|
"success": 5,
|
||||||
|
"failed": 0
|
||||||
|
},
|
||||||
|
"rekam_medis": {
|
||||||
|
"lastRunAt": "2025-11-14T03:35:57.788Z",
|
||||||
|
"processed": 5,
|
||||||
|
"success": 5,
|
||||||
|
"failed": 0
|
||||||
|
},
|
||||||
|
"pemberian_tindakan": {
|
||||||
|
"lastRunAt": "2025-11-14T03:35:57.788Z",
|
||||||
|
"processed": 5,
|
||||||
|
"success": 5,
|
||||||
|
"failed": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
389
backend/api/package-lock.json
generated
389
backend/api/package-lock.json
generated
|
|
@ -14,17 +14,22 @@
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/jwt": "^11.0.1",
|
"@nestjs/jwt": "^11.0.1",
|
||||||
"@nestjs/mapped-types": "^2.1.0",
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "^11.1.8",
|
||||||
|
"@nestjs/websockets": "^11.1.8",
|
||||||
"@prisma/client": "^6.17.1",
|
"@prisma/client": "^6.17.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"snarkjs": "^0.7.5"
|
"snarkjs": "^0.7.5",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|
@ -33,6 +38,7 @@
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/cookie-parser": "^1.4.9",
|
"@types/cookie-parser": "^1.4.9",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
|
@ -2487,6 +2493,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/event-emitter": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter2": "6.4.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0 || ^11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/jwt": {
|
"node_modules/@nestjs/jwt": {
|
||||||
"version": "11.0.1",
|
"version": "11.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz",
|
||||||
|
|
@ -2541,6 +2560,25 @@
|
||||||
"@nestjs/core": "^11.0.0"
|
"@nestjs/core": "^11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/platform-socket.io": {
|
||||||
|
"version": "11.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.8.tgz",
|
||||||
|
"integrity": "sha512-nMUvwcdztso8BjN9czRl4sm0Ewc5xrCcgLvy+QPt6VAnTdu06KcZqtA6Cl3MKxViSQsQ8NBN5foKvZehNt/tug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"socket.io": "4.8.1",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/nest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^11.0.0",
|
||||||
|
"@nestjs/websockets": "^11.0.0",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/schematics": {
|
"node_modules/@nestjs/schematics": {
|
||||||
"version": "11.0.9",
|
"version": "11.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz",
|
||||||
|
|
@ -2667,6 +2705,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/websockets": {
|
||||||
|
"version": "11.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.8.tgz",
|
||||||
|
"integrity": "sha512-RXo2336p/vyAwJ0qPInglzNSQ//qz+JTLr2LE1vlbmN5WcyB7zV6+gY06YgNdsr3oy/cXRh7fnC3Ph/VAu1EVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"object-hash": "3.0.0",
|
||||||
|
"tslib": "2.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^11.0.0",
|
||||||
|
"@nestjs/core": "^11.0.0",
|
||||||
|
"@nestjs/platform-socket.io": "^11.0.0",
|
||||||
|
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@nestjs/platform-socket.io": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/curves": {
|
"node_modules/@noble/curves": {
|
||||||
"version": "1.9.7",
|
"version": "1.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||||
|
|
@ -2958,6 +3019,12 @@
|
||||||
"@sinonjs/commons": "^3.0.1"
|
"@sinonjs/commons": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
|
@ -3104,6 +3171,13 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/cookie-parser": {
|
"node_modules/@types/cookie-parser": {
|
||||||
"version": "1.4.9",
|
"version": "1.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
|
||||||
|
|
@ -3121,6 +3195,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cors": {
|
||||||
|
"version": "2.8.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
|
@ -4492,6 +4575,15 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.18",
|
"version": "2.8.18",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz",
|
||||||
|
|
@ -5235,12 +5327,12 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie-parser": {
|
"node_modules/cookie-parser": {
|
||||||
|
|
@ -5256,6 +5348,15 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie-parser/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
|
@ -5616,6 +5717,104 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
|
|
@ -6034,6 +6233,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter2": {
|
||||||
|
"version": "6.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
|
||||||
|
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/events": {
|
"node_modules/events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
|
@ -6145,6 +6350,15 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
||||||
|
|
@ -9069,6 +9283,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
@ -10190,6 +10413,141 @@
|
||||||
"snarkjs": "build/cli.cjs"
|
"snarkjs": "build/cli.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.3.4",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||||
|
|
@ -11712,6 +12070,27 @@
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,22 @@
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
"@nestjs/jwt": "^11.0.1",
|
"@nestjs/jwt": "^11.0.1",
|
||||||
"@nestjs/mapped-types": "^2.1.0",
|
"@nestjs/mapped-types": "^2.1.0",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "^11.1.8",
|
||||||
|
"@nestjs/websockets": "^11.1.8",
|
||||||
"@prisma/client": "^6.17.1",
|
"@prisma/client": "^6.17.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"snarkjs": "^0.7.5"
|
"snarkjs": "^0.7.5",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|
@ -44,6 +49,7 @@
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/cookie-parser": "^1.4.9",
|
"@types/cookie-parser": "^1.4.9",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { FabricModule } from './modules/fabric/fabric.module';
|
import { FabricModule } from './modules/fabric/fabric.module';
|
||||||
import { AuditModule } from './modules/audit/audit.module';
|
import { AuditModule } from './modules/audit/audit.module';
|
||||||
import { ProofModule } from './modules/proof/proof.module';
|
import { ProofModule } from './modules/proof/proof.module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -28,6 +29,7 @@ import { ProofModule } from './modules/proof/proof.module';
|
||||||
FabricModule,
|
FabricModule,
|
||||||
AuditModule,
|
AuditModule,
|
||||||
ProofModule,
|
ProofModule,
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ class FabricGateway {
|
||||||
`Evaluating getLogWithPagination transaction with pageSize: ${pageSize}, bookmark: ${bookmark}...`,
|
`Evaluating getLogWithPagination transaction with pageSize: ${pageSize}, bookmark: ${bookmark}...`,
|
||||||
);
|
);
|
||||||
const resultBytes = await this.contract.evaluateTransaction(
|
const resultBytes = await this.contract.evaluateTransaction(
|
||||||
'getLogsWithPagination',
|
'getLogWithPagination',
|
||||||
pageSize.toString(),
|
pageSize.toString(),
|
||||||
bookmark,
|
bookmark,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ async function bootstrap() {
|
||||||
const configService = app.get(ConfigService);
|
const configService = app.get(ConfigService);
|
||||||
app.setGlobalPrefix('api/');
|
app.setGlobalPrefix('api/');
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: 'http://localhost:5173',
|
origin: 'https://64spbch3-5173.asse.devtunnels.ms',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
allowedHeaders: 'Content-Type, Accept, X-CSRF-Token',
|
allowedHeaders: 'Content-Type, Accept, X-CSRF-Token',
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,63 @@
|
||||||
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { AuthGuard } from '../auth/guard/auth.guard';
|
import { AuthGuard } from '../auth/guard/auth.guard';
|
||||||
import { AuditService } from './audit.service';
|
import { AuditService } from './audit.service';
|
||||||
|
import { fromEvent, interval, map, Observable } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
@Controller('audit')
|
@Controller('audit')
|
||||||
export class AuditController {
|
export class AuditController {
|
||||||
constructor(private readonly auditService: AuditService) {}
|
constructor(
|
||||||
|
private readonly auditService: AuditService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('/trail')
|
@Get('/trail')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async getAuditTrail(
|
async getAuditTrail(
|
||||||
|
@Query('search') search: string,
|
||||||
|
@Query('page') page: number,
|
||||||
@Query('pageSize') pageSize: number,
|
@Query('pageSize') pageSize: number,
|
||||||
@Query('bookmark') bookmark: string,
|
@Query('type') type: string,
|
||||||
|
@Query('tampered') tampered: string,
|
||||||
) {
|
) {
|
||||||
const result = await this.auditService.getAuditTrails(pageSize, bookmark);
|
const result = await this.auditService.getAuditTrails(
|
||||||
|
search,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
type,
|
||||||
|
tampered,
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/trail')
|
@Post('trail')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async createAuditTrail() {
|
createAuditTrail() {
|
||||||
const result = await this.auditService.storeAuditTrail();
|
this.auditService.storeAuditTrail();
|
||||||
return result;
|
return { message: 'Proses audit trail dijalankan', status: 'STARTED' };
|
||||||
|
// return interval(1000).pipe(
|
||||||
|
// map(
|
||||||
|
// (_) =>
|
||||||
|
// ({ data: { message: 'Audit trail in progress...' } }) as MessageEvent,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('stream')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
auditStream(): Observable<MessageEvent> {
|
||||||
|
return fromEvent(this.eventEmitter, 'audit.*').pipe(
|
||||||
|
map((data: any) => {
|
||||||
|
return new MessageEvent('message', { data: data });
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
backend/api/src/modules/audit/audit.gateway.spec.ts
Normal file
18
backend/api/src/modules/audit/audit.gateway.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuditGateway } from './audit.gateway';
|
||||||
|
|
||||||
|
describe('AuditGateway', () => {
|
||||||
|
let gateway: AuditGateway;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [AuditGateway],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
gateway = module.get<AuditGateway>(AuditGateway);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(gateway).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
38
backend/api/src/modules/audit/audit.gateway.ts
Normal file
38
backend/api/src/modules/audit/audit.gateway.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||||
|
import { UseGuards, Logger } from '@nestjs/common';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { WebsocketGuard } from '../auth/guard/websocket.guard';
|
||||||
|
|
||||||
|
@WebSocketGateway({
|
||||||
|
cors: {
|
||||||
|
origin: 'https://64spbch3-5173.asse.devtunnels.ms',
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class AuditGateway {
|
||||||
|
@WebSocketServer()
|
||||||
|
server: Server;
|
||||||
|
|
||||||
|
private logger = new Logger('AuditGateway');
|
||||||
|
|
||||||
|
@UseGuards(WebsocketGuard)
|
||||||
|
handleConnection(client: Socket) {
|
||||||
|
this.logger.log(`Klien terhubung: ${client.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client: Socket) {
|
||||||
|
this.logger.log(`Klien terputus: ${client.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendProgress(progressData: any) {
|
||||||
|
this.server.emit('audit.progress', progressData);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendComplete(completeData: any) {
|
||||||
|
this.server.emit('audit.complete', completeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendError(errorData: any) {
|
||||||
|
this.server.emit('audit.error', errorData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ import { ObatModule } from '../obat/obat.module';
|
||||||
import { RekamMedisModule } from '../rekammedis/rekammedis.module';
|
import { RekamMedisModule } from '../rekammedis/rekammedis.module';
|
||||||
import { TindakanDokterModule } from '../tindakandokter/tindakandokter.module';
|
import { TindakanDokterModule } from '../tindakandokter/tindakandokter.module';
|
||||||
import { LogModule } from '../log/log.module';
|
import { LogModule } from '../log/log.module';
|
||||||
|
import { AuditGateway } from './audit.gateway';
|
||||||
|
import { WebsocketGuard } from '../auth/guard/websocket.guard';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -17,7 +19,7 @@ import { LogModule } from '../log/log.module';
|
||||||
RekamMedisModule,
|
RekamMedisModule,
|
||||||
TindakanDokterModule,
|
TindakanDokterModule,
|
||||||
],
|
],
|
||||||
providers: [AuditService],
|
providers: [AuditService, AuditGateway, WebsocketGuard],
|
||||||
controllers: [AuditController],
|
controllers: [AuditController],
|
||||||
})
|
})
|
||||||
export class AuditModule {}
|
export class AuditModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { LogService } from '../log/log.service';
|
import { LogService } from '../log/log.service';
|
||||||
import { ObatService } from '../obat/obat.service';
|
import { ObatService } from '../obat/obat.service';
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
AuditEvent,
|
AuditEvent,
|
||||||
resultStatus as ResultStatus,
|
resultStatus as ResultStatus,
|
||||||
} from '@dist/generated/prisma';
|
} from '@dist/generated/prisma';
|
||||||
|
import { AuditGateway } from './audit.gateway';
|
||||||
|
|
||||||
type AuditRecordPayload = {
|
type AuditRecordPayload = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -22,23 +23,62 @@ type AuditRecordPayload = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuditService {
|
export class AuditService {
|
||||||
|
private readonly logger = new Logger(AuditService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly logService: LogService,
|
private readonly logService: LogService,
|
||||||
private readonly obatService: ObatService,
|
private readonly obatService: ObatService,
|
||||||
private readonly rekamMedisService: RekammedisService,
|
private readonly rekamMedisService: RekammedisService,
|
||||||
private readonly tindakanDokterService: TindakanDokterService,
|
private readonly tindakanDokterService: TindakanDokterService,
|
||||||
|
private auditGateway: AuditGateway,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getAuditTrails(pageSize: number, bookmark: string) {
|
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({
|
const auditLogs = await this.prisma.audit.findMany({
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip: bookmark ? 1 : 0,
|
skip: (page - 1) * pageSize,
|
||||||
cursor: bookmark ? { id: bookmark } : undefined,
|
|
||||||
orderBy: { timestamp: 'asc' },
|
orderBy: { timestamp: 'asc' },
|
||||||
|
where: {
|
||||||
|
id: type && type !== 'all' ? { startsWith: type } : undefined,
|
||||||
|
result: tampered ? (tampered as ResultStatus) : undefined,
|
||||||
|
OR: search ? [{ id: { contains: search } }] : undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return auditLogs;
|
console.log(auditLogs);
|
||||||
|
|
||||||
|
const count = await this.prisma.audit.count({
|
||||||
|
where: {
|
||||||
|
id: type && type !== 'all' ? { startsWith: type } : undefined,
|
||||||
|
result: tampered ? (tampered as ResultStatus) : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...auditLogs,
|
||||||
|
totalCount: count,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeAuditTrail() {
|
async storeAuditTrail() {
|
||||||
|
|
@ -47,6 +87,30 @@ export class AuditService {
|
||||||
let bookmark = '';
|
let bookmark = '';
|
||||||
let processedCount = 0;
|
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 {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const pageResults = await this.logService.getLogsWithPagination(
|
const pageResults = await this.logService.getLogsWithPagination(
|
||||||
|
|
@ -66,11 +130,23 @@ export class AuditService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = (
|
// const records = (
|
||||||
await Promise.all(
|
// await Promise.all(
|
||||||
logs.map((logEntry) => this.buildAuditRecord(logEntry)),
|
// logs.map((logEntry, index) =>
|
||||||
)
|
// this.buildAuditRecord(logEntry, index),
|
||||||
).filter((record): record is AuditRecordPayload => record !== null);
|
// ),
|
||||||
|
// )
|
||||||
|
// ).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) {
|
if (records.length > 0) {
|
||||||
await this.prisma.$transaction(
|
await this.prisma.$transaction(
|
||||||
|
|
@ -93,6 +169,9 @@ export class AuditService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextBookmark === '' || nextBookmark === bookmark) {
|
if (nextBookmark === '' || nextBookmark === bookmark) {
|
||||||
|
const completeData = { status: 'COMPLETED' };
|
||||||
|
this.logger.log('Mengirim selesai via WebSocket:', completeData);
|
||||||
|
this.auditGateway.sendComplete(completeData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +185,7 @@ export class AuditService {
|
||||||
|
|
||||||
private async buildAuditRecord(
|
private async buildAuditRecord(
|
||||||
logEntry: any,
|
logEntry: any,
|
||||||
|
index?: number,
|
||||||
): Promise<AuditRecordPayload | null> {
|
): Promise<AuditRecordPayload | null> {
|
||||||
if (!logEntry?.value) {
|
if (!logEntry?.value) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -128,7 +208,7 @@ export class AuditService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dbHash: string | null = null;
|
let dbHash: string | null = null;
|
||||||
|
//
|
||||||
try {
|
try {
|
||||||
if (logId.startsWith('OBAT_')) {
|
if (logId.startsWith('OBAT_')) {
|
||||||
const obatId = this.extractNumericId(logId);
|
const obatId = this.extractNumericId(logId);
|
||||||
|
|
@ -184,6 +264,14 @@ export class AuditService {
|
||||||
|
|
||||||
const result: ResultStatus = isNotTampered ? 'non_tampered' : 'tampered';
|
const result: ResultStatus = isNotTampered ? 'non_tampered' : 'tampered';
|
||||||
|
|
||||||
|
const progressData = {
|
||||||
|
status: 'RUNNING',
|
||||||
|
progress_count: index ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log('Mengirim progres via WebSocket:', progressData);
|
||||||
|
this.auditGateway.sendProgress(progressData);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: logId,
|
id: logId,
|
||||||
event: value.event as AuditEvent,
|
event: value.event as AuditEvent,
|
||||||
|
|
|
||||||
11
backend/api/src/modules/auth/guard/websocket.guard.spec.ts
Normal file
11
backend/api/src/modules/auth/guard/websocket.guard.spec.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { WebsocketGuard } from './websocket.guard';
|
||||||
|
|
||||||
|
describe('WebsocketGuard', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(
|
||||||
|
new WebsocketGuard(new JwtService(), new ConfigService()),
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
60
backend/api/src/modules/auth/guard/websocket.guard.ts
Normal file
60
backend/api/src/modules/auth/guard/websocket.guard.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { Socket } from 'socket.io';
|
||||||
|
import * as cookie from 'cookie';
|
||||||
|
|
||||||
|
interface AuthPayload {
|
||||||
|
sub: number;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocketWithAuth extends Socket {
|
||||||
|
user: AuthPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebsocketGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private jwtService: JwtService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const client: Socket = context.switchToWs().getClient<Socket>();
|
||||||
|
|
||||||
|
const token = this.extractTokenFromCookie(client.handshake.headers.cookie);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
client.disconnect();
|
||||||
|
throw new UnauthorizedException('No token provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await this.jwtService.verifyAsync(token, {
|
||||||
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.data.user = payload as AuthPayload;
|
||||||
|
} catch {
|
||||||
|
client.disconnect();
|
||||||
|
throw new UnauthorizedException('Invalid token');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTokenFromCookie(
|
||||||
|
cookieHeader: string | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
if (!cookieHeader) return undefined;
|
||||||
|
|
||||||
|
const cookies = cookie.parse(cookieHeader);
|
||||||
|
return cookies.access_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ export class StoreLogDto {
|
||||||
@IsEnum(
|
@IsEnum(
|
||||||
[
|
[
|
||||||
'tindakan_dokter_created',
|
'tindakan_dokter_created',
|
||||||
'obat_given',
|
'obat_created',
|
||||||
'rekam_medis_created',
|
'rekam_medis_created',
|
||||||
'tindakan_dokter_updated',
|
'tindakan_dokter_updated',
|
||||||
'obat_updated',
|
'obat_updated',
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
import { Controller, Post, UseGuards } from '@nestjs/common';
|
||||||
import { StoreLogDto } from './dto/store-log.dto';
|
|
||||||
import { LogService } from './log.service';
|
import { LogService } from './log.service';
|
||||||
import { FabricService } from '../fabric/fabric.service';
|
import { AuthGuard } from '../auth/guard/auth.guard';
|
||||||
|
|
||||||
@Controller('log')
|
@Controller('log')
|
||||||
export class LogController {
|
export class LogController {
|
||||||
constructor(
|
constructor(private readonly logService: LogService) {}
|
||||||
private readonly logService: LogService,
|
|
||||||
private readonly fabricService: FabricService,
|
@Post('/store-to-blockchain')
|
||||||
) {}
|
@UseGuards(AuthGuard)
|
||||||
|
async storeLog() {
|
||||||
|
return this.logService.storeFromDBToBlockchain();
|
||||||
|
}
|
||||||
|
|
||||||
// @Post()
|
// @Post()
|
||||||
// storeLog(@Body() dto: StoreLogDto) {
|
// storeLog(@Body() dto: StoreLogDto) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||||
import { LogController } from './log.controller';
|
import { LogController } from './log.controller';
|
||||||
import { LogService } from './log.service';
|
import { LogService } from './log.service';
|
||||||
import { FabricModule } from '../fabric/fabric.module';
|
import { FabricModule } from '../fabric/fabric.module';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FabricModule],
|
imports: [FabricModule, PrismaModule],
|
||||||
controllers: [LogController],
|
controllers: [LogController],
|
||||||
providers: [LogService],
|
providers: [LogService],
|
||||||
exports: [LogService],
|
exports: [LogService],
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { LogService } from './log.service';
|
import { LogService } from './log.service';
|
||||||
|
import { FabricService } from '../fabric/fabric.service';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
describe('LogService', () => {
|
describe('LogService', () => {
|
||||||
let service: LogService;
|
let service: LogService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
const fabricServiceMock = {
|
||||||
|
storeLog: jest.fn(),
|
||||||
|
getLogById: jest.fn(),
|
||||||
|
getLogsWithPagination: jest.fn(),
|
||||||
|
} as unknown as FabricService;
|
||||||
|
|
||||||
|
const prismaServiceMock = {
|
||||||
|
pemberian_obat: { findMany: jest.fn() },
|
||||||
|
rekam_medis: { findMany: jest.fn() },
|
||||||
|
pemberian_tindakan: { findMany: jest.fn() },
|
||||||
|
} as unknown as PrismaService;
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [LogService],
|
providers: [
|
||||||
|
LogService,
|
||||||
|
{ provide: FabricService, useValue: fabricServiceMock },
|
||||||
|
{ provide: PrismaService, useValue: prismaServiceMock },
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<LogService>(LogService);
|
service = module.get<LogService>(LogService);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,389 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { StoreLogDto } from './dto/store-log.dto';
|
import { promises as fs } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { sha256 } from '@api/common/crypto/hash';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { FabricService } from '../fabric/fabric.service';
|
import { FabricService } from '../fabric/fabric.service';
|
||||||
|
import { StoreLogDto } from './dto/store-log.dto';
|
||||||
|
import type {
|
||||||
|
pemberian_obat as PemberianObat,
|
||||||
|
pemberian_tindakan as PemberianTindakan,
|
||||||
|
rekam_medis as RekamMedis,
|
||||||
|
} from '@dist/generated/prisma';
|
||||||
|
|
||||||
|
export interface BackfillFailure {
|
||||||
|
entity: EntityKey;
|
||||||
|
id: string;
|
||||||
|
reason: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackfillState {
|
||||||
|
cursors: Partial<Record<EntityKey, string>>;
|
||||||
|
failures: Record<string, BackfillFailure>;
|
||||||
|
metadata?: Partial<
|
||||||
|
Record<
|
||||||
|
EntityKey,
|
||||||
|
{
|
||||||
|
lastRunAt: string;
|
||||||
|
processed: number;
|
||||||
|
success: number;
|
||||||
|
failed: number;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackfillSummary {
|
||||||
|
processed: number;
|
||||||
|
success: number;
|
||||||
|
failed: number;
|
||||||
|
lastCursor: string | null;
|
||||||
|
failures: BackfillFailure[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityKey = 'pemberian_obat' | 'rekam_medis' | 'pemberian_tindakan';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogService {
|
export class LogService {
|
||||||
constructor(private readonly fabricService: FabricService) {}
|
private readonly logger = new Logger(LogService.name);
|
||||||
|
private readonly statePath = path.resolve(
|
||||||
|
process.cwd(),
|
||||||
|
'backfill-state.json',
|
||||||
|
);
|
||||||
|
private readonly backfillUserId = process.env.BACKFILL_USER_ID ?? '9';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly fabricService: FabricService,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async storeLog(dto: StoreLogDto) {
|
async storeLog(dto: StoreLogDto) {
|
||||||
const { id, event, user_id, payload } = dto;
|
const { id, event, user_id, payload } = dto;
|
||||||
return this.fabricService.storeLog(id, event, user_id.toString(), payload);
|
return this.fabricService.storeLog(id, event, user_id.toString(), payload);
|
||||||
}
|
}
|
||||||
async getLogById(id: string) {
|
|
||||||
const result = await this.fabricService.getLogById(id);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
async getAllLogs() {
|
|
||||||
return this.fabricService.getAllLogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
async countLogs() {
|
async getLogById(id: string) {
|
||||||
const countLogs = await this.fabricService.getAllLogs();
|
return this.fabricService.getLogById(id);
|
||||||
return countLogs.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogsWithPagination(pageSize: number, bookmark: string) {
|
async getLogsWithPagination(pageSize: number, bookmark: string) {
|
||||||
return this.fabricService.getLogsWithPagination(pageSize, bookmark);
|
return this.fabricService.getLogsWithPagination(pageSize, bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async storeFromDBToBlockchain() {}
|
||||||
|
|
||||||
|
// async storeFromDBToBlockchain(
|
||||||
|
// limitPerEntity = 5,
|
||||||
|
// batchSize = 1,
|
||||||
|
// ): Promise<{
|
||||||
|
// summaries: Record<string, BackfillSummary>;
|
||||||
|
// checkpointFile: string;
|
||||||
|
// }> {
|
||||||
|
// const state = await this.loadState();
|
||||||
|
|
||||||
|
// const summaries = {
|
||||||
|
// pemberian_obat: await this.syncPemberianObat(
|
||||||
|
// state,
|
||||||
|
// limitPerEntity,
|
||||||
|
// batchSize,
|
||||||
|
// ),
|
||||||
|
// rekam_medis: await this.syncRekamMedis(state, limitPerEntity, batchSize),
|
||||||
|
// pemberian_tindakan: await this.syncPemberianTindakan(
|
||||||
|
// state,
|
||||||
|
// limitPerEntity,
|
||||||
|
// batchSize,
|
||||||
|
// ),
|
||||||
|
// } as Record<EntityKey, BackfillSummary>;
|
||||||
|
|
||||||
|
// const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// await this.persistState({
|
||||||
|
// ...state,
|
||||||
|
// metadata: {
|
||||||
|
// ...(state.metadata ?? {}),
|
||||||
|
// pemberian_obat: {
|
||||||
|
// lastRunAt: timestamp,
|
||||||
|
// processed: summaries.pemberian_obat.processed,
|
||||||
|
// success: summaries.pemberian_obat.success,
|
||||||
|
// failed: summaries.pemberian_obat.failed,
|
||||||
|
// },
|
||||||
|
// rekam_medis: {
|
||||||
|
// lastRunAt: timestamp,
|
||||||
|
// processed: summaries.rekam_medis.processed,
|
||||||
|
// success: summaries.rekam_medis.success,
|
||||||
|
// failed: summaries.rekam_medis.failed,
|
||||||
|
// },
|
||||||
|
// pemberian_tindakan: {
|
||||||
|
// lastRunAt: timestamp,
|
||||||
|
// processed: summaries.pemberian_tindakan.processed,
|
||||||
|
// success: summaries.pemberian_tindakan.success,
|
||||||
|
// failed: summaries.pemberian_tindakan.failed,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// summaries,
|
||||||
|
// checkpointFile: this.statePath,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
private async syncPemberianObat(
|
||||||
|
state: BackfillState,
|
||||||
|
limit: number,
|
||||||
|
batchSize: number,
|
||||||
|
): Promise<BackfillSummary> {
|
||||||
|
return this.syncEntity<PemberianObat>(
|
||||||
|
state,
|
||||||
|
'pemberian_obat',
|
||||||
|
limit,
|
||||||
|
batchSize,
|
||||||
|
async (cursor, take) => {
|
||||||
|
const query: any = {
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
take,
|
||||||
|
};
|
||||||
|
if (cursor) {
|
||||||
|
query.cursor = { id: Number(cursor) };
|
||||||
|
query.skip = 1;
|
||||||
|
}
|
||||||
|
return this.prisma.pemberian_obat.findMany(query);
|
||||||
|
},
|
||||||
|
async (record) => {
|
||||||
|
const payload = {
|
||||||
|
obat: record.obat,
|
||||||
|
jumlah_obat: record.jumlah_obat,
|
||||||
|
aturan_pakai: record.aturan_pakai,
|
||||||
|
};
|
||||||
|
const payloadHash = sha256(JSON.stringify(payload));
|
||||||
|
await this.fabricService.storeLog(
|
||||||
|
`OBAT_${record.id}`,
|
||||||
|
'obat_created',
|
||||||
|
this.backfillUserId,
|
||||||
|
payloadHash,
|
||||||
|
);
|
||||||
|
return `${record.id}`;
|
||||||
|
},
|
||||||
|
(record) => `${record.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncRekamMedis(
|
||||||
|
state: BackfillState,
|
||||||
|
limit: number,
|
||||||
|
batchSize: number,
|
||||||
|
): Promise<BackfillSummary> {
|
||||||
|
return this.syncEntity<RekamMedis>(
|
||||||
|
state,
|
||||||
|
'rekam_medis',
|
||||||
|
limit,
|
||||||
|
batchSize,
|
||||||
|
async (cursor, take) => {
|
||||||
|
const query: any = {
|
||||||
|
orderBy: { id_visit: 'asc' },
|
||||||
|
take,
|
||||||
|
};
|
||||||
|
if (cursor) {
|
||||||
|
query.cursor = { id_visit: cursor };
|
||||||
|
query.skip = 1;
|
||||||
|
}
|
||||||
|
return this.prisma.rekam_medis.findMany(query);
|
||||||
|
},
|
||||||
|
async (record) => {
|
||||||
|
const payload = {
|
||||||
|
dokter_id: 123,
|
||||||
|
visit_id: record.id_visit,
|
||||||
|
anamnese: record.anamnese ?? '',
|
||||||
|
jenis_kasus: record.jenis_kasus ?? '',
|
||||||
|
tindak_lanjut: record.tindak_lanjut ?? '',
|
||||||
|
};
|
||||||
|
const payloadHash = sha256(JSON.stringify(payload));
|
||||||
|
await this.fabricService.storeLog(
|
||||||
|
`REKAM_${record.id_visit}`,
|
||||||
|
'rekam_medis_created',
|
||||||
|
this.backfillUserId,
|
||||||
|
payloadHash,
|
||||||
|
);
|
||||||
|
return record.id_visit;
|
||||||
|
},
|
||||||
|
(record) => record.id_visit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncPemberianTindakan(
|
||||||
|
state: BackfillState,
|
||||||
|
limit: number,
|
||||||
|
batchSize: number,
|
||||||
|
): Promise<BackfillSummary> {
|
||||||
|
return this.syncEntity<PemberianTindakan>(
|
||||||
|
state,
|
||||||
|
'pemberian_tindakan',
|
||||||
|
limit,
|
||||||
|
batchSize,
|
||||||
|
async (cursor, take) => {
|
||||||
|
const query: any = {
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
take,
|
||||||
|
};
|
||||||
|
if (cursor) {
|
||||||
|
query.cursor = { id: Number(cursor) };
|
||||||
|
query.skip = 1;
|
||||||
|
}
|
||||||
|
return this.prisma.pemberian_tindakan.findMany(query);
|
||||||
|
},
|
||||||
|
async (record) => {
|
||||||
|
const payload = {
|
||||||
|
id_visit: record.id_visit,
|
||||||
|
tindakan: record.tindakan,
|
||||||
|
kategori_tindakan: record.kategori_tindakan ?? null,
|
||||||
|
kelompok_tindakan: record.kelompok_tindakan ?? null,
|
||||||
|
};
|
||||||
|
const payloadHash = sha256(JSON.stringify(payload));
|
||||||
|
await this.fabricService.storeLog(
|
||||||
|
`TINDAKAN_${record.id}`,
|
||||||
|
'tindakan_dokter_created',
|
||||||
|
this.backfillUserId,
|
||||||
|
payloadHash,
|
||||||
|
);
|
||||||
|
return `${record.id}`;
|
||||||
|
},
|
||||||
|
(record) => `${record.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncEntity<T>(
|
||||||
|
state: BackfillState,
|
||||||
|
entity: EntityKey,
|
||||||
|
limit: number,
|
||||||
|
batchSize: number,
|
||||||
|
fetchBatch: (cursor: string | null, take: number) => Promise<T[]>,
|
||||||
|
processRecord: (record: T) => Promise<string>,
|
||||||
|
recordIdentifier: (record: T) => string,
|
||||||
|
): Promise<BackfillSummary> {
|
||||||
|
let cursor = state.cursors[entity] ?? null;
|
||||||
|
let processed = 0;
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
while (processed < limit) {
|
||||||
|
const remaining = limit - processed;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const take = Math.min(batchSize, remaining);
|
||||||
|
const records = await fetchBatch(cursor, take);
|
||||||
|
if (!records || records.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
records.map(async (record) => processRecord(record)),
|
||||||
|
);
|
||||||
|
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const id = recordIdentifier(records[index]);
|
||||||
|
const key = this.failureKey(entity, id);
|
||||||
|
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
success += 1;
|
||||||
|
delete state.failures[key];
|
||||||
|
} else {
|
||||||
|
failed += 1;
|
||||||
|
const failure = {
|
||||||
|
entity,
|
||||||
|
id,
|
||||||
|
reason: this.serializeError(result.reason),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
} satisfies BackfillFailure;
|
||||||
|
state.failures[key] = failure;
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to backfill ${entity} ${id}: ${failure.reason}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
processed += records.length;
|
||||||
|
cursor = recordIdentifier(records[records.length - 1]);
|
||||||
|
state.cursors[entity] = cursor;
|
||||||
|
await this.persistState(state);
|
||||||
|
|
||||||
|
if (records.length < take) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processed,
|
||||||
|
success,
|
||||||
|
failed,
|
||||||
|
lastCursor: cursor,
|
||||||
|
failures: this.collectFailures(entity, state),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadState(): Promise<BackfillState> {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(this.statePath, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return {
|
||||||
|
cursors: parsed.cursors ?? {},
|
||||||
|
failures: parsed.failures ?? {},
|
||||||
|
metadata: parsed.metadata ?? {},
|
||||||
|
} satisfies BackfillState;
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as NodeJS.ErrnoException;
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
return {
|
||||||
|
cursors: {},
|
||||||
|
failures: {},
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistState(state: BackfillState) {
|
||||||
|
const serializable = {
|
||||||
|
cursors: state.cursors,
|
||||||
|
failures: state.failures,
|
||||||
|
metadata: state.metadata ?? {},
|
||||||
|
};
|
||||||
|
await fs.mkdir(path.dirname(this.statePath), { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
this.statePath,
|
||||||
|
JSON.stringify(serializable, null, 2),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectFailures(
|
||||||
|
entity: EntityKey,
|
||||||
|
state: BackfillState,
|
||||||
|
): BackfillFailure[] {
|
||||||
|
return Object.values(state.failures).filter(
|
||||||
|
(entry) => entry.entity === entity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private failureKey(entity: EntityKey, id: string) {
|
||||||
|
return `${entity}:${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeError(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(error);
|
||||||
|
} catch {
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
import {
|
import {
|
||||||
ArrayMaxSize,
|
|
||||||
ArrayMinSize,
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDate,
|
|
||||||
isDateString,
|
|
||||||
IsIn,
|
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
|
||||||
IsString,
|
IsString,
|
||||||
Length,
|
Length,
|
||||||
Matches,
|
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
|
||||||
|
|
||||||
export class LogProofDto {
|
export class LogProofDto {
|
||||||
@IsNotEmpty({ message: 'Proof wajib diisi' })
|
@IsNotEmpty({ message: 'Proof wajib diisi' })
|
||||||
@IsObject({ message: 'Proof harus berupa objek' })
|
@IsObject({ message: 'Proof harus berupa objek' })
|
||||||
proofHash: object;
|
proofHash: any;
|
||||||
|
|
||||||
@IsNotEmpty({ message: 'ID Visit wajib diisi' })
|
@IsNotEmpty({ message: 'ID Visit wajib diisi' })
|
||||||
@IsString({ message: 'ID Visit harus berupa string' })
|
@IsString({ message: 'ID Visit harus berupa string' })
|
||||||
|
|
@ -32,5 +22,5 @@ export class LogProofDto {
|
||||||
|
|
||||||
@IsNotEmpty({ message: 'Timestamp wajib diisi' })
|
@IsNotEmpty({ message: 'Timestamp wajib diisi' })
|
||||||
@IsString({ message: 'Timestamp harus berupa string' })
|
@IsString({ message: 'Timestamp harus berupa string' })
|
||||||
timestamp: Date;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
109
frontend/hospital-log/package-lock.json
generated
109
frontend/hospital-log/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"cally": "^0.8.0",
|
"cally": "^0.8.0",
|
||||||
"daisyui": "^5.3.10",
|
"daisyui": "^5.3.10",
|
||||||
"nouislider": "^15.8.1",
|
"nouislider": "^15.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "4",
|
"vue-router": "4",
|
||||||
|
|
@ -158,6 +159,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.16",
|
"version": "4.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
||||||
|
|
@ -795,6 +802,23 @@
|
||||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|
@ -809,6 +833,28 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
|
|
@ -984,6 +1030,12 @@
|
||||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/muggle-string": {
|
"node_modules/muggle-string": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -1129,6 +1181,34 @@
|
||||||
"version": "1.0.0-beta.41",
|
"version": "1.0.0-beta.41",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
|
|
@ -1397,6 +1477,35 @@
|
||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"cally": "^0.8.0",
|
"cally": "^0.8.0",
|
||||||
"daisyui": "^5.3.10",
|
"daisyui": "^5.3.10",
|
||||||
"nouislider": "^15.8.1",
|
"nouislider": "^15.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "4",
|
"vue-router": "4",
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,23 @@ interface BlockchainLog {
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { RekamMedis, Users, TindakanDokter, BlockchainLog, Obat };
|
type AuditLogType = "obat" | "rekam_medis" | "tindakan" | "proof" | "unknown";
|
||||||
|
|
||||||
|
interface AuditLogEntry extends BlockchainLog {
|
||||||
|
type: AuditLogType;
|
||||||
|
typeLabel: string;
|
||||||
|
tamperedLabel: string;
|
||||||
|
last_sync: string;
|
||||||
|
isTampered: boolean;
|
||||||
|
txId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
RekamMedis,
|
||||||
|
Users,
|
||||||
|
TindakanDokter,
|
||||||
|
BlockchainLog,
|
||||||
|
Obat,
|
||||||
|
AuditLogType,
|
||||||
|
AuditLogEntry,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
|
AuditLogEntry,
|
||||||
BlockchainLog,
|
BlockchainLog,
|
||||||
RekamMedis,
|
RekamMedis,
|
||||||
TindakanDokter,
|
TindakanDokter,
|
||||||
|
|
@ -60,6 +61,18 @@ export const FILTER = {
|
||||||
tindakan: "TINDAKAN",
|
tindakan: "TINDAKAN",
|
||||||
laboratorium: "LABORATORIUM",
|
laboratorium: "LABORATORIUM",
|
||||||
},
|
},
|
||||||
|
AUDIT_TYPE: {
|
||||||
|
all: "Semua Tipe",
|
||||||
|
rekam_medis: "Rekam Medis",
|
||||||
|
tindakan: "Tindakan",
|
||||||
|
obat: "Obat",
|
||||||
|
proof: "Proof",
|
||||||
|
},
|
||||||
|
AUDIT_TAMPERED: {
|
||||||
|
all: "Semua Data",
|
||||||
|
tampered: "Termanipulasi",
|
||||||
|
clean: "Tidak Termanipulasi",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SORT_OPTIONS = {
|
export const SORT_OPTIONS = {
|
||||||
|
|
@ -227,3 +240,16 @@ export const LOG_TABLE_COLUMNS = [
|
||||||
class: "text-dark",
|
class: "text-dark",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const AUDIT_TABLE_COLUMNS = [
|
||||||
|
{ key: "id", label: "ID Log", class: "text-dark" },
|
||||||
|
{ key: "typeLabel", label: "Tipe Data", class: "text-dark" },
|
||||||
|
{ key: "event", label: "Event", class: "text-dark" },
|
||||||
|
{ key: "last_sync", label: "Last Sync", class: "text-dark" },
|
||||||
|
{ key: "userId", label: "User ID", class: "text-dark" },
|
||||||
|
{ key: "status", label: "Status Data", class: "text-dark" },
|
||||||
|
] satisfies Array<{
|
||||||
|
key: keyof AuditLogEntry;
|
||||||
|
label: string;
|
||||||
|
class?: string;
|
||||||
|
}>;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from "vue";
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import Sidebar from "../../components/dashboard/Sidebar.vue";
|
import Sidebar from "../../components/dashboard/Sidebar.vue";
|
||||||
import Footer from "../../components/dashboard/Footer.vue";
|
import Footer from "../../components/dashboard/Footer.vue";
|
||||||
|
|
@ -13,21 +13,13 @@ import {
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEBOUNCE_DELAY,
|
DEBOUNCE_DELAY,
|
||||||
ITEMS_PER_PAGE_OPTIONS,
|
ITEMS_PER_PAGE_OPTIONS,
|
||||||
|
AUDIT_TABLE_COLUMNS,
|
||||||
} from "../../constants/pagination";
|
} from "../../constants/pagination";
|
||||||
import type { BlockchainLog } from "../../constants/interfaces";
|
|
||||||
import ButtonDark from "../../components/dashboard/ButtonDark.vue";
|
import ButtonDark from "../../components/dashboard/ButtonDark.vue";
|
||||||
import DialogConfirm from "../../components/DialogConfirm.vue";
|
import DialogConfirm from "../../components/DialogConfirm.vue";
|
||||||
import PaginationControls from "../../components/dashboard/PaginationControls.vue";
|
import PaginationControls from "../../components/dashboard/PaginationControls.vue";
|
||||||
|
import type { AuditLogEntry, AuditLogType } from "../../constants/interfaces";
|
||||||
type AuditLogType = "obat" | "rekam_medis" | "tindakan" | "unknown";
|
import { io, Socket } from "socket.io-client";
|
||||||
|
|
||||||
interface AuditLogEntry extends BlockchainLog {
|
|
||||||
type: AuditLogType;
|
|
||||||
typeLabel: string;
|
|
||||||
tamperedLabel: string;
|
|
||||||
isTampered: boolean;
|
|
||||||
txId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuditLogResponse {
|
interface AuditLogResponse {
|
||||||
data: AuditLogEntry[];
|
data: AuditLogEntry[];
|
||||||
|
|
@ -48,36 +40,10 @@ const pageSize = ref<number>(Number(route.query.take) || DEFAULT_PAGE_SIZE);
|
||||||
const logs = ref<AuditLogEntry[]>([]);
|
const logs = ref<AuditLogEntry[]>([]);
|
||||||
const searchId = ref("");
|
const searchId = ref("");
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
type: (route.query.type as string) || "all",
|
type: (route.query.type as string) || "initial",
|
||||||
tampered: (route.query.tampered as string) || "all",
|
tampered: (route.query.tampered as string) || "initial",
|
||||||
});
|
});
|
||||||
|
|
||||||
const TYPE_OPTIONS = [
|
|
||||||
{ value: "all", label: "Semua Tipe" },
|
|
||||||
{ value: "rekam_medis", label: "Rekam Medis" },
|
|
||||||
{ value: "tindakan", label: "Tindakan" },
|
|
||||||
{ value: "obat", label: "Obat" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const TAMPER_OPTIONS = [
|
|
||||||
{ value: "all", label: "Semua Data" },
|
|
||||||
{ value: "tampered", label: "Termanipulasi" },
|
|
||||||
{ value: "clean", label: "Tidak Termanipulasi" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const AUDIT_TABLE_COLUMNS = [
|
|
||||||
{ key: "id", label: "ID Log", class: "text-dark" },
|
|
||||||
{ key: "typeLabel", label: "Tipe Data", class: "text-dark" },
|
|
||||||
{ key: "event", label: "Event", class: "text-dark" },
|
|
||||||
{ key: "timestamp", label: "Timestamp", class: "text-dark" },
|
|
||||||
{ key: "userId", label: "User ID", class: "text-dark" },
|
|
||||||
{ key: "status", label: "Status Data", class: "text-dark" },
|
|
||||||
] satisfies Array<{
|
|
||||||
key: keyof AuditLogEntry;
|
|
||||||
label: string;
|
|
||||||
class?: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const formatTimestamp = (rawValue?: string) => {
|
const formatTimestamp = (rawValue?: string) => {
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
return "-";
|
return "-";
|
||||||
|
|
@ -102,13 +68,18 @@ const formatTimestamp = (rawValue?: string) => {
|
||||||
|
|
||||||
return date.toLocaleString("id-ID", options).replace(/\./g, ":");
|
return date.toLocaleString("id-ID", options).replace(/\./g, ":");
|
||||||
};
|
};
|
||||||
|
const progres = ref(0);
|
||||||
|
const anomali = ref(0);
|
||||||
|
const status = ref("IDLE");
|
||||||
|
|
||||||
const runAuditTrail = async () => {
|
const runAuditTrail = async () => {
|
||||||
console.log("Running audit trail...");
|
console.log("Running audit trail...");
|
||||||
|
status.value = "STARTING";
|
||||||
|
progres.value = 0;
|
||||||
|
anomali.value = 0;
|
||||||
try {
|
try {
|
||||||
const result = await api.post("/audit/trail", {});
|
const result = await api.post("/audit/trail", {});
|
||||||
|
console.log(result);
|
||||||
console.log("Audit trail run result:", result);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error running audit trail:", error);
|
// console.error("Error running audit trail:", error);
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +122,7 @@ const typeLabelMap: Record<AuditLogType, string> = {
|
||||||
rekam_medis: "Rekam Medis",
|
rekam_medis: "Rekam Medis",
|
||||||
tindakan: "Tindakan",
|
tindakan: "Tindakan",
|
||||||
obat: "Obat",
|
obat: "Obat",
|
||||||
|
proof: "Proof",
|
||||||
unknown: "Tidak Diketahui",
|
unknown: "Tidak Diketahui",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -183,6 +155,7 @@ const normalizeEntry = (entry: any): AuditLogEntry => {
|
||||||
txId: flattened.txId ?? entry.txId ?? "",
|
txId: flattened.txId ?? entry.txId ?? "",
|
||||||
event: flattened.event ?? entry.event ?? "-",
|
event: flattened.event ?? entry.event ?? "-",
|
||||||
timestamp: formatTimestamp(flattened.timestamp ?? entry.timestamp),
|
timestamp: formatTimestamp(flattened.timestamp ?? entry.timestamp),
|
||||||
|
last_sync: formatTimestamp(flattened.last_sync ?? entry.last_sync),
|
||||||
hash: String(flattened.hash ?? flattened.payload ?? entry.hash ?? "-"),
|
hash: String(flattened.hash ?? flattened.payload ?? entry.hash ?? "-"),
|
||||||
userId: Number.isFinite(numericUserId) ? numericUserId : 0,
|
userId: Number.isFinite(numericUserId) ? numericUserId : 0,
|
||||||
status: statusLabel,
|
status: statusLabel,
|
||||||
|
|
@ -200,6 +173,7 @@ const showAuditModal = () => {
|
||||||
|
|
||||||
const updateQueryParams = () => {
|
const updateQueryParams = () => {
|
||||||
const query: Record<string, string> = {
|
const query: Record<string, string> = {
|
||||||
|
page: pagination.page.value.toString(),
|
||||||
pageSize: pageSize.value.toString(),
|
pageSize: pageSize.value.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -218,9 +192,10 @@ const updateQueryParams = () => {
|
||||||
router.replace({ query });
|
router.replace({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (bookmarkParam?: string, isInitial?: boolean) => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
page: pagination.page.value.toString(),
|
||||||
pageSize: pageSize.value.toString(),
|
pageSize: pageSize.value.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -228,41 +203,36 @@ const fetchData = async (bookmarkParam?: string, isInitial?: boolean) => {
|
||||||
params.append("search", searchId.value.trim());
|
params.append("search", searchId.value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bookmarkParam) {
|
|
||||||
params.append("bookmark", bookmarkParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.value.type !== "all") {
|
if (filters.value.type !== "all") {
|
||||||
params.append("type", filters.value.type);
|
params.append("type", filters.value.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.value.tampered !== "all") {
|
if (filters.value.tampered !== "all") {
|
||||||
params.append(
|
params.append("tampered", filters.value.tampered);
|
||||||
"tampered",
|
|
||||||
filters.value.tampered === "tampered" ? "true" : "false"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.get<AuditLogResponse | AuditLogEntry[]>(
|
const response = await api.get<AuditLogResponse | AuditLogEntry[]>(
|
||||||
`/audit/trail${params.toString() ? `?${params.toString()}` : ""}`
|
`/audit/trail${params.toString() ? `?${params.toString()}` : ""}`
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(response);
|
const apiResponse = response as any;
|
||||||
|
pagination.totalCount.value = apiResponse.totalCount;
|
||||||
|
|
||||||
const payload = Array.isArray(response)
|
const dataArray: AuditLogEntry[] = [];
|
||||||
? { data: response, totalCount: response.length }
|
Object.keys(apiResponse).forEach((key) => {
|
||||||
: response;
|
if (key !== "totalCount") {
|
||||||
|
const item = apiResponse[Number(key)];
|
||||||
|
if (item) {
|
||||||
|
dataArray.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const normalized = Array.isArray(payload.data)
|
logs.value = dataArray.map((item) => normalizeEntry(item));
|
||||||
? payload.data.map((item) => normalizeEntry(item))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
logs.value = normalized;
|
// if (!isInitial) {
|
||||||
pagination.totalCount.value = payload.totalCount ?? normalized.length;
|
updateQueryParams();
|
||||||
|
// }
|
||||||
if (!isInitial) {
|
|
||||||
updateQueryParams();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching audit logs:", error);
|
console.error("Error fetching audit logs:", error);
|
||||||
logs.value = [];
|
logs.value = [];
|
||||||
|
|
@ -289,15 +259,6 @@ const handleResetFilters = () => {
|
||||||
pagination.reset();
|
pagination.reset();
|
||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
||||||
// const filteredTamperedLabel = computed(() =>
|
|
||||||
// filters.value.tampered === "tampered"
|
|
||||||
// ? "Termanipulasi"
|
|
||||||
// : filters.value.tampered === "clean"
|
|
||||||
// ? "Tidak Termanipulasi"
|
|
||||||
// : "Semua Data"
|
|
||||||
// );
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => pagination.page.value,
|
() => pagination.page.value,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -328,14 +289,53 @@ watch(searchId, (newValue, oldValue) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let socket: Socket;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (route.query.search) {
|
if (route.query.search) {
|
||||||
searchId.value = route.query.search as string;
|
searchId.value = route.query.search as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchData("", true);
|
const csrfToken = localStorage.getItem("csrf_token") || "";
|
||||||
|
|
||||||
|
socket = io("https://64spbch3-1323.asse.devtunnels.ms", {
|
||||||
|
withCredentials: true,
|
||||||
|
extraHeaders: {
|
||||||
|
"X-CSRF-TOKEN": csrfToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log("Berhasil terhubung ke WebSocket:", socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("audit.progress", (data) => {
|
||||||
|
console.log("Menerima progres:", data);
|
||||||
|
status.value = "RUNNING";
|
||||||
|
progres.value = data.progress_count;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("audit.complete", (data) => {
|
||||||
|
console.log("Menerima selesai:", data);
|
||||||
|
fetchData();
|
||||||
|
status.value = "COMPLETED";
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("audit.error", (data) => {
|
||||||
|
console.log("Menerima error:", data);
|
||||||
|
status.value = "ERROR";
|
||||||
|
socket.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchData();
|
||||||
document.title = "Audit Trail - Hospital Log";
|
document.title = "Audit Trail - Hospital Log";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -354,38 +354,43 @@ onMounted(async () => {
|
||||||
Filter
|
Filter
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content text-sm flex flex-col gap-4">
|
<div class="collapse-content text-sm flex flex-col gap-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="flex gap-x-4">
|
||||||
<label class="form-control w-full">
|
<div class="flex gap-x-4 items-end">
|
||||||
<span class="label-text font-semibold">Tipe Data</span>
|
<div class="h-full">
|
||||||
<select
|
<label for="jenis_kelamin" class="font-bold">Tipe Data</label>
|
||||||
v-model="filters.type"
|
<select
|
||||||
class="select select-bordered bg-white"
|
v-model="filters.type"
|
||||||
>
|
class="select bg-white border border-gray-300 mt-1"
|
||||||
<option
|
|
||||||
v-for="option in TYPE_OPTIONS"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<option disabled selected value="initial">
|
||||||
</option>
|
Pilih Tipe Data
|
||||||
</select>
|
</option>
|
||||||
</label>
|
<option value="rekam_medis">Rekam Medis</option>
|
||||||
|
<option value="tindakan">Tindakan</option>
|
||||||
<label class="form-control w-full">
|
<option value="obat">Obat</option>
|
||||||
<span class="label-text font-semibold">Status Manipulasi</span>
|
<option value="proof">Proof</option>
|
||||||
<select
|
<option value="all">Semua Tipe</option>
|
||||||
v-model="filters.tampered"
|
</select>
|
||||||
class="select select-bordered bg-white"
|
</div>
|
||||||
>
|
</div>
|
||||||
<option
|
<div class="flex gap-x-4 items-end">
|
||||||
v-for="option in TAMPER_OPTIONS"
|
<div class="h-full">
|
||||||
:key="option.value"
|
<label for="jenis_kelamin" class="font-bold"
|
||||||
:value="option.value"
|
>Status Manipulasi</label
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<select
|
||||||
</option>
|
v-model="filters.tampered"
|
||||||
</select>
|
class="select bg-white border border-gray-300 mt-1"
|
||||||
</label>
|
>
|
||||||
|
<option disabled selected value="initial">
|
||||||
|
Pilih Status Manipulasi
|
||||||
|
</option>
|
||||||
|
<option value="tampered">Tampered</option>
|
||||||
|
<option value="non_tampered">Non-tampered</option>
|
||||||
|
<option value="all">Semua</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
|
|
@ -417,10 +422,38 @@ onMounted(async () => {
|
||||||
cancel-text="Batal"
|
cancel-text="Batal"
|
||||||
@confirm="runAuditTrail"
|
@confirm="runAuditTrail"
|
||||||
/>
|
/>
|
||||||
<ButtonDark text="Lakukan Audit" @click="showAuditModal" />
|
<ButtonDark
|
||||||
|
:disabled="
|
||||||
|
(progres != 0 && status != 'COMPLETED') ||
|
||||||
|
status === 'STARTING' ||
|
||||||
|
status === 'RUNNING'
|
||||||
|
"
|
||||||
|
text="Lakukan Audit"
|
||||||
|
@click="showAuditModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(progres != 0 && status != 'COMPLETED') ||
|
||||||
|
status === 'STARTING' ||
|
||||||
|
status === 'RUNNING'
|
||||||
|
"
|
||||||
|
class="p-4"
|
||||||
|
>
|
||||||
|
<div class="h-48 flex justify-center items-center flex-col gap-4">
|
||||||
|
<span class="loading loading-ring loading-xl"></span>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="font-semibold mb-2">
|
||||||
|
Proses audit sedang berjalan...
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
{{ progres + 1 }} data telah diperiksa
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
|
v-if="status === 'COMPLETED' || status === 'IDLE'"
|
||||||
:data="logs"
|
:data="logs"
|
||||||
:columns="AUDIT_TABLE_COLUMNS"
|
:columns="AUDIT_TABLE_COLUMNS"
|
||||||
:is-loading="api.isLoading.value"
|
:is-loading="api.isLoading.value"
|
||||||
|
|
@ -428,7 +461,11 @@ onMounted(async () => {
|
||||||
:is-aksi="false"
|
:is-aksi="false"
|
||||||
/>
|
/>
|
||||||
<PaginationControls
|
<PaginationControls
|
||||||
v-if="!api.isLoading.value && logs.length > 0"
|
v-if="
|
||||||
|
!api.isLoading.value &&
|
||||||
|
logs.length > 0 &&
|
||||||
|
(status === 'COMPLETED' || status === 'IDLE')
|
||||||
|
"
|
||||||
:page="pagination.page"
|
:page="pagination.page"
|
||||||
:page-size="pagination.pageSize"
|
:page-size="pagination.pageSize"
|
||||||
:total-count="pagination.totalCount"
|
:total-count="pagination.totalCount"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user