From 3b9beaeae539666d59343b01d828e77906a08b90 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 28 Oct 2025 23:10:00 +1000 Subject: [PATCH] Various tweaks and backend improvements --- backend/internal/certificate.js | 51 ++++++++++++++----- backend/models/certificate.js | 12 +++++ frontend/src/api/backend/expansions.ts | 2 +- .../Formatter/CertificateInUseFormatter.tsx | 29 +++++++++-- .../Table/Formatter/EventFormatter.tsx | 3 +- frontend/src/hooks/useDeadHost.ts | 1 + frontend/src/hooks/useProxyHost.ts | 1 + frontend/src/hooks/useRedirectionHost.ts | 1 + frontend/src/hooks/useStream.ts | 1 + frontend/src/pages/Certificates/Table.tsx | 1 + .../src/pages/Certificates/TableWrapper.tsx | 1 + frontend/src/pages/Dashboard/index.tsx | 1 - 12 files changed, 85 insertions(+), 19 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 77ebe407..ca02e2d9 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -24,7 +24,7 @@ const certbotLogsDir = "/data/logs"; const certbotWorkDir = "/tmp/letsencrypt-lib"; const omissions = () => { - return ["is_deleted", "owner.is_deleted"]; + return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"]; }; const internalCertificate = { @@ -122,7 +122,7 @@ const internalCertificate = { } // this command really should clean up and delete the cert if it can't fully succeed - const certificate = await certificateModel.query().insertAndFetch(data).then(utils.omitRow(omissions())); + const certificate = await certificateModel.query().insertAndFetch(data); try { if (certificate.provider === "letsencrypt") { @@ -202,6 +202,9 @@ const internalCertificate = { savedRow.meta = _.assign({}, savedRow.meta, { letsencrypt_certificate: certInfo, }); + + await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); + return savedRow; } catch (err) { // Delete the certificate from the database if it was not created successfully @@ -218,14 +221,18 @@ const internalCertificate = { data.meta = _.assign({}, data.meta || {}, certificate.meta); // Add to audit log + await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); + + return utils.omitRow(omissions())(certificate); + }, + + addCreatedAuditLog: async (access, certificate_id, meta) => { await internalAuditLog.add(access, { action: "created", object_type: "certificate", - object_id: certificate.id, - meta: data, + object_id: certificate_id, + meta: meta, }); - - return certificate; }, /** @@ -285,10 +292,7 @@ const internalCertificate = { .query() .where("is_deleted", 0) .andWhere("id", data.id) - .allowGraph("[owner]") - .allowGraph("[proxy_hosts]") - .allowGraph("[redirection_hosts]") - .allowGraph("[dead_hosts]") + .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") .first(); if (accessData.permission_visibility !== "all") { @@ -305,7 +309,24 @@ const internalCertificate = { } // Custom omissions if (typeof data.omit !== "undefined" && data.omit !== null) { - return _.omit(row, data.omit); + return _.omit(row, [...data.omit]); + } + + return internalCertificate.cleanExpansions(row); + }, + + cleanExpansions: (row) => { + if (typeof row.proxy_hosts !== "undefined") { + row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts); + } + if (typeof row.redirection_hosts !== "undefined") { + row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts); + } + if (typeof row.dead_hosts !== "undefined") { + row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts); + } + if (typeof row.streams !== "undefined") { + row.streams = utils.omitRows(["is_deleted"])(row.streams); } return row; }, @@ -415,7 +436,7 @@ const internalCertificate = { .query() .where("is_deleted", 0) .groupBy("id") - .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts]") + .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]") .orderBy("nice_name", "ASC"); if (accessData.permission_visibility !== "all") { @@ -433,7 +454,11 @@ const internalCertificate = { query.withGraphFetched(`[${expand.join(", ")}]`); } - return await query.then(utils.omitRows(omissions())); + const r = await query.then(utils.omitRows(omissions())); + for (let i = 0; i < r.length; i++) { + r[i] = internalCertificate.cleanExpansions(r[i]); + } + return r; }, /** diff --git a/backend/models/certificate.js b/backend/models/certificate.js index 052d3187..9ad03c89 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -8,6 +8,7 @@ import deadHostModel from "./dead_host.js"; import now from "./now_helper.js"; import proxyHostModel from "./proxy_host.js"; import redirectionHostModel from "./redirection_host.js"; +import streamModel from "./stream.js"; import userModel from "./user.js"; Model.knex(db); @@ -114,6 +115,17 @@ class Certificate extends Model { qb.where("redirection_host.is_deleted", 0); }, }, + streams: { + relation: Model.HasManyRelation, + modelClass: streamModel, + join: { + from: "certificate.id", + to: "stream.certificate_id", + }, + modify: (qb) => { + qb.where("stream.is_deleted", 0); + }, + }, }; } } diff --git a/frontend/src/api/backend/expansions.ts b/frontend/src/api/backend/expansions.ts index 2f31e4d0..e098a490 100644 --- a/frontend/src/api/backend/expansions.ts +++ b/frontend/src/api/backend/expansions.ts @@ -1,6 +1,6 @@ export type AccessListExpansion = "owner" | "items" | "clients"; export type AuditLogExpansion = "user"; -export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; +export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts" | "streams"; export type HostExpansion = "owner" | "certificate"; export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; export type UserExpansion = "permissions"; diff --git a/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx index bb4c314f..7a9c592d 100644 --- a/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx +++ b/frontend/src/components/Table/Formatter/CertificateInUseFormatter.tsx @@ -1,6 +1,6 @@ import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import Popover from "react-bootstrap/Popover"; -import type { DeadHost, ProxyHost, RedirectionHost } from "src/api/backend"; +import type { DeadHost, ProxyHost, RedirectionHost, Stream } from "src/api/backend"; import { T } from "src/locale"; const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => { @@ -23,13 +23,34 @@ const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | Dead ); }; +const getSectionStream = (items: Stream[]) => { + if (items.length === 0) { + return null; + } + return ( + <> +
+ + + +
+ {items.map((stream) => ( +
+ {stream.forwardingHost}:{stream.forwardingPort} +
+ ))} + + ); +}; + interface Props { proxyHosts: ProxyHost[]; redirectionHosts: RedirectionHost[]; deadHosts: DeadHost[]; + streams: Stream[]; } -export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts }: Props) { - const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length; +export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }: Props) { + const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length + streams?.length; if (totalCount === 0) { return ( @@ -41,6 +62,7 @@ export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHo proxyHosts.sort(); redirectionHosts.sort(); deadHosts.sort(); + streams.sort(); const popover = ( @@ -48,6 +70,7 @@ export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHo {getSection("proxy-hosts", proxyHosts)} {getSection("redirection-hosts", redirectionHosts)} {getSection("dead-hosts", deadHosts)} + {getSectionStream(streams)} ); diff --git a/frontend/src/components/Table/Formatter/EventFormatter.tsx b/frontend/src/components/Table/Formatter/EventFormatter.tsx index 806f2200..f6f7787d 100644 --- a/frontend/src/components/Table/Formatter/EventFormatter.tsx +++ b/frontend/src/components/Table/Formatter/EventFormatter.tsx @@ -1,4 +1,5 @@ import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; +import cn from "classnames"; import type { AuditLog } from "src/api/backend"; import { DateTimeFormat, T } from "src/locale"; @@ -32,7 +33,7 @@ const getColorForAction = (action: string) => { }; const getIcon = (row: AuditLog) => { - const c = getColorForAction(row.action); + const c = cn(getColorForAction(row.action), "me-1"); let ico = null; switch (row.objectType) { case "user": diff --git a/frontend/src/hooks/useDeadHost.ts b/frontend/src/hooks/useDeadHost.ts index 9f0a1f90..dd8355e3 100644 --- a/frontend/src/hooks/useDeadHost.ts +++ b/frontend/src/hooks/useDeadHost.ts @@ -52,6 +52,7 @@ const useSetDeadHost = () => { queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); }, }); }; diff --git a/frontend/src/hooks/useProxyHost.ts b/frontend/src/hooks/useProxyHost.ts index b36c52f3..e6a2adea 100644 --- a/frontend/src/hooks/useProxyHost.ts +++ b/frontend/src/hooks/useProxyHost.ts @@ -59,6 +59,7 @@ const useSetProxyHost = () => { queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); }, }); }; diff --git a/frontend/src/hooks/useRedirectionHost.ts b/frontend/src/hooks/useRedirectionHost.ts index 342fe562..ff212653 100644 --- a/frontend/src/hooks/useRedirectionHost.ts +++ b/frontend/src/hooks/useRedirectionHost.ts @@ -63,6 +63,7 @@ const useSetRedirectionHost = () => { queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); }, }); }; diff --git a/frontend/src/hooks/useStream.ts b/frontend/src/hooks/useStream.ts index a844e5a2..dfdddc1a 100644 --- a/frontend/src/hooks/useStream.ts +++ b/frontend/src/hooks/useStream.ts @@ -48,6 +48,7 @@ const useSetStream = () => { queryClient.invalidateQueries({ queryKey: ["streams"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); }, }); }; diff --git a/frontend/src/pages/Certificates/Table.tsx b/frontend/src/pages/Certificates/Table.tsx index 7e38b322..ece9b83c 100644 --- a/frontend/src/pages/Certificates/Table.tsx +++ b/frontend/src/pages/Certificates/Table.tsx @@ -79,6 +79,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload, proxyHosts={r.proxyHosts} redirectionHosts={r.redirectionHosts} deadHosts={r.deadHosts} + streams={r.streams} /> ); }, diff --git a/frontend/src/pages/Certificates/TableWrapper.tsx b/frontend/src/pages/Certificates/TableWrapper.tsx index 97534273..03399854 100644 --- a/frontend/src/pages/Certificates/TableWrapper.tsx +++ b/frontend/src/pages/Certificates/TableWrapper.tsx @@ -22,6 +22,7 @@ export default function TableWrapper() { "dead_hosts", "proxy_hosts", "redirection_hosts", + "streams", ]); if (isLoading) { diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index 2c8f9543..5bb92451 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -124,7 +124,6 @@ const Dashboard = () => { - check permissions in all places More for api, then implement here: -- Properly implement refresh tokens - Add error message_18n for all backend errors - minor: certificates expand with hosts needs to omit 'is_deleted' - properly wrap all logger.debug called in isDebug check