mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-31 15:06:42 +00:00
Add basic provisioning room v1 and update keys
This commit is contained in:
@@ -365,9 +365,17 @@
|
|||||||
"provisioningKeysNeverUsed": "Never",
|
"provisioningKeysNeverUsed": "Never",
|
||||||
"provisioningKeysEdit": "Edit Provisioning Key",
|
"provisioningKeysEdit": "Edit Provisioning Key",
|
||||||
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
|
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
|
||||||
|
"provisioningKeysApproveNewSites": "Approve new sites",
|
||||||
|
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
|
||||||
"provisioningKeysUpdateError": "Error updating provisioning key",
|
"provisioningKeysUpdateError": "Error updating provisioning key",
|
||||||
"provisioningKeysUpdated": "Provisioning key updated",
|
"provisioningKeysUpdated": "Provisioning key updated",
|
||||||
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
|
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
|
||||||
|
"provisioningKeysBannerTitle": "Site Provisioning Keys",
|
||||||
|
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
|
||||||
|
"provisioningKeysBannerButtonText": "Learn More",
|
||||||
|
"pendingSitesBannerTitle": "Pending Sites",
|
||||||
|
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
|
||||||
|
"pendingSitesBannerButtonText": "Learn More",
|
||||||
"apiKeysSettings": "{apiKeyName} Settings",
|
"apiKeysSettings": "{apiKeyName} Settings",
|
||||||
"userTitle": "Manage All Users",
|
"userTitle": "Manage All Users",
|
||||||
"userDescription": "View and manage all users in the system",
|
"userDescription": "View and manage all users in the system",
|
||||||
|
|||||||
@@ -391,7 +391,8 @@ export const siteProvisioningKeys = pgTable("siteProvisioningKeys", {
|
|||||||
lastUsed: varchar("lastUsed", { length: 255 }),
|
lastUsed: varchar("lastUsed", { length: 255 }),
|
||||||
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||||
numUsed: integer("numUsed").notNull().default(0),
|
numUsed: integer("numUsed").notNull().default(0),
|
||||||
validUntil: varchar("validUntil", { length: 255 })
|
validUntil: varchar("validUntil", { length: 255 }),
|
||||||
|
approveNewSites: boolean("approveNewSites").notNull().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteProvisioningKeyOrg = pgTable(
|
export const siteProvisioningKeyOrg = pgTable(
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const sites = pgTable("sites", {
|
|||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||||
status: varchar("status").$type<"pending" | "accepted">()
|
status: varchar("status").$type<"pending" | "approved">()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
|
|||||||
@@ -375,7 +375,10 @@ export const siteProvisioningKeys = sqliteTable("siteProvisioningKeys", {
|
|||||||
lastUsed: text("lastUsed"),
|
lastUsed: text("lastUsed"),
|
||||||
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
maxBatchSize: integer("maxBatchSize"), // null = no limit
|
||||||
numUsed: integer("numUsed").notNull().default(0),
|
numUsed: integer("numUsed").notNull().default(0),
|
||||||
validUntil: text("validUntil")
|
validUntil: text("validUntil"),
|
||||||
|
approveNewSites: integer("approveNewSites", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteProvisioningKeyOrg = sqliteTable(
|
export const siteProvisioningKeyOrg = sqliteTable(
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
status: text("status").$type<"pending" | "accepted">()
|
status: text("status").$type<"pending" | "approved">()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ const bodySchema = z
|
|||||||
z.null(),
|
z.null(),
|
||||||
z.coerce.number().int().positive().max(1_000_000)
|
z.coerce.number().int().positive().max(1_000_000)
|
||||||
]),
|
]),
|
||||||
validUntil: z.string().max(255).optional()
|
validUntil: z.string().max(255).optional(),
|
||||||
|
approveNewSites: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const v = data.validUntil;
|
const v = data.validUntil;
|
||||||
@@ -82,7 +83,7 @@ export async function createSiteProvisioningKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { name, maxBatchSize } = parsedBody.data;
|
const { name, maxBatchSize, approveNewSites } = parsedBody.data;
|
||||||
const vuRaw = parsedBody.data.validUntil;
|
const vuRaw = parsedBody.data.validUntil;
|
||||||
const validUntil =
|
const validUntil =
|
||||||
vuRaw == null || vuRaw.trim() === ""
|
vuRaw == null || vuRaw.trim() === ""
|
||||||
@@ -106,7 +107,8 @@ export async function createSiteProvisioningKey(
|
|||||||
lastUsed: null,
|
lastUsed: null,
|
||||||
maxBatchSize,
|
maxBatchSize,
|
||||||
numUsed: 0,
|
numUsed: 0,
|
||||||
validUntil
|
validUntil,
|
||||||
|
approveNewSites
|
||||||
});
|
});
|
||||||
|
|
||||||
await trx.insert(siteProvisioningKeyOrg).values({
|
await trx.insert(siteProvisioningKeyOrg).values({
|
||||||
@@ -127,7 +129,8 @@ export async function createSiteProvisioningKey(
|
|||||||
lastUsed: null,
|
lastUsed: null,
|
||||||
maxBatchSize,
|
maxBatchSize,
|
||||||
numUsed: 0,
|
numUsed: 0,
|
||||||
validUntil
|
validUntil,
|
||||||
|
approveNewSites
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ function querySiteProvisioningKeys(orgId: string) {
|
|||||||
lastUsed: siteProvisioningKeys.lastUsed,
|
lastUsed: siteProvisioningKeys.lastUsed,
|
||||||
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
||||||
numUsed: siteProvisioningKeys.numUsed,
|
numUsed: siteProvisioningKeys.numUsed,
|
||||||
validUntil: siteProvisioningKeys.validUntil
|
validUntil: siteProvisioningKeys.validUntil,
|
||||||
|
approveNewSites: siteProvisioningKeys.approveNewSites
|
||||||
})
|
})
|
||||||
.from(siteProvisioningKeyOrg)
|
.from(siteProvisioningKeyOrg)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
|
|||||||
@@ -39,16 +39,18 @@ const bodySchema = z
|
|||||||
z.coerce.number().int().positive().max(1_000_000)
|
z.coerce.number().int().positive().max(1_000_000)
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
validUntil: z.string().max(255).optional()
|
validUntil: z.string().max(255).optional(),
|
||||||
|
approveNewSites: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (
|
if (
|
||||||
data.maxBatchSize === undefined &&
|
data.maxBatchSize === undefined &&
|
||||||
data.validUntil === undefined
|
data.validUntil === undefined &&
|
||||||
|
data.approveNewSites === undefined
|
||||||
) {
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "Provide maxBatchSize and/or validUntil",
|
message: "Provide maxBatchSize and/or validUntil and/or approveNewSites",
|
||||||
path: ["maxBatchSize"]
|
path: ["maxBatchSize"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -129,6 +131,7 @@ export async function updateSiteProvisioningKey(
|
|||||||
const setValues: {
|
const setValues: {
|
||||||
maxBatchSize?: number | null;
|
maxBatchSize?: number | null;
|
||||||
validUntil?: string | null;
|
validUntil?: string | null;
|
||||||
|
approveNewSites?: boolean;
|
||||||
} = {};
|
} = {};
|
||||||
if (body.maxBatchSize !== undefined) {
|
if (body.maxBatchSize !== undefined) {
|
||||||
setValues.maxBatchSize = body.maxBatchSize;
|
setValues.maxBatchSize = body.maxBatchSize;
|
||||||
@@ -139,6 +142,9 @@ export async function updateSiteProvisioningKey(
|
|||||||
? null
|
? null
|
||||||
: new Date(Date.parse(body.validUntil)).toISOString();
|
: new Date(Date.parse(body.validUntil)).toISOString();
|
||||||
}
|
}
|
||||||
|
if (body.approveNewSites !== undefined) {
|
||||||
|
setValues.approveNewSites = body.approveNewSites;
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(siteProvisioningKeys)
|
.update(siteProvisioningKeys)
|
||||||
@@ -160,7 +166,8 @@ export async function updateSiteProvisioningKey(
|
|||||||
lastUsed: siteProvisioningKeys.lastUsed,
|
lastUsed: siteProvisioningKeys.lastUsed,
|
||||||
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
||||||
numUsed: siteProvisioningKeys.numUsed,
|
numUsed: siteProvisioningKeys.numUsed,
|
||||||
validUntil: siteProvisioningKeys.validUntil
|
validUntil: siteProvisioningKeys.validUntil,
|
||||||
|
approveNewSites: siteProvisioningKeys.approveNewSites
|
||||||
})
|
})
|
||||||
.from(siteProvisioningKeys)
|
.from(siteProvisioningKeys)
|
||||||
.where(
|
.where(
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export async function registerNewt(
|
|||||||
orgId: siteProvisioningKeyOrg.orgId,
|
orgId: siteProvisioningKeyOrg.orgId,
|
||||||
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
maxBatchSize: siteProvisioningKeys.maxBatchSize,
|
||||||
numUsed: siteProvisioningKeys.numUsed,
|
numUsed: siteProvisioningKeys.numUsed,
|
||||||
validUntil: siteProvisioningKeys.validUntil
|
validUntil: siteProvisioningKeys.validUntil,
|
||||||
|
approveNewSites: siteProvisioningKeys.approveNewSites,
|
||||||
})
|
})
|
||||||
.from(siteProvisioningKeys)
|
.from(siteProvisioningKeys)
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
@@ -197,7 +198,7 @@ export async function registerNewt(
|
|||||||
niceId,
|
niceId,
|
||||||
type: "newt",
|
type: "newt",
|
||||||
dockerSocketEnabled: true,
|
dockerSocketEnabled: true,
|
||||||
status: "pending"
|
status: keyRecord.approveNewSites ? "approved" : "pending",
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export async function createSite(
|
|||||||
address: updatedAddress || null,
|
address: updatedAddress || null,
|
||||||
type,
|
type,
|
||||||
dockerSocketEnabled: true,
|
dockerSocketEnabled: true,
|
||||||
status: "accepted"
|
status: "approved"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else if (type == "wireguard") {
|
} else if (type == "wireguard") {
|
||||||
@@ -357,7 +357,7 @@ export async function createSite(
|
|||||||
subnet,
|
subnet,
|
||||||
type,
|
type,
|
||||||
pubKey: pubKey || null,
|
pubKey: pubKey || null,
|
||||||
status: "accepted"
|
status: "approved"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else if (type == "local") {
|
} else if (type == "local") {
|
||||||
@@ -373,7 +373,7 @@ export async function createSite(
|
|||||||
dockerSocketEnabled: false,
|
dockerSocketEnabled: false,
|
||||||
online: true,
|
online: true,
|
||||||
subnet: "0.0.0.0/32",
|
subnet: "0.0.0.0/32",
|
||||||
status: "accepted"
|
status: "approved"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -137,12 +137,12 @@ const listSitesSchema = z.object({
|
|||||||
description: "Filter by online status"
|
description: "Filter by online status"
|
||||||
}),
|
}),
|
||||||
status: z
|
status: z
|
||||||
.enum(["pending", "accepted"])
|
.enum(["pending", "approved"])
|
||||||
.optional()
|
.optional()
|
||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
.openapi({
|
.openapi({
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["pending", "accepted"],
|
enum: ["pending", "approved"],
|
||||||
description: "Filter by site status"
|
description: "Filter by site status"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const updateSiteBodySchema = z
|
|||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
niceId: z.string().min(1).max(255).optional(),
|
niceId: z.string().min(1).max(255).optional(),
|
||||||
dockerSocketEnabled: z.boolean().optional(),
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
status: z.enum(["pending", "accepted"]).optional(),
|
status: z.enum(["pending", "approved"]).optional(),
|
||||||
// remoteSubnets: z.string().optional()
|
// remoteSubnets: z.string().optional()
|
||||||
// subdomain: z
|
// subdomain: z
|
||||||
// .string()
|
// .string()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SiteProvisioningKeyListItem = {
|
|||||||
maxBatchSize: number | null;
|
maxBatchSize: number | null;
|
||||||
numUsed: number;
|
numUsed: number;
|
||||||
validUntil: string | null;
|
validUntil: string | null;
|
||||||
|
approveNewSites: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListSiteProvisioningKeysResponse = {
|
export type ListSiteProvisioningKeysResponse = {
|
||||||
@@ -26,6 +27,7 @@ export type CreateSiteProvisioningKeyResponse = {
|
|||||||
maxBatchSize: number | null;
|
maxBatchSize: number | null;
|
||||||
numUsed: number;
|
numUsed: number;
|
||||||
validUntil: string | null;
|
validUntil: string | null;
|
||||||
|
approveNewSites: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateSiteProvisioningKeyResponse = {
|
export type UpdateSiteProvisioningKeyResponse = {
|
||||||
@@ -38,4 +40,5 @@ export type UpdateSiteProvisioningKeyResponse = {
|
|||||||
maxBatchSize: number | null;
|
maxBatchSize: number | null;
|
||||||
numUsed: number;
|
numUsed: number;
|
||||||
validUntil: string | null;
|
validUntil: string | null;
|
||||||
|
approveNewSites: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import SiteProvisioningKeysTable, {
|
|||||||
import { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
|
import { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import DismissableBanner from "@app/components/DismissableBanner";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { ArrowRight, Plug } from "lucide-react";
|
||||||
|
|
||||||
type ProvisioningKeysPageProps = {
|
type ProvisioningKeysPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
@@ -46,6 +50,29 @@ export default async function ProvisioningKeysPage(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<DismissableBanner
|
||||||
|
storageKey="sites-banner-dismissed"
|
||||||
|
version={1}
|
||||||
|
title={t("provisioningKeysBannerTitle")}
|
||||||
|
titleIcon={<Plug className="w-5 h-5 text-primary" />}
|
||||||
|
description={t("provisioningKeysBannerDescription")}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
{t("provisioningKeysBannerButtonText")}
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</DismissableBanner>
|
||||||
|
|
||||||
<PaidFeaturesAlert
|
<PaidFeaturesAlert
|
||||||
tiers={tierMatrix[TierFeature.SiteProvisioningKeys]}
|
tiers={tierMatrix[TierFeature.SiteProvisioningKeys]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { AxiosResponse } from "axios";
|
|||||||
import { SiteRow } from "@app/components/SitesTable";
|
import { SiteRow } from "@app/components/SitesTable";
|
||||||
import PendingSitesTable from "@app/components/PendingSitesTable";
|
import PendingSitesTable from "@app/components/PendingSitesTable";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import DismissableBanner from "@app/components/DismissableBanner";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { ArrowRight, Plug } from "lucide-react";
|
||||||
|
|
||||||
type PendingSitesPageProps = {
|
type PendingSitesPageProps = {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
@@ -69,6 +73,29 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<DismissableBanner
|
||||||
|
storageKey="sites-banner-dismissed"
|
||||||
|
version={1}
|
||||||
|
title={t("pendingSitesBannerTitle")}
|
||||||
|
titleIcon={<Plug className="w-5 h-5 text-primary" />}
|
||||||
|
description={t("pendingSitesBannerDescription")}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||||
|
>
|
||||||
|
{t("pendingSitesBannerButtonText")}
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</DismissableBanner>
|
||||||
<PendingSitesTable
|
<PendingSitesTable
|
||||||
sites={siteRows}
|
sites={siteRows}
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
@@ -78,5 +105,6 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) {
|
|||||||
pageSize: pagination.pageSize
|
pageSize: pagination.pageSize
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
|||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(await props.searchParams);
|
const searchParams = new URLSearchParams(await props.searchParams);
|
||||||
searchParams.set("status", "accepted");
|
searchParams.set("status", "approved");
|
||||||
|
|
||||||
let sites: ListSitesResponse["sites"] = [];
|
let sites: ListSitesResponse["sites"] = [];
|
||||||
let pagination: ListSitesResponse["pagination"] = {
|
let pagination: ListSitesResponse["pagination"] = {
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
.max(1_000_000, {
|
.max(1_000_000, {
|
||||||
message: t("provisioningKeysMaxBatchSizeInvalid")
|
message: t("provisioningKeysMaxBatchSizeInvalid")
|
||||||
}),
|
}),
|
||||||
validUntil: z.string().optional()
|
validUntil: z.string().optional(),
|
||||||
|
approveNewSites: z.boolean()
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const v = data.validUntil;
|
const v = data.validUntil;
|
||||||
@@ -103,7 +104,8 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
name: "",
|
name: "",
|
||||||
unlimitedBatchSize: false,
|
unlimitedBatchSize: false,
|
||||||
maxBatchSize: 100,
|
maxBatchSize: 100,
|
||||||
validUntil: ""
|
validUntil: "",
|
||||||
|
approveNewSites: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,7 +116,8 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
name: "",
|
name: "",
|
||||||
unlimitedBatchSize: false,
|
unlimitedBatchSize: false,
|
||||||
maxBatchSize: 100,
|
maxBatchSize: 100,
|
||||||
validUntil: ""
|
validUntil: "",
|
||||||
|
approveNewSites: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
@@ -123,18 +126,21 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api
|
const res = await api
|
||||||
.put<
|
.put<AxiosResponse<CreateSiteProvisioningKeyResponse>>(
|
||||||
AxiosResponse<CreateSiteProvisioningKeyResponse>
|
`/org/${orgId}/site-provisioning-key`,
|
||||||
>(`/org/${orgId}/site-provisioning-key`, {
|
{
|
||||||
name: data.name,
|
name: data.name,
|
||||||
maxBatchSize: data.unlimitedBatchSize
|
maxBatchSize: data.unlimitedBatchSize
|
||||||
? null
|
? null
|
||||||
: data.maxBatchSize,
|
: data.maxBatchSize,
|
||||||
validUntil:
|
validUntil:
|
||||||
data.validUntil == null || data.validUntil.trim() === ""
|
data.validUntil == null ||
|
||||||
|
data.validUntil.trim() === ""
|
||||||
? undefined
|
? undefined
|
||||||
: data.validUntil
|
: data.validUntil,
|
||||||
})
|
approveNewSites: data.approveNewSites
|
||||||
|
}
|
||||||
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
@@ -152,9 +158,7 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential =
|
const credential = created && created.siteProvisioningKey;
|
||||||
created &&
|
|
||||||
created.siteProvisioningKey;
|
|
||||||
|
|
||||||
const unlimitedBatchSize = form.watch("unlimitedBatchSize");
|
const unlimitedBatchSize = form.watch("unlimitedBatchSize");
|
||||||
|
|
||||||
@@ -213,15 +217,12 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
min={1}
|
min={1}
|
||||||
max={1_000_000}
|
max={1_000_000}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={
|
disabled={unlimitedBatchSize}
|
||||||
unlimitedBatchSize
|
|
||||||
}
|
|
||||||
name={field.name}
|
name={field.name}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v =
|
const v = e.target.value;
|
||||||
e.target.value;
|
|
||||||
field.onChange(
|
field.onChange(
|
||||||
v === ""
|
v === ""
|
||||||
? 100
|
? 100
|
||||||
@@ -269,9 +270,7 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
const dateTimeValue: DateTimeValue =
|
const dateTimeValue: DateTimeValue =
|
||||||
(() => {
|
(() => {
|
||||||
if (!field.value) return {};
|
if (!field.value) return {};
|
||||||
const d = new Date(
|
const d = new Date(field.value);
|
||||||
field.value
|
|
||||||
);
|
|
||||||
if (isNaN(d.getTime()))
|
if (isNaN(d.getTime()))
|
||||||
return {};
|
return {};
|
||||||
const hours = d
|
const hours = d
|
||||||
@@ -313,11 +312,7 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
value.date
|
value.date
|
||||||
);
|
);
|
||||||
if (value.time) {
|
if (value.time) {
|
||||||
const [
|
const [h, m, s] =
|
||||||
h,
|
|
||||||
m,
|
|
||||||
s
|
|
||||||
] =
|
|
||||||
value.time.split(
|
value.time.split(
|
||||||
":"
|
":"
|
||||||
);
|
);
|
||||||
@@ -352,6 +347,40 @@ export default function CreateSiteProvisioningKeyCredenza({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="approveNewSites"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start gap-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="provisioning-approve-new-sites"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(c) =>
|
||||||
|
field.onChange(
|
||||||
|
c === true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<FormLabel
|
||||||
|
htmlFor="provisioning-approve-new-sites"
|
||||||
|
className="cursor-pointer font-normal !mt-0"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"provisioningKeysApproveNewSites"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"provisioningKeysApproveNewSitesDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export type EditableSiteProvisioningKey = {
|
|||||||
name: string;
|
name: string;
|
||||||
maxBatchSize: number | null;
|
maxBatchSize: number | null;
|
||||||
validUntil: string | null;
|
validUntil: string | null;
|
||||||
|
approveNewSites: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditSiteProvisioningKeyCredenzaProps = {
|
type EditSiteProvisioningKeyCredenzaProps = {
|
||||||
@@ -76,7 +77,8 @@ export default function EditSiteProvisioningKeyCredenza({
|
|||||||
.max(1_000_000, {
|
.max(1_000_000, {
|
||||||
message: t("provisioningKeysMaxBatchSizeInvalid")
|
message: t("provisioningKeysMaxBatchSizeInvalid")
|
||||||
}),
|
}),
|
||||||
validUntil: z.string().optional()
|
validUntil: z.string().optional(),
|
||||||
|
approveNewSites: z.boolean()
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const v = data.validUntil;
|
const v = data.validUntil;
|
||||||
@@ -100,7 +102,8 @@ export default function EditSiteProvisioningKeyCredenza({
|
|||||||
name: "",
|
name: "",
|
||||||
unlimitedBatchSize: false,
|
unlimitedBatchSize: false,
|
||||||
maxBatchSize: 100,
|
maxBatchSize: 100,
|
||||||
validUntil: ""
|
validUntil: "",
|
||||||
|
approveNewSites: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +115,8 @@ export default function EditSiteProvisioningKeyCredenza({
|
|||||||
name: provisioningKey.name,
|
name: provisioningKey.name,
|
||||||
unlimitedBatchSize: provisioningKey.maxBatchSize == null,
|
unlimitedBatchSize: provisioningKey.maxBatchSize == null,
|
||||||
maxBatchSize: provisioningKey.maxBatchSize ?? 100,
|
maxBatchSize: provisioningKey.maxBatchSize ?? 100,
|
||||||
validUntil: provisioningKey.validUntil ?? ""
|
validUntil: provisioningKey.validUntil ?? "",
|
||||||
|
approveNewSites: provisioningKey.approveNewSites
|
||||||
});
|
});
|
||||||
}, [open, provisioningKey, form]);
|
}, [open, provisioningKey, form]);
|
||||||
|
|
||||||
@@ -135,7 +139,8 @@ export default function EditSiteProvisioningKeyCredenza({
|
|||||||
data.validUntil == null ||
|
data.validUntil == null ||
|
||||||
data.validUntil.trim() === ""
|
data.validUntil.trim() === ""
|
||||||
? ""
|
? ""
|
||||||
: data.validUntil
|
: data.validUntil,
|
||||||
|
approveNewSites: data.approveNewSites
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -255,6 +260,38 @@ export default function EditSiteProvisioningKeyCredenza({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="approveNewSites"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start gap-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="provisioning-edit-approve-new-sites"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(c) =>
|
||||||
|
field.onChange(c === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<FormLabel
|
||||||
|
htmlFor="provisioning-edit-approve-new-sites"
|
||||||
|
className="cursor-pointer font-normal !mt-0"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"provisioningKeysApproveNewSites"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"provisioningKeysApproveNewSitesDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="validUntil"
|
name="validUntil"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function PendingSitesTable({
|
|||||||
async function approveSite(siteId: number) {
|
async function approveSite(siteId: number) {
|
||||||
setApprovingIds((prev) => new Set(prev).add(siteId));
|
setApprovingIds((prev) => new Set(prev).add(siteId));
|
||||||
try {
|
try {
|
||||||
await api.post(`/site/${siteId}`, { status: "accepted" });
|
await api.post(`/site/${siteId}`, { status: "approved" });
|
||||||
toast({
|
toast({
|
||||||
title: t("success"),
|
title: t("success"),
|
||||||
description: t("siteApproveSuccess"),
|
description: t("siteApproveSuccess"),
|
||||||
|
|||||||
Reference in New Issue
Block a user