diff --git a/docker/logto-bootstrap.sh b/docker/logto-bootstrap.sh index 04f076a..b1c2fc9 100644 --- a/docker/logto-bootstrap.sh +++ b/docker/logto-bootstrap.sh @@ -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 "" diff --git a/src/main/java/net/siegeln/cameleer/saas/config/BootstrapDataSeeder.java b/src/main/java/net/siegeln/cameleer/saas/config/BootstrapDataSeeder.java deleted file mode 100644 index 96341d2..0000000 --- a/src/main/java/net/siegeln/cameleer/saas/config/BootstrapDataSeeder.java +++ /dev/null @@ -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; - } -} diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index c8d5de4..23e8340 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -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 && ( } - label="Tenants" - open={false} - active={isActive(location, '/vendor/tenants')} + label="Vendor" + open={onVendorRoute} + active={isActive(location, '/vendor')} onToggle={() => navigate('/vendor/tenants')} > - {null} +
navigate('/vendor/tenants')} + > + Tenants +
+
window.open('/console', '_blank', 'noopener')} + > + + Identity (Logto) +
)}