add toggle resource visibility closes #442

This commit is contained in:
miloschwartz 2025-03-31 10:10:28 -04:00
parent fbd78ab842
commit e7ca7fe89c
No known key found for this signature in database
12 changed files with 121 additions and 16 deletions

View file

@ -7,7 +7,7 @@ RUN npm ci
COPY . . 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 RUN npm run build
@ -16,7 +16,7 @@ FROM node:20-alpine AS runner
WORKDIR /app WORKDIR /app
# Curl used for the health checks # Curl used for the health checks
RUN apk add --no-cache curl RUN apk add --no-cache curl
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force RUN npm ci --only=production && npm cache clean --force

View file

@ -4,10 +4,10 @@ import path from "path";
export default defineConfig({ export default defineConfig({
dialect: "sqlite", dialect: "sqlite",
schema: path.join("server", "db", "schema.ts"), schema: path.join("server", "db", "schemas"),
out: path.join("server", "migrations"), out: path.join("server", "migrations"),
verbose: true, verbose: true,
dbCredentials: { dbCredentials: {
url: path.join(APP_PATH, "db", "db.sqlite"), url: path.join(APP_PATH, "db", "db.sqlite")
}, }
}); });

View file

@ -14,7 +14,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod"; const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port; const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {

View file

@ -76,7 +76,8 @@ export const resources = sqliteTable("resources", {
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }), isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
applyRules: integer("applyRules", { mode: "boolean" }) applyRules: integer("applyRules", { mode: "boolean" })
.notNull() .notNull()
.default(false) .default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
}); });
export const targets = sqliteTable("targets", { export const targets = sqliteTable("targets", {

View file

@ -163,6 +163,8 @@ export class Config {
supporterHiddenUntil: number | null = null; supporterHiddenUntil: number | null = null;
isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() { constructor() {
this.loadConfig(); this.loadConfig();
} }
@ -245,7 +247,9 @@ export class Config {
: "false"; : "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
this.checkSupporterKey(); if (!this.isDev) {
this.checkSupporterKey();
}
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig.data;
} }

View file

@ -66,7 +66,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled, whitelist: resources.emailWhitelistEnabled,
http: resources.http, http: resources.http,
protocol: resources.protocol, protocol: resources.protocol,
proxyPort: resources.proxyPort proxyPort: resources.proxyPort,
enabled: resources.enabled
}) })
.from(resources) .from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))
@ -99,7 +100,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled, whitelist: resources.emailWhitelistEnabled,
http: resources.http, http: resources.http,
protocol: resources.protocol, protocol: resources.protocol,
proxyPort: resources.proxyPort proxyPort: resources.proxyPort,
enabled: resources.enabled
}) })
.from(resources) .from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))

View file

@ -39,7 +39,8 @@ const updateHttpResourceBodySchema = z
emailWhitelistEnabled: z.boolean().optional(), emailWhitelistEnabled: z.boolean().optional(),
isBaseDomain: z.boolean().optional(), isBaseDomain: z.boolean().optional(),
applyRules: z.boolean().optional(), applyRules: z.boolean().optional(),
domainId: z.string().optional() domainId: z.string().optional(),
enabled: z.boolean().optional()
}) })
.strict() .strict()
.refine((data) => Object.keys(data).length > 0, { .refine((data) => Object.keys(data).length > 0, {
@ -73,7 +74,8 @@ export type UpdateResourceResponse = Resource;
const updateRawResourceBodySchema = z const updateRawResourceBodySchema = z
.object({ .object({
name: z.string().min(1).max(255).optional(), 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() .strict()
.refine((data) => Object.keys(data).length > 0, { .refine((data) => Object.keys(data).length > 0, {

View file

@ -39,7 +39,8 @@ export async function traefikConfigProvider(
// Org fields // Org fields
org: { org: {
orgId: orgs.orgId orgId: orgs.orgId
} },
enabled: resources.enabled
}) })
.from(resources) .from(resources)
.innerJoin(sites, eq(sites.siteId, resources.siteId)) .innerJoin(sites, eq(sites.siteId, resources.siteId))
@ -136,6 +137,10 @@ export async function traefikConfigProvider(
const serviceName = `${resource.resourceId}-service`; const serviceName = `${resource.resourceId}-service`;
const fullDomain = `${resource.fullDomain}`; const fullDomain = `${resource.fullDomain}`;
if (!resource.enabled) {
continue;
}
if (resource.http) { if (resource.http) {
if (!resource.domainId) { if (!resource.domainId) {
continue; continue;

View file

@ -30,6 +30,9 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import CopyToClipboard from "@app/components/CopyToClipboard"; 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 = { export type ResourceRow = {
id: number; id: number;
@ -42,6 +45,7 @@ export type ResourceRow = {
http: boolean; http: boolean;
protocol: string; protocol: string;
proxyPort: number | null; proxyPort: number | null;
enabled: boolean;
}; };
type ResourcesTableProps = { 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<AxiosResponse<UpdateResourceResponse>>(
`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<ResourceRow>[] = [ const columns: ColumnDef<ResourceRow>[] = [
{ {
accessorKey: "dots", accessorKey: "dots",
@ -224,6 +248,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
); );
} }
}, },
{
accessorKey: "enabled",
header: "Enabled",
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
onCheckedChange={(val) =>
toggleResourceEnabled(val, row.original.id)
}
/>
)
},
{ {
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {

View file

@ -12,6 +12,7 @@ import {
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import Link from "next/link"; import Link from "next/link";
import { Switch } from "@app/components/ui/switch";
type ResourceInfoBoxType = {}; type ResourceInfoBoxType = {};
@ -27,7 +28,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
Resource Information Resource Information
</AlertTitle> </AlertTitle>
<AlertDescription className="mt-4"> <AlertDescription className="mt-4">
<InfoSections cols={3}> <InfoSections cols={4}>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection> <InfoSection>
@ -88,6 +89,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSection> </InfoSection>
</> </>
)} )}
<InfoSection>
<InfoSectionTitle>Visibilty</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.enabled ? "Enabled" : "Disabled"}</span>
</InfoSectionContent>
</InfoSection>
</InfoSections> </InfoSections>
</AlertDescription> </AlertDescription>
</Alert> </Alert>

View file

@ -60,7 +60,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } 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 const GeneralFormSchema = z
.object({ .object({
@ -275,9 +279,52 @@ export default function GeneralForm() {
setTransferLoading(false); setTransferLoading(false);
} }
async function toggleResourceEnabled(val: boolean) {
const res = await api
.post<AxiosResponse<UpdateResourceResponse>>(
`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 ( return (
!loadingPage && ( !loadingPage && (
<SettingsContainer> <SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>Visibility</SettingsSectionTitle>
<SettingsSectionDescription>
Completely enable or disable resource visibility
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="enable-resource"
label="Enable Resource"
defaultChecked={resource.enabled}
onCheckedChange={async (val) => {
await toggleResourceEnabled(val);
}}
/>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>

View file

@ -63,7 +63,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
resource.pincodeId !== null || resource.pincodeId !== null ||
resource.whitelist resource.whitelist
? "protected" ? "protected"
: "not_protected" : "not_protected",
enabled: resource.enabled
}; };
}); });