From 278ff47ad92a018e50237b584657471abe6a15a9 Mon Sep 17 00:00:00 2001 From: yosaphatprs Date: Mon, 2 Feb 2026 15:45:27 +0700 Subject: [PATCH] add readme, add unit test for backend app, reconfigure IP on pc 1 --- README.md | 157 ++++++ backend/api/src/app.controller.spec.ts | 153 +++++- backend/api/src/app.controller.ts | 5 - backend/api/src/app.service.spec.ts | 471 ++++++++++++++++++ backend/api/src/app.service.ts | 8 - backend/api/src/main.ts | 1 + backend/blockchain/.gitignore | 3 +- backend/blockchain/chaincode/README.md | 23 + .../logVerification/npm-shrinkwrap.json | 399 ++++++++++++++- .../chaincode/logVerification/package.json | 1 + backend/blockchain/network/README.md | 43 ++ .../blockchain/network/docker/.env.example | 3 + .../network/docker/docker-compose-swarm.yaml | 16 +- frontend/hospital-log/README.md | 7 +- 14 files changed, 1245 insertions(+), 45 deletions(-) create mode 100644 README.md create mode 100644 backend/api/src/app.service.spec.ts create mode 100644 backend/blockchain/chaincode/README.md create mode 100644 backend/blockchain/network/README.md create mode 100644 backend/blockchain/network/docker/.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..e907893 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# Pengembangan Sistem Audit Rekam Medis Menggunakan Teknologi Blockchain + +Repositori ini berisi source code untuk pengembangan sistem audit rekam medis berbasis blockchain. + +## Gambaran Proyek + +Dalam proyek ini terdapat 3 node peer dan 1 node orderer. Keempat peer tersebut dijalankan pada 3 pc desktop menggunakan docker swarm dengan detail sebagai berikut: + +- PC 1 (Ubuntu 24.04.3): PC 1 ini sebagai host docker swarm dan menjalankan peer serta orderer. +- PC 2 (Ubuntu 24.04.3): PC 2 ini sebagai worker docker swarm pertama dan menjalankan peer. +- PC 3 (Windows WSL dengan distro Ubuntu 24.04.1): PC 3 ini sebagai worker docker swarm keuda dan menjalankan peer. + +Proyek ini mengimplementasikan sistem rekam medis menggunakan teknologi blockchain dengan rincian sebagai berikut: + +- Hyperledger Fabric sebagai framework blockchain dengan chaincode dibangun menggunakan bahasa pemrograman javascript. +- Mekanisme konsensus RAFT untuk ordering service. +- PostgreSQL sebagai basis data dan penyimpanan rekam medis offchain. +- REST API Gateway sebagai penghubung antara pengguna dan jaringan blockchain, dibangun menggunakan javascript. + +## Struktur Repositori + +- `/backend` - Folder backend yang di dalamnya terdapat kode program backend api dan blockchain. +- `/frontend` - Folder frontend yang di dalamnya terdapat kode program frontend. + +## Prasyarat + +- Docker & Docker Compose +- Node.js (v20 atau lebih tinggi) +- Instalasi Hyperledger Fabric (v2.5.13) +- Jika menggunakan Windows, pastikan WSL2 telah diinstal dan diaktifkan serta jalankan proyek ini di dalam WSL2. +- PostgreSQL (PostgreSQL 16.11 atau lebih tinggi) +- **Pastikan semua pc/vm yang digunakan memiliki IP Statis** +- **Pastikan port berikut tersedia karena diperlukan untuk koneksi docker swarm: 2377 (TCP), 7946 (TCP/UDP), 4789 (UDP)** + +## Instalasi + +1. Clone repositori +2. Masuk ke direktori [network](/backend/blockchain/network/), ikuti instruksi di file `README.md` untuk konfigurasi jaringan Hyperledger Fabric. +3. Masuk ke direktori [chaincode](/chaincode), ikuti instruksi di file `README.md` jika ingin mengubah logika bisnis dalam smartcontract, jika tidak, lanjut pada langkah ke-4. +4. Jalankan command berikut di pc/vm yang akan menjadi gateway utama/docker swarm leader: + ```bash + docker swarm init --advertise-addr [IP_PC_UTAMA] + ``` +5. Kemudian jalankan command berikut untuk mendapatkan token docker swarm yang akan digunakan pc/vm lain untuk bergabung ke dalam docker swarm: + ```bash + docker swarm join-token worker + ``` + Setelah itu, salin output dari command tersebut. +
+ \*Output command tersebut kurang lebih adalah seperti berikut + ```bash + docker swarm join --token SWMTKN-1-2ig... 192.168.11.74:2377 + ``` +6. Selanjutnya, jalankan output command yang telah disalin tadi pada masing-masing pc/vm yang akan bergabung dalam jaringan menjadi docker swarm worker. +7. Pada pc/vm docker swarm leader, jalankan command berikut untuk mengidentifikasi pc/vm yang bergabung ke dalam docker swarm: + ```bash + docker node ls + ``` +8. Setelah list node yang bergabung ke dalam docker swarm susah sesuai, maka selanjutnya adalah memberi nama masing-masing node yang bergabung ke dalam docker swarm. Untuk pemberian nama ini, pastikan sesuai dengan nama label yang ada dalam [docker-compose-swarm.yaml](/backend/blockchain/network/docker/docker-compose-swarm.yaml). (Contohnya dalam file docker compose, peer 1 ada constraint placement yang bernilai label lokasi pc-tengah, maka pelabelan lokasi pc/vm yang harus jadi peer 1 haruslah pc-tengah). Untuk memberi label jalankan command berikut: + ```bash + docker node update --label-add lokasi=[LABEL] + ``` +9. Kemudian, jalankan command berikut untuk membuat jaringan overlay yang berfungsi untuk membuat jalur komunikasi virtual agar container di pc/vm berbeda bisa saling bicara. + ```bash + docker network create --driver overlay --attachable hospital-net + ``` +10. Selanjutnya jalankan command berikut pada pc/vm yang berperan sebagai docker swarm leader di dalam folder yang ada [docker-compose-swarm.yaml](/backend/blockchain/network/docker/docker-compose-swarm.yaml), kalau dalam repositori ini berada dalam folder [docker](/backend/blockchain/network/docker/). + + ```bash + docker stack deploy -c docker-compose.yaml hospital + ``` + +11. Verifikasi status docker swarm dengan menjalankan command berikut pada pc/vm yang berperan sebagai docker swarm leader. + ```bash + docker service ls + ``` +12. Setelah jaringan berjalan, langkah selanjutnya adalah membuat channel, membuat peer dan orderer bergabung ke channel, dan melakukan deploy chaincode. +13. Untuk membuat channel dan membuat peer dan orderer bergabung ke channel, jalankan command berikut: + + ```bash + docker exec -it cli bash + ``` + + Setelah masuk ke dalam CLI fabric, lanjutkan dengan menjalankan command berikut: + + ```bash + export ORDERER_CA=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/ordererOrganizations/example.com/tlsca/tlsca.example.com-cert.pem + + # Buat channel + peer channel create -o [ip_orderer_atau_domain_orderer_sesuai_konfigurasi]:[port_orderer] -c mychannel \ + -f ./channel-artifacts/mychannel.tx \ + --outputBlock ./channel-artifacts/mychannel.block \ + --tls --cafile "$ORDERER_CA" + + # Export peer address + export CORE_PEER_ADDRESS=[ip_peer_atau_domain_peer_sesuai_konfigurasi]:[port_peer] + # Gabung ke dalam channel + peer channel join -b ./channel-artifacts/mychannel.block + + # !PENTING! Jika memiliki lebih dari satu peer, peer tersebut juga + # harus bergabung ke dalam channel + export CORE_PEER_ADDRESS=[ip_peer_atau_domain_peer_sesuai_konfigurasi]:[port_peer] + peer channel join -b ./channel-artifacts/mychannel.block + ``` + + ### Setelah berhasil, jangan keluar dari CLI terlebih dahulu. + +14. Masih dalam CLI fabric, untuk melakukan deploy chaincode modifikasi dan jalankan command berikut sesuai konfigurasi jaringan anda: + + ```bash + # Sesuaikan domain dengan peer yang digunakan, file ini dapat dilihat dalam + # folder organizations + export CORE_PEER_TLS_CERT_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/medorg.example.com/peers/peer0.medorg.example.com/tls/server.crt + export CORE_PEER_TLS_KEY_FILE=/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/medorg.example.com/peers/peer0.medorg.example.com/tls/server.key + + peer lifecycle chaincode package logVerification.tar.gz \ + --path /opt/gopath/src/github.com/hyperledger/fabric/peer/chaincode/logVerification \ + --lang golang \ + --label logVerification_1.0 + # Jika anda mengembangkan dan ingin mengubah versi chaincode supaya + # dapat mempermudah dalam hal version control, anda dapat mengubah labelnya + # Contoh, untuk pengembangan selanjutnya dapat menggunakan label berikut: + # --label logVerification_1.1 + + export CORE_PEER_ADDRESS=[ip_peer_atau_domain_peer_sesuai_konfigurasi]:[port_peer] + peer lifecycle chaincode install logVerification.tar.gz + + # Install chaincode pada semua peer yang ada. + # Contoh, jika terdapat dua peer, maka jalankan export CORE_PEER_ADDRESS + # lagi dengan ip_peer untuk peer kedua. + export CORE_PEER_ADDRESS=[ip_peer_atau_domain_peer_kedua_sesuai_konfigurasi]:[port_peer] + peer lifecycle chaincode install logVerification.tar.gz + + # Setelah anda menjalankan command peer lifecycle chaincode queryinstalled berikut + # maka akan muncul package_id chaincode anda, simpan id tersebut. + peer lifecycle chaincode queryinstalled + + export NEW_CC_PACKAGE_ID=[isi_dari_output_command_peer_lifecycle_chaincode_queryinstalled] + + peer lifecycle chaincode approveformyorg -o [ip_orderer_atau_domain_orderer_sesuai_konfigurasi]:[port_orderer] --channelID mychannel \ + --name test-med --version [isi_dengan_versi_contoh_1.0] --package-id $CC_PACKAGE_ID --sequence [isi_dengan_sequence_ke_berapa_dan_sequence_harus_selalu_bertambah_sehingga_catat_selalu_sequence_ke_berapa] \ + --tls --cafile "$ORDERER_CA" + + # Command berikut berfungsi untuk melakukan commit chaincode. + # Perhatikan pada --peerAddressess, jika memiliki lebih dari satu peer + # sertakan juga dengan flag --peerAdressess dan format yang sesuai. + peer lifecycle chaincode commit -o [ip_orderer_atau_domain_orderer_sesuai_konfigurasi]:[port_orderer] --channelID mychannel \ + --name test-med --version [isi_dengan_versi_contoh_1.0] --sequence [isi_dengan_sequence_ke_berapa_dan_sequence_harus_selalu_bertambah_sehingga_catat_selalu_sequence_ke_berapa]\ + --collections-config /opt/gopath/src/github.com/hyperledger/fabric/peer/config/collections_config.json \ + --tls --cafile "$ORDERER_CA" \ + --peerAddresses [ip_peer_atau_domain_peer_sesuai_konfigurasi]:[port_peer] --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/medorg.example.com/peers/peer0.medorg.example.com/tls/ca.crt \ + --peerAddresses [ip_peer_atau_domain_peer_sesuai_konfigurasi]:[port_peer] --tlsRootCertFiles /opt/gopath/src/github.com/hyperledger/fabric/peer/organizations/peerOrganizations/medorg.example.com/peers/peer1.medorg.example.com/tls/ca.crt + ``` + +15. Setelah blockchain berhasil di deploy, anda dapat menjalankan backend dan frontend. Panduan menjalankan backend dan front end dapat diakses pada masing-masing folder: + - [/backend](/backend/api/README.md) + - [/frontend](/frontend/hospital-log/README.md) diff --git a/backend/api/src/app.controller.spec.ts b/backend/api/src/app.controller.spec.ts index d22f389..e223ce9 100644 --- a/backend/api/src/app.controller.spec.ts +++ b/backend/api/src/app.controller.spec.ts @@ -1,22 +1,157 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AuthGuard } from './modules/auth/guard/auth.guard'; describe('AppController', () => { - let appController: AppController; + let controller: AppController; + let mockAppService: { + getDashboard: jest.Mock; + }; + + const mockDashboardData = { + countRekamMedis: 100, + countTindakanDokter: 50, + countObat: 75, + auditTrailData: { tampered: 2, total: 100 }, + validasiData: [{ id: 1, status: 'PENDING' }], + last7DaysRekamMedis: [ + { date: '2025-12-10', count: 10 }, + { date: '2025-12-09', count: 8 }, + ], + }; beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); + mockAppService = { + getDashboard: jest.fn(), + }; - appController = app.get(AppController); + const module: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [{ provide: AppService, useValue: mockAppService }], + }) + .overrideGuard(AuthGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(AppController); }); - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + // ============================================================ + // getDashboard (GET /dashboard) + // ============================================================ + + describe('getDashboard', () => { + it('should return dashboard data from service', async () => { + mockAppService.getDashboard.mockResolvedValue(mockDashboardData); + + const result = await controller.getDashboard(); + + expect(mockAppService.getDashboard).toHaveBeenCalledTimes(1); + expect(result).toEqual(mockDashboardData); + }); + + it('should return all expected dashboard properties', async () => { + mockAppService.getDashboard.mockResolvedValue(mockDashboardData); + + const result = await controller.getDashboard(); + + expect(result).toHaveProperty('countRekamMedis'); + expect(result).toHaveProperty('countTindakanDokter'); + expect(result).toHaveProperty('countObat'); + expect(result).toHaveProperty('auditTrailData'); + expect(result).toHaveProperty('validasiData'); + expect(result).toHaveProperty('last7DaysRekamMedis'); + }); + + it('should handle empty dashboard data', async () => { + const emptyData = { + countRekamMedis: 0, + countTindakanDokter: 0, + countObat: 0, + auditTrailData: { tampered: 0, total: 0 }, + validasiData: [], + last7DaysRekamMedis: [], + }; + mockAppService.getDashboard.mockResolvedValue(emptyData); + + const result = await controller.getDashboard(); + + expect(result.countRekamMedis).toBe(0); + expect(result.validasiData).toEqual([]); + }); + + it('should propagate service errors', async () => { + mockAppService.getDashboard.mockRejectedValue( + new Error('Database error'), + ); + + await expect(controller.getDashboard()).rejects.toThrow('Database error'); + }); + + it('should handle service timeout', async () => { + mockAppService.getDashboard.mockRejectedValue( + new Error('Request timeout'), + ); + + await expect(controller.getDashboard()).rejects.toThrow( + 'Request timeout', + ); + }); + }); + + // ============================================================ + // Security Tests + // ============================================================ + + describe('Security', () => { + it('getDashboard should have AuthGuard protection', () => { + const guards = Reflect.getMetadata( + '__guards__', + AppController.prototype.getDashboard, + ); + expect(guards).toBeDefined(); + expect(guards.length).toBeGreaterThan(0); + }); + + // ISSUE: No RolesGuard - any authenticated user can access dashboard + it('ISSUE: getDashboard has no role restriction', () => { + // Any authenticated user can access dashboard data + // Consider if this should be restricted to Admin only + const roles = Reflect.getMetadata( + 'roles', + AppController.prototype.getDashboard, + ); + expect(roles).toBeUndefined(); // Documents missing role restriction }); }); }); + +/* + * ============================================================ + * CODE ISSUES DOCUMENTATION + * ============================================================ + * + * 1. ISSUE - No role-based access control: + * - getDashboard only uses AuthGuard + * - Any authenticated user can access sensitive dashboard data + * - Consider: Should regular users see tampered audit data counts? + * - Fix: Add @UseGuards(RolesGuard) and @Roles(UserRole.Admin) if needed + * + * 2. ISSUE - No error handling in controller: + * - Service errors propagate directly to client + * - No custom error messages or status codes + * - Consider: Wrap in try-catch for better error responses + * + * 3. SUGGESTION - Add caching: + * - Dashboard data is likely expensive to compute + * - Consider adding @CacheKey() and @CacheTTL() decorators + */ diff --git a/backend/api/src/app.controller.ts b/backend/api/src/app.controller.ts index eca8f3f..5baa58d 100644 --- a/backend/api/src/app.controller.ts +++ b/backend/api/src/app.controller.ts @@ -6,11 +6,6 @@ import { AuthGuard } from './modules/auth/guard/auth.guard'; export class AppController { constructor(private readonly appService: AppService) {} - @Get() - getHello(): string { - return this.appService.getHello(); - } - @Get('/dashboard') @UseGuards(AuthGuard) getDashboard() { diff --git a/backend/api/src/app.service.spec.ts b/backend/api/src/app.service.spec.ts new file mode 100644 index 0000000..724bc34 --- /dev/null +++ b/backend/api/src/app.service.spec.ts @@ -0,0 +1,471 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppService } from './app.service'; +import { RekammedisService } from './modules/rekammedis/rekammedis.service'; +import { TindakanDokterService } from './modules/tindakandokter/tindakandokter.service'; +import { ObatService } from './modules/obat/obat.service'; +import { AuditService } from './modules/audit/audit.service'; +import { ValidationService } from './modules/validation/validation.service'; + +describe('AppService', () => { + let service: AppService; + let mockRekamMedisService: { + countRekamMedis: jest.Mock; + getLast7DaysCount: jest.Mock; + }; + let mockTindakanDokterService: { + countTindakanDokter: jest.Mock; + }; + let mockObatService: { + countObat: jest.Mock; + }; + let mockAuditService: { + getCountAuditTamperedData: jest.Mock; + }; + let mockValidationService: { + getAllValidationQueueDashboard: jest.Mock; + }; + + beforeEach(async () => { + mockRekamMedisService = { + countRekamMedis: jest.fn(), + getLast7DaysCount: jest.fn(), + }; + mockTindakanDokterService = { + countTindakanDokter: jest.fn(), + }; + mockObatService = { + countObat: jest.fn(), + }; + mockAuditService = { + getCountAuditTamperedData: jest.fn(), + }; + mockValidationService = { + getAllValidationQueueDashboard: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AppService, + { provide: RekammedisService, useValue: mockRekamMedisService }, + { provide: TindakanDokterService, useValue: mockTindakanDokterService }, + { provide: ObatService, useValue: mockObatService }, + { provide: AuditService, useValue: mockAuditService }, + { provide: ValidationService, useValue: mockValidationService }, + ], + }).compile(); + + service = module.get(AppService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + // ============================================================ + // getDashboard + // ============================================================ + + describe('getDashboard', () => { + const mockCountRekamMedis = 100; + const mockCountTindakanDokter = 50; + const mockCountObat = 75; + const mockAuditTrailData = { tampered: 2, total: 100 }; + const mockValidasiData = [ + { id: 1, status: 'PENDING', table_name: 'rekam_medis' }, + { id: 2, status: 'PENDING', table_name: 'pemberian_obat' }, + ]; + const mockLast7DaysRekamMedis = [ + { date: '2025-12-10', count: 10 }, + { date: '2025-12-09', count: 8 }, + { date: '2025-12-08', count: 12 }, + ]; + + beforeEach(() => { + mockRekamMedisService.countRekamMedis.mockResolvedValue( + mockCountRekamMedis, + ); + mockTindakanDokterService.countTindakanDokter.mockResolvedValue( + mockCountTindakanDokter, + ); + mockObatService.countObat.mockResolvedValue(mockCountObat); + mockAuditService.getCountAuditTamperedData.mockResolvedValue( + mockAuditTrailData, + ); + mockValidationService.getAllValidationQueueDashboard.mockResolvedValue( + mockValidasiData, + ); + mockRekamMedisService.getLast7DaysCount.mockResolvedValue( + mockLast7DaysRekamMedis, + ); + }); + + it('should call all required services', async () => { + await service.getDashboard(); + + expect(mockRekamMedisService.countRekamMedis).toHaveBeenCalledTimes(1); + expect( + mockTindakanDokterService.countTindakanDokter, + ).toHaveBeenCalledTimes(1); + expect(mockObatService.countObat).toHaveBeenCalledTimes(1); + expect(mockAuditService.getCountAuditTamperedData).toHaveBeenCalledTimes( + 1, + ); + expect( + mockValidationService.getAllValidationQueueDashboard, + ).toHaveBeenCalledTimes(1); + expect(mockRekamMedisService.getLast7DaysCount).toHaveBeenCalledTimes(1); + }); + + it('should return complete dashboard data structure', async () => { + const result = await service.getDashboard(); + + expect(result).toEqual({ + countRekamMedis: mockCountRekamMedis, + countTindakanDokter: mockCountTindakanDokter, + countObat: mockCountObat, + auditTrailData: mockAuditTrailData, + validasiData: mockValidasiData, + last7DaysRekamMedis: mockLast7DaysRekamMedis, + }); + }); + + it('should return countRekamMedis from rekamMedisService', async () => { + const result = await service.getDashboard(); + + expect(result.countRekamMedis).toBe(100); + }); + + it('should return countTindakanDokter from tindakanDokterService', async () => { + const result = await service.getDashboard(); + + expect(result.countTindakanDokter).toBe(50); + }); + + it('should return countObat from obatService', async () => { + const result = await service.getDashboard(); + + expect(result.countObat).toBe(75); + }); + + it('should return auditTrailData from auditService', async () => { + const result = await service.getDashboard(); + + expect(result.auditTrailData).toEqual({ tampered: 2, total: 100 }); + }); + + it('should return validasiData from validationService', async () => { + const result = await service.getDashboard(); + + expect(result.validasiData).toEqual(mockValidasiData); + }); + + it('should return last7DaysRekamMedis from rekamMedisService', async () => { + const result = await service.getDashboard(); + + expect(result.last7DaysRekamMedis).toEqual(mockLast7DaysRekamMedis); + }); + + // ============================================================ + // Zero/Empty Values + // ============================================================ + + describe('Zero/Empty Values', () => { + it('should handle zero counts', async () => { + mockRekamMedisService.countRekamMedis.mockResolvedValue(0); + mockTindakanDokterService.countTindakanDokter.mockResolvedValue(0); + mockObatService.countObat.mockResolvedValue(0); + + const result = await service.getDashboard(); + + expect(result.countRekamMedis).toBe(0); + expect(result.countTindakanDokter).toBe(0); + expect(result.countObat).toBe(0); + }); + + it('should handle empty validasiData array', async () => { + mockValidationService.getAllValidationQueueDashboard.mockResolvedValue( + [], + ); + + const result = await service.getDashboard(); + + expect(result.validasiData).toEqual([]); + }); + + it('should handle empty last7DaysRekamMedis array', async () => { + mockRekamMedisService.getLast7DaysCount.mockResolvedValue([]); + + const result = await service.getDashboard(); + + expect(result.last7DaysRekamMedis).toEqual([]); + }); + + it('should handle zero tampered data', async () => { + mockAuditService.getCountAuditTamperedData.mockResolvedValue({ + tampered: 0, + total: 0, + }); + + const result = await service.getDashboard(); + + expect(result.auditTrailData).toEqual({ tampered: 0, total: 0 }); + }); + }); + + // ============================================================ + // Large Values + // ============================================================ + + describe('Large Values', () => { + it('should handle large counts', async () => { + mockRekamMedisService.countRekamMedis.mockResolvedValue(1000000); + mockTindakanDokterService.countTindakanDokter.mockResolvedValue(500000); + mockObatService.countObat.mockResolvedValue(750000); + + const result = await service.getDashboard(); + + expect(result.countRekamMedis).toBe(1000000); + expect(result.countTindakanDokter).toBe(500000); + expect(result.countObat).toBe(750000); + }); + + it('should handle many validation items', async () => { + const manyItems = Array.from({ length: 100 }, (_, i) => ({ + id: i + 1, + status: 'PENDING', + })); + mockValidationService.getAllValidationQueueDashboard.mockResolvedValue({ + data: manyItems, + totalCount: 100, + }); + + const result = await service.getDashboard(); + + expect(result.validasiData.totalCount).toBe(100); + }); + }); + + // ============================================================ + // Error Handling - ISSUES DOCUMENTED + // ============================================================ + + describe('Error Handling', () => { + // ISSUE: No error handling - any service error propagates directly + it('ISSUE: rekamMedisService.countRekamMedis error propagates unhandled', async () => { + mockRekamMedisService.countRekamMedis.mockRejectedValue( + new Error('Database connection failed'), + ); + + // Error propagates directly - no graceful handling + await expect(service.getDashboard()).rejects.toThrow( + 'Database connection failed', + ); + }); + + it('ISSUE: tindakanDokterService.countTindakanDokter error propagates unhandled', async () => { + mockTindakanDokterService.countTindakanDokter.mockRejectedValue( + new Error('Query timeout'), + ); + + await expect(service.getDashboard()).rejects.toThrow('Query timeout'); + }); + + it('ISSUE: obatService.countObat error propagates unhandled', async () => { + mockObatService.countObat.mockRejectedValue( + new Error('Service unavailable'), + ); + + await expect(service.getDashboard()).rejects.toThrow( + 'Service unavailable', + ); + }); + + it('ISSUE: auditService.getCountAuditTamperedData error propagates unhandled', async () => { + mockAuditService.getCountAuditTamperedData.mockRejectedValue( + new Error('Audit service error'), + ); + + await expect(service.getDashboard()).rejects.toThrow( + 'Audit service error', + ); + }); + + it('ISSUE: validationService.getAllValidationQueueDashboard error propagates unhandled', async () => { + mockValidationService.getAllValidationQueueDashboard.mockRejectedValue( + new Error('Validation service error'), + ); + + await expect(service.getDashboard()).rejects.toThrow( + 'Validation service error', + ); + }); + + it('ISSUE: rekamMedisService.getLast7DaysCount error propagates unhandled', async () => { + mockRekamMedisService.getLast7DaysCount.mockRejectedValue( + new Error('Date range query failed'), + ); + + await expect(service.getDashboard()).rejects.toThrow( + 'Date range query failed', + ); + }); + }); + + // ============================================================ + // Null/Undefined Values - ISSUES DOCUMENTED + // ============================================================ + + describe('Null/Undefined Values', () => { + // ISSUE: No null checks - service returns whatever dependencies return + it('ISSUE: returns null countRekamMedis without validation', async () => { + mockRekamMedisService.countRekamMedis.mockResolvedValue(null); + + const result = await service.getDashboard(); + + // No validation - null is returned directly + expect(result.countRekamMedis).toBeNull(); + }); + + it('ISSUE: returns undefined countTindakanDokter without validation', async () => { + mockTindakanDokterService.countTindakanDokter.mockResolvedValue( + undefined, + ); + + const result = await service.getDashboard(); + + // No validation - undefined is returned directly + expect(result.countTindakanDokter).toBeUndefined(); + }); + + it('ISSUE: returns null validasiData without validation', async () => { + mockValidationService.getAllValidationQueueDashboard.mockResolvedValue( + null, + ); + + const result = await service.getDashboard(); + + // No validation - null is returned directly + expect(result.validasiData).toBeNull(); + }); + + it('ISSUE: returns null auditTrailData without validation', async () => { + mockAuditService.getCountAuditTamperedData.mockResolvedValue(null); + + const result = await service.getDashboard(); + + // No validation - null is returned directly + expect(result.auditTrailData).toBeNull(); + }); + }); + + // ============================================================ + // Performance - Sequential vs Parallel + // ============================================================ + + describe('Performance', () => { + // ISSUE: All service calls are sequential (await one by one) + // This is inefficient - they could run in parallel with Promise.all + it('ISSUE: service calls are sequential instead of parallel', async () => { + const callOrder: string[] = []; + + mockRekamMedisService.countRekamMedis.mockImplementation(async () => { + callOrder.push('countRekamMedis'); + return 100; + }); + mockTindakanDokterService.countTindakanDokter.mockImplementation( + async () => { + callOrder.push('countTindakanDokter'); + return 50; + }, + ); + mockObatService.countObat.mockImplementation(async () => { + callOrder.push('countObat'); + return 75; + }); + mockAuditService.getCountAuditTamperedData.mockImplementation( + async () => { + callOrder.push('auditTrailData'); + return { tampered: 0, total: 0 }; + }, + ); + mockValidationService.getAllValidationQueueDashboard.mockImplementation( + async () => { + callOrder.push('validasiData'); + return []; + }, + ); + mockRekamMedisService.getLast7DaysCount.mockImplementation(async () => { + callOrder.push('last7DaysRekamMedis'); + return []; + }); + + await service.getDashboard(); + + // Documents that calls happen sequentially in specific order + expect(callOrder).toEqual([ + 'countRekamMedis', + 'countTindakanDokter', + 'countObat', + 'auditTrailData', + 'validasiData', + 'last7DaysRekamMedis', + ]); + }); + }); + }); +}); + +/* + * ============================================================ + * CODE ISSUES DOCUMENTATION + * ============================================================ + * + * 1. ISSUE - No error handling: + * - Any service error propagates directly to the controller + * - No try-catch, no graceful degradation + * - Fix: Wrap in try-catch, return partial data or default values + * + * 2. ISSUE - No null/undefined validation: + * - Service returns whatever dependencies return + * - If any dependency returns null, dashboard has null values + * - Fix: Add null coalescing (value ?? defaultValue) + * + * 3. PERFORMANCE - Sequential service calls: + * - All 6 service calls are awaited sequentially + * - If each takes 100ms, total = 600ms + * - Fix: Use Promise.all() for parallel execution + * + * Example fix: + * ```typescript + * async getDashboard() { + * const [ + * countRekamMedis, + * countTindakanDokter, + * countObat, + * auditTrailData, + * validasiData, + * last7DaysRekamMedis, + * ] = await Promise.all([ + * this.rekamMedisService.countRekamMedis(), + * this.tindakanDokterService.countTindakanDokter(), + * this.obatService.countObat(), + * this.auditService.getCountAuditTamperedData(), + * this.validationService.getAllValidationQueueDashboard(), + * this.rekamMedisService.getLast7DaysCount(), + * ]); + * return { ... }; + * } + * ``` + * + * 4. SUGGESTION - Add response DTO: + * - No type safety on return value + * - Consider creating DashboardResponseDto + * + * 5. SUGGESTION - Add caching: + * - Dashboard data is expensive to compute + * - Consider caching with a short TTL (e.g., 30 seconds) + */ diff --git a/backend/api/src/app.service.ts b/backend/api/src/app.service.ts index 0d50cca..95ba560 100644 --- a/backend/api/src/app.service.ts +++ b/backend/api/src/app.service.ts @@ -1,28 +1,20 @@ import { Injectable } from '@nestjs/common'; -import { PrismaService } from './modules/prisma/prisma.service'; import { TindakanDokterService } from './modules/tindakandokter/tindakandokter.service'; import { RekammedisService } from './modules/rekammedis/rekammedis.service'; import { ObatService } from './modules/obat/obat.service'; -import { LogService } from './modules/log/log.service'; import { AuditService } from './modules/audit/audit.service'; import { ValidationService } from './modules/validation/validation.service'; @Injectable() export class AppService { constructor( - private prisma: PrismaService, private rekamMedisService: RekammedisService, private tindakanDokterService: TindakanDokterService, private obatService: ObatService, - private logService: LogService, private auditService: AuditService, private validationService: ValidationService, ) {} - getHello(): string { - return 'Hello World!'; - } - async getDashboard() { const countRekamMedis = await this.rekamMedisService.countRekamMedis(); const countTindakanDokter = diff --git a/backend/api/src/main.ts b/backend/api/src/main.ts index fc9894c..bca4a4c 100644 --- a/backend/api/src/main.ts +++ b/backend/api/src/main.ts @@ -20,6 +20,7 @@ async function bootstrap() { 'http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175', + 'http://localhost:5176', ], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], diff --git a/backend/blockchain/.gitignore b/backend/blockchain/.gitignore index f918421..32edf97 100644 --- a/backend/blockchain/.gitignore +++ b/backend/blockchain/.gitignore @@ -1,4 +1,5 @@ node_modules/* network/channel-artifacts/ network/organizations/ -backup/* \ No newline at end of file +backup/* +.env \ No newline at end of file diff --git a/backend/blockchain/chaincode/README.md b/backend/blockchain/chaincode/README.md new file mode 100644 index 0000000..42e934b --- /dev/null +++ b/backend/blockchain/chaincode/README.md @@ -0,0 +1,23 @@ +# Dokumentasi mengenai Smartcontract. + +Smartcontract, atau dalam Fabric disebut chaincode ini dikembangkan menggunakan bahasa pemrograman Javascript. Dalam folder [logVerification](/backend/blockchain/chaincode/logVerification/), source file mengenai smartcontract dapat dilihat pada folder [logVerification](logVerification/). File [index.js](logVerification/index.js) berisi smartcontract yang memuat logika bisnis dalam sistem yang dikembangkan. + +--- + +## Pengembangan pada smartcontract + +### Prasyarat + +- NodeJs (v20 atau lebih tinggi) +- Hyperledger Fabric samples dan binary (v2.5.13) + +### Langkah pengembangan + +1. Pastikan NodeJs dan Fabric samples dan binary sudah terinstall. +2. Jalankan command berikut: + ```bash + npm install + ``` +3. Lakukan pengembangan +4. Untuk menjalankan hasil pengembangan, pastikan anda sudah melakukan mulai dari langkah ke-4 hingga ke-8 dalam [README.md](/README.md) pada root project. +5. Kemudian lakukan deploy chaincode ke dalam channel seperti pada langkah ke-9 dalam [README.md](/README.md) pada root project. diff --git a/backend/blockchain/chaincode/logVerification/npm-shrinkwrap.json b/backend/blockchain/chaincode/logVerification/npm-shrinkwrap.json index 1655019..9b07128 100644 --- a/backend/blockchain/chaincode/logVerification/npm-shrinkwrap.json +++ b/backend/blockchain/chaincode/logVerification/npm-shrinkwrap.json @@ -12,6 +12,7 @@ "fabric-contract-api": "^2.5.8", "fabric-shim": "^2.5.8", "json-stringify-deterministic": "^1.0.0", + "snarkjs": "^0.7.5", "sort-keys-recursive": "^2.1.0" }, "devDependencies": { @@ -515,6 +516,22 @@ "node": ">=16.13.0" } }, + "node_modules/@iden3/bigarray": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@iden3/bigarray/-/bigarray-0.0.2.tgz", + "integrity": "sha512-Xzdyxqm1bOFF6pdIsiHLLl3HkSLjbhqJHVyqaTxXt3RqXBEnmsUmEW47H7VOi/ak7TdkRpNkxjyK5Zbkm+y52g==", + "license": "GPL-3.0" + }, + "node_modules/@iden3/binfileutils": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@iden3/binfileutils/-/binfileutils-0.0.12.tgz", + "integrity": "sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==", + "license": "GPL-3.0", + "dependencies": { + "fastfile": "0.0.20", + "ffjavascript": "^0.3.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -969,15 +986,45 @@ } }, "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bfj": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", + "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2", + "check-types": "^11.2.3", + "hoopy": "^0.1.4", + "jsonpath": "^1.1.1", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -991,6 +1038,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "license": "MIT", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1151,6 +1214,12 @@ "node": "*" } }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "license": "MIT" + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1190,6 +1259,18 @@ "node": ">= 6" } }, + "node_modules/circom_runtime": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/circom_runtime/-/circom_runtime-0.1.28.tgz", + "integrity": "sha512-ACagpQ7zBRLKDl5xRZ4KpmYIcZDUjOiNRuxvXLqhnnlLSVY1Dbvh73TI853nqoR0oEbihtWmMSjgc5f+pXf/jQ==", + "license": "Apache-2.0", + "dependencies": { + "ffjavascript": "0.3.1" + }, + "bin": { + "calcwit": "calcwit.js" + } + }, "node_modules/class-transformer": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", @@ -1348,8 +1429,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/default-require-extensions": { "version": "3.0.1", @@ -1388,6 +1468,21 @@ "node": ">=6.0.0" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.802", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", @@ -1430,6 +1525,87 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/eslint": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", @@ -1534,7 +1710,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -1580,7 +1755,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1652,14 +1826,19 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fastfile": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/fastfile/-/fastfile-0.0.20.tgz", + "integrity": "sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA==", + "license": "GPL-3.0" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -1674,6 +1853,17 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/ffjavascript": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.1.tgz", + "integrity": "sha512-4PbK1WYodQtuF47D4pRI5KUg3Q392vuP5WjE1THSnceHdXwU3ijaoS0OqxTzLknCtz4Z2TtABzkBdBdMn3B/Aw==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16", + "wasmcurves": "0.2.2", + "web-worker": "1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1686,6 +1876,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1988,6 +2208,15 @@ "he": "bin/he" } }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2290,6 +2519,29 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2358,6 +2610,29 @@ "node": ">=6" } }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/just-extend": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", @@ -2470,6 +2745,12 @@ "node": ">= 12.0.0" } }, + "node_modules/logplease": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/logplease/-/logplease-1.2.15.tgz", + "integrity": "sha512-jLlHnlsPSJjpwUfcNyUxXCl33AYg2cHhIf9QhGL2T4iPT0XPB+xP1LRKFPgIg1M/sg9kAJvy94w9CzBNrfnstA==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -2645,6 +2926,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nanoassert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", + "license": "ISC" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3039,7 +3326,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3191,6 +3477,29 @@ } ] }, + "node_modules/r1csfile": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.48.tgz", + "integrity": "sha512-kHRkKUJNaor31l05f2+RFzvcH5XSa7OfEfd/l4hzjte6NL6fjRkSMfZ4BjySW9wmfdwPOtq3mXurzPvPGEf5Tw==", + "license": "GPL-3.0", + "dependencies": { + "@iden3/bigarray": "0.0.2", + "@iden3/binfileutils": "0.0.12", + "fastfile": "0.0.20", + "ffjavascript": "0.3.0" + } + }, + "node_modules/r1csfile/node_modules/ffjavascript": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.0.tgz", + "integrity": "sha512-l7sR5kmU3gRwDy8g0Z2tYBXy5ttmafRPFOqY7S6af5cq51JqJWt5eQ/lSR/rs2wQNbDYaYlQr5O+OSUf/oMLoQ==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16", + "wasmcurves": "0.2.2", + "web-worker": "1.2.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3451,6 +3760,27 @@ "sinon": ">=4.0.0" } }, + "node_modules/snarkjs": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.5.tgz", + "integrity": "sha512-h+3c4rXZKLhLuHk4LHydZCk/h5GcNvk5GjVKRRkHmfb6Ntf8gHOA9zea3g656iclRuhqQ3iKDWFgiD9ypLrKiA==", + "license": "GPL-3.0", + "dependencies": { + "@iden3/binfileutils": "0.0.12", + "bfj": "^7.0.2", + "blake2b-wasm": "^2.4.0", + "circom_runtime": "0.1.28", + "ejs": "^3.1.6", + "fastfile": "0.0.20", + "ffjavascript": "0.3.1", + "js-sha3": "^0.8.0", + "logplease": "^1.2.15", + "r1csfile": "0.0.48" + }, + "bin": { + "snarkjs": "build/cli.cjs" + } + }, "node_modules/sort-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", @@ -3481,7 +3811,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -3517,6 +3847,15 @@ "node": "*" } }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3648,6 +3987,12 @@ "node": ">= 14.0.0" } }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "license": "MIT" + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -3695,6 +4040,12 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", @@ -3747,6 +4098,27 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/wasmbuilder": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/wasmbuilder/-/wasmbuilder-0.0.16.tgz", + "integrity": "sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA==", + "license": "GPL-3.0" + }, + "node_modules/wasmcurves": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.2.tgz", + "integrity": "sha512-JRY908NkmKjFl4ytnTu5ED6AwPD+8VJ9oc94kdq7h5bIwbj0L4TDJ69mG+2aLs2SoCmGfqIesMWTEJjtYsoQXQ==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3806,7 +4178,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/backend/blockchain/chaincode/logVerification/package.json b/backend/blockchain/chaincode/logVerification/package.json index 7ff7d7a..19f30d5 100644 --- a/backend/blockchain/chaincode/logVerification/package.json +++ b/backend/blockchain/chaincode/logVerification/package.json @@ -19,6 +19,7 @@ "fabric-contract-api": "^2.5.8", "fabric-shim": "^2.5.8", "json-stringify-deterministic": "^1.0.0", + "snarkjs": "^0.7.5", "sort-keys-recursive": "^2.1.0" }, "devDependencies": { diff --git a/backend/blockchain/network/README.md b/backend/blockchain/network/README.md new file mode 100644 index 0000000..d2a05ff --- /dev/null +++ b/backend/blockchain/network/README.md @@ -0,0 +1,43 @@ +# Konfigurasi Jaringan untuk Blockchain Rekam Medis + +Direktori ini berisi file konfigurasi jaringan untuk jaringan blockchain Hyperledger Fabric yang digunakan dalam sistem rekam medis. + +## Struktur Direktori + +- \*`organizations/` - Berisi material kripto dan sertifikat untuk organisasi dalam jaringan +- \*`channel-artifacts/` - Berisi artefak channel seperti genesis block dan transaksi +- `docker/` - File Docker compose untuk konfigurasi deployment jaringan +- `config/` - File konfigurasi jaringan + +**\*Jika sudah menjalankan generate artifact** + +## Prasyarat + +- Git +- WSL2 (jika menjalankan melalui sistem operasi Windows) +- Docker dan Docker Compose +- Hyperledger Fabric samples dan binary (v2.5.13) + +## Petunjuk Instalasi dan Konfigurasi + +1. Pastikan anda telah menginstal Docker, Docker Compose, dan [Hyperledger Fabric](https://hyperledger-fabric.readthedocs.io/en/release-2.5/install.html). +2. Sebelum membuat artefak jaringan, sesuaikan topologi jaringan dengan kebutuhan. Hal ini terkait dengan: + - Jumlah Peer dan Orderer: edit file [network/config/crypto-config.yaml](config/crypto-config.yaml) untuk mengubah jumlah Peer atau Orderer yang akan dibuat. + - Channel: edit file [network/config/configtx.yaml](config/configtx.yaml) untuk menyesuaikan profil channel atau menambahkan organisasi baru. +3. Jalankan skrip `generate-artifacts.sh` pada folder [blockchain](/backend/blockchain/) untuk menghasilkan artefak jaringan: + ```bash + ./generate-artifacts.sh + ``` +4. Skrip di atas akan menghasilkan artefak jaringan yang diperlukan, termasuk material kripto dan sertifikat untuk organisasi. Hasilnya adalah dua folder sebagai berikut: + - `organizations/` - Berisi material kripto untuk organisasi + - `channel-artifacts/` - Berisi artefak channel seperti genesis block dan transaksi +5. Jika menjalankan node pada beberapa VM yang berbeda, distribusikan artefak jaringan yang telah dibuat. + - Arsipkan artefak jaringan dengan menjalankan command + ```bash + tar -czvf artifacts.tar.gz ./network/organizations ./network/channel-artifacts + ``` + - Setelah didistribusikan pada VM yang lain, ekstrak arsip tersebut. +6. Selanjutnya adalah melakukan konfigurasi environment yang akan digunakan oleh sistem ini. Anda dapat menduplikat file `.env.example` yang berada dalam directory [docker](docker/) menjadi `.env` dan mengedit sesuai dengan petunjuk yang ada dalam file .env tersebut. +7. Sesuaikan isi dari [docker-compose-swarm.yaml](docker/docker-compose-swarm.yaml) dengan konfigurasi node milik anda. +8. Ubah isi dari masing-masing file konfigurasi kontainer docker sesuai petunjuk dalam file tersebut. +9. Setelah selesai, lanjut menuju direktori [chaincode](/backend/blockchain/chaincode/) jika ingin melakukan pengembangan terkait dengan chaincode/smartcontract. diff --git a/backend/blockchain/network/docker/.env.example b/backend/blockchain/network/docker/.env.example new file mode 100644 index 0000000..7bce260 --- /dev/null +++ b/backend/blockchain/network/docker/.env.example @@ -0,0 +1,3 @@ +# POSTGRES_PASSWORD=password +# JWT_SECRET_KEY=masukkan_jwt_secret_dengan_format_SHA256 +# ENCRYPTION_KEY=masukkan_key_32byte \ No newline at end of file diff --git a/backend/blockchain/network/docker/docker-compose-swarm.yaml b/backend/blockchain/network/docker/docker-compose-swarm.yaml index 469060f..a91159b 100644 --- a/backend/blockchain/network/docker/docker-compose-swarm.yaml +++ b/backend/blockchain/network/docker/docker-compose-swarm.yaml @@ -42,6 +42,7 @@ services: deploy: placement: constraints: + # Pastikan label sesuai dengan yang ada dalam node swarm - node.labels.lokasi == pc-kiri peer0: @@ -75,6 +76,7 @@ services: deploy: placement: constraints: + # Pastikan label sesuai dengan yang ada dalam node swarm - node.labels.lokasi == pc-kiri cli: @@ -98,6 +100,7 @@ services: - /home/labai1/josafat/hospital-log/backend/blockchain/network/organizations:/opt/gopath/src/github.com/hyperledger/fabric/peer/organizations - /home/labai1/josafat/hospital-log/backend/blockchain/network/channel-artifacts:/opt/gopath/src/github.com/hyperledger/fabric/peer/channel-artifacts extra_hosts: + # Pastikan IP sesuai dengan node swarm - "peer1.hospital.com:192.168.11.94" - "peer2.hospital.com:192.168.11.63" depends_on: @@ -108,6 +111,7 @@ services: deploy: placement: constraints: + # Pastikan label sesuai dengan yang ada dalam node swarm - node.labels.lokasi == pc-kiri peer1: @@ -132,8 +136,9 @@ services: - /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/msp:/etc/hyperledger/fabric/msp - /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/tls:/etc/hyperledger/fabric/tls extra_hosts: - - "peer0.hospital.com:192.168.11.211" - - "orderer.hospital.com:192.168.11.211" + # Pastikan IP sesuai dengan node swarm + - "peer0.hospital.com:192.168.11.74" + - "orderer.hospital.com:192.168.11.74" - "peer2.hospital.com:192.168.11.63" ports: - target: 8051 @@ -145,6 +150,7 @@ services: deploy: placement: constraints: + # Pastikan label sesuai dengan yang ada dalam node swarm - node.labels.lokasi == pc-tengah peer2: @@ -170,8 +176,9 @@ services: - /home/my_device/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer2.hospital.com/tls:/etc/hyperledger/fabric/tls - /home/my_device/josafat/hospital-log/backend/blockchain/data:/var/hyperledger/production extra_hosts: - - "peer0.hospital.com:192.168.11.211" - - "orderer.hospital.com:192.168.11.211" + # Pastikan IP sesuai dengan node swarm + - "peer0.hospital.com:192.168.11.74" + - "orderer.hospital.com:192.168.11.74" - "peer1.hospital.com:192.168.11.94" ports: - target: 9051 @@ -183,4 +190,5 @@ services: deploy: placement: constraints: + # Pastikan label sesuai dengan yang ada dalam node swarm - node.labels.lokasi == pc-kanan diff --git a/frontend/hospital-log/README.md b/frontend/hospital-log/README.md index 33895ab..e80268f 100644 --- a/frontend/hospital-log/README.md +++ b/frontend/hospital-log/README.md @@ -1,5 +1,4 @@ -# Vue 3 + TypeScript + Vite +# How to start -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `