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
|
# PHASE 6: Create organization + add users
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
log "Checking for organization '$TENANT_NAME'..."
|
# No example organization created — the vendor creates tenants via the SaaS UI.
|
||||||
EXISTING_ORGS=$(api_get "/api/organizations")
|
# Users (admin, viewer) are created above but not added to any org.
|
||||||
ORG_ID=$(echo "$EXISTING_ORGS" | jq -r ".[] | select(.name == \"$TENANT_NAME\") | .id")
|
ORG_ID=""
|
||||||
|
log "Skipping example organization (tenants are created by the vendor)."
|
||||||
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
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# PHASE 7: Configure cameleer3-server OIDC
|
# PHASE 7: Configure cameleer3-server OIDC
|
||||||
@@ -795,12 +769,10 @@ fi
|
|||||||
log ""
|
log ""
|
||||||
log "=== Bootstrap complete! ==="
|
log "=== Bootstrap complete! ==="
|
||||||
# dev only — remove credential logging in production
|
# 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"
|
log " SPA Client ID: $SPA_ID"
|
||||||
if [ "$VENDOR_SEED_ENABLED" = "true" ]; then
|
if [ "$VENDOR_SEED_ENABLED" = "true" ]; then
|
||||||
log " Vendor: $VENDOR_USER / $VENDOR_PASS (role: saas-vendor)"
|
log " Vendor: $VENDOR_USER / $VENDOR_PASS (role: saas-vendor)"
|
||||||
fi
|
fi
|
||||||
log ""
|
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,
|
Sidebar,
|
||||||
TopBar,
|
TopBar,
|
||||||
} from '@cameleer/design-system';
|
} 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 { useAuth } from '../auth/useAuth';
|
||||||
import { useScopes } from '../auth/useScopes';
|
import { useScopes } from '../auth/useScopes';
|
||||||
import { useOrgStore } from '../auth/useOrganization';
|
import { useOrgStore } from '../auth/useOrganization';
|
||||||
@@ -62,12 +62,25 @@ export function Layout() {
|
|||||||
{isVendor && (
|
{isVendor && (
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<Building size={16} />}
|
icon={<Building size={16} />}
|
||||||
label="Tenants"
|
label="Vendor"
|
||||||
open={false}
|
open={onVendorRoute}
|
||||||
active={isActive(location, '/vendor/tenants')}
|
active={isActive(location, '/vendor')}
|
||||||
onToggle={() => navigate('/vendor/tenants')}
|
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>
|
</Sidebar.Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user