add readme, add unit test for backend app, reconfigure IP on pc 1

This commit is contained in:
yosaphatprs 2026-02-02 15:45:27 +07:00
parent aa47a38c7a
commit 278ff47ad9
14 changed files with 1245 additions and 45 deletions

157
README.md Normal file
View File

@ -0,0 +1,157 @@
# Pengembangan Sistem Audit Rekam Medis Menggunakan Teknologi Blockchain
Repositori ini berisi <i>source code</i> 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 <i>ordering service</i>.
- 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.
<br>
\*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] <ID_NODE>
```
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)

View File

@ -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>(AppController);
const module: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [{ provide: AppService, useValue: mockAppService }],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<AppController>(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
*/

View File

@ -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() {

View File

@ -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>(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)
*/

View File

@ -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 =

View File

@ -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'],

View File

@ -1,4 +1,5 @@
node_modules/*
network/channel-artifacts/
network/organizations/
backup/*
backup/*
.env

View File

@ -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/), <i>source file</i> 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.

View File

@ -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"
}

View File

@ -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": {

View File

@ -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.

View File

@ -0,0 +1,3 @@
# POSTGRES_PASSWORD=password
# JWT_SECRET_KEY=masukkan_jwt_secret_dengan_format_SHA256
# ENCRYPTION_KEY=masukkan_key_32byte

View File

@ -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

View File

@ -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 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
1. npm install
2. npm run dev / bun run dev