diff --git a/server/lib/config.ts b/server/lib/config.ts index 2f5b5e99..a071a955 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -95,6 +95,10 @@ export class Config { ? "true" : "false"; + process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.disable_clients + ? "true" + : "false"; + this.rawConfig = parsedConfig; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 5bce0db1..f07e5628 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -225,7 +225,8 @@ export const configSchema = z enable_redis: z.boolean().optional(), disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), - disable_config_managed_domains: z.boolean().optional() + disable_config_managed_domains: z.boolean().optional(), + enable_clients: z.boolean().optional() }) .optional() }) diff --git a/server/middlewares/verifyClientsEnabled.ts b/server/middlewares/verifyClientsEnabled.ts new file mode 100644 index 00000000..2b573dba --- /dev/null +++ b/server/middlewares/verifyClientsEnabled.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import config from "@server/lib/config"; + +export async function verifyClientsEnabled( + req: Request, + res: Response, + next: NextFunction +) { + try { + if (!config.getRawConfig().flags?.enable_redis) { + return next( + createHttpError( + HttpCode.NOT_IMPLEMENTED, + "Clients are not enabled on this server." + ) + ); + } + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to check if clients are enabled" + ) + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index b0980fa5..946dfd88 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -41,6 +41,7 @@ import { createNewt, getNewtToken } from "./newt"; import { getOlmToken } from "./olm"; import rateLimit from "express-rate-limit"; import createHttpError from "http-errors"; +import { verifyClientsEnabled } from "@server/middlewares/verifyClientsEnabled"; // Root routes export const unauthenticated = Router(); @@ -116,6 +117,7 @@ authenticated.get( authenticated.get( "/org/:orgId/pick-client-defaults", + verifyClientsEnabled, verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), client.pickClientDefaults @@ -123,6 +125,7 @@ authenticated.get( authenticated.get( "/org/:orgId/clients", + verifyClientsEnabled, verifyOrgAccess, verifyUserHasAction(ActionsEnum.listClients), client.listClients @@ -130,6 +133,7 @@ authenticated.get( authenticated.get( "/org/:orgId/client/:clientId", + verifyClientsEnabled, verifyOrgAccess, verifyUserHasAction(ActionsEnum.getClient), client.getClient @@ -137,6 +141,7 @@ authenticated.get( authenticated.put( "/org/:orgId/client", + verifyClientsEnabled, verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), client.createClient @@ -144,6 +149,7 @@ authenticated.put( authenticated.delete( "/client/:clientId", + verifyClientsEnabled, verifyClientAccess, verifyUserHasAction(ActionsEnum.deleteClient), client.deleteClient @@ -151,6 +157,7 @@ authenticated.delete( authenticated.post( "/client/:clientId", + verifyClientsEnabled, verifyClientAccess, // this will check if the user has access to the client verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client client.updateClient diff --git a/src/app/[orgId]/settings/clients/layout.tsx b/src/app/[orgId]/settings/clients/layout.tsx new file mode 100644 index 00000000..59a46414 --- /dev/null +++ b/src/app/[orgId]/settings/clients/layout.tsx @@ -0,0 +1,21 @@ +import { redirect } from "next/navigation"; +import { pullEnv } from "@app/lib/pullEnv"; + +export const dynamic = "force-dynamic"; + +interface SettingsLayoutProps { + children: React.ReactNode; + params: Promise<{ orgId: string }>; +} + +export default async function SettingsLayout(props: SettingsLayoutProps) { + const params = await props.params; + const { children } = props; + const env = pullEnv(); + + if (!env.flags.enableClients) { + redirect(`/${params.orgId}/settings`); + } + + return children; +} diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index a05a2718..3d63c9ed 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -19,7 +19,8 @@ import UserProvider from "@app/providers/UserProvider"; import { Layout } from "@app/components/Layout"; import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav"; import { orgNavItems } from "@app/app/navigation"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; +import { pullEnv } from "@app/lib/pullEnv"; export const dynamic = "force-dynamic"; @@ -28,39 +29,6 @@ export const metadata: Metadata = { description: "" }; -const topNavItems = [ - { - title: "Sites", - href: "/{orgId}/settings/sites", - icon: - }, - { - title: "Resources", - href: "/{orgId}/settings/resources", - icon: - }, - { - title: "Clients", - href: "/{orgId}/settings/clients", - icon: - }, - { - title: "Users & Roles", - href: "/{orgId}/settings/access", - icon: - }, - { - title: "Shareable Links", - href: "/{orgId}/settings/share-links", - icon: - }, - { - title: "General", - href: "/{orgId}/settings/general", - icon: - } -]; - interface SettingsLayoutProps { children: React.ReactNode; params: Promise<{ orgId: string }>; @@ -74,6 +42,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const getUser = cache(verifySession); const user = await getUser(); + const env = pullEnv(); + if (!user) { redirect(`/`); } @@ -92,7 +62,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const orgUser = await getOrgUser(); if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) { - throw new Error(t('userErrorNotAdminOrOwner')); + throw new Error(t("userErrorNotAdminOrOwner")); } } catch { redirect(`/${params.orgId}`); @@ -112,6 +82,21 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { } } catch (e) {} + if (env.flags.enableClients) { + const existing = orgNavItems.find( + (item) => item.title === "sidebarClients" + ); + if (!existing) { + const clientsNavItem = { + title: "sidebarClients", + href: "/{orgId}/settings/clients", + icon: + }; + + orgNavItems.splice(1, 0, clientsNavItem); + } + } + return ( diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 59cccefd..37d12306 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -39,11 +39,6 @@ export const orgNavItems: SidebarNavItem[] = [ href: "/{orgId}/settings/resources", icon: }, - { - title: "sidebarClients", - href: "/{orgId}/settings/clients", - icon: - }, { title: "sidebarAccessControl", href: "/{orgId}/settings/access", diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index fea07e22..71dcbe68 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -45,7 +45,9 @@ export function pullEnv(): Env { disableBasicWireguardSites: process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES === "true" ? true - : false + : false, + enableClients: + process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false } }; } diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index 5e4599aa..ab2dd66c 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -24,5 +24,6 @@ export type Env = { allowBaseDomainResources: boolean; disableLocalSites: boolean; disableBasicWireguardSites: boolean; + enableClients: boolean; }; };