Certificates Renewal + SSE

- Certificate renewal is just a re-request as it's forced already
- Rejig the routes for readability
- Added Server Side Events so that the UI would invalidate the
cache when changes happen on the backend, such as certs being
provided or failing
- Added a SSE Token, which has the same shelf life as normal token
but can't be used interchangeably. The reason for this is, the
SSE endpoint needs a token for auth as a Query param, so it would
be stored in log files. If someone where to get a hold of that,
it's pretty useless as it can't be used to change anything, only
to listen for events until it expires
- Added test endpoint for SSE testing only availabe in debug mode
This commit is contained in:
Jamie Curnow
2023-03-07 16:42:26 +10:00
parent 35550082bf
commit 215083f6cf
29 changed files with 665 additions and 197 deletions

View File

@ -0,0 +1,14 @@
import * as api from "./base";
import { TokenResponse } from "./responseTypes";
export async function getSSEToken(
abortController?: AbortController,
): Promise<TokenResponse> {
const { result } = await api.post(
{
url: "/tokens/sse",
},
abortController,
);
return result;
}

View File

@ -14,6 +14,7 @@ export * from "./getHealth";
export * from "./getHosts";
export * from "./getNginxTemplates";
export * from "./getSettings";
export * from "./getSSEToken";
export * from "./getToken";
export * from "./getUpstreamNginxConfig";
export * from "./getUpstreams";
@ -22,6 +23,7 @@ export * from "./getUsers";
export * from "./helpers";
export * from "./models";
export * from "./refreshToken";
export * from "./renewCertificate";
export * from "./responseTypes";
export * from "./setAuth";
export * from "./setCertificate";

View File

@ -165,3 +165,10 @@ export interface Upstream {
advancedConfig: string;
isDisabled: boolean;
}
export interface SSEMessage {
lang?: string;
langParams?: string;
type?: "info" | "warning" | "success" | "error" | "loading";
affects?: string | string[];
}

View File

@ -0,0 +1,15 @@
import * as api from "./base";
import { Certificate } from "./models";
export async function renewCertificate(
id: number,
abortController?: AbortController,
): Promise<Certificate> {
const { result } = await api.post(
{
url: `/certificates/${id}/renew`,
},
abortController,
);
return result;
}

View File

@ -1,12 +1,56 @@
import { ReactNode } from "react";
import { useEffect, ReactNode } from "react";
import { Box, Container } from "@chakra-ui/react";
import { Box, Container, useToast } from "@chakra-ui/react";
import { getSSEToken, SSEMessage } from "api/npm";
import { Footer, Navigation } from "components";
import { intl } from "locale";
import AuthStore from "modules/AuthStore";
import { useQueryClient } from "react-query";
interface Props {
children?: ReactNode;
}
function SiteWrapper({ children }: Props) {
const queryClient = useQueryClient();
const toast = useToast();
// TODO: fix bug where this will fail if the browser is kept open longer
// than the expiry of the sse token
useEffect(() => {
async function fetchData() {
const response = await getSSEToken();
const eventSource = new EventSource(
`/api/sse/changes?jwt=${response.token}`,
);
eventSource.onmessage = (e: any) => {
const j: SSEMessage = JSON.parse(e.data);
if (j) {
if (j.affects) {
queryClient.invalidateQueries(j.affects);
}
if (j.type) {
toast({
description: intl.formatMessage({ id: j.lang }),
status: j.type || "info",
position: "top",
duration: 3000,
isClosable: true,
});
}
}
};
eventSource.onerror = (e) => {
console.error("SSE EventSource failed:", e);
};
return () => {
eventSource.close();
};
}
if (AuthStore.token) {
fetchData();
}
}, [queryClient, toast]);
return (
<Box display="flex" flexDir="column" height="100vh">
<Box flexShrink={0}>

View File

@ -71,6 +71,9 @@
"certificate.create": {
"defaultMessage": "Zertifikat erstellen"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "Zertifikate"
},

View File

@ -347,6 +347,9 @@
"certificate.create": {
"defaultMessage": "Create Certificate"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "Certificates"
},

View File

@ -71,6 +71,9 @@
"certificate.create": {
"defaultMessage": "ایجاد گواهی"
},
"certificate.renewal-requested": {
"defaultMessage": "Renewal has been queued"
},
"certificates.title": {
"defaultMessage": "گواهینامه ها"
},

View File

@ -25,6 +25,7 @@ export interface TableProps {
sortBy: TableSortBy[];
filters: TableFilter[];
onTableEvent: any;
onRenewal: (id: number) => void;
}
function Table({
data,
@ -32,6 +33,7 @@ function Table({
onTableEvent,
sortBy,
filters,
onRenewal,
}: TableProps) {
const [editId, setEditId] = useState(0);
const [columns, tableData] = useMemo(() => {
@ -85,7 +87,7 @@ function Table({
title: intl.formatMessage({
id: "action.renew",
}),
onClick: (e: any, { id }: any) => alert(id),
onClick: (e: any, { id }: any) => onRenewal(id),
icon: <FiRefreshCw />,
disabled: (data: any) =>
data.type !== "dns" && data.type !== "http",
@ -110,7 +112,7 @@ function Table({
},
];
return [columns, data];
}, [data]);
}, [data, onRenewal]);
const tableInstance = useTable(
{

View File

@ -1,9 +1,11 @@
import { useEffect, useReducer, useState } from "react";
import { Alert, AlertIcon } from "@chakra-ui/react";
import { Alert, AlertIcon, useToast } from "@chakra-ui/react";
import { renewCertificate } from "api/npm";
import { EmptyList, SpinnerPage, tableEventReducer } from "components";
import { useCertificates } from "hooks";
import { intl } from "locale";
import { useQueryClient } from "react-query";
import Table from "./Table";
@ -19,6 +21,9 @@ const initialState = {
filters: [],
};
function TableWrapper() {
const toast = useToast();
const queryClient = useQueryClient();
const [{ offset, limit, sortBy, filters }, dispatch] = useReducer(
tableEventReducer,
initialState,
@ -36,6 +41,32 @@ function TableWrapper() {
setTableData(data as any);
}, [data]);
const renewCert = async (id: number) => {
try {
await renewCertificate(id);
toast({
description: intl.formatMessage({
id: `certificate.renewal-requested`,
}),
status: "info",
position: "top",
duration: 3000,
isClosable: true,
});
setTimeout(() => {
queryClient.invalidateQueries("certificates");
}, 500);
} catch (err: any) {
toast({
description: err.message,
status: "error",
position: "top",
duration: 3000,
isClosable: true,
});
}
};
if (isFetching || isLoading || !tableData) {
return <SpinnerPage />;
}
@ -76,6 +107,7 @@ function TableWrapper() {
sortBy={sortBy}
filters={filters}
onTableEvent={dispatch}
onRenewal={renewCert}
/>
);
}

View File

@ -25,4 +25,4 @@
"include": [
"src"
]
}
}