diff --git a/server/lib/config.ts b/server/lib/config.ts index 13a1f6a4..b49814f0 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -89,6 +89,16 @@ export class Config { ? "true" : "false"; + process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED = parsedConfig.app + .notifications.product_updates + ? "true" + : "false"; + + process.env.NEW_RELEASES_NOTIFICATION_ENABLED = parsedConfig.app + .notifications.new_releases + ? "true" + : "false"; + if (parsedConfig.server.maxmind_db_path) { process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 571708ef..9d6cafb9 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -31,6 +31,13 @@ export const configSchema = z anonymous_usage: z.boolean().optional().default(true) }) .optional() + .default({}), + notifications: z + .object({ + product_updates: z.boolean().optional().default(true), + new_releases: z.boolean().optional().default(true) + }) + .optional() .default({}) }) .optional() @@ -40,6 +47,10 @@ export const configSchema = z log_failed_attempts: false, telemetry: { anonymous_usage: true + }, + notifications: { + product_updates: true, + new_releases: true } }), domains: z @@ -205,7 +216,10 @@ export const configSchema = z .default(["newt", "wireguard", "local"]), allow_raw_resources: z.boolean().optional().default(true), file_mode: z.boolean().optional().default(false), - pp_transport_prefix: z.string().optional().default("pp-transport-v") + pp_transport_prefix: z + .string() + .optional() + .default("pp-transport-v") }) .optional() .default({}), @@ -315,8 +329,15 @@ export const configSchema = z nameservers: z .array(z.string().optional().optional()) .optional() - .default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]), - cname_extension: z.string().optional().default("cname.pangolin.net") + .default([ + "ns1.pangolin.net", + "ns2.pangolin.net", + "ns3.pangolin.net" + ]), + cname_extension: z + .string() + .optional() + .default("cname.pangolin.net") }) .optional() .default({}) diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index 6c56d69d..be7436be 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -3,7 +3,11 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLocalStorage } from "@app/hooks/useLocalStorage"; import { cn } from "@app/lib/cn"; -import { type ProductUpdate, productUpdatesQueries } from "@app/lib/queries"; +import { + type LatestVersionResponse, + type ProductUpdate, + productUpdatesQueries +} from "@app/lib/queries"; import { useQueries } from "@tanstack/react-query"; import { ArrowRight, @@ -32,10 +36,14 @@ export default function ProductUpdates({ }: { isCollapsed?: boolean; }) { + const { env } = useEnvContext(); + const data = useQueries({ queries: [ - productUpdatesQueries.list, - productUpdatesQueries.latestVersion + productUpdatesQueries.list(env.app.notifications.product_updates), + productUpdatesQueries.latestVersion( + env.app.notifications.new_releases + ) ], combine(result) { if (result[0].isLoading || result[1].isLoading) return null; @@ -45,7 +53,6 @@ export default function ProductUpdates({ }; } }); - const { env } = useEnvContext(); const t = useTranslations(); const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false); @@ -302,15 +309,7 @@ function ProductUpdatesListPopup({ type NewVersionAvailableProps = { onDimiss: () => void; show: boolean; - version: - | Awaited< - ReturnType< - NonNullable< - typeof productUpdatesQueries.latestVersion.queryFn - > - > - >["data"] - | undefined; + version: LatestVersionResponse | null | undefined; }; function NewVersionAvailable({ diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index 7893a8c6..4b0567c8 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -21,6 +21,14 @@ const envSchema = z.object({ .transform((val) => val === "true"), APP_VERSION: z.string(), DASHBOARD_URL: z.string(), + PRODUCT_UPDATES_NOTIFICATION_ENABLED: z + .string() + .default("true") + .transform((val) => val === "true"), + NEW_RELEASES_NOTIFICATION_ENABLED: z + .string() + .default("true") + .transform((val) => val === "true"), // Email configuration EMAIL_ENABLED: z @@ -112,7 +120,11 @@ export function pullEnv(): Env { environment: env.ENVIRONMENT, sandbox_mode: env.SANDBOX_MODE, version: env.APP_VERSION, - dashboardUrl: env.DASHBOARD_URL + dashboardUrl: env.DASHBOARD_URL, + notifications: { + product_updates: env.PRODUCT_UPDATES_NOTIFICATION_ENABLED, + new_releases: env.NEW_RELEASES_NOTIFICATION_ENABLED + } }, email: { emailEnabled: env.EMAIL_ENABLED diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 7ea4b07b..3ddf32bf 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -15,47 +15,53 @@ export type ProductUpdate = { showUntil: Date; }; -export const productUpdatesQueries = { - list: queryOptions({ - queryKey: ["PRODUCT_UPDATES"] as const, - queryFn: async ({ signal }) => { - const sp = new URLSearchParams({ - build - }); - const data = await remote.get>( - `/product-updates?${sp.toString()}`, - { signal } - ); - return data.data; - }, - refetchInterval: (query) => { - if (query.state.data) { - return durationToMs(5, "minutes"); - } - return false; - } - }), - latestVersion: queryOptions({ - queryKey: ["LATEST_VERSION"] as const, - queryFn: async ({ signal }) => { - const data = await remote.get< - ResponseT<{ - pangolin: { - latestVersion: string; - releaseNotes: string; - }; - }> - >("/versions", { signal }); - return data.data; - }, - placeholderData: keepPreviousData, - refetchInterval: (query) => { - if (query.state.data) { - return durationToMs(30, "minutes"); - } - return false; - }, - enabled: build === "oss" || build === "enterprise" // disabled in cloud version - // because we don't need to listen for new versions there - }) +export type LatestVersionResponse = { + pangolin: { + latestVersion: string; + releaseNotes: string; + }; +}; + +export const productUpdatesQueries = { + list: (enabled: boolean) => + queryOptions({ + queryKey: ["PRODUCT_UPDATES"] as const, + queryFn: async ({ signal }) => { + const sp = new URLSearchParams({ + build + }); + const data = await remote.get>( + `/product-updates?${sp.toString()}`, + { signal } + ); + return data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(5, "minutes"); + } + return false; + }, + enabled + }), + latestVersion: (enabled: boolean) => + queryOptions({ + queryKey: ["LATEST_VERSION"] as const, + queryFn: async ({ signal }) => { + const data = await remote.get>( + "/versions", + { signal } + ); + return data.data; + }, + placeholderData: keepPreviousData, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "minutes"); + } + return false; + }, + enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version + // because we don't need to listen for new versions there + }) }; diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index 6ebf758a..ff2a67bf 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -4,6 +4,10 @@ export type Env = { sandbox_mode: boolean; version: string; dashboardUrl: string; + notifications: { + product_updates: boolean; + new_releases: boolean; + }; }; server: { externalPort: string;