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 `