refactor: rename tiers and rewrite LicenseDefaults to 13-key cap matrix
- Tier enum: LOW→STARTER, MID→TEAM, HIGH→BUSINESS, BUSINESS→ENTERPRISE - LicenseDefaults: 13-key limits per tier matching server handoff cap matrix - Drop features concept from LicenseEntity, LicenseResponse, portal DTOs - Add label and gracePeriodDays to LicenseEntity - Fix agent limit key from 'agents' to 'max_agents' in VendorTenantController Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
614
docs/superpowers/plans/2026-04-26-license-minter-integration.md
Normal file
614
docs/superpowers/plans/2026-04-26-license-minter-integration.md
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
# License Minter Integration — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace UUID-based license tokens with Ed25519-signed tokens minted by `cameleer-license-minter`, with full vendor UI for configurable minting, distribution, and verification.
|
||||||
|
|
||||||
|
**Architecture:** The SaaS platform embeds `cameleer-license-minter` as a Maven dependency and calls `LicenseMinter.mint()` with an Ed25519 private key stored in the DB. Signed tokens are pushed to tenant servers via env vars and REST API. The vendor UI provides tier presets with per-limit customization, copy/email distribution as env-var bundles, and a token verification tool.
|
||||||
|
|
||||||
|
**Tech Stack:** Spring Boot 3.4, JPA/Flyway/PostgreSQL, Ed25519 (JCE), `cameleer-license-minter` + `cameleer-server-core` (LicenseInfo, LicenseValidator), React 19, @cameleer/design-system, TanStack Query.
|
||||||
|
|
||||||
|
**Decisions:**
|
||||||
|
- Tiers renamed: LOW→STARTER, MID→TEAM, HIGH→BUSINESS, BUSINESS→ENTERPRISE
|
||||||
|
- Tiers are presets only — vendor can customize any limit (becomes "Custom" in UI)
|
||||||
|
- Private key stored in DB (signing_keys table)
|
||||||
|
- Features concept dropped — server enforces caps, not feature flags
|
||||||
|
- Standalone distribution: license bundle = token + public key + tenant ID as env vars
|
||||||
|
- Verify tool: paste token → decode + validate signature → show envelope + state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Backend Foundation
|
||||||
|
|
||||||
|
### Task 1: Maven dependency + Flyway migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `pom.xml`
|
||||||
|
- Create: `src/main/resources/db/migration/V002__license_minter.sql`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add minter dependency to pom.xml**
|
||||||
|
|
||||||
|
Add inside `<dependencies>`:
|
||||||
|
```xml
|
||||||
|
<!-- License Minter (Ed25519 signing) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.cameleer</groupId>
|
||||||
|
<artifactId>cameleer-license-minter</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
This transitively brings `cameleer-server-core` (for `LicenseInfo`, `LicenseValidator`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create Flyway V002 migration**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- V002: License minter integration
|
||||||
|
-- Signing keys for Ed25519 license minting
|
||||||
|
CREATE TABLE signing_keys (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
public_key_b64 TEXT NOT NULL,
|
||||||
|
private_key_b64 TEXT NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Rename tiers: LOW→STARTER, MID→TEAM, HIGH→BUSINESS, BUSINESS→ENTERPRISE
|
||||||
|
UPDATE tenants SET tier = 'STARTER' WHERE tier = 'LOW';
|
||||||
|
UPDATE tenants SET tier = 'TEAM' WHERE tier = 'MID';
|
||||||
|
UPDATE tenants SET tier = 'BUSINESS' WHERE tier = 'HIGH';
|
||||||
|
UPDATE tenants SET tier = 'ENTERPRISE' WHERE tier = 'BUSINESS';
|
||||||
|
-- Fix double-rename: HIGH→BUSINESS rows that got caught by BUSINESS→ENTERPRISE
|
||||||
|
-- Use a single pass via CASE to avoid this:
|
||||||
|
-- Actually, redo with CASE statement in a single UPDATE:
|
||||||
|
|
||||||
|
-- (Replace the 4 UPDATEs above with this single safe statement)
|
||||||
|
UPDATE tenants SET tier = CASE tier
|
||||||
|
WHEN 'LOW' THEN 'STARTER'
|
||||||
|
WHEN 'MID' THEN 'TEAM'
|
||||||
|
WHEN 'HIGH' THEN 'BUSINESS'
|
||||||
|
WHEN 'BUSINESS' THEN 'ENTERPRISE'
|
||||||
|
ELSE tier
|
||||||
|
END WHERE tier IN ('LOW', 'MID', 'HIGH', 'BUSINESS');
|
||||||
|
|
||||||
|
-- Same for licenses table
|
||||||
|
UPDATE licenses SET tier = CASE tier
|
||||||
|
WHEN 'LOW' THEN 'STARTER'
|
||||||
|
WHEN 'MID' THEN 'TEAM'
|
||||||
|
WHEN 'HIGH' THEN 'BUSINESS'
|
||||||
|
WHEN 'BUSINESS' THEN 'ENTERPRISE'
|
||||||
|
ELSE tier
|
||||||
|
END WHERE tier IN ('LOW', 'MID', 'HIGH', 'BUSINESS');
|
||||||
|
|
||||||
|
-- Add new license columns
|
||||||
|
ALTER TABLE licenses ADD COLUMN label VARCHAR(255);
|
||||||
|
ALTER TABLE licenses ADD COLUMN grace_period_days INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Drop features column (server enforces caps, not feature flags)
|
||||||
|
ALTER TABLE licenses DROP COLUMN features;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify build compiles**
|
||||||
|
|
||||||
|
Run: `mvn compile -q` (just compile, no tests yet — tests will break until Tier enum is updated)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add cameleer-license-minter dependency and V002 migration
|
||||||
|
|
||||||
|
Adds Ed25519 license minting library, signing_keys table,
|
||||||
|
renames tiers (LOW→STARTER, MID→TEAM, HIGH→BUSINESS, BUSINESS→ENTERPRISE),
|
||||||
|
adds label + grace_period_days to licenses, drops features column.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Tier enum rename + LicenseDefaults rewrite
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/tenant/Tier.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/tenant/TenantEntity.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/tenant/TenantService.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/license/LicenseDefaults.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update Tier enum**
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.siegeln.cameleer.saas.tenant;
|
||||||
|
|
||||||
|
public enum Tier {
|
||||||
|
STARTER, TEAM, BUSINESS, ENTERPRISE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update TenantEntity default**
|
||||||
|
|
||||||
|
Change `private Tier tier = Tier.LOW;` to `private Tier tier = Tier.STARTER;`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update TenantService fallback**
|
||||||
|
|
||||||
|
Change `Tier.valueOf(request.tier()) : Tier.LOW` to `Tier.valueOf(request.tier()) : Tier.STARTER`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Rewrite LicenseDefaults**
|
||||||
|
|
||||||
|
Replace entire file with 13-key limits per tier matching the handoff cap matrix. Drop `featuresForTier()`. Only `limitsForTier()`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package net.siegeln.cameleer.saas.license;
|
||||||
|
|
||||||
|
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class LicenseDefaults {
|
||||||
|
|
||||||
|
private LicenseDefaults() {}
|
||||||
|
|
||||||
|
public static final int DEFAULT_GRACE_PERIOD_DAYS = 14;
|
||||||
|
public static final int DEFAULT_LICENSE_DAYS = 365;
|
||||||
|
|
||||||
|
public static Map<String, Integer> limitsForTier(Tier tier) {
|
||||||
|
return switch (tier) {
|
||||||
|
case STARTER -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 2),
|
||||||
|
Map.entry("max_apps", 10),
|
||||||
|
Map.entry("max_agents", 20),
|
||||||
|
Map.entry("max_users", 5),
|
||||||
|
Map.entry("max_outbound_connections", 5),
|
||||||
|
Map.entry("max_alert_rules", 10),
|
||||||
|
Map.entry("max_total_cpu_millis", 8000),
|
||||||
|
Map.entry("max_total_memory_mb", 8192),
|
||||||
|
Map.entry("max_total_replicas", 25),
|
||||||
|
Map.entry("max_execution_retention_days", 7),
|
||||||
|
Map.entry("max_log_retention_days", 7),
|
||||||
|
Map.entry("max_metric_retention_days", 7),
|
||||||
|
Map.entry("max_jar_retention_count", 5)
|
||||||
|
);
|
||||||
|
case TEAM -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 5),
|
||||||
|
Map.entry("max_apps", 50),
|
||||||
|
Map.entry("max_agents", 100),
|
||||||
|
Map.entry("max_users", 25),
|
||||||
|
Map.entry("max_outbound_connections", 25),
|
||||||
|
Map.entry("max_alert_rules", 50),
|
||||||
|
Map.entry("max_total_cpu_millis", 32000),
|
||||||
|
Map.entry("max_total_memory_mb", 32768),
|
||||||
|
Map.entry("max_total_replicas", 100),
|
||||||
|
Map.entry("max_execution_retention_days", 30),
|
||||||
|
Map.entry("max_log_retention_days", 30),
|
||||||
|
Map.entry("max_metric_retention_days", 30),
|
||||||
|
Map.entry("max_jar_retention_count", 10)
|
||||||
|
);
|
||||||
|
case BUSINESS -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 10),
|
||||||
|
Map.entry("max_apps", 200),
|
||||||
|
Map.entry("max_agents", 500),
|
||||||
|
Map.entry("max_users", 100),
|
||||||
|
Map.entry("max_outbound_connections", 100),
|
||||||
|
Map.entry("max_alert_rules", 200),
|
||||||
|
Map.entry("max_total_cpu_millis", 128000),
|
||||||
|
Map.entry("max_total_memory_mb", 131072),
|
||||||
|
Map.entry("max_total_replicas", 500),
|
||||||
|
Map.entry("max_execution_retention_days", 90),
|
||||||
|
Map.entry("max_log_retention_days", 90),
|
||||||
|
Map.entry("max_metric_retention_days", 90),
|
||||||
|
Map.entry("max_jar_retention_count", 25)
|
||||||
|
);
|
||||||
|
case ENTERPRISE -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 50),
|
||||||
|
Map.entry("max_apps", 1000),
|
||||||
|
Map.entry("max_agents", 5000),
|
||||||
|
Map.entry("max_users", 1000),
|
||||||
|
Map.entry("max_outbound_connections", 500),
|
||||||
|
Map.entry("max_alert_rules", 1000),
|
||||||
|
Map.entry("max_total_cpu_millis", 512000),
|
||||||
|
Map.entry("max_total_memory_mb", 524288),
|
||||||
|
Map.entry("max_total_replicas", 2000),
|
||||||
|
Map.entry("max_execution_retention_days", 365),
|
||||||
|
Map.entry("max_log_retention_days", 180),
|
||||||
|
Map.entry("max_metric_retention_days", 180),
|
||||||
|
Map.entry("max_jar_retention_count", 50)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
refactor: rename tiers and rewrite LicenseDefaults to 13-key cap matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: SigningKeyService + SigningKeyEntity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/SigningKeyEntity.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/SigningKeyRepository.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/SigningKeyService.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create SigningKeyEntity**
|
||||||
|
|
||||||
|
JPA entity for the `signing_keys` table: id (UUID), publicKeyB64 (text), privateKeyB64 (text), active (boolean), createdAt (Instant).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create SigningKeyRepository**
|
||||||
|
|
||||||
|
JpaRepository with `Optional<SigningKeyEntity> findByActiveTrue()`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create SigningKeyService**
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `getOrCreateActiveKey()` → returns the active key, generating a new Ed25519 keypair on first call
|
||||||
|
- `getPublicKeyBase64()` → convenience for the active key's public key
|
||||||
|
- `getPrivateKey()` → reconstructs `PrivateKey` from stored base64
|
||||||
|
|
||||||
|
Key generation:
|
||||||
|
```java
|
||||||
|
KeyPair kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
|
||||||
|
String pubB64 = Base64.getEncoder().encodeToString(kp.getPublic().getEncoded());
|
||||||
|
String privB64 = Base64.getEncoder().encodeToString(kp.getPrivate().getEncoded());
|
||||||
|
```
|
||||||
|
|
||||||
|
Private key reconstruction:
|
||||||
|
```java
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(entity.getPrivateKeyB64());
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||||
|
return KeyFactory.getInstance("Ed25519").generatePrivate(spec);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add SigningKeyService for Ed25519 keypair management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Rewrite LicenseService + LicenseEntity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/license/LicenseEntity.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/license/LicenseService.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/license/dto/LicenseResponse.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update LicenseEntity**
|
||||||
|
|
||||||
|
- Remove `features` field + getter/setter
|
||||||
|
- Add `label` (String) field + getter/setter
|
||||||
|
- Add `gracePeriodDays` (int) field + getter/setter
|
||||||
|
|
||||||
|
- [ ] **Step 2: Rewrite LicenseService**
|
||||||
|
|
||||||
|
- Add `SigningKeyService` dependency
|
||||||
|
- Rewrite `generateLicense(TenantEntity, Map<String,Integer> limits, Instant expiresAt, int gracePeriodDays, String label, UUID actorId)`:
|
||||||
|
- Build `LicenseInfo(UUID.randomUUID(), tenant.getSlug(), label, limits, Instant.now(), expiresAt, gracePeriodDays)`
|
||||||
|
- Call `LicenseMinter.mint(info, signingKeyService.getPrivateKey())`
|
||||||
|
- Store signed token in entity
|
||||||
|
- Add convenience overload `generateLicense(TenantEntity, Duration, UUID actorId)` that uses tier presets
|
||||||
|
- Remove `verifyLicenseToken()` (server validates cryptographically)
|
||||||
|
- Add `verifyToken(String token)` that uses `LicenseValidator`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update LicenseResponse DTO**
|
||||||
|
|
||||||
|
Replace `features` with `label` and `gracePeriodDays`. Add `publicKeyB64` for bundle distribution.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: rewrite LicenseService to mint Ed25519-signed tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Update controllers + portal service
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/license/LicenseController.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalService.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/portal/TenantPortalController.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update VendorTenantController**
|
||||||
|
|
||||||
|
- `POST /{id}/license` now takes a request body with limits, expiresAt, gracePeriodDays, label
|
||||||
|
- Add `GET /license-presets` endpoint returning tier presets
|
||||||
|
- Add `POST /license/verify` endpoint
|
||||||
|
- Add `GET /signing-key/public` endpoint
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update VendorTenantService**
|
||||||
|
|
||||||
|
- `renewLicense()` updated to accept customizable parameters
|
||||||
|
- Add `mintLicense()` method with full limit configuration
|
||||||
|
- Add `verifyToken()` delegation
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update VendorTenantController response types**
|
||||||
|
|
||||||
|
- `VendorTenantSummary` — fix `agentLimit` to use `max_agents` key
|
||||||
|
- `VendorTenantDetail` — license field uses updated LicenseResponse
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update TenantPortalService**
|
||||||
|
|
||||||
|
- `DashboardData` — drop features, keep limits
|
||||||
|
- `LicenseData` — drop features, add label + gracePeriodDays
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: update vendor/portal APIs for Ed25519 license minting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Fix tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/license/LicenseServiceTest.java`
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/license/LicenseControllerTest.java`
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/vendor/VendorTenantServiceTest.java`
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/tenant/TenantServiceTest.java`
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/portal/TenantPortalServiceTest.java`
|
||||||
|
- Modify: `src/test/java/net/siegeln/cameleer/saas/portal/TenantPortalControllerTest.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update all Tier.LOW→STARTER, Tier.MID→TEAM, Tier.HIGH→BUSINESS, Tier.BUSINESS→ENTERPRISE**
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update LicenseServiceTest**
|
||||||
|
|
||||||
|
- `generateLicense_producesUuidToken` → rename to `generateLicense_producesSignedToken`, assert token contains `.` separator
|
||||||
|
- Remove feature-related assertions
|
||||||
|
- Mock `SigningKeyService` to return a test keypair
|
||||||
|
- Remove `verifyLicenseToken` tests
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update LicenseControllerTest**
|
||||||
|
|
||||||
|
- Remove feature assertions (`features.correlation`)
|
||||||
|
- Update tier values in assertions
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests**
|
||||||
|
|
||||||
|
Run: `mvn test -q`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
test: update tests for Ed25519 license minting and tier rename
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2: Provisioning Integration
|
||||||
|
|
||||||
|
### Task 7: Push public key to tenant containers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Inject SigningKeyService into DockerTenantProvisioner**
|
||||||
|
|
||||||
|
Add `SigningKeyService` as a constructor dependency.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add CAMELEER_SERVER_LICENSE_PUBLICKEY env var**
|
||||||
|
|
||||||
|
In `createServerContainer()`, after the existing env vars, add:
|
||||||
|
```java
|
||||||
|
"CAMELEER_SERVER_LICENSE_PUBLICKEY=" + signingKeyService.getPublicKeyBase64()
|
||||||
|
```
|
||||||
|
|
||||||
|
`CAMELEER_SERVER_TENANT_ID` is already set to slug (line 218).
|
||||||
|
`CAMELEER_SERVER_LICENSE_TOKEN` is already set (line 225).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: push Ed25519 public key to tenant server containers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Vendor API — Configurable Minting
|
||||||
|
|
||||||
|
### Task 8: Vendor license endpoints
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java`
|
||||||
|
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/dto/MintLicenseRequest.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/dto/VerifyLicenseRequest.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/dto/VerifyLicenseResponse.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/dto/LicensePreset.java`
|
||||||
|
- Create: `src/main/java/net/siegeln/cameleer/saas/license/dto/LicenseBundleResponse.java`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create DTOs**
|
||||||
|
|
||||||
|
`MintLicenseRequest`: tier (optional String), limits (Map<String,Integer>), expiresAt (Instant), gracePeriodDays (Integer), label (String), pushToServer (boolean)
|
||||||
|
|
||||||
|
`VerifyLicenseRequest`: token (String)
|
||||||
|
|
||||||
|
`VerifyLicenseResponse`: valid (boolean), state (String), envelope fields (tenantId, label, limits, issuedAt, expiresAt, gracePeriodDays), error (String)
|
||||||
|
|
||||||
|
`LicensePreset`: tier (String), limits (Map<String,Integer>)
|
||||||
|
|
||||||
|
`LicenseBundleResponse`: extends LicenseResponse + adds publicKeyB64, tenantSlug (for the env-var bundle)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update VendorTenantService**
|
||||||
|
|
||||||
|
Add `mintLicense(UUID tenantId, MintLicenseRequest request, UUID actorId)`:
|
||||||
|
- Resolves limits from request (or tier preset)
|
||||||
|
- Calls `licenseService.generateLicense()` with full params
|
||||||
|
- Optionally pushes to server
|
||||||
|
- Returns the license + public key + slug for the bundle
|
||||||
|
|
||||||
|
Add `verifyToken(String token)`:
|
||||||
|
- Uses LicenseValidator from server-core
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update VendorTenantController**
|
||||||
|
|
||||||
|
- `POST /{id}/license` — takes MintLicenseRequest body, returns LicenseBundleResponse
|
||||||
|
- `GET /license-presets` — returns list of LicensePreset
|
||||||
|
- `POST /license/verify` — takes VerifyLicenseRequest, returns VerifyLicenseResponse
|
||||||
|
- `GET /signing-key/public` — returns `{"publicKey": "<base64>"}`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add vendor license minting, presets, and verify endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: Vendor UI — License Minting
|
||||||
|
|
||||||
|
### Task 9: Update frontend types + hooks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/types/api.ts`
|
||||||
|
- Modify: `ui/src/api/vendor-hooks.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update types**
|
||||||
|
|
||||||
|
- `LicenseResponse` — remove `features`, add `label`, `gracePeriodDays`, `publicKeyB64`, `tenantSlug`
|
||||||
|
- Add `MintLicenseRequest`, `VerifyLicenseRequest`, `VerifyLicenseResponse`, `LicensePreset`, `LicenseBundleResponse`
|
||||||
|
- `DashboardData` — remove `features`
|
||||||
|
- `TenantLicenseData` — remove `features`, add `label`, `gracePeriodDays`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update hooks**
|
||||||
|
|
||||||
|
- `useRenewLicense()` → replace with `useMintLicense(tenantId)` that takes MintLicenseRequest body
|
||||||
|
- Add `useLicensePresets()`
|
||||||
|
- Add `useVerifyLicense()`
|
||||||
|
- Add `usePublicKey()`
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(ui): update types and hooks for Ed25519 license minting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 10: License minting form on TenantDetailPage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/pages/vendor/TenantDetailPage.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace License card**
|
||||||
|
|
||||||
|
Replace the simple "Renew License" button with a minting form:
|
||||||
|
- Tier preset dropdown (STARTER/TEAM/BUSINESS/ENTERPRISE) that pre-fills limits
|
||||||
|
- All 13 limits editable in a grid
|
||||||
|
- Expiry date picker, grace period input, label input
|
||||||
|
- "Custom" indicator when limits diverge from preset
|
||||||
|
- Actions: "Mint & Push to Server" (default), "Mint & Copy Bundle", "Mint & Email Bundle"
|
||||||
|
|
||||||
|
- [ ] **Step 2: License bundle display**
|
||||||
|
|
||||||
|
After minting, show a dialog/card with the full env-var bundle:
|
||||||
|
```
|
||||||
|
CAMELEER_SERVER_TENANT_ID=<slug>
|
||||||
|
CAMELEER_SERVER_LICENSE_PUBLICKEY=<public_key>
|
||||||
|
CAMELEER_SERVER_LICENSE_TOKEN=<token>
|
||||||
|
```
|
||||||
|
With a "Copy Bundle" button.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(ui): add license minting form with tier presets and bundle distribution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 11: License verify tool + public key viewer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ui/src/pages/vendor/LicenseVerifyPage.tsx`
|
||||||
|
- Modify: `ui/src/router.tsx` (add route)
|
||||||
|
- Modify: `ui/src/Layout.tsx` (add nav item)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create LicenseVerifyPage**
|
||||||
|
|
||||||
|
- Textarea to paste a token
|
||||||
|
- "Verify" button
|
||||||
|
- Results: valid/invalid badge, decoded envelope (tenantId, label, limits, expiry, grace period)
|
||||||
|
- State badge (ACTIVE/GRACE/EXPIRED/INVALID)
|
||||||
|
- Public key display section with copy button
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add route and navigation**
|
||||||
|
|
||||||
|
Route: `/vendor/license-verify`
|
||||||
|
Nav: "License Tools" section in vendor sidebar
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(ui): add license verify tool and public key viewer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 12: Update tier color utility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/utils/tier.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update tierColor**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function tierColor(tier: string): 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto' {
|
||||||
|
switch (tier?.toUpperCase()) {
|
||||||
|
case 'ENTERPRISE': return 'success';
|
||||||
|
case 'BUSINESS': return 'primary';
|
||||||
|
case 'TEAM': return 'running';
|
||||||
|
case 'STARTER': return 'warning';
|
||||||
|
default: return 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(ui): update tier color mapping for renamed tiers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5: Tenant UI Updates
|
||||||
|
|
||||||
|
### Task 13: Update TenantLicensePage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/pages/tenant/TenantLicensePage.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Remove features card, update limits card**
|
||||||
|
|
||||||
|
- Drop the "Features" card entirely
|
||||||
|
- Update "Limits & Usage" card to show all 13 limit keys with proper labels
|
||||||
|
- Show grace period and label if present
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(ui): update tenant license page for Ed25519 model
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 14: Update TenantDashboardPage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/pages/tenant/TenantDashboardPage.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Remove features references**
|
||||||
|
|
||||||
|
Drop any `features` display. Keep limits display.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(ui): remove features from tenant dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 15: Update CreateTenantPage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ui/src/pages/vendor/CreateTenantPage.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update tier options**
|
||||||
|
|
||||||
|
Change tier dropdown options from LOW/MID/HIGH/BUSINESS to STARTER/TEAM/BUSINESS/ENTERPRISE.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(ui): update tier options in create tenant form
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After all tasks:
|
||||||
|
- [ ] `mvn test` passes
|
||||||
|
- [ ] `cd ui && npm run build` succeeds
|
||||||
|
- [ ] Docker compose boots (if available)
|
||||||
|
- [ ] Verify a tenant can be created with STARTER tier
|
||||||
|
- [ ] Verify license is minted with Ed25519 signature (token contains `.`)
|
||||||
|
- [ ] Verify CAMELEER_SERVER_LICENSE_PUBLICKEY appears in container env
|
||||||
@@ -55,15 +55,6 @@ public class LicenseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private LicenseResponse toResponse(LicenseEntity entity) {
|
private LicenseResponse toResponse(LicenseEntity entity) {
|
||||||
return new LicenseResponse(
|
return LicenseResponse.from(entity);
|
||||||
entity.getId(),
|
|
||||||
entity.getTenantId(),
|
|
||||||
entity.getTier(),
|
|
||||||
entity.getFeatures(),
|
|
||||||
entity.getLimits(),
|
|
||||||
entity.getIssuedAt(),
|
|
||||||
entity.getExpiresAt(),
|
|
||||||
entity.getToken()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,37 +8,71 @@ public final class LicenseDefaults {
|
|||||||
|
|
||||||
private LicenseDefaults() {}
|
private LicenseDefaults() {}
|
||||||
|
|
||||||
public static Map<String, Object> featuresForTier(Tier tier) {
|
public static final int DEFAULT_GRACE_PERIOD_DAYS = 14;
|
||||||
return switch (tier) {
|
public static final int DEFAULT_LICENSE_DAYS = 365;
|
||||||
case LOW -> Map.of(
|
|
||||||
"topology", true, "lineage", false,
|
|
||||||
"correlation", false, "debugger", false, "replay", false);
|
|
||||||
case MID -> Map.of(
|
|
||||||
"topology", true, "lineage", true,
|
|
||||||
"correlation", true, "debugger", false, "replay", false);
|
|
||||||
case HIGH -> Map.of(
|
|
||||||
"topology", true, "lineage", true,
|
|
||||||
"correlation", true, "debugger", true, "replay", true);
|
|
||||||
case BUSINESS -> Map.of(
|
|
||||||
"topology", true, "lineage", true,
|
|
||||||
"correlation", true, "debugger", true, "replay", true);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, Object> limitsForTier(Tier tier) {
|
public static Map<String, Integer> limitsForTier(Tier tier) {
|
||||||
return switch (tier) {
|
return switch (tier) {
|
||||||
case LOW -> Map.of(
|
case STARTER -> Map.ofEntries(
|
||||||
"max_agents", 3, "retention_days", 7,
|
Map.entry("max_environments", 2),
|
||||||
"max_environments", 1);
|
Map.entry("max_apps", 10),
|
||||||
case MID -> Map.of(
|
Map.entry("max_agents", 20),
|
||||||
"max_agents", 10, "retention_days", 30,
|
Map.entry("max_users", 5),
|
||||||
"max_environments", 2);
|
Map.entry("max_outbound_connections", 5),
|
||||||
case HIGH -> Map.of(
|
Map.entry("max_alert_rules", 10),
|
||||||
"max_agents", 50, "retention_days", 90,
|
Map.entry("max_total_cpu_millis", 8000),
|
||||||
"max_environments", -1);
|
Map.entry("max_total_memory_mb", 8192),
|
||||||
case BUSINESS -> Map.of(
|
Map.entry("max_total_replicas", 25),
|
||||||
"max_agents", -1, "retention_days", 365,
|
Map.entry("max_execution_retention_days", 7),
|
||||||
"max_environments", -1);
|
Map.entry("max_log_retention_days", 7),
|
||||||
|
Map.entry("max_metric_retention_days", 7),
|
||||||
|
Map.entry("max_jar_retention_count", 5)
|
||||||
|
);
|
||||||
|
case TEAM -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 5),
|
||||||
|
Map.entry("max_apps", 50),
|
||||||
|
Map.entry("max_agents", 100),
|
||||||
|
Map.entry("max_users", 25),
|
||||||
|
Map.entry("max_outbound_connections", 25),
|
||||||
|
Map.entry("max_alert_rules", 50),
|
||||||
|
Map.entry("max_total_cpu_millis", 32000),
|
||||||
|
Map.entry("max_total_memory_mb", 32768),
|
||||||
|
Map.entry("max_total_replicas", 100),
|
||||||
|
Map.entry("max_execution_retention_days", 30),
|
||||||
|
Map.entry("max_log_retention_days", 30),
|
||||||
|
Map.entry("max_metric_retention_days", 30),
|
||||||
|
Map.entry("max_jar_retention_count", 10)
|
||||||
|
);
|
||||||
|
case BUSINESS -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 10),
|
||||||
|
Map.entry("max_apps", 200),
|
||||||
|
Map.entry("max_agents", 500),
|
||||||
|
Map.entry("max_users", 100),
|
||||||
|
Map.entry("max_outbound_connections", 100),
|
||||||
|
Map.entry("max_alert_rules", 200),
|
||||||
|
Map.entry("max_total_cpu_millis", 128000),
|
||||||
|
Map.entry("max_total_memory_mb", 131072),
|
||||||
|
Map.entry("max_total_replicas", 500),
|
||||||
|
Map.entry("max_execution_retention_days", 90),
|
||||||
|
Map.entry("max_log_retention_days", 90),
|
||||||
|
Map.entry("max_metric_retention_days", 90),
|
||||||
|
Map.entry("max_jar_retention_count", 25)
|
||||||
|
);
|
||||||
|
case ENTERPRISE -> Map.ofEntries(
|
||||||
|
Map.entry("max_environments", 50),
|
||||||
|
Map.entry("max_apps", 1000),
|
||||||
|
Map.entry("max_agents", 5000),
|
||||||
|
Map.entry("max_users", 1000),
|
||||||
|
Map.entry("max_outbound_connections", 500),
|
||||||
|
Map.entry("max_alert_rules", 1000),
|
||||||
|
Map.entry("max_total_cpu_millis", 512000),
|
||||||
|
Map.entry("max_total_memory_mb", 524288),
|
||||||
|
Map.entry("max_total_replicas", 2000),
|
||||||
|
Map.entry("max_execution_retention_days", 365),
|
||||||
|
Map.entry("max_log_retention_days", 180),
|
||||||
|
Map.entry("max_metric_retention_days", 180),
|
||||||
|
Map.entry("max_jar_retention_count", 50)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,16 @@ public class LicenseEntity {
|
|||||||
@Column(name = "tier", nullable = false, length = 20)
|
@Column(name = "tier", nullable = false, length = 20)
|
||||||
private String tier;
|
private String tier;
|
||||||
|
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
@Column(name = "label")
|
||||||
@Column(name = "features", nullable = false, columnDefinition = "jsonb")
|
private String label;
|
||||||
private Map<String, Object> features;
|
|
||||||
|
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
@Column(name = "limits", nullable = false, columnDefinition = "jsonb")
|
@Column(name = "limits", nullable = false, columnDefinition = "jsonb")
|
||||||
private Map<String, Object> limits;
|
private Map<String, Object> limits;
|
||||||
|
|
||||||
|
@Column(name = "grace_period_days", nullable = false)
|
||||||
|
private int gracePeriodDays;
|
||||||
|
|
||||||
@Column(name = "issued_at", nullable = false)
|
@Column(name = "issued_at", nullable = false)
|
||||||
private Instant issuedAt;
|
private Instant issuedAt;
|
||||||
|
|
||||||
@@ -62,10 +64,12 @@ public class LicenseEntity {
|
|||||||
public void setTenantId(UUID tenantId) { this.tenantId = tenantId; }
|
public void setTenantId(UUID tenantId) { this.tenantId = tenantId; }
|
||||||
public String getTier() { return tier; }
|
public String getTier() { return tier; }
|
||||||
public void setTier(String tier) { this.tier = tier; }
|
public void setTier(String tier) { this.tier = tier; }
|
||||||
public Map<String, Object> getFeatures() { return features; }
|
public String getLabel() { return label; }
|
||||||
public void setFeatures(Map<String, Object> features) { this.features = features; }
|
public void setLabel(String label) { this.label = label; }
|
||||||
public Map<String, Object> getLimits() { return limits; }
|
public Map<String, Object> getLimits() { return limits; }
|
||||||
public void setLimits(Map<String, Object> limits) { this.limits = limits; }
|
public void setLimits(Map<String, Object> limits) { this.limits = limits; }
|
||||||
|
public int getGracePeriodDays() { return gracePeriodDays; }
|
||||||
|
public void setGracePeriodDays(int gracePeriodDays) { this.gracePeriodDays = gracePeriodDays; }
|
||||||
public Instant getIssuedAt() { return issuedAt; }
|
public Instant getIssuedAt() { return issuedAt; }
|
||||||
public void setIssuedAt(Instant issuedAt) { this.issuedAt = issuedAt; }
|
public void setIssuedAt(Instant issuedAt) { this.issuedAt = issuedAt; }
|
||||||
public Instant getExpiresAt() { return expiresAt; }
|
public Instant getExpiresAt() { return expiresAt; }
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public class LicenseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
|
public LicenseEntity generateLicense(TenantEntity tenant, Duration validity, UUID actorId) {
|
||||||
var features = LicenseDefaults.featuresForTier(tenant.getTier());
|
|
||||||
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
|
var limits = LicenseDefaults.limitsForTier(tenant.getTier());
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
Instant expiresAt = now.plus(validity);
|
Instant expiresAt = now.plus(validity);
|
||||||
@@ -33,10 +32,10 @@ public class LicenseService {
|
|||||||
var entity = new LicenseEntity();
|
var entity = new LicenseEntity();
|
||||||
entity.setTenantId(tenant.getId());
|
entity.setTenantId(tenant.getId());
|
||||||
entity.setTier(tenant.getTier().name());
|
entity.setTier(tenant.getTier().name());
|
||||||
entity.setFeatures(features);
|
entity.setLimits(new java.util.HashMap<>(limits));
|
||||||
entity.setLimits(limits);
|
|
||||||
entity.setIssuedAt(now);
|
entity.setIssuedAt(now);
|
||||||
entity.setExpiresAt(expiresAt);
|
entity.setExpiresAt(expiresAt);
|
||||||
|
entity.setGracePeriodDays(LicenseDefaults.DEFAULT_GRACE_PERIOD_DAYS);
|
||||||
entity.setToken(token);
|
entity.setToken(token);
|
||||||
|
|
||||||
var saved = licenseRepository.save(entity);
|
var saved = licenseRepository.save(entity);
|
||||||
@@ -75,7 +74,6 @@ public class LicenseService {
|
|||||||
.map(e -> Map.<String, Object>of(
|
.map(e -> Map.<String, Object>of(
|
||||||
"tenant_id", e.getTenantId().toString(),
|
"tenant_id", e.getTenantId().toString(),
|
||||||
"tier", e.getTier(),
|
"tier", e.getTier(),
|
||||||
"features", e.getFeatures(),
|
|
||||||
"limits", e.getLimits()
|
"limits", e.getLimits()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ public record LicenseResponse(
|
|||||||
UUID id,
|
UUID id,
|
||||||
UUID tenantId,
|
UUID tenantId,
|
||||||
String tier,
|
String tier,
|
||||||
Map<String, Object> features,
|
String label,
|
||||||
Map<String, Object> limits,
|
Map<String, Object> limits,
|
||||||
|
int gracePeriodDays,
|
||||||
Instant issuedAt,
|
Instant issuedAt,
|
||||||
Instant expiresAt,
|
Instant expiresAt,
|
||||||
String token
|
String token
|
||||||
@@ -19,7 +20,7 @@ public record LicenseResponse(
|
|||||||
public static LicenseResponse from(LicenseEntity e) {
|
public static LicenseResponse from(LicenseEntity e) {
|
||||||
return new LicenseResponse(
|
return new LicenseResponse(
|
||||||
e.getId(), e.getTenantId(), e.getTier(),
|
e.getId(), e.getTenantId(), e.getTier(),
|
||||||
e.getFeatures(), e.getLimits(),
|
e.getLabel(), e.getLimits(), e.getGracePeriodDays(),
|
||||||
e.getIssuedAt(), e.getExpiresAt(),
|
e.getIssuedAt(), e.getExpiresAt(),
|
||||||
e.getToken()
|
e.getToken()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,13 +61,14 @@ public class TenantPortalService {
|
|||||||
String name, String slug, String tier, String status,
|
String name, String slug, String tier, String status,
|
||||||
boolean serverHealthy, String serverStatus, String serverEndpoint,
|
boolean serverHealthy, String serverStatus, String serverEndpoint,
|
||||||
String licenseTier, long licenseDaysRemaining,
|
String licenseTier, long licenseDaysRemaining,
|
||||||
Map<String, Object> limits, Map<String, Object> features,
|
Map<String, Object> limits,
|
||||||
int agentCount, int environmentCount
|
int agentCount, int environmentCount
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public record LicenseData(
|
public record LicenseData(
|
||||||
UUID id, String tier, Map<String, Object> features, Map<String, Object> limits,
|
UUID id, String tier, String label, Map<String, Object> limits,
|
||||||
Instant issuedAt, Instant expiresAt, String token, long daysRemaining
|
int gracePeriodDays, Instant issuedAt, Instant expiresAt,
|
||||||
|
String token, long daysRemaining
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public record TenantSettingsData(
|
public record TenantSettingsData(
|
||||||
@@ -118,7 +119,6 @@ public class TenantPortalService {
|
|||||||
String licenseTier = null;
|
String licenseTier = null;
|
||||||
long licenseDaysRemaining = 0;
|
long licenseDaysRemaining = 0;
|
||||||
Map<String, Object> limits = Map.of();
|
Map<String, Object> limits = Map.of();
|
||||||
Map<String, Object> features = Map.of();
|
|
||||||
|
|
||||||
var licenseOpt = licenseService.getActiveLicense(tenant.getId());
|
var licenseOpt = licenseService.getActiveLicense(tenant.getId());
|
||||||
if (licenseOpt.isPresent()) {
|
if (licenseOpt.isPresent()) {
|
||||||
@@ -126,7 +126,6 @@ public class TenantPortalService {
|
|||||||
licenseTier = lic.getTier();
|
licenseTier = lic.getTier();
|
||||||
licenseDaysRemaining = daysUntil(lic.getExpiresAt());
|
licenseDaysRemaining = daysUntil(lic.getExpiresAt());
|
||||||
limits = lic.getLimits() != null ? lic.getLimits() : Map.of();
|
limits = lic.getLimits() != null ? lic.getLimits() : Map.of();
|
||||||
features = lic.getFeatures() != null ? lic.getFeatures() : Map.of();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DashboardData(
|
return new DashboardData(
|
||||||
@@ -134,7 +133,7 @@ public class TenantPortalService {
|
|||||||
tenant.getTier().name(), tenant.getStatus().name(),
|
tenant.getTier().name(), tenant.getStatus().name(),
|
||||||
serverHealthy, serverStatus, endpoint,
|
serverHealthy, serverStatus, endpoint,
|
||||||
licenseTier, licenseDaysRemaining,
|
licenseTier, licenseDaysRemaining,
|
||||||
limits, features, agentCount, environmentCount
|
limits, agentCount, environmentCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +141,9 @@ public class TenantPortalService {
|
|||||||
TenantEntity tenant = resolveTenant();
|
TenantEntity tenant = resolveTenant();
|
||||||
return licenseService.getActiveLicense(tenant.getId())
|
return licenseService.getActiveLicense(tenant.getId())
|
||||||
.map(lic -> new LicenseData(
|
.map(lic -> new LicenseData(
|
||||||
lic.getId(), lic.getTier(),
|
lic.getId(), lic.getTier(), lic.getLabel(),
|
||||||
lic.getFeatures() != null ? lic.getFeatures() : Map.of(),
|
|
||||||
lic.getLimits() != null ? lic.getLimits() : Map.of(),
|
lic.getLimits() != null ? lic.getLimits() : Map.of(),
|
||||||
|
lic.getGracePeriodDays(),
|
||||||
lic.getIssuedAt(), lic.getExpiresAt(),
|
lic.getIssuedAt(), lic.getExpiresAt(),
|
||||||
lic.getToken(), daysUntil(lic.getExpiresAt())
|
lic.getToken(), daysUntil(lic.getExpiresAt())
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class TenantEntity {
|
|||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(name = "tier", nullable = false, length = 20)
|
@Column(name = "tier", nullable = false, length = 20)
|
||||||
private Tier tier = Tier.LOW;
|
private Tier tier = Tier.STARTER;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(name = "status", nullable = false, length = 20)
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class TenantService {
|
|||||||
var entity = new TenantEntity();
|
var entity = new TenantEntity();
|
||||||
entity.setName(request.name());
|
entity.setName(request.name());
|
||||||
entity.setSlug(request.slug());
|
entity.setSlug(request.slug());
|
||||||
entity.setTier(request.tier() != null ? Tier.valueOf(request.tier()) : Tier.LOW);
|
entity.setTier(request.tier() != null ? Tier.valueOf(request.tier()) : Tier.STARTER);
|
||||||
entity.setStatus(TenantStatus.PROVISIONING);
|
entity.setStatus(TenantStatus.PROVISIONING);
|
||||||
|
|
||||||
var saved = tenantRepository.save(entity);
|
var saved = tenantRepository.save(entity);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package net.siegeln.cameleer.saas.tenant;
|
package net.siegeln.cameleer.saas.tenant;
|
||||||
|
|
||||||
public enum Tier {
|
public enum Tier {
|
||||||
LOW, MID, HIGH, BUSINESS
|
STARTER, TEAM, BUSINESS, ENTERPRISE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ public class VendorTenantController {
|
|||||||
var license = vendorTenantService.getLicenseForTenant(tenant.getId());
|
var license = vendorTenantService.getLicenseForTenant(tenant.getId());
|
||||||
if (license.isPresent() && license.get().getLimits() != null) {
|
if (license.isPresent() && license.get().getLimits() != null) {
|
||||||
var limits = license.get().getLimits();
|
var limits = license.get().getLimits();
|
||||||
if (limits.containsKey("agents")) {
|
if (limits.containsKey("max_agents")) {
|
||||||
agentLimit = ((Number) limits.get("agents")).intValue();
|
agentLimit = ((Number) limits.get("max_agents")).intValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new VendorTenantSummary(
|
return new VendorTenantSummary(
|
||||||
|
|||||||
Reference in New Issue
Block a user