From e7ca7fe89cb1c240d3ed3a8cc2300e598cf0a579 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 31 Mar 2025 10:10:28 -0400 Subject: [PATCH] add toggle resource visibility closes #442 --- Dockerfile | 4 +- drizzle.config.ts | 6 +-- server/apiServer.ts | 2 +- server/db/schemas/schema.ts | 3 +- server/lib/config.ts | 6 ++- server/routers/resource/listResources.ts | 6 ++- server/routers/resource/updateResource.ts | 6 ++- server/routers/traefik/getTraefikConfig.ts | 7 ++- .../settings/resources/ResourcesTable.tsx | 36 ++++++++++++++ .../[resourceId]/ResourceInfoBox.tsx | 9 +++- .../resources/[resourceId]/general/page.tsx | 49 ++++++++++++++++++- src/app/[orgId]/settings/resources/page.tsx | 3 +- 12 files changed, 121 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index daa47e8..b0798e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN npm ci COPY . . -RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schema.ts --out init +RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init RUN npm run build @@ -16,7 +16,7 @@ FROM node:20-alpine AS runner WORKDIR /app # Curl used for the health checks -RUN apk add --no-cache curl +RUN apk add --no-cache curl COPY package.json package-lock.json ./ RUN npm ci --only=production && npm cache clean --force diff --git a/drizzle.config.ts b/drizzle.config.ts index 4336cb6..dcfc55c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,10 +4,10 @@ import path from "path"; export default defineConfig({ dialect: "sqlite", - schema: path.join("server", "db", "schema.ts"), + schema: path.join("server", "db", "schemas"), out: path.join("server", "migrations"), verbose: true, dbCredentials: { - url: path.join(APP_PATH, "db", "db.sqlite"), - }, + url: path.join(APP_PATH, "db", "db.sqlite") + } }); diff --git a/server/apiServer.ts b/server/apiServer.ts index 2ba0ab9..824a860 100644 --- a/server/apiServer.ts +++ b/server/apiServer.ts @@ -14,7 +14,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import helmet from "helmet"; -const dev = process.env.ENVIRONMENT !== "prod"; +const dev = config.isDev; const externalPort = config.getRawConfig().server.external_port; export function createApiServer() { diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 02fe02e..a862755 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -76,7 +76,8 @@ export const resources = sqliteTable("resources", { isBaseDomain: integer("isBaseDomain", { mode: "boolean" }), applyRules: integer("applyRules", { mode: "boolean" }) .notNull() - .default(false) + .default(false), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) }); export const targets = sqliteTable("targets", { diff --git a/server/lib/config.ts b/server/lib/config.ts index f73ea30..fb89f62 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -163,6 +163,8 @@ export class Config { supporterHiddenUntil: number | null = null; + isDev: boolean = process.env.ENVIRONMENT !== "prod"; + constructor() { this.loadConfig(); } @@ -245,7 +247,9 @@ export class Config { : "false"; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; - this.checkSupporterKey(); + if (!this.isDev) { + this.checkSupporterKey(); + } this.rawConfig = parsedConfig.data; } diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 7ea70c6..82e884d 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -66,7 +66,8 @@ function queryResources( whitelist: resources.emailWhitelistEnabled, http: resources.http, protocol: resources.protocol, - proxyPort: resources.proxyPort + proxyPort: resources.proxyPort, + enabled: resources.enabled }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) @@ -99,7 +100,8 @@ function queryResources( whitelist: resources.emailWhitelistEnabled, http: resources.http, protocol: resources.protocol, - proxyPort: resources.proxyPort + proxyPort: resources.proxyPort, + enabled: resources.enabled }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 1f0b738..121b34e 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -39,7 +39,8 @@ const updateHttpResourceBodySchema = z emailWhitelistEnabled: z.boolean().optional(), isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), - domainId: z.string().optional() + domainId: z.string().optional(), + enabled: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -73,7 +74,8 @@ export type UpdateResourceResponse = Resource; const updateRawResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), - proxyPort: z.number().int().min(1).max(65535).optional() + proxyPort: z.number().int().min(1).max(65535).optional(), + enabled: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 16bd155..35264c1 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -39,7 +39,8 @@ export async function traefikConfigProvider( // Org fields org: { orgId: orgs.orgId - } + }, + enabled: resources.enabled }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -136,6 +137,10 @@ export async function traefikConfigProvider( const serviceName = `${resource.resourceId}-service`; const fullDomain = `${resource.fullDomain}`; + if (!resource.enabled) { + continue; + } + if (resource.http) { if (!resource.domainId) { continue; diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 6ff9e73..63a2b41 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -30,6 +30,9 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import CopyToClipboard from "@app/components/CopyToClipboard"; +import { Switch } from "@app/components/ui/switch"; +import { AxiosResponse } from "axios"; +import { UpdateResourceResponse } from "@server/routers/resource"; export type ResourceRow = { id: number; @@ -42,6 +45,7 @@ export type ResourceRow = { http: boolean; protocol: string; proxyPort: number | null; + enabled: boolean; }; type ResourcesTableProps = { @@ -75,6 +79,26 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { }); }; + async function toggleResourceEnabled(val: boolean, resourceId: number) { + const res = await api + .post>( + `resource/${resourceId}`, + { + enabled: val + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to toggle resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + } + const columns: ColumnDef[] = [ { accessorKey: "dots", @@ -224,6 +248,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); } }, + { + accessorKey: "enabled", + header: "Enabled", + cell: ({ row }) => ( + + toggleResourceEnabled(val, row.original.id) + } + /> + ) + }, { id: "actions", cell: ({ row }) => { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx index 0ab634d..330e0b6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -12,6 +12,7 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import Link from "next/link"; +import { Switch } from "@app/components/ui/switch"; type ResourceInfoBoxType = {}; @@ -27,7 +28,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { Resource Information - + {resource.http ? ( <> @@ -88,6 +89,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { )} + + Visibilty + + {resource.enabled ? "Enabled" : "Disabled"} + + diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index b60c68d..5d6cc81 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -60,7 +60,11 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { UpdateResourceResponse } from "@server/routers/resource"; +import { + UpdateResourceResponse, + updateResourceRule +} from "@server/routers/resource"; +import { SwitchInput } from "@app/components/SwitchInput"; const GeneralFormSchema = z .object({ @@ -275,9 +279,52 @@ export default function GeneralForm() { setTransferLoading(false); } + async function toggleResourceEnabled(val: boolean) { + const res = await api + .post>( + `resource/${resource.resourceId}`, + { + enabled: val + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to toggle resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + + updateResource({ + enabled: val + }); + } + return ( !loadingPage && ( + + + Visibility + + Completely enable or disable resource visibility + + + + { + await toggleResourceEnabled(val); + }} + /> + + + diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index b08ffd6..018e9fe 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -63,7 +63,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) { resource.pincodeId !== null || resource.whitelist ? "protected" - : "not_protected" + : "not_protected", + enabled: resource.enabled }; });