mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-06-18 10:06:26 +00:00
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:
14
frontend/src/api/npm/getSSEToken.ts
Normal file
14
frontend/src/api/npm/getSSEToken.ts
Normal 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;
|
||||
}
|
@ -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";
|
||||
|
@ -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[];
|
||||
}
|
||||
|
15
frontend/src/api/npm/renewCertificate.ts
Normal file
15
frontend/src/api/npm/renewCertificate.ts
Normal 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;
|
||||
}
|
@ -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}>
|
||||
|
@ -71,6 +71,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "Zertifikat erstellen"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Zertifikate"
|
||||
},
|
||||
|
@ -347,6 +347,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "Create Certificate"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Certificates"
|
||||
},
|
||||
|
@ -71,6 +71,9 @@
|
||||
"certificate.create": {
|
||||
"defaultMessage": "ایجاد گواهی"
|
||||
},
|
||||
"certificate.renewal-requested": {
|
||||
"defaultMessage": "Renewal has been queued"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "گواهینامه ها"
|
||||
},
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -25,4 +25,4 @@
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user