81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { getSessionUser, canMutate } from "@/lib/session";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { redirect } from "next/navigation";
|
|
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
|
|
import { BackLink } from "@/components/ui/back-link";
|
|
import { listTenants } from "@/lib/k8s";
|
|
import { listActiveTenantRequestsByOrgId } from "@/lib/db";
|
|
import { personalAccountAtCapacity } from "@/lib/personal-org";
|
|
|
|
/**
|
|
* /dashboard/new — wizard for creating an additional instance for an
|
|
* existing customer. Reachable from the dashboard "+ Create new instance"
|
|
* link.
|
|
*
|
|
* Slice 3: this page is the entry point for follow-up instances. The
|
|
* first-instance case is still served inline on /dashboard. Both paths
|
|
* mount the same <OnboardingFlow>; the API resolves the difference
|
|
* server-side based on whether prior approved rows exist for the org.
|
|
*
|
|
* Platform admins are redirected to /dashboard — they shouldn't be
|
|
* creating tenant instances under their own org.
|
|
*
|
|
* Slice 5: customer-side `user` role is also redirected — only owners
|
|
* may create new instances. The server-side POST handler enforces the
|
|
* same; this redirect is purely UX so /user-role members don't land on
|
|
* a wizard that will 403 on submit.
|
|
*
|
|
* Bug 5: personal accounts that already hold a tenant or have one
|
|
* in-flight are sent back to the dashboard with the same UX rationale.
|
|
* Matching API guard lives in `/api/onboarding`.
|
|
*/
|
|
export default async function NewInstancePage() {
|
|
const user = await getSessionUser();
|
|
if (!user) redirect("/login");
|
|
if (user.isPlatform) redirect("/dashboard");
|
|
if (!canMutate(user)) redirect("/dashboard");
|
|
|
|
if (user.isPersonal) {
|
|
const [allTenants, activeRequests] = await Promise.all([
|
|
listTenants(),
|
|
listActiveTenantRequestsByOrgId(user.orgId),
|
|
]);
|
|
const ownTenants = allTenants.filter(
|
|
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
|
);
|
|
if (
|
|
personalAccountAtCapacity(
|
|
user.isPersonal,
|
|
ownTenants.length,
|
|
activeRequests.length
|
|
)
|
|
) {
|
|
redirect("/dashboard");
|
|
}
|
|
}
|
|
|
|
const t = await getTranslations("dashboard");
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-8 animate-in">
|
|
<BackLink href="/dashboard" label={t("title")} />
|
|
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
|
{t("createInstance")}
|
|
</h1>
|
|
<p className="text-text-secondary text-sm mt-4">
|
|
{t("createInstanceDescription")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="animate-in animate-in-delay-1">
|
|
<OnboardingFlow
|
|
orgName={user.orgName}
|
|
userName={user.name}
|
|
userEmail={user.email}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|