feat: vendor sidebar section, remove example tenant, add Logto link
- 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:
@@ -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 ""
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user