mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-26 18:52:41 +00:00
Control updates from the ui
This commit is contained in:
@@ -1602,6 +1602,17 @@
|
|||||||
"parsedContents": "Parsed Contents (Read Only)",
|
"parsedContents": "Parsed Contents (Read Only)",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
||||||
|
"newtAutoUpdate": "Enable Newt Auto-Update",
|
||||||
|
"newtAutoUpdateDescription": "When enabled, Newt clients will automatically update to the latest version when a new release is available.",
|
||||||
|
"newtAutoUpdateDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
||||||
|
"siteAutoUpdate": "Newt Auto-Update",
|
||||||
|
"siteAutoUpdateLabel": "Enable Auto-Update",
|
||||||
|
"siteAutoUpdateDescription": "Control whether this site's Newt client automatically updates. When not overriding, the organization default is used.",
|
||||||
|
"siteAutoUpdateOrgDefault": "Organization default: {state}",
|
||||||
|
"siteAutoUpdateOverriding": "Overriding organization setting",
|
||||||
|
"siteAutoUpdateResetToOrg": "Reset to organization default",
|
||||||
|
"siteAutoUpdateEnabled": "enabled",
|
||||||
|
"siteAutoUpdateDisabled": "disabled",
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export const orgs = pgTable("orgs", {
|
|||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: boolean("isBillingOrg"),
|
isBillingOrg: boolean("isBillingOrg"),
|
||||||
billingOrgId: varchar("billingOrgId")
|
billingOrgId: varchar("billingOrgId"),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: boolean(
|
||||||
|
"settingsEnableGlobalNewtAutoUpdate"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -103,6 +108,10 @@ 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),
|
||||||
|
autoUpdateEnabled: boolean("autoUpdateEnabled").notNull().default(false),
|
||||||
|
autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
status: varchar("status")
|
status: varchar("status")
|
||||||
.$type<"pending" | "approved">()
|
.$type<"pending" | "approved">()
|
||||||
.default("approved")
|
.default("approved")
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
||||||
billingOrgId: text("billingOrgId")
|
billingOrgId: text("billingOrgId"),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: integer(
|
||||||
|
"settingsEnableGlobalNewtAutoUpdate",
|
||||||
|
{ mode: "boolean" }
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -116,6 +122,14 @@ export const sites = sqliteTable("sites", {
|
|||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
autoUpdateEnabled: integer("autoUpdateEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
autoUpdateOverrideOrg: integer("autoUpdateOverrideOrg", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
status: text("status").$type<"pending" | "approved">().default("approved")
|
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export enum TierFeature {
|
|||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
Labels = "labels"
|
Labels = "labels",
|
||||||
|
NewtAutoUpdate = "newtAutoUpdate"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -68,5 +69,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
|
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
|
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db } from "@server/db";
|
import { db, orgs, sites } from "@server/db";
|
||||||
import { newts } from "@server/db";
|
import { newts } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
@@ -148,12 +148,13 @@ export async function getNewtVersion(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify newt credentials
|
// Verify newt credentials
|
||||||
const existingNewtRes = await db
|
const [existingNewt] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(newts)
|
||||||
.where(eq(newts.newtId, newtId));
|
.where(eq(newts.newtId, newtId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!existingNewtRes || !existingNewtRes.length) {
|
if (!existingNewt) {
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Newt version check: no newt found with ID ${newtId}. IP: ${req.ip}.`
|
`Newt version check: no newt found with ID ${newtId}. IP: ${req.ip}.`
|
||||||
@@ -164,7 +165,15 @@ export async function getNewtVersion(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingNewt = existingNewtRes[0];
|
if (!existingNewt.siteId) {
|
||||||
|
logger.warn(`Newt ${newtId} has no associated site`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"Not associated with a site"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const validSecret = await verifyPassword(
|
const validSecret = await verifyPassword(
|
||||||
secret,
|
secret,
|
||||||
@@ -181,6 +190,64 @@ export async function getNewtVersion(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if udpates are enabled for the org or the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, existingNewt.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn(`Site with ID ${existingNewt.siteId} not found`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Associated site not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, site.orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
logger.warn(`Org with ID ${site.orgId} not found`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Associated organization not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doUpdate = false;
|
||||||
|
|
||||||
|
if (site.autoUpdateOverrideOrg) {
|
||||||
|
doUpdate = site.autoUpdateEnabled;
|
||||||
|
} else {
|
||||||
|
doUpdate = org.settingsEnableGlobalNewtAutoUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doUpdate) {
|
||||||
|
// return no content http code
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
latestVersion: existingNewt.version ?? "",
|
||||||
|
currentIsLatest: true,
|
||||||
|
downloadUrl: "",
|
||||||
|
sha256: ""
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message:
|
||||||
|
"Auto-updates are disabled for this site and organization",
|
||||||
|
status: HttpCode.NO_CONTENT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch latest release info (version + asset digests) in one API call.
|
// Fetch latest release info (version + asset digests) in one API call.
|
||||||
const releaseInfo = await getLatestReleaseInfo();
|
const releaseInfo = await getLatestReleaseInfo();
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const updateOrgBodySchema = z
|
|||||||
settingsLogRetentionDaysConnection: z
|
settingsLogRetentionDaysConnection: z
|
||||||
.number()
|
.number()
|
||||||
.min(build === "saas" ? 0 : -1)
|
.min(build === "saas" ? 0 : -1)
|
||||||
.optional()
|
.optional(),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -118,6 +119,15 @@ export async function updateOrg(
|
|||||||
if (!hasPasswordExpirationFeature) {
|
if (!hasPasswordExpirationFeature) {
|
||||||
parsedBody.data.passwordExpiryDays = undefined;
|
parsedBody.data.passwordExpiryDays = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNewtAutoUpdateFeature = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
if (!hasNewtAutoUpdateFeature) {
|
||||||
|
parsedBody.data.settingsEnableGlobalNewtAutoUpdate = false; // force it off
|
||||||
|
}
|
||||||
|
|
||||||
if (build == "saas") {
|
if (build == "saas") {
|
||||||
const { tier } = await getOrgTierData(orgId);
|
const { tier } = await getOrgTierData(orgId);
|
||||||
|
|
||||||
@@ -136,8 +146,10 @@ export async function updateOrg(
|
|||||||
|
|
||||||
if (maxRetentionDays !== null) {
|
if (maxRetentionDays !== null) {
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysRequest !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysRequest !==
|
||||||
parsedBody.data.settingsLogRetentionDaysRequest > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysRequest >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -147,8 +159,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysAccess !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysAccess !==
|
||||||
parsedBody.data.settingsLogRetentionDaysAccess > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysAccess >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -158,8 +172,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysAction !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysAction !==
|
||||||
parsedBody.data.settingsLogRetentionDaysAction > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysAction >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -169,8 +185,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysConnection !==
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysConnection >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -196,7 +214,9 @@ export async function updateOrg(
|
|||||||
settingsLogRetentionDaysAction:
|
settingsLogRetentionDaysAction:
|
||||||
parsedBody.data.settingsLogRetentionDaysAction,
|
parsedBody.data.settingsLogRetentionDaysAction,
|
||||||
settingsLogRetentionDaysConnection:
|
settingsLogRetentionDaysConnection:
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection
|
parsedBody.data.settingsLogRetentionDaysConnection,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
parsedBody.data.settingsEnableGlobalNewtAutoUpdate
|
||||||
})
|
})
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, Site } from "@server/db";
|
||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, ne } from "drizzle-orm";
|
import { eq, and, ne } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -9,7 +9,8 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { isValidCIDR } from "@server/lib/validators";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
|
||||||
const updateSiteParamsSchema = z.strictObject({
|
const updateSiteParamsSchema = z.strictObject({
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive())
|
siteId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -21,18 +22,8 @@ const updateSiteBodySchema = z
|
|||||||
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", "approved"]).optional(),
|
status: z.enum(["pending", "approved"]).optional(),
|
||||||
// remoteSubnets: z.string().optional()
|
autoUpdateEnabled: z.boolean().optional(),
|
||||||
// subdomain: z
|
autoUpdateOverrideOrg: z.boolean().optional()
|
||||||
// .string()
|
|
||||||
// .min(1)
|
|
||||||
// .max(255)
|
|
||||||
// .transform((val) => val.toLowerCase())
|
|
||||||
// .optional()
|
|
||||||
// pubKey: z.string().optional(),
|
|
||||||
// subnet: z.string().optional(),
|
|
||||||
// exitNode: z.number().int().positive().optional(),
|
|
||||||
// megabytesIn: z.number().int().nonnegative().optional(),
|
|
||||||
// megabytesOut: z.number().int().nonnegative().optional(),
|
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -85,9 +76,24 @@ export async function updateSite(
|
|||||||
const { siteId } = parsedParams.data;
|
const { siteId } = parsedParams.data;
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
const [existingSite] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingSite) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Site with ID ${siteId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// if niceId is provided, check if it's already in use by another site
|
// if niceId is provided, check if it's already in use by another site
|
||||||
if (updateData.niceId) {
|
if (updateData.niceId) {
|
||||||
const [existingSite] = await db
|
const [existingSiteNiceIdOverlap] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
@@ -99,7 +105,7 @@ export async function updateSite(
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingSite) {
|
if (existingSiteNiceIdOverlap) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.CONFLICT,
|
HttpCode.CONFLICT,
|
||||||
@@ -109,6 +115,15 @@ export async function updateSite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNewtAutoUpdateFeature = await isLicensedOrSubscribed(
|
||||||
|
existingSite.orgId,
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
if (!hasNewtAutoUpdateFeature) {
|
||||||
|
parsedBody.data.autoUpdateEnabled = false; // force it off
|
||||||
|
parsedBody.data.autoUpdateOverrideOrg = false; // force it off
|
||||||
|
}
|
||||||
|
|
||||||
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
||||||
// if (updateData.remoteSubnets) {
|
// if (updateData.remoteSubnets) {
|
||||||
// const subnets = updateData.remoteSubnets
|
// const subnets = updateData.remoteSubnets
|
||||||
|
|||||||
@@ -38,11 +38,16 @@ import { useUserContext } from "@app/hooks/useUserContext";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import type { OrgContextType } from "@app/contexts/orgContext";
|
import type { OrgContextType } from "@app/contexts/orgContext";
|
||||||
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
|
||||||
// Schema for general organization settings
|
// Schema for general organization settings
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
subnet: z.string().optional()
|
subnet: z.string().optional(),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
@@ -163,17 +168,24 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
GeneralFormSchema.pick({
|
GeneralFormSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
subnet: true
|
subnet: true,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: true
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: org.name,
|
name: org.name,
|
||||||
subnet: org.subnet || "" // Add default value for subnet
|
subnet: org.subnet || "",
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
org.settingsEnableGlobalNewtAutoUpdate ?? false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const hasAutoUpdateFeature = isPaidUser(
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const [, formAction, loadingSave] = useActionState(performSave, null);
|
const [, formAction, loadingSave] = useActionState(performSave, null);
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -186,7 +198,9 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const reqData = {
|
const reqData = {
|
||||||
name: data.name
|
name: data.name,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
data.settingsEnableGlobalNewtAutoUpdate
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
// Update organization
|
// Update organization
|
||||||
@@ -194,7 +208,9 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
|
|
||||||
// Update the org context to reflect the change in the info card
|
// Update the org context to reflect the change in the info card
|
||||||
updateOrg({
|
updateOrg({
|
||||||
name: data.name
|
name: data.name,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
data.settingsEnableGlobalNewtAutoUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -243,6 +259,34 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsEnableGlobalNewtAutoUpdate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="settings-enable-global-newt-auto-update"
|
||||||
|
label={t("newtAutoUpdate")}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={
|
||||||
|
!hasAutoUpdateFeature
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{hasAutoUpdateFeature
|
||||||
|
? t("newtAutoUpdateDescription")
|
||||||
|
: t(
|
||||||
|
"newtAutoUpdateDisabledDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -36,35 +36,53 @@ import { useState } from "react";
|
|||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { Button as ButtonUI } from "@/components/ui/button";
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string().nonempty("Name is required"),
|
name: z.string().nonempty("Name is required"),
|
||||||
niceId: z.string().min(1).max(255).optional(),
|
niceId: z.string().min(1).max(255).optional(),
|
||||||
dockerSocketEnabled: z.boolean().optional()
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
|
autoUpdateEnabled: z.boolean().optional(),
|
||||||
|
autoUpdateOverrideOrg: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const { site, updateSite } = useSiteContext();
|
const { site, updateSite } = useSiteContext();
|
||||||
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const hasAutoUpdateFeature = isPaidUser(
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const orgAutoUpdate =
|
||||||
|
org.org.settingsEnableGlobalNewtAutoUpdate ?? false;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: site?.name,
|
name: site?.name,
|
||||||
niceId: site?.niceId || "",
|
niceId: site?.niceId || "",
|
||||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||||
|
autoUpdateEnabled: site?.autoUpdateOverrideOrg
|
||||||
|
? (site?.autoUpdateEnabled ?? false)
|
||||||
|
: orgAutoUpdate,
|
||||||
|
autoUpdateOverrideOrg: site?.autoUpdateOverrideOrg ?? false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
@@ -76,13 +94,17 @@ export default function GeneralPage() {
|
|||||||
await api.post(`/site/${site?.siteId}`, {
|
await api.post(`/site/${site?.siteId}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
autoUpdateEnabled: data.autoUpdateEnabled,
|
||||||
|
autoUpdateOverrideOrg: data.autoUpdateOverrideOrg
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSite({
|
updateSite({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
autoUpdateEnabled: data.autoUpdateEnabled,
|
||||||
|
autoUpdateOverrideOrg: data.autoUpdateOverrideOrg
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.niceId && data.niceId !== site?.niceId) {
|
if (data.niceId && data.niceId !== site?.niceId) {
|
||||||
@@ -217,6 +239,91 @@ export default function GeneralPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{site && site.type === "newt" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="autoUpdateEnabled"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isOverriding =
|
||||||
|
form.watch(
|
||||||
|
"autoUpdateOverrideOrg"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="auto-update-enabled"
|
||||||
|
label={t(
|
||||||
|
"siteAutoUpdateLabel"
|
||||||
|
)}
|
||||||
|
checked={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(
|
||||||
|
checked
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateOverrideOrg",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!hasAutoUpdateFeature
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{isOverriding ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
"siteAutoUpdateOverriding"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ButtonUI
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-0 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateOverrideOrg",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateEnabled",
|
||||||
|
orgAutoUpdate
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"siteAutoUpdateResetToOrg"
|
||||||
|
)}
|
||||||
|
</ButtonUI>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t(
|
||||||
|
"siteAutoUpdateOrgDefault",
|
||||||
|
{
|
||||||
|
state: orgAutoUpdate
|
||||||
|
? t(
|
||||||
|
"siteAutoUpdateEnabled"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"siteAutoUpdateDisabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
Reference in New Issue
Block a user