Various tweaks and backend improvements

This commit is contained in:
Jamie Curnow
2025-10-28 23:10:00 +10:00
parent 7331cb3675
commit 3b9beaeae5
12 changed files with 85 additions and 19 deletions

View File

@@ -24,7 +24,7 @@ const certbotLogsDir = "/data/logs";
const certbotWorkDir = "/tmp/letsencrypt-lib"; const certbotWorkDir = "/tmp/letsencrypt-lib";
const omissions = () => { const omissions = () => {
return ["is_deleted", "owner.is_deleted"]; return ["is_deleted", "owner.is_deleted", "meta.dns_provider_credentials"];
}; };
const internalCertificate = { const internalCertificate = {
@@ -122,7 +122,7 @@ const internalCertificate = {
} }
// this command really should clean up and delete the cert if it can't fully succeed // 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 { try {
if (certificate.provider === "letsencrypt") { if (certificate.provider === "letsencrypt") {
@@ -202,6 +202,9 @@ const internalCertificate = {
savedRow.meta = _.assign({}, savedRow.meta, { savedRow.meta = _.assign({}, savedRow.meta, {
letsencrypt_certificate: certInfo, letsencrypt_certificate: certInfo,
}); });
await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow);
return savedRow; return savedRow;
} catch (err) { } catch (err) {
// Delete the certificate from the database if it was not created successfully // 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); data.meta = _.assign({}, data.meta || {}, certificate.meta);
// Add to audit log // 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, { await internalAuditLog.add(access, {
action: "created", action: "created",
object_type: "certificate", object_type: "certificate",
object_id: certificate.id, object_id: certificate_id,
meta: data, meta: meta,
}); });
return certificate;
}, },
/** /**
@@ -285,10 +292,7 @@ const internalCertificate = {
.query() .query()
.where("is_deleted", 0) .where("is_deleted", 0)
.andWhere("id", data.id) .andWhere("id", data.id)
.allowGraph("[owner]") .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
.allowGraph("[proxy_hosts]")
.allowGraph("[redirection_hosts]")
.allowGraph("[dead_hosts]")
.first(); .first();
if (accessData.permission_visibility !== "all") { if (accessData.permission_visibility !== "all") {
@@ -305,7 +309,24 @@ const internalCertificate = {
} }
// Custom omissions // Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) { 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; return row;
}, },
@@ -415,7 +436,7 @@ const internalCertificate = {
.query() .query()
.where("is_deleted", 0) .where("is_deleted", 0)
.groupBy("id") .groupBy("id")
.allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts]") .allowGraph("[owner,proxy_hosts,redirection_hosts,dead_hosts,streams]")
.orderBy("nice_name", "ASC"); .orderBy("nice_name", "ASC");
if (accessData.permission_visibility !== "all") { if (accessData.permission_visibility !== "all") {
@@ -433,7 +454,11 @@ const internalCertificate = {
query.withGraphFetched(`[${expand.join(", ")}]`); 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;
}, },
/** /**

View File

@@ -8,6 +8,7 @@ import deadHostModel from "./dead_host.js";
import now from "./now_helper.js"; import now from "./now_helper.js";
import proxyHostModel from "./proxy_host.js"; import proxyHostModel from "./proxy_host.js";
import redirectionHostModel from "./redirection_host.js"; import redirectionHostModel from "./redirection_host.js";
import streamModel from "./stream.js";
import userModel from "./user.js"; import userModel from "./user.js";
Model.knex(db); Model.knex(db);
@@ -114,6 +115,17 @@ class Certificate extends Model {
qb.where("redirection_host.is_deleted", 0); 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);
},
},
}; };
} }
} }

View File

@@ -1,6 +1,6 @@
export type AccessListExpansion = "owner" | "items" | "clients"; export type AccessListExpansion = "owner" | "items" | "clients";
export type AuditLogExpansion = "user"; 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 HostExpansion = "owner" | "certificate";
export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; export type ProxyHostExpansion = "owner" | "access_list" | "certificate";
export type UserExpansion = "permissions"; export type UserExpansion = "permissions";

View File

@@ -1,6 +1,6 @@
import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import OverlayTrigger from "react-bootstrap/OverlayTrigger";
import Popover from "react-bootstrap/Popover"; 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"; import { T } from "src/locale";
const getSection = (title: string, items: ProxyHost[] | RedirectionHost[] | DeadHost[]) => { 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 (
<>
<div>
<strong>
<T id="streams" />
</strong>
</div>
{items.map((stream) => (
<div key={stream.id} className="ms-1">
{stream.forwardingHost}:{stream.forwardingPort}
</div>
))}
</>
);
};
interface Props { interface Props {
proxyHosts: ProxyHost[]; proxyHosts: ProxyHost[];
redirectionHosts: RedirectionHost[]; redirectionHosts: RedirectionHost[];
deadHosts: DeadHost[]; deadHosts: DeadHost[];
streams: Stream[];
} }
export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts }: Props) { export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHosts, streams }: Props) {
const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length; const totalCount = proxyHosts?.length + redirectionHosts?.length + deadHosts?.length + streams?.length;
if (totalCount === 0) { if (totalCount === 0) {
return ( return (
<span className="badge bg-red-lt"> <span className="badge bg-red-lt">
@@ -41,6 +62,7 @@ export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHo
proxyHosts.sort(); proxyHosts.sort();
redirectionHosts.sort(); redirectionHosts.sort();
deadHosts.sort(); deadHosts.sort();
streams.sort();
const popover = ( const popover = (
<Popover id="popover-basic"> <Popover id="popover-basic">
@@ -48,6 +70,7 @@ export function CertificateInUseFormatter({ proxyHosts, redirectionHosts, deadHo
{getSection("proxy-hosts", proxyHosts)} {getSection("proxy-hosts", proxyHosts)}
{getSection("redirection-hosts", redirectionHosts)} {getSection("redirection-hosts", redirectionHosts)}
{getSection("dead-hosts", deadHosts)} {getSection("dead-hosts", deadHosts)}
{getSectionStream(streams)}
</Popover.Body> </Popover.Body>
</Popover> </Popover>
); );

View File

@@ -1,4 +1,5 @@
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react"; import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconLock, IconShield, IconUser } from "@tabler/icons-react";
import cn from "classnames";
import type { AuditLog } from "src/api/backend"; import type { AuditLog } from "src/api/backend";
import { DateTimeFormat, T } from "src/locale"; import { DateTimeFormat, T } from "src/locale";
@@ -32,7 +33,7 @@ const getColorForAction = (action: string) => {
}; };
const getIcon = (row: AuditLog) => { const getIcon = (row: AuditLog) => {
const c = getColorForAction(row.action); const c = cn(getColorForAction(row.action), "me-1");
let ico = null; let ico = null;
switch (row.objectType) { switch (row.objectType) {
case "user": case "user":

View File

@@ -52,6 +52,7 @@ const useSetDeadHost = () => {
queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); queryClient.invalidateQueries({ queryKey: ["dead-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
}, },
}); });
}; };

View File

@@ -59,6 +59,7 @@ const useSetProxyHost = () => {
queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] }); queryClient.invalidateQueries({ queryKey: ["proxy-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
}, },
}); });
}; };

View File

@@ -63,6 +63,7 @@ const useSetRedirectionHost = () => {
queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] }); queryClient.invalidateQueries({ queryKey: ["redirection-hosts"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
}, },
}); });
}; };

View File

@@ -48,6 +48,7 @@ const useSetStream = () => {
queryClient.invalidateQueries({ queryKey: ["streams"] }); queryClient.invalidateQueries({ queryKey: ["streams"] });
queryClient.invalidateQueries({ queryKey: ["audit-logs"] }); queryClient.invalidateQueries({ queryKey: ["audit-logs"] });
queryClient.invalidateQueries({ queryKey: ["host-report"] }); queryClient.invalidateQueries({ queryKey: ["host-report"] });
queryClient.invalidateQueries({ queryKey: ["certificates"] });
}, },
}); });
}; };

View File

@@ -79,6 +79,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
proxyHosts={r.proxyHosts} proxyHosts={r.proxyHosts}
redirectionHosts={r.redirectionHosts} redirectionHosts={r.redirectionHosts}
deadHosts={r.deadHosts} deadHosts={r.deadHosts}
streams={r.streams}
/> />
); );
}, },

View File

@@ -22,6 +22,7 @@ export default function TableWrapper() {
"dead_hosts", "dead_hosts",
"proxy_hosts", "proxy_hosts",
"redirection_hosts", "redirection_hosts",
"streams",
]); ]);
if (isLoading) { if (isLoading) {

View File

@@ -124,7 +124,6 @@ const Dashboard = () => {
- check permissions in all places - check permissions in all places
More for api, then implement here: More for api, then implement here:
- Properly implement refresh tokens
- Add error message_18n for all backend errors - Add error message_18n for all backend errors
- minor: certificates expand with hosts needs to omit 'is_deleted' - minor: certificates expand with hosts needs to omit 'is_deleted'
- properly wrap all logger.debug called in isDebug check - properly wrap all logger.debug called in isDebug check