diff --git a/src/app/[locale]/dashboard/edit/[id]/page.tsx b/src/app/[locale]/dashboard/edit/[id]/page.tsx new file mode 100644 index 0000000..a44f2c2 --- /dev/null +++ b/src/app/[locale]/dashboard/edit/[id]/page.tsx @@ -0,0 +1,87 @@ +import { getSessionUser, canMutate } from "@/lib/session"; +import { redirect } from "next/navigation"; +import { getTranslations } from "next-intl/server"; +import { getTenantRequestById } from "@/lib/db"; +import { OnboardingFlow } from "@/components/onboarding/onboarding-flow"; +import { BackLink } from "@/components/ui/back-link"; + +/** + * /dashboard/edit/[id] — re-opens the onboarding wizard with the + * fields of a still-pending request pre-filled (Bug 6). On submit, + * the wizard PATCHes /api/onboarding/[id] instead of POSTing to + * /api/onboarding. + * + * Hard guards + * ----------- + * - Logged-in customer owner (or platform user) only — same as the + * /dashboard/new page. + * - Request must exist, belong to the caller's org, and be in 'pending' + * status. Editing approved/provisioning rows would race against the + * operator; we redirect such cases back to the dashboard rather than + * render an invalid wizard. + * + * Pre-fill + * -------- + * The wizard takes a single `editingRequest` prop — when present, it + * (a) pre-populates state from those values and (b) targets the PATCH + * endpoint on submit. When absent, it behaves exactly as today (POST + * to /api/onboarding). + * + * Note on encrypted secrets + * ------------------------- + * Per-package secrets are NEVER decrypted server-side and exposed to + * the client (would be a clear security regression). When editing, + * the wizard opens with empty secret fields and the user re-enters + * any they want to change. If they don't touch the package-secrets + * UI, the existing encrypted blob in the DB is preserved by the + * PATCH endpoint (it only re-encrypts when the wizard sends a + * non-empty secrets payload). + */ +export default async function EditRequestPage({ + params, +}: { + params: Promise<{ id: string; locale: string }>; +}) { + const { id } = await params; + const user = await getSessionUser(); + if (!user) redirect("/login"); + if (user.isPlatform) redirect("/dashboard"); + if (!canMutate(user)) redirect("/dashboard"); + + const tr = await getTenantRequestById(id); + if (!tr) redirect("/dashboard"); + if (tr.zitadelOrgId !== user.orgId) redirect("/dashboard"); + if (tr.status !== "pending") redirect("/dashboard"); + + const t = await getTranslations("dashboard"); + const tOnboarding = await getTranslations("onboarding"); + + return ( +
+ {tOnboarding("editRequestDescription")} +
+{label}
++ {label} +
)}{t("pendingDescription")} @@ -150,12 +223,76 @@ export function ProvisioningStatus({ requestId }: { requestId: string }) {
)} + + {/* Bug 6 — owner-only edit + cancel actions while still + pending. Once admin acts, both buttons disappear (the + status branch changes). */} + {canAct && ( +{error}
+ )} + + {confirmCancel && ( ++ {t("cancelConfirmRequestDescription")} +
+{label}
++ {label} +
)}{t("rejectedDescription")}
{data.request.adminNotes && ( -- {data.request.adminNotes} -
+{error}
}+ {label} +
+ )} ++ {t("cancelledDescription")} +
+ {canAct && ( +{error}
} +{label}
++ {label} +
)}
{t("provisioningDescription")}
@@ -249,7 +460,7 @@ export function ProvisioningStatus({ requestId }: { requestId: string }) {
);
}
- // Active / Ready
+ // ─── Active / Ready ─────────────────────────────────────────────────
if (status === "active") {
return (
{label}
+ {label}
+
{t("readyDescription")}
diff --git a/src/components/onboarding/wizard.tsx b/src/components/onboarding/wizard.tsx
index 47052b6..97808a3 100644
--- a/src/components/onboarding/wizard.tsx
+++ b/src/components/onboarding/wizard.tsx
@@ -64,6 +64,35 @@ interface WizardProps {
*/
userName?: string;
userEmail?: string;
+ /**
+ * Bug 6: when present, the wizard renders in "edit" mode — fields
+ * are pre-populated from the request, the SOUL.md auto-fetch is
+ * skipped (we trust the existing values), and the submit button
+ * PATCHes /api/onboarding/[id] instead of POSTing /api/onboarding.
+ *
+ * Per-package secrets are deliberately NOT pre-filled, even if the
+ * customer originally supplied them — server-side decryption to
+ * the client would be a security regression. The user re-enters
+ * any secrets they want to change; if they leave them blank, the
+ * existing encrypted blob in the DB is preserved by the PATCH
+ * endpoint.
+ */
+ editingRequest?: {
+ id: string;
+ instanceName: string;
+ agentName: string;
+ soulMd: string;
+ agentsMd: string;
+ packages: string[];
+ billingAddress: {
+ company?: string;
+ street?: string;
+ city?: string;
+ postalCode?: string;
+ country?: string;
+ };
+ billingNotes: string;
+ };
onComplete: () => void;
}
@@ -71,6 +100,7 @@ export function OnboardingWizard({
orgName,
userName,
userEmail,
+ editingRequest,
onComplete,
}: WizardProps) {
const t = useTranslations("onboarding");
@@ -91,30 +121,55 @@ export function OnboardingWizard({
orgName,
isPersonal,
});
+ const isEditing = Boolean(editingRequest);
- const [step, setStep] = useState