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/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-socket.io": "^11.1.8",
|
||||
"@nestjs/websockets": "^11.1.8",
|
||||
"@prisma/client": "^6.17.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"snarkjs": "^0.7.5"
|
||||
"snarkjs": "^0.7.5",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
|
@ -33,6 +38,7 @@
|
|||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/express": "^5.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": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz",
|
||||
|
|
@ -2541,6 +2560,25 @@
|
|||
"@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": {
|
||||
"version": "11.0.9",
|
||||
"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": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
|
|
@ -2958,6 +3019,12 @@
|
|||
"@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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||
|
|
@ -3104,6 +3171,13 @@
|
|||
"@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": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
|
||||
|
|
@ -3121,6 +3195,15 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
|
|
@ -4492,6 +4575,15 @@
|
|||
],
|
||||
"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": {
|
||||
"version": "2.8.18",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz",
|
||||
|
|
@ -5235,12 +5327,12 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
|
|
@ -5256,6 +5348,15 @@
|
|||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
|
|
@ -5616,6 +5717,104 @@
|
|||
"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": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
|
|
@ -6034,6 +6233,12 @@
|
|||
"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": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
|
|
@ -6145,6 +6350,15 @@
|
|||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
|
||||
|
|
@ -9069,6 +9283,15 @@
|
|||
"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": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
|
@ -10190,6 +10413,141 @@
|
|||
"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": {
|
||||
"version": "0.7.4",
|
||||
"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_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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -25,17 +25,22 @@
|
|||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/jwt": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/platform-socket.io": "^11.1.8",
|
||||
"@nestjs/websockets": "^11.1.8",
|
||||
"@prisma/client": "^6.17.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"snarkjs": "^0.7.5"
|
||||
"snarkjs": "^0.7.5",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
|
@ -44,6 +49,7 @@
|
|||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/express": "^5.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 { AuditModule } from './modules/audit/audit.module';
|
||||
import { ProofModule } from './modules/proof/proof.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -28,6 +29,7 @@ import { ProofModule } from './modules/proof/proof.module';
|
|||
FabricModule,
|
||||
AuditModule,
|
||||
ProofModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class FabricGateway {
|
|||
`Evaluating getLogWithPagination transaction with pageSize: ${pageSize}, bookmark: ${bookmark}...`,
|
||||
);
|
||||
const resultBytes = await this.contract.evaluateTransaction(
|
||||
'getLogsWithPagination',
|
||||
'getLogWithPagination',
|
||||
pageSize.toString(),
|
||||
bookmark,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ async function bootstrap() {
|
|||
const configService = app.get(ConfigService);
|
||||
app.setGlobalPrefix('api/');
|
||||
app.enableCors({
|
||||
origin: 'http://localhost:5173',
|
||||
origin: 'https://64spbch3-5173.asse.devtunnels.ms',
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
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 { AuditService } from './audit.service';
|
||||
import { fromEvent, interval, map, Observable } from 'rxjs';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
@Controller('audit')
|
||||
export class AuditController {
|
||||
constructor(private readonly auditService: AuditService) {}
|
||||
constructor(
|
||||
private readonly auditService: AuditService,
|
||||
private eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
@Get('/trail')
|
||||
@UseGuards(AuthGuard)
|
||||
async getAuditTrail(
|
||||
@Query('search') search: string,
|
||||
@Query('page') page: 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;
|
||||
}
|
||||
|
||||
@Post('/trail')
|
||||
@Post('trail')
|
||||
@UseGuards(AuthGuard)
|
||||
async createAuditTrail() {
|
||||
const result = await this.auditService.storeAuditTrail();
|
||||
return result;
|
||||
createAuditTrail() {
|
||||
this.auditService.storeAuditTrail();
|
||||
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 { TindakanDokterModule } from '../tindakandokter/tindakandokter.module';
|
||||
import { LogModule } from '../log/log.module';
|
||||
import { AuditGateway } from './audit.gateway';
|
||||
import { WebsocketGuard } from '../auth/guard/websocket.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -17,7 +19,7 @@ import { LogModule } from '../log/log.module';
|
|||
RekamMedisModule,
|
||||
TindakanDokterModule,
|
||||
],
|
||||
providers: [AuditService],
|
||||
providers: [AuditService, AuditGateway, WebsocketGuard],
|
||||
controllers: [AuditController],
|
||||
})
|
||||
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 { LogService } from '../log/log.service';
|
||||
import { ObatService } from '../obat/obat.service';
|
||||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
AuditEvent,
|
||||
resultStatus as ResultStatus,
|
||||
} from '@dist/generated/prisma';
|
||||
import { AuditGateway } from './audit.gateway';
|
||||
|
||||
type AuditRecordPayload = {
|
||||
id: string;
|
||||
|
|
@ -22,23 +23,62 @@ type AuditRecordPayload = {
|
|||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private readonly logger = new Logger(AuditService.name);
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly logService: LogService,
|
||||
private readonly obatService: ObatService,
|
||||
private readonly rekamMedisService: RekammedisService,
|
||||
private readonly tindakanDokterService: TindakanDokterService,
|
||||
private auditGateway: AuditGateway,
|
||||
) {}
|
||||
|
||||
async getAuditTrails(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({
|
||||
take: pageSize,
|
||||
skip: bookmark ? 1 : 0,
|
||||
cursor: bookmark ? { id: bookmark } : undefined,
|
||||
skip: (page - 1) * pageSize,
|
||||
orderBy: { timestamp: 'asc' },
|
||||
where: {
|
||||
id: type && type !== 'all' ? { startsWith: type } : undefined,
|
||||
result: tampered ? (tampered as ResultStatus) : undefined,
|
||||
OR: search ? [{ id: { contains: search } }] : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
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() {
|
||||
|
|
@ -47,6 +87,30 @@ export class AuditService {
|
|||
let bookmark = '';
|
||||
let processedCount = 0;
|
||||
|
||||
// try {
|
||||
// const intervalId = setInterval(() => {
|
||||
// processedCount++;
|
||||
// const progressData = {
|
||||
// status: 'RUNNING',
|
||||
// progress_count: processedCount,
|
||||
// };
|
||||
|
||||
// this.logger.log('Mengirim progres via WebSocket:', progressData);
|
||||
// // PANGGIL FUNGSI GATEWAY
|
||||
// this.auditGateway.sendProgress(progressData);
|
||||
|
||||
// if (processedCount >= BATCH_SIZE) {
|
||||
// clearInterval(intervalId);
|
||||
// const completeData = { status: 'COMPLETED' };
|
||||
// this.logger.log('Mengirim selesai via WebSocket:', completeData);
|
||||
// // PANGGIL FUNGSI GATEWAY
|
||||
// this.auditGateway.sendComplete(completeData);
|
||||
// }
|
||||
// }, 500);
|
||||
// } catch (error) {
|
||||
// console.error('Tes streaming GAGAL', error);
|
||||
// }
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const pageResults = await this.logService.getLogsWithPagination(
|
||||
|
|
@ -66,11 +130,23 @@ export class AuditService {
|
|||
break;
|
||||
}
|
||||
|
||||
const records = (
|
||||
await Promise.all(
|
||||
logs.map((logEntry) => this.buildAuditRecord(logEntry)),
|
||||
)
|
||||
).filter((record): record is AuditRecordPayload => record !== null);
|
||||
// const records = (
|
||||
// await Promise.all(
|
||||
// logs.map((logEntry, index) =>
|
||||
// this.buildAuditRecord(logEntry, index),
|
||||
// ),
|
||||
// )
|
||||
// ).filter((record): record is AuditRecordPayload => record !== null);
|
||||
|
||||
const records: AuditRecordPayload[] = [];
|
||||
for (let index = 0; index < logs.length; index++) {
|
||||
const record = await this.buildAuditRecord(logs[index], index);
|
||||
if (record !== null) {
|
||||
records.push(record);
|
||||
}
|
||||
// Wait 1 second before processing next item
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
|
||||
if (records.length > 0) {
|
||||
await this.prisma.$transaction(
|
||||
|
|
@ -93,6 +169,9 @@ export class AuditService {
|
|||
}
|
||||
|
||||
if (nextBookmark === '' || nextBookmark === bookmark) {
|
||||
const completeData = { status: 'COMPLETED' };
|
||||
this.logger.log('Mengirim selesai via WebSocket:', completeData);
|
||||
this.auditGateway.sendComplete(completeData);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +185,7 @@ export class AuditService {
|
|||
|
||||
private async buildAuditRecord(
|
||||
logEntry: any,
|
||||
index?: number,
|
||||
): Promise<AuditRecordPayload | null> {
|
||||
if (!logEntry?.value) {
|
||||
return null;
|
||||
|
|
@ -128,7 +208,7 @@ export class AuditService {
|
|||
}
|
||||
|
||||
let dbHash: string | null = null;
|
||||
|
||||
//
|
||||
try {
|
||||
if (logId.startsWith('OBAT_')) {
|
||||
const obatId = this.extractNumericId(logId);
|
||||
|
|
@ -184,6 +264,14 @@ export class AuditService {
|
|||
|
||||
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 {
|
||||
id: logId,
|
||||
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(
|
||||
[
|
||||
'tindakan_dokter_created',
|
||||
'obat_given',
|
||||
'obat_created',
|
||||
'rekam_medis_created',
|
||||
'tindakan_dokter_updated',
|
||||
'obat_updated',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { StoreLogDto } from './dto/store-log.dto';
|
||||
import { Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { LogService } from './log.service';
|
||||
import { FabricService } from '../fabric/fabric.service';
|
||||
import { AuthGuard } from '../auth/guard/auth.guard';
|
||||
|
||||
@Controller('log')
|
||||
export class LogController {
|
||||
constructor(
|
||||
private readonly logService: LogService,
|
||||
private readonly fabricService: FabricService,
|
||||
) {}
|
||||
constructor(private readonly logService: LogService) {}
|
||||
|
||||
@Post('/store-to-blockchain')
|
||||
@UseGuards(AuthGuard)
|
||||
async storeLog() {
|
||||
return this.logService.storeFromDBToBlockchain();
|
||||
}
|
||||
|
||||
// @Post()
|
||||
// storeLog(@Body() dto: StoreLogDto) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
|||
import { LogController } from './log.controller';
|
||||
import { LogService } from './log.service';
|
||||
import { FabricModule } from '../fabric/fabric.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [FabricModule],
|
||||
imports: [FabricModule, PrismaModule],
|
||||
controllers: [LogController],
|
||||
providers: [LogService],
|
||||
exports: [LogService],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LogService } from './log.service';
|
||||
import { FabricService } from '../fabric/fabric.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('LogService', () => {
|
||||
let service: LogService;
|
||||
|
||||
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({
|
||||
providers: [LogService],
|
||||
providers: [
|
||||
LogService,
|
||||
{ provide: FabricService, useValue: fabricServiceMock },
|
||||
{ provide: PrismaService, useValue: prismaServiceMock },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LogService>(LogService);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,389 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { StoreLogDto } from './dto/store-log.dto';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
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 { 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()
|
||||
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) {
|
||||
const { id, event, user_id, payload } = dto;
|
||||
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() {
|
||||
const countLogs = await this.fabricService.getAllLogs();
|
||||
return countLogs.length;
|
||||
async getLogById(id: string) {
|
||||
return this.fabricService.getLogById(id);
|
||||
}
|
||||
|
||||
async getLogsWithPagination(pageSize: number, bookmark: string) {
|
||||
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 {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
isDateString,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Length,
|
||||
Matches,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class LogProofDto {
|
||||
@IsNotEmpty({ message: 'Proof wajib diisi' })
|
||||
@IsObject({ message: 'Proof harus berupa objek' })
|
||||
proofHash: object;
|
||||
proofHash: any;
|
||||
|
||||
@IsNotEmpty({ message: 'ID Visit wajib diisi' })
|
||||
@IsString({ message: 'ID Visit harus berupa string' })
|
||||
|
|
@ -32,5 +22,5 @@ export class LogProofDto {
|
|||
|
||||
@IsNotEmpty({ message: 'Timestamp wajib diisi' })
|
||||
@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",
|
||||
"daisyui": "^5.3.10",
|
||||
"nouislider": "^15.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "4",
|
||||
|
|
@ -158,6 +159,12 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
||||
|
|
@ -795,6 +802,23 @@
|
|||
"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": {
|
||||
"version": "2.1.2",
|
||||
"license": "Apache-2.0",
|
||||
|
|
@ -809,6 +833,28 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
|
|
@ -984,6 +1030,12 @@
|
|||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"dev": true,
|
||||
|
|
@ -1129,6 +1181,34 @@
|
|||
"version": "1.0.0-beta.41",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"license": "BSD-3-Clause",
|
||||
|
|
@ -1397,6 +1477,35 @@
|
|||
"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": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"cally": "^0.8.0",
|
||||
"daisyui": "^5.3.10",
|
||||
"nouislider": "^15.8.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "4",
|
||||
|
|
|
|||
|
|
@ -57,4 +57,23 @@ interface BlockchainLog {
|
|||
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 {
|
||||
AuditLogEntry,
|
||||
BlockchainLog,
|
||||
RekamMedis,
|
||||
TindakanDokter,
|
||||
|
|
@ -60,6 +61,18 @@ export const FILTER = {
|
|||
tindakan: "TINDAKAN",
|
||||
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 = {
|
||||
|
|
@ -227,3 +240,16 @@ export const LOG_TABLE_COLUMNS = [
|
|||
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">
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import Sidebar from "../../components/dashboard/Sidebar.vue";
|
||||
import Footer from "../../components/dashboard/Footer.vue";
|
||||
|
|
@ -13,21 +13,13 @@ import {
|
|||
DEFAULT_PAGE_SIZE,
|
||||
DEBOUNCE_DELAY,
|
||||
ITEMS_PER_PAGE_OPTIONS,
|
||||
AUDIT_TABLE_COLUMNS,
|
||||
} 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";
|
||||
|
||||
interface AuditLogEntry extends BlockchainLog {
|
||||
type: AuditLogType;
|
||||
typeLabel: string;
|
||||
tamperedLabel: string;
|
||||
isTampered: boolean;
|
||||
txId?: string;
|
||||
}
|
||||
import type { AuditLogEntry, AuditLogType } from "../../constants/interfaces";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
interface AuditLogResponse {
|
||||
data: AuditLogEntry[];
|
||||
|
|
@ -48,36 +40,10 @@ const pageSize = ref<number>(Number(route.query.take) || DEFAULT_PAGE_SIZE);
|
|||
const logs = ref<AuditLogEntry[]>([]);
|
||||
const searchId = ref("");
|
||||
const filters = ref({
|
||||
type: (route.query.type as string) || "all",
|
||||
tampered: (route.query.tampered as string) || "all",
|
||||
type: (route.query.type as string) || "initial",
|
||||
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) => {
|
||||
if (!rawValue) {
|
||||
return "-";
|
||||
|
|
@ -102,13 +68,18 @@ const formatTimestamp = (rawValue?: string) => {
|
|||
|
||||
return date.toLocaleString("id-ID", options).replace(/\./g, ":");
|
||||
};
|
||||
const progres = ref(0);
|
||||
const anomali = ref(0);
|
||||
const status = ref("IDLE");
|
||||
|
||||
const runAuditTrail = async () => {
|
||||
console.log("Running audit trail...");
|
||||
status.value = "STARTING";
|
||||
progres.value = 0;
|
||||
anomali.value = 0;
|
||||
try {
|
||||
const result = await api.post("/audit/trail", {});
|
||||
|
||||
console.log("Audit trail run result:", result);
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
// console.error("Error running audit trail:", error);
|
||||
}
|
||||
|
|
@ -151,6 +122,7 @@ const typeLabelMap: Record<AuditLogType, string> = {
|
|||
rekam_medis: "Rekam Medis",
|
||||
tindakan: "Tindakan",
|
||||
obat: "Obat",
|
||||
proof: "Proof",
|
||||
unknown: "Tidak Diketahui",
|
||||
};
|
||||
|
||||
|
|
@ -183,6 +155,7 @@ const normalizeEntry = (entry: any): AuditLogEntry => {
|
|||
txId: flattened.txId ?? entry.txId ?? "",
|
||||
event: flattened.event ?? entry.event ?? "-",
|
||||
timestamp: formatTimestamp(flattened.timestamp ?? entry.timestamp),
|
||||
last_sync: formatTimestamp(flattened.last_sync ?? entry.last_sync),
|
||||
hash: String(flattened.hash ?? flattened.payload ?? entry.hash ?? "-"),
|
||||
userId: Number.isFinite(numericUserId) ? numericUserId : 0,
|
||||
status: statusLabel,
|
||||
|
|
@ -200,6 +173,7 @@ const showAuditModal = () => {
|
|||
|
||||
const updateQueryParams = () => {
|
||||
const query: Record<string, string> = {
|
||||
page: pagination.page.value.toString(),
|
||||
pageSize: pageSize.value.toString(),
|
||||
};
|
||||
|
||||
|
|
@ -218,9 +192,10 @@ const updateQueryParams = () => {
|
|||
router.replace({ query });
|
||||
};
|
||||
|
||||
const fetchData = async (bookmarkParam?: string, isInitial?: boolean) => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: pagination.page.value.toString(),
|
||||
pageSize: pageSize.value.toString(),
|
||||
});
|
||||
|
||||
|
|
@ -228,41 +203,36 @@ const fetchData = async (bookmarkParam?: string, isInitial?: boolean) => {
|
|||
params.append("search", searchId.value.trim());
|
||||
}
|
||||
|
||||
if (bookmarkParam) {
|
||||
params.append("bookmark", bookmarkParam);
|
||||
}
|
||||
|
||||
if (filters.value.type !== "all") {
|
||||
params.append("type", filters.value.type);
|
||||
}
|
||||
|
||||
if (filters.value.tampered !== "all") {
|
||||
params.append(
|
||||
"tampered",
|
||||
filters.value.tampered === "tampered" ? "true" : "false"
|
||||
);
|
||||
params.append("tampered", filters.value.tampered);
|
||||
}
|
||||
|
||||
const response = await api.get<AuditLogResponse | AuditLogEntry[]>(
|
||||
`/audit/trail${params.toString() ? `?${params.toString()}` : ""}`
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
const apiResponse = response as any;
|
||||
pagination.totalCount.value = apiResponse.totalCount;
|
||||
|
||||
const payload = Array.isArray(response)
|
||||
? { data: response, totalCount: response.length }
|
||||
: response;
|
||||
const dataArray: AuditLogEntry[] = [];
|
||||
Object.keys(apiResponse).forEach((key) => {
|
||||
if (key !== "totalCount") {
|
||||
const item = apiResponse[Number(key)];
|
||||
if (item) {
|
||||
dataArray.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const normalized = Array.isArray(payload.data)
|
||||
? payload.data.map((item) => normalizeEntry(item))
|
||||
: [];
|
||||
logs.value = dataArray.map((item) => normalizeEntry(item));
|
||||
|
||||
logs.value = normalized;
|
||||
pagination.totalCount.value = payload.totalCount ?? normalized.length;
|
||||
|
||||
if (!isInitial) {
|
||||
updateQueryParams();
|
||||
}
|
||||
// if (!isInitial) {
|
||||
updateQueryParams();
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error("Error fetching audit logs:", error);
|
||||
logs.value = [];
|
||||
|
|
@ -289,15 +259,6 @@ const handleResetFilters = () => {
|
|||
pagination.reset();
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// const filteredTamperedLabel = computed(() =>
|
||||
// filters.value.tampered === "tampered"
|
||||
// ? "Termanipulasi"
|
||||
// : filters.value.tampered === "clean"
|
||||
// ? "Tidak Termanipulasi"
|
||||
// : "Semua Data"
|
||||
// );
|
||||
|
||||
watch(
|
||||
() => pagination.page.value,
|
||||
() => {
|
||||
|
|
@ -328,14 +289,53 @@ watch(searchId, (newValue, oldValue) => {
|
|||
}
|
||||
});
|
||||
|
||||
let socket: Socket;
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.search) {
|
||||
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";
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -354,38 +354,43 @@ onMounted(async () => {
|
|||
Filter
|
||||
</div>
|
||||
<div class="collapse-content text-sm flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<label class="form-control w-full">
|
||||
<span class="label-text font-semibold">Tipe Data</span>
|
||||
<select
|
||||
v-model="filters.type"
|
||||
class="select select-bordered bg-white"
|
||||
>
|
||||
<option
|
||||
v-for="option in TYPE_OPTIONS"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
<div class="flex gap-x-4">
|
||||
<div class="flex gap-x-4 items-end">
|
||||
<div class="h-full">
|
||||
<label for="jenis_kelamin" class="font-bold">Tipe Data</label>
|
||||
<select
|
||||
v-model="filters.type"
|
||||
class="select bg-white border border-gray-300 mt-1"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<span class="label-text font-semibold">Status Manipulasi</span>
|
||||
<select
|
||||
v-model="filters.tampered"
|
||||
class="select select-bordered bg-white"
|
||||
>
|
||||
<option
|
||||
v-for="option in TAMPER_OPTIONS"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
<option disabled selected value="initial">
|
||||
Pilih Tipe Data
|
||||
</option>
|
||||
<option value="rekam_medis">Rekam Medis</option>
|
||||
<option value="tindakan">Tindakan</option>
|
||||
<option value="obat">Obat</option>
|
||||
<option value="proof">Proof</option>
|
||||
<option value="all">Semua Tipe</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-x-4 items-end">
|
||||
<div class="h-full">
|
||||
<label for="jenis_kelamin" class="font-bold"
|
||||
>Status Manipulasi</label
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<select
|
||||
v-model="filters.tampered"
|
||||
class="select bg-white border border-gray-300 mt-1"
|
||||
>
|
||||
<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 class="flex justify-end">
|
||||
|
|
@ -417,10 +422,38 @@ onMounted(async () => {
|
|||
cancel-text="Batal"
|
||||
@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>
|
||||
|
||||
<DataTable
|
||||
v-if="status === 'COMPLETED' || status === 'IDLE'"
|
||||
:data="logs"
|
||||
:columns="AUDIT_TABLE_COLUMNS"
|
||||
:is-loading="api.isLoading.value"
|
||||
|
|
@ -428,7 +461,11 @@ onMounted(async () => {
|
|||
:is-aksi="false"
|
||||
/>
|
||||
<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-size="pagination.pageSize"
|
||||
:total-count="pagination.totalCount"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user