CEC-749 Generate cert UI (#141)

* Add Create Certificate page

* Tests

* Update permission check

* Use Azure
This commit is contained in:
John Wu
2022-04-18 16:50:51 -07:00
committed by GitHub
parent 81aeedc521
commit 56bef0c34d
28 changed files with 2449 additions and 289 deletions

View File

@@ -37,22 +37,35 @@ const check = async (path, selector, compare) => {
const sleepAndCheck = async (path, selector, compare) => {
const container = await renderRoute(path);
await waitFor(() => { });
await waitFor(() => {});
expect(container.querySelector(selector).innerHTML).toEqual(compare);
expect(container).toMatchSnapshot();
};
describe("App", () => {
const rxMakeStyles = /makeStyles-(\w+)-(\d+)/gi;
beforeAll(() => {
// Stablize Table Pagination control ids
expect.addSnapshotSerializer({
test: function (val) {
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
return val && typeof val === "string" && val.indexOf("mui-") > -1;
},
print: function (val) {
let str = val;
str = str.replace(/mui-\d*/g, "mui-00000");
return `"${str}"`;
},
});
expect.addSnapshotSerializer({
test: (val) => {
return val && typeof val === "string" && val.search(rxMakeStyles) > -1;
},
print: function (val) {
let str = val;
str = str.replace(rxMakeStyles, "makeStyles-$1-0000");
return `"${str}"`;
},
});
@@ -107,6 +120,10 @@ describe("App", () => {
);
});
it("Route /tools/certificates/add unauthenticated", async () => {
await check("/tools/certificates/add", "span.MuiButton-label", "Sign In");
});
it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found");
});
@@ -159,4 +176,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
});
it("Route /tools/certificates/add authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/tools/certificates/add", "h6", "Create Certificate");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -194,6 +194,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -230,6 +247,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -266,6 +300,23 @@ exports[`CANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -193,6 +193,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -229,6 +246,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"456-789\\""
>
<svg
aria-hidden="true"
aria-label="Delete 456-789"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -265,6 +299,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -0,0 +1,90 @@
import React, { useRef, useState } from "react";
import {
Button,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { CertTypes } from "../../Contexts/CertificateContext";
const CreateForm = ({ onCreate, busy }) => {
const classes = useStyles();
const vinEl = useRef(null);
const [certType, setCertType] = useState(CertTypes.TREX);
const onSubmit = async (event) => {
event.preventDefault();
if (onCreate)
onCreate({
vin: vinEl.current.value,
type: certType,
});
};
const onCertTypeChange = (event) => {
setCertType(event.target.value);
};
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="vin"
name="vin"
label="VIN"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "17",
}}
required
fullWidth
inputRef={vinEl}
/>
<FormLabel id="cert-type-group-label">Type</FormLabel>
<RadioGroup
row
aria-labelledby="cert-type-group-label"
name="cert-type"
value={certType}
onChange={onCertTypeChange}
margin="normal"
>
<FormControlLabel
value={CertTypes.TREX}
control={<Radio />}
label="TREX"
/>
<FormControlLabel
value={CertTypes.HMI}
control={<Radio />}
label="HMI"
/>
<FormControlLabel
value={CertTypes.Charging}
control={<Radio />}
label="Charging"
/>
</RadioGroup>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Submitting..." : "Submit"}
</Button>
</form>
</div>
);
};
export default CreateForm;

View File

@@ -0,0 +1,50 @@
import { Button } from "@material-ui/core";
import React from "react";
import DownloadFileLink from "../../Controls/DownloadFileLink";
import useStyles from "../../useStyles";
const CertMimeType = "application/x-pem-file";
const DownloadCerts = ({ vin, publicCert, privateCert, onChangeView }) => {
const classes = useStyles();
const onNewCert = (event) => {
event.preventDefault();
if (!onChangeView) return;
onChangeView();
};
return (
<div>
<h2>Download Certifcates</h2>
<ul>
<li>
<DownloadFileLink
data={publicCert}
filename={`${vin}_cert.pem`}
mimetype={CertMimeType}
/>
</li>
<li>
<DownloadFileLink
data={privateCert}
filename={`${vin}_key.pem`}
mimetype={CertMimeType}
/>
</li>
</ul>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onNewCert}
>
Done
</Button>
</div>
);
};
export default DownloadCerts;

View File

@@ -0,0 +1,25 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadCerts from "./DownloadCerts";
describe("DownloadCerts", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadCerts
vin={"TESTVIN"}
publicCert={"PUBLIC"}
privateCert={"PRIVATE"}
/>
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadCerts Render 1`] = `
<div>
<div>
<h2>
Download Certifcates
</h2>
<ul>
<li>
<a
download="TESTVIN_cert.pem"
>
TESTVIN_cert.pem
</a>
</li>
<li>
<a
download="TESTVIN_key.pem"
>
TESTVIN_key.pem
</a>
</li>
</ul>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Done
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
`;

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useState } from "react";
import {
useCertificateContext,
CertificateProvider,
} from "../../Contexts/CertificateContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
import CreateForm from "./CreateForm";
import DownloadCerts from "./DownloadCerts";
const VIEW_FORM = 0;
const VIEW_DOWNLOAD = 1;
const MainForm = () => {
const { busy, createCert } = useCertificateContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [view, setView] = useState(VIEW_FORM);
const [pubCert, setPubCert] = useState(null);
const [privCert, setPrivCert] = useState(null);
const [vin, setVIN] = useState(null);
useEffect(() => {
setTitle("Create Certificate");
setSitePath([
{
label: "Tools",
link: "/tools/certificates/add",
},
{
label: "Create Certificate",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onCreate = async (data) => {
try {
const result = await createCert(data, token);
setPubCert(result.public_key);
setPrivCert(result.private_key);
setVIN(data.vin);
setMessage(`Created ${data.vin} certificate`);
setView(VIEW_DOWNLOAD);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const onChangeView = () => {
setPubCert(null);
setPrivCert(null);
setVIN(null);
setView(VIEW_FORM);
};
if (view === VIEW_DOWNLOAD)
return (
<DownloadCerts
vin={vin}
publicCert={pubCert}
privateCert={privCert}
onChangeView={onChangeView}
/>
);
return <CreateForm onCreate={onCreate} busy={busy} />;
};
const CertificateCreate = () => (
<CertificateProvider>
<MainForm />
</CertificateProvider>
);
export default CertificateCreate;

View File

@@ -0,0 +1,50 @@
import React, { useContext, useState } from "react";
import api from "../../services/certificatesAPI";
const CertificateContext = React.createContext();
export const CertTypes = {
TREX: "TREX",
HMI: "HMI",
Charging: "CHARGE",
};
const validateCreate = (data) => {
if (!data.type) throw new Error("type is required");
if (!data.vin) throw new Error("vin is required");
};
export const CertificateProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const createCert = async (data, token) => {
try {
setBusy(true);
validateCreate(data);
const result = await api.create(data, token);
if (result.error) {
throw new Error(`Create certificate error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
return (
<CertificateContext.Provider
value={{
busy,
createCert,
}}
>
{children}
</CertificateContext.Provider>
);
};
export const useCertificateContext = () => useContext(CertificateContext);

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownloadFileLink Render 1`] = `
<div>
<a
download="test.txt"
>
test.txt
</a>
</div>
`;

View File

@@ -0,0 +1,32 @@
import React, { useEffect, useState } from "react";
const DownloadFileLink = ({ data, filename, mimetype }) => {
const [link, setLink] = useState("");
const releaseLink = () => {
if (link === "") return;
URL.revokeObjectURL(link);
};
const makeFile = () => {
const file = new Blob([data], { type: mimetype ?? "text/plain" });
releaseLink();
setLink(URL.createObjectURL(file));
};
useEffect(() => {
if (!data) return;
makeFile();
return releaseLink;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, filename, mimetype]);
return (
<a download={filename ?? "file.txt"} href={link}>
{filename}
</a>
);
};
export default DownloadFileLink;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import DownloadFileLink from ".";
describe("DownloadFileLink", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<DownloadFileLink data={"ABCDEFGHIJK"} filename="test.txt" />
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -195,6 +195,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -231,6 +248,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -267,6 +301,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -153,7 +153,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345678\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345678"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -171,7 +187,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345679\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345679"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -189,7 +221,23 @@ exports[`FleetVehiclesTable Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345670\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345670"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -194,6 +194,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"123-456\\""
>
<svg
aria-hidden="true"
aria-label="Delete 123-456"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -230,6 +247,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -266,6 +300,23 @@ exports[`CANFiltersTab Render 1`] = `
/>
</svg>
</a>
<a
class=""
href="/"
title="Delete \\"1000\\""
>
<svg
aria-hidden="true"
aria-label="Delete 1000"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -152,7 +152,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345678\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345678"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -170,7 +186,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345679\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345679"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
<tr
@@ -188,7 +220,23 @@ exports[`VehiclesTab Render 1`] = `
<td
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
>
No actions
<a
class=""
href="/"
title="Delete \\"USWESTVIN12345670\\""
>
<svg
aria-hidden="true"
aria-label="Delete USWESTVIN12345670"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
</svg>
</a>
</td>
</tr>
</tbody>

View File

@@ -1,10 +1,11 @@
import React from "react";
import { List } from "@material-ui/core";
import HomeIcon from "@material-ui/icons/Home";
import DirectionsCarIcon from '@material-ui/icons/DirectionsCar';
import DirectionsCarIcon from "@material-ui/icons/DirectionsCar";
import CommuteIcon from "@material-ui/icons/Commute";
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
import AssessmentIcon from "@material-ui/icons/Assessment";
import BuildIcon from "@material-ui/icons/Build";
import ListItemLink from "../ListItemLink";
import ListItemExternalLink from "../ListItemExternalLink";
@@ -43,6 +44,19 @@ const menuData = [
icon: <AssessmentIcon />,
roles: [Roles.READ, Roles.CREATE],
},
{
label: "Tools",
to: "/tools/certificates/add",
icon: <BuildIcon />,
roles: [Roles.CERTIFICATES],
submenus: [
{
label: "Certificate",
to: "/tools/certificates/add",
roles: [Roles.CERTIFICATES],
},
],
},
];
const MenuItem = ({ item, children }) => {

View File

@@ -190,6 +190,70 @@ exports[`SideMenu Authenticated 1`] = `
/>
</a>
</li>
<span>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Tools
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</span>
<ul
style="margin-left: 50px;"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/certificates/add"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Certificate
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul>
</ul>
</div>
</div>

View File

@@ -6,8 +6,8 @@ import { MessageBar } from "../MessageBar";
import { useUserContext } from "../Contexts/UserContext";
import { Roles } from "../../utils/roles";
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"))
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"))
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"));
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"));
const CarsList = React.lazy(() => import("../Cars/List"));
const CarStatus = React.lazy(() => import("../Cars/Status"));
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
@@ -15,9 +15,15 @@ const FleetsList = React.lazy(() => import("../Fleets/Table"));
const FleetStatus = React.lazy(() => import("../Fleets/Status"));
const FleetAddForm = React.lazy(() => import("../Fleets/Add"));
const FleetUpdateForm = React.lazy(() => import("../Fleets/Update"));
const FleetAddVehicleForm = React.lazy(() => import("../Fleets/Status/Vehicles/Add"));
const FleetAddCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Add"));
const FleetUpdateCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Update"));
const FleetAddVehicleForm = React.lazy(() =>
import("../Fleets/Status/Vehicles/Add")
);
const FleetAddCANFilterForm = React.lazy(() =>
import("../Fleets/Status/CANFilters/Add")
);
const FleetUpdateCANFilterForm = React.lazy(() =>
import("../Fleets/Status/CANFilters/Update")
);
const Home = React.lazy(() => import("../Home"));
const Manifests = React.lazy(() => import("../Manifest/List"));
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
@@ -26,7 +32,8 @@ const ManifestCreate = React.lazy(() => import("../Manifest/Create"));
const PageNotFound = React.lazy(() => import("../404"));
const SSOForm = React.lazy(() => import("../SSOForm"));
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
const VehicleUpdateForm = React.lazy(() => import("../Cars/Update"))
const VehicleUpdateForm = React.lazy(() => import("../Cars/Update"));
const CertificateCreate = React.lazy(() => import("../Certificates/Add"));
const SiteRoutes = () => {
const { token, groups } = useUserContext();
@@ -191,6 +198,14 @@ const SiteRoutes = () => {
groups={groups}
roles={[Roles.CREATE]}
/>
<AuthRoute
path="/tools/certificates/add"
render={() => <CertificateCreate />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
roles={[Roles.CERTIFICATES]}
/>
<PageNotFound />
</Switch>
</Suspense>

View File

@@ -0,0 +1,17 @@
import { getAuthHeaderOptions, fetchRespHandler } from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_CERT_SERVICE_URL;
const certificatesAPI = {
create: async (data, token) =>
fetch(`${API_ENDPOINT}/create`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler),
};
export default certificatesAPI;

View File

@@ -3,8 +3,9 @@ import { parsePayload } from "./jwt";
export const Roles = {
READ: "a729bbd4-2038-4649-9127-16782bb1e701",
CREATE: "efcc3025-e2d8-4212-8227-805c7be39d2c",
DELETE: "8f78dce7-f5f9-4033-a10c-c9c7408bfcfe"
}
DELETE: "8f78dce7-f5f9-4033-a10c-c9c7408bfcfe",
CERTIFICATES: "746f34b0-9ba0-4b5d-8d84-0256a9c8e390",
};
export const hasRoleToken = (roles, token) => {
if (!roles || roles.length === 0) return true;
@@ -14,7 +15,7 @@ export const hasRoleToken = (roles, token) => {
if (!groups) return false;
return hasRole(roles, groups);
}
};
export const getGroups = (token) => {
const payload = parsePayload(token);
@@ -22,7 +23,7 @@ export const getGroups = (token) => {
if (!payload || !payload["custom:groups"]) return null;
return payload["custom:groups"];
}
};
export const hasRole = (roles, groups) => {
if (!roles || roles.length === 0) return true;
@@ -33,4 +34,4 @@ export const hasRole = (roles, groups) => {
}
return false;
}
};

View File

@@ -1,8 +1,9 @@
export const TEST_TOKEN = "eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiOUlyV2RLaUxJU0FZUnFha1F2b2xmZyIsInN1YiI6IjJiMDk1NTY2LTllNDYtNGQ4ZS1iMTA5LTI0MTM1ZGYyMmVlNiIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJjdXN0b206Z3JvdXBzIjoiWzhkODI3OGE1LTljMGUtNGM3Zi05MThhLTgxMWZkMWQyMzZlNCwgNmMzY2Y5OGQtMGFkYS00OGM2LWFlOTQtYjE3MWNmYTI3NWZjLCA1NmVmNGJlYy1kNzM5LTRkZGYtYTAwMy1lY2M4MTMwODViOGQsIGVmY2MzMDI1LWUyZDgtNDIxMi04MjI3LTgwNWM3YmUzOWQyYywgNTUxNWE5OGYtNDY2OC00MTIxLThlOGQtZmVlMjgyNTY5OWNmLCA4Njk1NmEyZi04ZDQ2LTQ3ZmYtOWIyOS1mOTkwNzlhZTNjMWQsIGM0ZDQzNjFjLTg4ODItNDdiNC04NjQxLWZkM2FiNjhhZTcyMiwgN2JjZGNkYjItMzI3OS00NGJmLWE5OTgtNzcxYmFiNGIzM2UxXSIsImF1ZCI6IjdjazJ0Zm9xYXZjNzJjNDVoaDd0Z2U0MmtkIiwiY3VzdG9tOnNlc3Npb24tZHVyYXRpb24iOiI5MDAiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxNDM2NDk3NDU4NSJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTU4MjEzMDksImV4cCI6MTYxNTkyNzM0OSwiaWF0IjoxNjE1OTIzNzUwLCJmYW1pbHlfbmFtZSI6Ild1IiwiZW1haWwiOiJqd3VAZmlza2VyaW5jLmNvbSJ9.R3k-YGK0MrUdW030Xj2WxM7mdsm1tlobeDq3YRMIKMtdkJsf5qjwM_wqVPbErH-8OrFLW7YIPuMo2Rh5PCGvg4I6kL-tWfDOY4o5b5r_VdiifXov0be_ukdt5pZblhgg0dYSLmFaFZsxNjEng8-obl_FnWp6VtG1lnRGwORY3pFe88W7OM3zLMC0g-otfAEQ2KSOaV9bfUoRAaZaGlHe8ooIQx8Qoer9qYsnymK0Sk7jSZKwhtFsziSarhreHmBkCLaWBHDjc9PQDtBvO8wg1KMKmM-6oewA0xTKPtsuHxnvtVANYaR7Nqp9cbF940YRf2IK5FB7KWFtcR7Y6igLXw";
export const TEST_TOKEN =
"eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoieDIzcUJvUDN6d1RnLTd6T3VJRUZUZyIsInN1YiI6IjJiMDk1NTY2LTllNDYtNGQ4ZS1iMTA5LTI0MTM1ZGYyMmVlNiIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJub25jZSI6InBqUXF6WHJ0M0NzMENqY3J2UXZHWFdZLUFKTXp5bTlIT0JHazh0YzdBb0s4bC1ycEFrc0xkSUZYZm9XcnZ3MFBtTjFzNEtGamdPMnZzdDRCdEZCdC1BSDcwNkQ5VDctN3VWODQ0Z1d0MXU5SnhLQUhXZ3JDM244YV9TQy1MV3pNNExaRVhYOHFscUJyYTBEdnJxOFFEd09HLWdrRVVsQWY5ZmJoRmprMy04cyIsImN1c3RvbTpncm91cHMiOiJbMjkxNGU2N2YtZmI4NS00Yjc4LWI3OWQtNjU2ZjRmMzdmYWExLCA4ZDgyNzhhNS05YzBlLTRjN2YtOTE4YS04MTFmZDFkMjM2ZTQsIDc0NmYzNGIwLTliYTAtNGI1ZC04ZDg0LTAyNTZhOWM4ZTM5MCwgNmMzY2Y5OGQtMGFkYS00OGM2LWFlOTQtYjE3MWNmYTI3NWZjLCBhY2JkNzJjOS05ZmYzLTQ2YTgtODNiYS1jNmZhNWRmM2YyNjQsIDVlNTlhNjE5LWM4OTAtNDQ3Mi05MWMyLWQ1ZWFlNWQ4ZmExOCwgNTZlZjRiZWMtZDczOS00ZGRmLWEwMDMtZWNjODEzMDg1YjhkLCA5MjliMDQ3MC1mN2ViLTRlMTgtOWY5Ny0yMmFjMmM1OTFhMTAsIDEzMWU2MjU3LWZkYjctNDI2YS05ODI1LTFjZDkxODgwMmZiYSwgMWFjNzk0Y2MtNzZhYy00N2Y2LWJlZTYtZDY2NjY5OThmMGZkLCA4Nzc3MGFlYS05MTYyLTQwOWUtYWE3MC0xMGQwZDFkZTU5MDIsIDVkZWE2YzMyLTY1NzUtNDUyNy05MjU0LWU1MGQyN2FlNWU5MiwgYmFmYzE3YTctZWM2NC00OWQ5LWEyYTctZGFmYzI4ZGNiMDM3LCAyNDQ5YzA2Ni05MTQ2LTQ0YTctYjRlNi00ODI4MDMxZDk1OGQsIGVmY2MzMDI1LWUyZDgtNDIxMi04MjI3LTgwNWM3YmUzOWQyYywgOGY3OGRjZTctZjVmOS00MDMzLWExMGMtYzljNzQwOGJmY2ZlLCBjZjY1MzE4My1jODI5LTRlZWQtYTZjZS00NTNmYTEwMTdjZDksIDc4M2M1OTc5LWY1ZTctNGNiNi1iMTRlLWMzNTUzZGRlOTU2YSwgNTUxNWE5OGYtNDY2OC00MTIxLThlOGQtZmVlMjgyNTY5OWNmLCA4Njk1NmEyZi04ZDQ2LTQ3ZmYtOWIyOS1mOTkwNzlhZTNjMWQsIGM0ZDQzNjFjLTg4ODItNDdiNC04NjQxLWZkM2FiNjhhZTcyMiwgOTcyYWQwOTUtMTZiNy00MGFkLWE0NjQtZjVkYmY0MTdhOGNkLCA3YmNkY2RiMi0zMjc5LTQ0YmYtYTk5OC03NzFiYWI0YjMzZTFdIiwiYXVkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJjdXN0b206c2Vzc2lvbi1kdXJhdGlvbiI6IjkwMCIsImlkZW50aXRpZXMiOlt7InVzZXJJZCI6Imp3dUBmaXNrZXJpbmMuY29tIiwicHJvdmlkZXJOYW1lIjoiQXp1cmVBRCIsInByb3ZpZGVyVHlwZSI6IlNBTUwiLCJpc3N1ZXIiOiJodHRwczpcL1wvc3RzLndpbmRvd3MubmV0XC81YWE0YjY0MC1jOWZjLTRhOWItYjNhMy1kNGE3ZDAwOGZiNWVcLyIsInByaW1hcnkiOiJ0cnVlIiwiZGF0ZUNyZWF0ZWQiOiIxNjE0MzY0OTc0NTg1In1dLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTY1MDMyMjc3MywiZXhwIjoxNjUwMzI2MzczLCJpYXQiOjE2NTAzMjI3NzMsImZhbWlseV9uYW1lIjoiV3UiLCJlbWFpbCI6Imp3dUBmaXNrZXJpbmMuY29tIn0.MCWWFAERmOVcFAHNqxpFeVKVMtZAHxY-_pbEtivnq8zhKUH8psDWorz68jT1Rt8_gqrevWIwWRMWkoiZQFGbCl0uxxZqIULrj2yeMyFAx-YTBDB-hZXqeDQ5RRxyJd3y1Opc0Rtusi7ENTigY90FAoRGHsA9l6cMlXStBSC9hWzRTPliMGTiHlzJCAW78ZqwKuiteE4LdsP0ROnjqrHcSpw6j5H2DvG1LhRAbektmhpBegQ5ncVtfWAF1W-WaUgzDoTnwZll0NTYaJVn1PUX7AoDUM9wi23y7QbMJm3Pb5nt39gDxZMMpJGzTsTe7CBR5frWphV6xyWEa8irERgSnQ";
export const TEST_AUTH_OBJECT = {
idToken: {
jwtToken: TEST_TOKEN,
},
};
export const TEST_EXPECTED_GROUPS =
"[8d8278a5-9c0e-4c7f-918a-811fd1d236e4, 6c3cf98d-0ada-48c6-ae94-b171cfa275fc, 56ef4bec-d739-4ddf-a003-ecc813085b8d, efcc3025-e2d8-4212-8227-805c7be39d2c, 5515a98f-4668-4121-8e8d-fee2825699cf, 86956a2f-8d46-47ff-9b29-f99079ae3c1d, c4d4361c-8882-47b4-8641-fd3ab68ae722, 7bcdcdb2-3279-44bf-a998-771bab4b33e1]";
"[2914e67f-fb85-4b78-b79d-656f4f37faa1, 8d8278a5-9c0e-4c7f-918a-811fd1d236e4, 746f34b0-9ba0-4b5d-8d84-0256a9c8e390, 6c3cf98d-0ada-48c6-ae94-b171cfa275fc, acbd72c9-9ff3-46a8-83ba-c6fa5df3f264, 5e59a619-c890-4472-91c2-d5eae5d8fa18, 56ef4bec-d739-4ddf-a003-ecc813085b8d, 929b0470-f7eb-4e18-9f97-22ac2c591a10, 131e6257-fdb7-426a-9825-1cd918802fba, 1ac794cc-76ac-47f6-bee6-d6666998f0fd, 87770aea-9162-409e-aa70-10d0d1de5902, 5dea6c32-6575-4527-9254-e50d27ae5e92, bafc17a7-ec64-49d9-a2a7-dafc28dcb037, 2449c066-9146-44a7-b4e6-4828031d958d, efcc3025-e2d8-4212-8227-805c7be39d2c, 8f78dce7-f5f9-4033-a10c-c9c7408bfcfe, cf653183-c829-4eed-a6ce-453fa1017cd9, 783c5979-f5e7-4cb6-b14e-c3553dde956a, 5515a98f-4668-4121-8e8d-fee2825699cf, 86956a2f-8d46-47ff-9b29-f99079ae3c1d, c4d4361c-8882-47b4-8641-fd3ab68ae722, 972ad095-16b7-40ad-a464-f5dbf417a8cd, 7bcdcdb2-3279-44bf-a998-771bab4b33e1]";