From 0dd5401d95bacb0ec984dc54697694b86ac5aaa2 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Mon, 23 Feb 2026 12:30:25 +0100 Subject: [PATCH] refactor(ui): make org setup idempotent with tenant external ID --- .../organizations/org-setup-form.tsx | 197 +++++++++--------- 1 file changed, 99 insertions(+), 98 deletions(-) diff --git a/ui/components/providers/organizations/org-setup-form.tsx b/ui/components/providers/organizations/org-setup-form.tsx index c3cff409a9..8051d1dd55 100644 --- a/ui/components/providers/organizations/org-setup-form.tsx +++ b/ui/components/providers/organizations/org-setup-form.tsx @@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Check, Copy, ExternalLink, Loader2 } from "lucide-react"; +import { useSession } from "next-auth/react"; import { FormEvent, useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; @@ -10,7 +11,10 @@ import { createOrganization, createOrganizationSecret, getDiscovery, + listOrganizationsByExternalId, + listOrganizationSecretsByOrganizationId, triggerDiscovery, + updateOrganizationSecret, } from "@/actions/organizations/organizations"; import { buildOrgTreeData, @@ -37,7 +41,6 @@ import { const DISCOVERY_POLL_INTERVAL_MS = 3000; const DISCOVERY_MAX_RETRIES = 60; -const DEBUG_SCOPE = "[OrgSetupForm]"; const orgSetupSchema = z.object({ organizationName: z.string().trim().optional(), @@ -77,20 +80,10 @@ export function OrgSetupForm({ onFooterChange, onPhaseChange, }: OrgSetupFormProps) { - const debugLog = (message: string, payload?: unknown) => { - if (process.env.NODE_ENV === "production") return; - - if (payload === undefined) { - console.error(`${DEBUG_SCOPE} ${message}`); - return; - } - - console.error(`${DEBUG_SCOPE} ${message}`, payload); - }; - + const { data: session } = useSession(); const [apiError, setApiError] = useState(null); const [isExternalIdCopied, setIsExternalIdCopied] = useState(false); - const [stackSetExternalId] = useState(() => generateStackSetExternalId()); + const stackSetExternalId = session?.tenantId ?? ""; const [setupPhase, setSetupPhase] = useState( ORG_SETUP_PHASE.DETAILS, ); @@ -116,10 +109,15 @@ export function OrgSetupForm({ stackSetDeployed: false, }, }); + const awsOrgIdField = register("awsOrgId", { + setValueAs: (value: unknown) => + typeof value === "string" ? value.toLowerCase() : value, + }); const awsOrgId = watch("awsOrgId") || ""; const isOrgIdValid = /^o-[a-z0-9]{10,32}$/.test(awsOrgId.trim()); const stackSetQuickLink = + stackSetExternalId && getAWSCredentialsTemplateLinks(stackSetExternalId).cloudformationQuickLink; useEffect(() => { @@ -148,7 +146,7 @@ export function OrgSetupForm({ onBack: () => setSetupPhase(ORG_SETUP_PHASE.DETAILS), showAction: true, actionLabel: "Authenticate", - actionDisabled: isSubmitting || !isValid, + actionDisabled: isSubmitting || !isValid || !stackSetExternalId, actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT, actionFormId: formId, }); @@ -159,6 +157,7 @@ export function OrgSetupForm({ isValid, onBack, onFooterChange, + stackSetExternalId, setupPhase, ]); @@ -190,44 +189,85 @@ export function OrgSetupForm({ const onSubmit = async (data: OrgSetupFormData) => { try { setApiError(null); - debugLog("Authenticate submit started", { - awsOrgId: data.awsOrgId, - hasRoleArn: Boolean(data.roleArn), - stackSetDeployed: data.stackSetDeployed, - }); const resolvedOrganizationName = data.organizationName?.trim() || data.awsOrgId; - // Step 1: Create Organization - const orgFormData = new FormData(); - orgFormData.set("name", resolvedOrganizationName); - orgFormData.set("externalId", data.awsOrgId); + // Step 1: Resolve existing organization by external_id. If missing, create it. + const existingOrganizationsResult = await listOrganizationsByExternalId( + data.awsOrgId, + ); - const orgResult = await createOrganization(orgFormData); - debugLog("createOrganization response received", { - hasError: Boolean(orgResult?.error), - hasErrorsArray: Boolean(orgResult?.errors?.length), - }); - - if (orgResult?.error) { - handleServerError(orgResult, "Organization"); + if (existingOrganizationsResult?.error) { + setApiError(existingOrganizationsResult.error); return; } - const orgId = orgResult.data.id; - setOrganization(orgId, resolvedOrganizationName, data.awsOrgId); + const existingOrganization = Array.isArray(existingOrganizationsResult?.data) + ? existingOrganizationsResult.data.find( + (organization: { + id: string; + attributes?: { external_id?: string; org_type?: string }; + }) => + organization?.attributes?.external_id === data.awsOrgId && + organization?.attributes?.org_type === "aws", + ) + : null; - // Step 2: Create Organization Secret - const secretFormData = new FormData(); - secretFormData.set("organizationId", orgId); - secretFormData.set("roleArn", data.roleArn); - secretFormData.set("externalId", stackSetExternalId); + let orgId = existingOrganization?.id as string | undefined; - const secretResult = await createOrganizationSecret(secretFormData); - debugLog("createOrganizationSecret response received", { - hasError: Boolean(secretResult?.error), - hasErrorsArray: Boolean(secretResult?.errors?.length), - }); + if (!orgId) { + const orgFormData = new FormData(); + orgFormData.set("name", resolvedOrganizationName); + orgFormData.set("externalId", data.awsOrgId); + + const orgResult = await createOrganization(orgFormData); + + if (orgResult?.error) { + handleServerError(orgResult, "Organization"); + return; + } + + orgId = orgResult.data.id; + } + + if (!orgId) { + setApiError("Unable to resolve organization ID for authentication."); + return; + } + + const organizationNameForStore = + existingOrganization?.attributes?.name ?? resolvedOrganizationName; + setOrganization(orgId, organizationNameForStore, data.awsOrgId); + + // Step 2: Create or update organization secret. + const existingSecretsResult = + await listOrganizationSecretsByOrganizationId(orgId); + + if (existingSecretsResult?.error) { + setApiError(existingSecretsResult.error); + return; + } + + const existingSecretId = + Array.isArray(existingSecretsResult?.data) && + existingSecretsResult.data.length > 0 + ? (existingSecretsResult.data[0]?.id as string | undefined) + : undefined; + + let secretResult; + if (existingSecretId) { + const patchSecretFormData = new FormData(); + patchSecretFormData.set("organizationSecretId", existingSecretId); + patchSecretFormData.set("roleArn", data.roleArn); + patchSecretFormData.set("externalId", stackSetExternalId); + secretResult = await updateOrganizationSecret(patchSecretFormData); + } else { + const createSecretFormData = new FormData(); + createSecretFormData.set("organizationId", orgId); + createSecretFormData.set("roleArn", data.roleArn); + createSecretFormData.set("externalId", stackSetExternalId); + secretResult = await createOrganizationSecret(createSecretFormData); + } if (secretResult?.error) { handleServerError(secretResult, "Secret"); @@ -236,10 +276,6 @@ export function OrgSetupForm({ // Step 3: Trigger Discovery const discoveryResult = await triggerDiscovery(orgId); - debugLog("triggerDiscovery response received", { - hasError: Boolean(discoveryResult?.error), - discoveryId: discoveryResult?.data?.id ?? null, - }); if (discoveryResult?.error) { setApiError(discoveryResult.error); @@ -254,7 +290,6 @@ export function OrgSetupForm({ ); if (!resolvedDiscoveryResult) { - debugLog("pollDiscoveryResult returned null (authentication failed)"); return; } @@ -266,12 +301,8 @@ export function OrgSetupForm({ setSelectedAccountIds(selectableAccountIds); // Discovery succeeded; advance to next wizard step. - debugLog("Authenticate flow succeeded, advancing to next step", { - discoveryId, - }); onNext(); - } catch (error) { - console.error(`${DEBUG_SCOPE} Unexpected authenticate error`, error); + } catch { setApiError( "Authentication failed. Please verify the StackSet deployment and Role ARN, then try again.", ); @@ -283,19 +314,9 @@ export function OrgSetupForm({ discoveryId: string, ): Promise => { for (let attempt = 0; attempt < DISCOVERY_MAX_RETRIES; attempt += 1) { - debugLog("Polling discovery status", { - attempt: attempt + 1, - maxAttempts: DISCOVERY_MAX_RETRIES, - organizationId, - discoveryId, - }); const result = await getDiscovery(organizationId, discoveryId); if (result?.error) { - console.error(`${DEBUG_SCOPE} getDiscovery returned error`, { - attempt: attempt + 1, - error: result.error, - }); setApiError( `Authentication failed. Please verify the StackSet deployment and Role ARN, then try again. ${result.error}`, ); @@ -303,22 +324,13 @@ export function OrgSetupForm({ } const status = result.data.attributes.status; - debugLog("Discovery status response", { - attempt: attempt + 1, - status, - }); if (status === DISCOVERY_STATUS.SUCCEEDED) { - debugLog("Discovery succeeded"); return result.data.attributes.result as DiscoveryResult; } if (status === DISCOVERY_STATUS.FAILED) { const backendError = result.data.attributes.error; - console.error(`${DEBUG_SCOPE} Discovery failed`, { - attempt: attempt + 1, - backendError, - }); setApiError( backendError ? `Authentication failed. Please verify the StackSet deployment and Role ARN, then try again. ${backendError}` @@ -335,10 +347,6 @@ export function OrgSetupForm({ setApiError( "Authentication timed out. Please verify the credentials and try again.", ); - console.error(`${DEBUG_SCOPE} Discovery polling timed out`, { - maxAttempts: DISCOVERY_MAX_RETRIES, - pollIntervalMs: DISCOVERY_POLL_INTERVAL_MS, - }); return null; }; @@ -349,11 +357,6 @@ export function OrgSetupForm({ }, context: string, ) => { - console.error(`${DEBUG_SCOPE} handleServerError`, { - context, - error: result.error ?? null, - errors: result.errors ?? [], - }); if (result.errors?.length) { for (const err of result.errors) { const pointer = err.source?.pointer ?? ""; @@ -401,7 +404,7 @@ export function OrgSetupForm({ )} - {setupPhase === ORG_SETUP_PHASE.ACCESS && !isSubmitting && ( + {setupPhase === ORG_SETUP_PHASE.ACCESS && (
@@ -440,7 +443,16 @@ export function OrgSetupForm({ placeholder="e.g. o-123456789-abcdefg" required aria-required="true" - {...register("awsOrgId")} + autoCapitalize="none" + autoCorrect="off" + spellCheck={false} + {...awsOrgIdField} + onInput={(event) => { + const loweredValue = event.currentTarget.value.toLowerCase(); + if (event.currentTarget.value !== loweredValue) { + event.currentTarget.value = loweredValue; + } + }} /> {errors.awsOrgId && ( @@ -481,10 +493,11 @@ export function OrgSetupForm({ variant="outline" size="lg" className="border-border-input-primary bg-bg-input-primary text-button-tertiary hover:bg-bg-input-primary active:bg-bg-input-primary h-12 w-full justify-start" + disabled={!stackSetQuickLink} asChild > @@ -507,10 +520,11 @@ export function OrgSetupForm({
- {stackSetExternalId} + {stackSetExternalId || "Loading organization external ID..."}