Lewati ke konten utama

Licensing Service — Analysis Design & Solution Modeling

Tujuan Dokumen

Dokumen ini berisi analisis desain dan pemodelan solusi untuk Licensing Framework pada TessaERP. Merupakan kelanjutan dari gitlab-issue-fr013-licensing-framework.md dan index.md, mencakup use case, flowchart, component diagram, sequence diagram, dan schema data.


Use Case

Aktor

AktorDeskripsi
Vendor / Admin NeuronMen-generate dan menandatangani file lisensi menggunakan Private Key
Customer / OperatorMengupload file lisensi ke instance ERP
ERP ApplicationMemvalidasi lisensi dan membatasi akses berdasarkan entitlement
License ServiceLayer abstraksi yang membaca dan memverifikasi lisensi

Use Case Diagram

graph TD
Vendor((Vendor\nAdmin Neuron))
Customer((Customer\nOperator))
ERP((ERP Application))
LS((License Service))

UC01[UC-01: Generate License Payload]
UC02[UC-02: Sign License dengan Private Key]
UC03[UC-03: Distribusi license.lic ke Customer]
UC04[UC-04: Upload license.lic ke ERP]
UC05[UC-05: Validasi Lisensi saat Startup]
UC06[UC-06: Query Entitlement Feature]
UC07[UC-07: Query Limit Resource]
UC08[UC-08: Tampilkan Halaman Upload\nJika Lisensi Invalid]
UC09[UC-09: Blokir Akses ERP\nJika Lisensi Invalid]

Vendor --> UC01
Vendor --> UC02
UC01 --> UC02
Vendor --> UC03
Customer --> UC04
ERP --> UC05
ERP --> UC06
ERP --> UC07
ERP --> UC08
ERP --> UC09

UC05 --> LS
UC06 --> LS
UC07 --> LS

Detail Use Case

IDUse CaseAktorPre-ConditionPost-Condition
UC-01Generate License PayloadVendorData tenant dan entitlement tersediaJSON payload terbentuk
UC-02Sign LicenseVendorPayload tersedia, Private Key tersedialicense.lic bertanda tangan
UC-03Distribusi license.licVendorlicense.lic sudah ditandatanganiFile diterima customer
UC-04Upload license.licCustomerInstance ERP running, lisensi invalid / expiredLisensi baru tersimpan
UC-05Validasi Lisensi saat StartupERPApplication startLisensi valid → ERP normal; Invalid → redirect /license
UC-06Query Entitlement FeatureERPLisensi valid dan di-cachetrue/false dikembalikan
UC-07Query Limit ResourceERPLisensi valid dan di-cacheNilai limit dikembalikan
UC-08Tampilkan Halaman UploadERPLisensi tidak validKasir diarahkan ke /license
UC-09Blokir Akses ERPERPLisensi tidak validSeluruh route selain /license diblokir

Flowchart

Alur Validasi Lisensi saat Startup

flowchart TD
A([Application Start]) --> B[Load license.lic\ndari LicenseFilePath]
B --> C{File ditemukan?}
C -- Tidak --> Z1[/"Reason: License file not found"\nLog error/]
C -- Ya --> D[Parse JSON payload]
D --> E{Format valid?}
E -- Tidak --> Z2[/"Reason: License file corrupted"\nLog error/]
E -- Ya --> F[Verify Signature\ngunakan Public Key]
F --> G{Signature valid?}
G -- Tidak --> Z3[/"Reason: License has been tampered"\nLog error/]
G -- Ya --> H{tenantId cocok\ndengan TENANT_ID env?}
H -- Tidak --> Z4[/"Reason: License issued for different tenant"\nLog error/]
H -- Ya --> I{License expired?}
I -- Ya --> Z5[/"Reason: License expired on date"\nLog error/]
I -- Tidak --> J[Cache License Information\ndi ILicenseProvider]
J --> K([ERP Startup Normal])

Z1 & Z2 & Z3 & Z4 & Z5 --> R[Redirect semua request → /license\nBlokir akses ERP]

Alur Upload License Baru

flowchart TD
A([User membuka /license]) --> B[Tampilkan halaman upload]
B --> C[User upload license.lic]
C --> D[Parse JSON]
D --> E{Format valid?}
E -- Tidak --> E1[Tampilkan error:\nFormat file tidak sesuai]
E -- Ya --> F[Verify Signature\ngunakan Public Key]
F --> G{Signature valid?}
G -- Tidak --> G1[Tampilkan error:\nFile corrupted or tampered]
G -- Ya --> H{tenantId cocok?}
H -- Tidak --> H1[Tampilkan error:\nLicense for different tenant]
H -- Ya --> I{Expired?}
I -- Ya --> I1[Tampilkan error:\nLicense expired on date]
I -- Tidak --> J[Simpan license.lic\nke LicenseFilePath]
J --> K[/"✅ Upload berhasil\nRestart application untuk menerapkan lisensi"/]
E1 & G1 & H1 & I1 --> B

Alur Generate License (Vendor Tool)

flowchart TD
A([Admin Neuron buka License Generator]) --> B[Input: TenantId, Edition,\nSubscription Type, Expiry,\nFeatures, Limits]
B --> C[Build JSON Payload]
C --> D[Sign Payload\ngunakan Private Key RSA ≥ 2048-bit]
D --> E[Wrap: payload + signature → license.lic]
E --> F([Export license.lic\nkirim ke customer])

Component Diagram

graph TD
subgraph Neuron_ERP["Neuron_ERP — Blazor Server"]
subgraph LIC_Pages["Pages / License"]
LP1[LicenseUpload.razor\n/license]
end

subgraph Middleware["Middleware Pipeline"]
MW1[LicenseValidationMiddleware\nBlokir route jika lisensi invalid]
end

subgraph ERP_Modules["ERP Modules — contoh penggunaan"]
EM1[MenuService.cs\nif HasFeature finance → ShowFinanceMenu]
EM2[UserService.cs\nif GetLimit users reached → blokir tambah user]
end

LP1 --> MW1
EM1 --> IL
EM2 --> IL
end

subgraph NeuronLibrary["NeuronLibrary — Business Logic"]
subgraph LIC_Svc["Helper / Licensing"]
IL[ILicenseProvider\ninterface]
LP[LicenseProvider.cs\nimplementasi]
LV[ILicenseVerificationService]
LVImpl[LicenseVerificationService.cs]
LM[LicenseModel.cs\nLicensePayload, LicenseValidationResult]
end

IL --> LP
LV --> LVImpl
LVImpl --> LM
LP --> LV
end

subgraph VendorTool["Vendor Tool — License Generator"]
LG[LicenseGenerator.cs\natau CLI tool]
PK[(Private Key\n.pem)]
LG --> PK
end

subgraph FS["File System"]
LF[(license.lic)]
PubK[(public.pem)]
end

LVImpl --> LF
LVImpl --> PubK
LG --> LF
MW1 --> IL

Sequence Diagram

Validasi Lisensi saat Application Startup

sequenceDiagram
participant App as Program.cs\nApplication Start
participant MW as LicenseValidationMiddleware
participant LP as LicenseProvider
participant LV as LicenseVerificationService
participant FS as File System

App->>LP: services.AddSingleton<ILicenseProvider>
App->>LV: Validate() on startup
LV->>FS: Read license.lic (LicenseFilePath)
FS-->>LV: file content (JSON)
LV->>LV: Verify RSA Signature (Public Key)
LV->>LV: Check tenantId == TENANT_ID env
LV->>LV: Check expiresAt > DateTime.Now
LV-->>LP: LicenseValidationResult {IsValid, Reason, ExpirationDate}
LP->>LP: Cache license info

Note over MW: Setiap HTTP request
MW->>LP: IsValid()
alt License Valid
LP-->>MW: true
MW->>MW: next() — lanjutkan request
else License Invalid
LP-->>MW: false
MW->>MW: Redirect → /license
end

UC-06: Query Entitlement Feature oleh ERP Module

sequenceDiagram
participant Mod as ERP Module\n(e.g. MenuService)
participant LP as ILicenseProvider
participant Cache as Cached License Info

Mod->>LP: HasFeature("finance")
LP->>Cache: Read features["finance"]
Cache-->>LP: true / false
LP-->>Mod: true / false

alt Feature tersedia
Mod->>Mod: tampilkan menu Finance
else Feature tidak tersedia
Mod->>Mod: sembunyikan menu Finance
end

Upload License Baru oleh Customer

sequenceDiagram
actor Customer
participant Page as LicenseUpload.razor
participant API as LicenseController
participant LV as LicenseVerificationService
participant FS as File System

Customer->>Page: upload license.lic
Page->>API: POST /api/license/upload (file)
API->>LV: ValidateFile(fileContent)
LV->>LV: Parse JSON
LV->>LV: Verify RSA Signature
LV->>LV: Check tenantId
LV->>LV: Check expiry
alt Semua valid
LV-->>API: IsValid = true
API->>FS: Save license.lic ke LicenseFilePath
API-->>Page: 200 OK
Page-->>Customer: ✅ Upload berhasil. Restart server untuk menerapkan.
else Ada error
LV-->>API: IsValid = false, Reason = ...
API-->>Page: 400 Bad Request + reason
Page-->>Customer: ❌ error message spesifik
end

Schema Data

License File Format (license.lic)

{
"payload": {
"licenseId": "LIC-2026-0001",
"tenantId": "TENANT001",
"edition": "Professional",
"subscription": {
"type": "annual",
"expiresAt": "2027-12-31"
},
"features": {
"sales": true,
"purchase": true,
"inventory": true,
"finance": false,
"manufacturing": true,
"hr": true
},
"limits": {
"users": 50
},
"constraints": {
"machineFingerprint": null
}
},
"signature": "BASE64_RSA_SIGNATURE"
}

Konfigurasi appsettings.json

{
"Licensing": {
"LicenseFilePath": "./license/license.lic",
"PublicKeyPath": "./license/public.pem",
"LicenseValidationEnabled": true
}
}

Class Diagram

classDiagram
class ILicenseProvider {
<<interface>>
+IsValid() bool
+HasFeature(featureCode: string) bool
+GetLimit(limitCode: string) int
+GetLicenseInfo() LicenseInfo
}

class LicenseProvider {
-_validationResult: LicenseValidationResult
-_payload: LicensePayload
+IsValid() bool
+HasFeature(featureCode: string) bool
+GetLimit(limitCode: string) int
+GetLicenseInfo() LicenseInfo
}

class ILicenseVerificationService {
<<interface>>
+Validate() LicenseValidationResult
+ValidateFile(content: string) LicenseValidationResult
}

class LicenseVerificationService {
-_config: LicensingOptions
+Validate() LicenseValidationResult
+ValidateFile(content: string) LicenseValidationResult
-VerifySignature(payload: string, sig: string) bool
-CheckTenantId(tenantId: string) bool
-CheckExpiry(expiresAt: DateTime) bool
}

class LicenseValidationResult {
+IsValid: bool
+Reason: string
+ExpirationDate: DateTime?
}

class LicensePayload {
+LicenseId: string
+TenantId: string
+Edition: string
+Subscription: SubscriptionInfo
+Features: Dictionary~string, bool~
+Limits: Dictionary~string, int~
+Constraints: ConstraintInfo
}

class SubscriptionInfo {
+Type: string
+ExpiresAt: DateTime
}

class ConstraintInfo {
+MachineFingerprint: string?
}

class LicensingOptions {
+LicenseFilePath: string
+PublicKeyPath: string
+LicenseValidationEnabled: bool
}

ILicenseProvider <|.. LicenseProvider
ILicenseVerificationService <|.. LicenseVerificationService
LicenseProvider --> ILicenseVerificationService
LicenseProvider --> LicensePayload
LicenseVerificationService --> LicenseValidationResult
LicenseVerificationService --> LicensePayload
LicenseVerificationService --> LicensingOptions
LicensePayload --> SubscriptionInfo
LicensePayload --> ConstraintInfo

Referensi