feat: vendor sidebar section, remove example tenant, add Logto link
All checks were successful
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 48s

- Sidebar: Tenants moved into expandable "Vendor" section with
  sub-items for Tenants and Identity (Logto console link)
- Bootstrap: removed example organization creation (Phase 6 org)
  — tenants are now created exclusively via the vendor console
- Removed BootstrapDataSeeder (no auto-seeded tenant/license)
- Bootstrap log updated to reflect clean-slate approach

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 08:19:46 +02:00
parent f5ef8e6488
commit aa663a9c9e
3 changed files with 24 additions and 145 deletions

View File

@@ -524,36 +524,10 @@ fi
# PHASE 6: Create organization + add users
# ============================================================
log "Checking for organization '$TENANT_NAME'..."
EXISTING_ORGS=$(api_get "/api/organizations")
ORG_ID=$(echo "$EXISTING_ORGS" | jq -r ".[] | select(.name == \"$TENANT_NAME\") | .id")
if [ -n "$ORG_ID" ]; then
log "Organization exists: $ORG_ID"
else
log "Creating organization '$TENANT_NAME'..."
ORG_RESPONSE=$(api_post "/api/organizations" "{
\"name\": \"$TENANT_NAME\",
\"description\": \"Bootstrap demo tenant\"
}")
ORG_ID=$(echo "$ORG_RESPONSE" | jq -r '.id')
log "Created organization: $ORG_ID"
fi
# Add users to organization
if [ -n "$ADMIN_USER_ID" ] && [ "$ADMIN_USER_ID" != "null" ]; then
log "Adding platform owner to organization..."
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$ADMIN_USER_ID\"]}" >/dev/null 2>&1
api_put "/api/organizations/$ORG_ID/users/$ADMIN_USER_ID/roles" "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" >/dev/null 2>&1
log "Platform owner added to org with owner role."
fi
if [ -n "$TENANT_USER_ID" ] && [ "$TENANT_USER_ID" != "null" ]; then
log "Adding viewer user to organization..."
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$TENANT_USER_ID\"]}" >/dev/null 2>&1
api_put "/api/organizations/$ORG_ID/users/$TENANT_USER_ID/roles" "{\"organizationRoleIds\": [\"$ORG_VIEWER_ROLE_ID\"]}" >/dev/null 2>&1
log "Viewer user added to org with viewer role."
fi
# No example organization created — the vendor creates tenants via the SaaS UI.
# Users (admin, viewer) are created above but not added to any org.
ORG_ID=""
log "Skipping example organization (tenants are created by the vendor)."
# ============================================================
# PHASE 7: Configure cameleer3-server OIDC
@@ -795,12 +769,10 @@ fi
log ""
log "=== Bootstrap complete! ==="
# dev only — remove credential logging in production
log " Platform Owner: $SAAS_ADMIN_USER / $SAAS_ADMIN_PASS (org role: owner)"
log " Viewer: $TENANT_ADMIN_USER / $TENANT_ADMIN_PASS (org role: viewer)"
log " Tenant: $TENANT_NAME (slug: $TENANT_SLUG)"
log " Organization: $ORG_ID"
log " SPA Client ID: $SPA_ID"
if [ "$VENDOR_SEED_ENABLED" = "true" ]; then
log " Vendor: $VENDOR_USER / $VENDOR_PASS (role: saas-vendor)"
fi
log ""
log " No tenants created — use the vendor console to create tenants."
log ""

View File

@@ -1,106 +0,0 @@
package net.siegeln.cameleer.saas.config;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.siegeln.cameleer.saas.tenant.TenantEntity;
import net.siegeln.cameleer.saas.tenant.TenantRepository;
import net.siegeln.cameleer.saas.tenant.TenantStatus;
import net.siegeln.cameleer.saas.tenant.Tier;
import net.siegeln.cameleer.saas.license.LicenseEntity;
import net.siegeln.cameleer.saas.license.LicenseRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.io.File;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
@Component
public class BootstrapDataSeeder implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(BootstrapDataSeeder.class);
private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json";
private final TenantRepository tenantRepository;
private final LicenseRepository licenseRepository;
private final ObjectMapper objectMapper = new ObjectMapper();
public BootstrapDataSeeder(TenantRepository tenantRepository,
LicenseRepository licenseRepository) {
this.tenantRepository = tenantRepository;
this.licenseRepository = licenseRepository;
}
@Override
public void run(ApplicationArguments args) {
File file = new File(BOOTSTRAP_FILE);
if (!file.exists()) {
log.info("No bootstrap file found at {} — skipping data seeding", BOOTSTRAP_FILE);
return;
}
try {
JsonNode config = objectMapper.readTree(file);
String orgId = getField(config, "organizationId");
String tenantName = getField(config, "tenantName");
String tenantSlug = getField(config, "tenantSlug");
if (orgId == null || tenantSlug == null) {
log.info("Bootstrap file missing organizationId or tenantSlug — skipping");
return;
}
// Check if tenant already exists
if (tenantRepository.existsBySlug(tenantSlug)) {
log.info("Tenant '{}' already exists — skipping bootstrap seeding", tenantSlug);
return;
}
log.info("Seeding bootstrap tenant '{}'...", tenantSlug);
// Create tenant
TenantEntity tenant = new TenantEntity();
tenant.setName(tenantName != null ? tenantName : "Example Tenant");
tenant.setSlug(tenantSlug);
tenant.setTier(Tier.LOW);
tenant.setStatus(TenantStatus.ACTIVE);
tenant.setLogtoOrgId(orgId);
tenant = tenantRepository.save(tenant);
log.info("Created tenant: {} ({})", tenant.getSlug(), tenant.getId());
// Create license
LicenseEntity license = new LicenseEntity();
license.setTenantId(tenant.getId());
license.setTier("LOW");
license.setFeatures(Map.of(
"topology", true,
"lineage", false,
"correlation", false,
"debugger", false,
"replay", false
));
license.setLimits(Map.of(
"max_agents", 3,
"retention_days", 7,
"max_environments", 1
));
license.setIssuedAt(Instant.now());
license.setExpiresAt(Instant.now().plus(365, ChronoUnit.DAYS));
license.setToken("bootstrap-license");
licenseRepository.save(license);
log.info("Created license for tenant '{}'", tenantSlug);
log.info("Bootstrap data seeding complete.");
} catch (Exception e) {
log.error("Failed to seed bootstrap data: {}", e.getMessage(), e);
}
}
private String getField(JsonNode node, String field) {
return node.has(field) ? node.get(field).asText() : null;
}
}

View File

@@ -4,7 +4,7 @@ import {
Sidebar,
TopBar,
} from '@cameleer/design-system';
import { LayoutDashboard, ShieldCheck, Server, Users, Settings, KeyRound, Building } from 'lucide-react';
import { LayoutDashboard, ShieldCheck, Server, Users, Settings, KeyRound, Building, Fingerprint } from 'lucide-react';
import { useAuth } from '../auth/useAuth';
import { useScopes } from '../auth/useScopes';
import { useOrgStore } from '../auth/useOrganization';
@@ -62,12 +62,25 @@ export function Layout() {
{isVendor && (
<Sidebar.Section
icon={<Building size={16} />}
label="Tenants"
open={false}
active={isActive(location, '/vendor/tenants')}
label="Vendor"
open={onVendorRoute}
active={isActive(location, '/vendor')}
onToggle={() => navigate('/vendor/tenants')}
>
{null}
<div
style={{ padding: '6px 12px 6px 36px', fontSize: 13, cursor: 'pointer',
color: isActive(location, '/vendor/tenants') ? 'var(--text-primary)' : 'var(--text-muted)' }}
onClick={() => navigate('/vendor/tenants')}
>
Tenants
</div>
<div
style={{ padding: '6px 12px 6px 36px', fontSize: 13, cursor: 'pointer', color: 'var(--text-muted)' }}
onClick={() => window.open('/console', '_blank', 'noopener')}
>
<Fingerprint size={13} style={{ verticalAlign: 'middle', marginRight: 6 }} />
Identity (Logto)
</div>
</Sidebar.Section>
)}