toggle clients with feature flag

This commit is contained in:
miloschwartz 2025-06-26 15:09:16 -04:00
parent 7bf9cccbf6
commit 8f1cfd8037
No known key found for this signature in database
9 changed files with 87 additions and 42 deletions

View file

@ -95,6 +95,10 @@ export class Config {
? "true" ? "true"
: "false"; : "false";
process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.disable_clients
? "true"
: "false";
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
} }

View file

@ -225,7 +225,8 @@ export const configSchema = z
enable_redis: z.boolean().optional(), enable_redis: z.boolean().optional(),
disable_local_sites: z.boolean().optional(), disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_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() .optional()
}) })

View file

@ -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"
)
);
}
}

View file

@ -41,6 +41,7 @@ import { createNewt, getNewtToken } from "./newt";
import { getOlmToken } from "./olm"; import { getOlmToken } from "./olm";
import rateLimit from "express-rate-limit"; import rateLimit from "express-rate-limit";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { verifyClientsEnabled } from "@server/middlewares/verifyClientsEnabled";
// Root routes // Root routes
export const unauthenticated = Router(); export const unauthenticated = Router();
@ -116,6 +117,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/pick-client-defaults", "/org/:orgId/pick-client-defaults",
verifyClientsEnabled,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createClient), verifyUserHasAction(ActionsEnum.createClient),
client.pickClientDefaults client.pickClientDefaults
@ -123,6 +125,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/clients", "/org/:orgId/clients",
verifyClientsEnabled,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listClients), verifyUserHasAction(ActionsEnum.listClients),
client.listClients client.listClients
@ -130,6 +133,7 @@ authenticated.get(
authenticated.get( authenticated.get(
"/org/:orgId/client/:clientId", "/org/:orgId/client/:clientId",
verifyClientsEnabled,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getClient), verifyUserHasAction(ActionsEnum.getClient),
client.getClient client.getClient
@ -137,6 +141,7 @@ authenticated.get(
authenticated.put( authenticated.put(
"/org/:orgId/client", "/org/:orgId/client",
verifyClientsEnabled,
verifyOrgAccess, verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createClient), verifyUserHasAction(ActionsEnum.createClient),
client.createClient client.createClient
@ -144,6 +149,7 @@ authenticated.put(
authenticated.delete( authenticated.delete(
"/client/:clientId", "/client/:clientId",
verifyClientsEnabled,
verifyClientAccess, verifyClientAccess,
verifyUserHasAction(ActionsEnum.deleteClient), verifyUserHasAction(ActionsEnum.deleteClient),
client.deleteClient client.deleteClient
@ -151,6 +157,7 @@ authenticated.delete(
authenticated.post( authenticated.post(
"/client/:clientId", "/client/:clientId",
verifyClientsEnabled,
verifyClientAccess, // this will check if the user has access to the client 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 verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
client.updateClient client.updateClient

View file

@ -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;
}

View file

@ -19,7 +19,8 @@ import UserProvider from "@app/providers/UserProvider";
import { Layout } from "@app/components/Layout"; import { Layout } from "@app/components/Layout";
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav"; import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
import { orgNavItems } from "@app/app/navigation"; 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"; export const dynamic = "force-dynamic";
@ -28,39 +29,6 @@ export const metadata: Metadata = {
description: "" description: ""
}; };
const topNavItems = [
{
title: "Sites",
href: "/{orgId}/settings/sites",
icon: <Combine className="h-4 w-4" />
},
{
title: "Resources",
href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" />
},
{
title: "Clients",
href: "/{orgId}/settings/clients",
icon: <Workflow className="h-4 w-4" />
},
{
title: "Users & Roles",
href: "/{orgId}/settings/access",
icon: <Users className="h-4 w-4" />
},
{
title: "Shareable Links",
href: "/{orgId}/settings/share-links",
icon: <LinkIcon className="h-4 w-4" />
},
{
title: "General",
href: "/{orgId}/settings/general",
icon: <Settings className="h-4 w-4" />
}
];
interface SettingsLayoutProps { interface SettingsLayoutProps {
children: React.ReactNode; children: React.ReactNode;
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@ -74,6 +42,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const getUser = cache(verifySession); const getUser = cache(verifySession);
const user = await getUser(); const user = await getUser();
const env = pullEnv();
if (!user) { if (!user) {
redirect(`/`); redirect(`/`);
} }
@ -92,7 +62,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const orgUser = await getOrgUser(); const orgUser = await getOrgUser();
if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) { if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) {
throw new Error(t('userErrorNotAdminOrOwner')); throw new Error(t("userErrorNotAdminOrOwner"));
} }
} catch { } catch {
redirect(`/${params.orgId}`); redirect(`/${params.orgId}`);
@ -112,6 +82,21 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
} }
} catch (e) {} } 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: <Workflow className="h-4 w-4" />
};
orgNavItems.splice(1, 0, clientsNavItem);
}
}
return ( return (
<UserProvider user={user}> <UserProvider user={user}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}> <Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>

View file

@ -39,11 +39,6 @@ export const orgNavItems: SidebarNavItem[] = [
href: "/{orgId}/settings/resources", href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" /> icon: <Waypoints className="h-4 w-4" />
}, },
{
title: "sidebarClients",
href: "/{orgId}/settings/clients",
icon: <Workflow className="h-4 w-4" />
},
{ {
title: "sidebarAccessControl", title: "sidebarAccessControl",
href: "/{orgId}/settings/access", href: "/{orgId}/settings/access",

View file

@ -45,7 +45,9 @@ export function pullEnv(): Env {
disableBasicWireguardSites: disableBasicWireguardSites:
process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES === "true" process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES === "true"
? true ? true
: false : false,
enableClients:
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false
} }
}; };
} }

View file

@ -24,5 +24,6 @@ export type Env = {
allowBaseDomainResources: boolean; allowBaseDomainResources: boolean;
disableLocalSites: boolean; disableLocalSites: boolean;
disableBasicWireguardSites: boolean; disableBasicWireguardSites: boolean;
enableClients: boolean;
}; };
}; };