88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
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 (
|
|
<div className="container max-w-3xl mx-auto px-4 py-8">
|
|
<div className="mb-8 animate-in">
|
|
<BackLink href="/dashboard" label={t("title")} />
|
|
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
|
{tOnboarding("editRequestTitle")}
|
|
</h1>
|
|
<p className="text-sm text-text-secondary">
|
|
{tOnboarding("editRequestDescription")}
|
|
</p>
|
|
</div>
|
|
<OnboardingFlow
|
|
orgName={user.orgName}
|
|
userName={user.name}
|
|
userEmail={user.email}
|
|
editingRequest={{
|
|
id: tr.id,
|
|
instanceName: tr.instanceName ?? "",
|
|
agentName: tr.agentName,
|
|
soulMd: tr.soulMd ?? "",
|
|
agentsMd: tr.agentsMd ?? "",
|
|
packages: tr.packages,
|
|
billingAddress: tr.billingAddress,
|
|
billingNotes: tr.billingNotes ?? "",
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|