Timestamp and registration checking
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getTranslations, getFormatter } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { listTenants } from "@/lib/k8s";
|
||||
import { getTenantRequestByOrgId } from "@/lib/db";
|
||||
@@ -7,6 +7,7 @@ import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { UsageDisplay } from "@/components/dashboard/usage-display";
|
||||
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
@@ -15,6 +16,7 @@ export default async function DashboardPage() {
|
||||
|
||||
const t = await getTranslations("dashboard");
|
||||
const tAdmin = await getTranslations("admin");
|
||||
const f = await getFormatter();
|
||||
|
||||
const allTenants = await listTenants();
|
||||
|
||||
@@ -110,9 +112,7 @@ export default async function DashboardPage() {
|
||||
{tenant.spec.packages?.join(", ") || "—"}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-xs text-text-muted tabular-nums">
|
||||
{tenant.metadata.creationTimestamp
|
||||
? new Date(tenant.metadata.creationTimestamp).toLocaleDateString()
|
||||
: "—"}
|
||||
{formatDateTime(tenant.metadata.creationTimestamp, f)}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<Link
|
||||
|
||||
@@ -44,6 +44,12 @@ export default function RegisterPage() {
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
// Localize known structured codes; fall back to server-supplied
|
||||
// English message for everything else (validation, ZITADEL errors,
|
||||
// generic 500s).
|
||||
if (data.code === "duplicate_domain" && data.domain) {
|
||||
throw new Error(t("duplicateDomain", { domain: data.domain }));
|
||||
}
|
||||
throw new Error(data.error || "Registration failed");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getTranslations, getFormatter } from "next-intl/server";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { getTenant } from "@/lib/k8s";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
@@ -7,6 +7,7 @@ import { UsageDisplay } from "@/components/dashboard/usage-display";
|
||||
import { PackageList } from "@/components/packages/package-list";
|
||||
import { WorkspaceEditor } from "@/components/packages/workspace-editor";
|
||||
import { ChannelUsers } from "@/components/channel-users/channel-users";
|
||||
import { formatDateTime, formatRelative } from "@/lib/format";
|
||||
|
||||
const CHANNEL_PACKAGES = ["telegram", "discord", "email"];
|
||||
|
||||
@@ -20,6 +21,7 @@ export default async function TenantDetailPage({
|
||||
|
||||
const { name } = await params;
|
||||
const t = await getTranslations("tenantDetail");
|
||||
const f = await getFormatter();
|
||||
|
||||
const tenant = await getTenant(name);
|
||||
if (!tenant) notFound();
|
||||
@@ -60,6 +62,18 @@ export default async function TenantDetailPage({
|
||||
{t("agent")}: {tenant.spec.agentName}
|
||||
</p>
|
||||
)}
|
||||
{tenant.metadata.creationTimestamp && (
|
||||
<p
|
||||
className="text-xs text-text-muted mt-1"
|
||||
title={formatDateTime(tenant.metadata.creationTimestamp, f)}
|
||||
>
|
||||
{t("provisioned")}{" "}
|
||||
{formatRelative(tenant.metadata.creationTimestamp, f)}{" "}
|
||||
<span className="text-text-muted/60">
|
||||
({formatDateTime(tenant.metadata.creationTimestamp, f)})
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Usage */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { registerCustomer } from "@/lib/zitadel";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
import { checkDuplicateDomain } from "@/lib/db";
|
||||
import type { RegistrationInput } from "@/types";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -53,6 +54,28 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const input: RegistrationInput = parsed.data;
|
||||
|
||||
// --- Duplicate-domain check ---
|
||||
//
|
||||
// Block if another active tenant_request or ZITADEL org already exists
|
||||
// for this corporate email domain. Public domains (gmail, gmx, etc.)
|
||||
// are exempted by checkDuplicateDomain.
|
||||
//
|
||||
// We return a structured `code: "duplicate_domain"` with the matched
|
||||
// domain so the client can render the localized message via
|
||||
// register.duplicateDomain (with {domain} interpolation). The fallback
|
||||
// English string is included for non-i18n clients (curl, monitoring).
|
||||
const dup = await checkDuplicateDomain(input.email);
|
||||
if (dup.blocked && dup.domain) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `An account for the email domain ${dup.domain} is already registered. Please contact your company administrator or PieCed IT support.`,
|
||||
code: "duplicate_domain",
|
||||
domain: dup.domain,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await registerCustomer({
|
||||
companyName: input.companyName,
|
||||
email: input.email,
|
||||
|
||||
Reference in New Issue
Block a user