CEC-749 Generate cert UI (#141)
* Add Create Certificate page * Tests * Update permission check * Use Azure
This commit is contained in:
1
.env.dev
1
.env.dev
@@ -2,3 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/compute_auth
|
|||||||
REACT_APP_UPLOAD_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/ota_update
|
REACT_APP_UPLOAD_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.cloud.fiskerinc.com
|
REACT_APP_AUTH_CALLBACK_URL=https://dev-ota-admin.cloud.fiskerinc.com
|
||||||
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
||||||
|
REACT_APP_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
|
|||||||
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
|
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
|
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
|
||||||
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
||||||
|
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate
|
||||||
|
|||||||
1
.env.prd
1
.env.prd
@@ -2,3 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=https://gw.cloud.fiskerinc.com/compute_auth
|
|||||||
REACT_APP_UPLOAD_SERVICE_URL=https://gw.cloud.fiskerinc.com/ota_update
|
REACT_APP_UPLOAD_SERVICE_URL=https://gw.cloud.fiskerinc.com/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.cloud.fiskerinc.com
|
REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.cloud.fiskerinc.com
|
||||||
REACT_APP_SUPERSET_URL=http://superset.fisker.internal
|
REACT_APP_SUPERSET_URL=http://superset.fisker.internal
|
||||||
|
REACT_APP_CERT_SERVICE_URL=https://gw.cloud.fiskerinc.com/certificate
|
||||||
|
|||||||
1
.env.stg
1
.env.stg
@@ -2,3 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/compute_auth
|
|||||||
REACT_APP_UPLOAD_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/ota_update
|
REACT_APP_UPLOAD_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.cloud.fiskerinc.com
|
REACT_APP_AUTH_CALLBACK_URL=https://stg-ota-admin.cloud.fiskerinc.com
|
||||||
REACT_APP_SUPERSET_URL=http://superset-stg.fisker.internal
|
REACT_APP_SUPERSET_URL=http://superset-stg.fisker.internal
|
||||||
|
REACT_APP_CERT_SERVICE_URL=https://stg-gw.cloud.fiskerinc.com/certificate
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ REACT_APP_AUTH_SERVICE_URL=http://localhost/compute_auth
|
|||||||
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
|
REACT_APP_UPLOAD_SERVICE_URL=http://localhost/ota_update
|
||||||
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
|
REACT_APP_AUTH_CALLBACK_URL=http://localhost:3000
|
||||||
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
REACT_APP_SUPERSET_URL=http://superset-dev.fisker.internal
|
||||||
|
REACT_APP_CERT_SERVICE_URL=http://localhost/certificate
|
||||||
|
|||||||
@@ -37,22 +37,35 @@ const check = async (path, selector, compare) => {
|
|||||||
|
|
||||||
const sleepAndCheck = async (path, selector, compare) => {
|
const sleepAndCheck = async (path, selector, compare) => {
|
||||||
const container = await renderRoute(path);
|
const container = await renderRoute(path);
|
||||||
await waitFor(() => { });
|
await waitFor(() => {});
|
||||||
expect(container.querySelector(selector).innerHTML).toEqual(compare);
|
expect(container.querySelector(selector).innerHTML).toEqual(compare);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
|
const rxMakeStyles = /makeStyles-(\w+)-(\d+)/gi;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// Stablize Table Pagination control ids
|
// Stablize Table Pagination control ids
|
||||||
expect.addSnapshotSerializer({
|
expect.addSnapshotSerializer({
|
||||||
test: function (val) {
|
test: function (val) {
|
||||||
return val && typeof val === "string" && val.indexOf("mui-") >= 0;
|
return val && typeof val === "string" && val.indexOf("mui-") > -1;
|
||||||
},
|
},
|
||||||
print: function (val) {
|
print: function (val) {
|
||||||
let str = val;
|
let str = val;
|
||||||
str = str.replace(/mui-\d*/g, "mui-00000");
|
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}"`;
|
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 () => {
|
it("Route /page-not-found unauthenticated", async () => {
|
||||||
await check("/page-not-found", "h1", "Page Not Found");
|
await check("/page-not-found", "h1", "Page Not Found");
|
||||||
});
|
});
|
||||||
@@ -159,4 +176,9 @@ describe("App", () => {
|
|||||||
setToken(TEST_AUTH_OBJECT);
|
setToken(TEST_AUTH_OBJECT);
|
||||||
await check("/vehicle-status/FISKER123", "h6", "Vehicle FISKER123 Details");
|
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
@@ -194,6 +194,23 @@ exports[`CANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -230,6 +247,23 @@ exports[`CANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -266,6 +300,23 @@ exports[`CANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -193,6 +193,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -229,6 +246,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -265,6 +299,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
90
src/components/Certificates/Add/CreateForm.jsx
Normal file
90
src/components/Certificates/Add/CreateForm.jsx
Normal 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;
|
||||||
50
src/components/Certificates/Add/DownloadCerts.jsx
Normal file
50
src/components/Certificates/Add/DownloadCerts.jsx
Normal 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;
|
||||||
25
src/components/Certificates/Add/DownloadCerts.test.jsx
Normal file
25
src/components/Certificates/Add/DownloadCerts.test.jsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
85
src/components/Certificates/Add/index.jsx
Normal file
85
src/components/Certificates/Add/index.jsx
Normal 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;
|
||||||
50
src/components/Contexts/CertificateContext.jsx
Normal file
50
src/components/Contexts/CertificateContext.jsx
Normal 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);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`DownloadFileLink Render 1`] = `
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
download="test.txt"
|
||||||
|
>
|
||||||
|
test.txt
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
32
src/components/Controls/DownloadFileLink/index.jsx
Normal file
32
src/components/Controls/DownloadFileLink/index.jsx
Normal 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;
|
||||||
21
src/components/Controls/DownloadFileLink/index.test.jsx
Normal file
21
src/components/Controls/DownloadFileLink/index.test.jsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -195,6 +195,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -231,6 +248,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -267,6 +301,23 @@ exports[`FleetCANFiltersTable Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -153,7 +153,23 @@ exports[`FleetVehiclesTable Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -171,7 +187,23 @@ exports[`FleetVehiclesTable Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -189,7 +221,23 @@ exports[`FleetVehiclesTable Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -194,6 +194,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -230,6 +247,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -266,6 +300,23 @@ exports[`CANFiltersTab Render 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -152,7 +152,23 @@ exports[`VehiclesTab Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -170,7 +186,23 @@ exports[`VehiclesTab Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr
|
<tr
|
||||||
@@ -188,7 +220,23 @@ exports[`VehiclesTab Render 1`] = `
|
|||||||
<td
|
<td
|
||||||
class="MuiTableCell-root MuiTableCell-body MuiTableCell-alignCenter"
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { List } from "@material-ui/core";
|
import { List } from "@material-ui/core";
|
||||||
import HomeIcon from "@material-ui/icons/Home";
|
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 CommuteIcon from "@material-ui/icons/Commute";
|
||||||
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
|
import CloudDownloadIcon from "@material-ui/icons/CloudDownload";
|
||||||
import AssessmentIcon from "@material-ui/icons/Assessment";
|
import AssessmentIcon from "@material-ui/icons/Assessment";
|
||||||
|
import BuildIcon from "@material-ui/icons/Build";
|
||||||
|
|
||||||
import ListItemLink from "../ListItemLink";
|
import ListItemLink from "../ListItemLink";
|
||||||
import ListItemExternalLink from "../ListItemExternalLink";
|
import ListItemExternalLink from "../ListItemExternalLink";
|
||||||
@@ -43,6 +44,19 @@ const menuData = [
|
|||||||
icon: <AssessmentIcon />,
|
icon: <AssessmentIcon />,
|
||||||
roles: [Roles.READ, Roles.CREATE],
|
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 }) => {
|
const MenuItem = ({ item, children }) => {
|
||||||
|
|||||||
@@ -190,6 +190,70 @@ exports[`SideMenu Authenticated 1`] = `
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { MessageBar } from "../MessageBar";
|
|||||||
import { useUserContext } from "../Contexts/UserContext";
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
import { Roles } from "../../utils/roles";
|
import { Roles } from "../../utils/roles";
|
||||||
|
|
||||||
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"))
|
const CANFilterCreate = React.lazy(() => import("../CANFilter/Add"));
|
||||||
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"))
|
const CANFilterUpdate = React.lazy(() => import("../CANFilter/Update"));
|
||||||
const CarsList = React.lazy(() => import("../Cars/List"));
|
const CarsList = React.lazy(() => import("../Cars/List"));
|
||||||
const CarStatus = React.lazy(() => import("../Cars/Status"));
|
const CarStatus = React.lazy(() => import("../Cars/Status"));
|
||||||
const CarUpdateStatus = React.lazy(() => import("../Cars/UpdateStatus"));
|
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 FleetStatus = React.lazy(() => import("../Fleets/Status"));
|
||||||
const FleetAddForm = React.lazy(() => import("../Fleets/Add"));
|
const FleetAddForm = React.lazy(() => import("../Fleets/Add"));
|
||||||
const FleetUpdateForm = React.lazy(() => import("../Fleets/Update"));
|
const FleetUpdateForm = React.lazy(() => import("../Fleets/Update"));
|
||||||
const FleetAddVehicleForm = React.lazy(() => import("../Fleets/Status/Vehicles/Add"));
|
const FleetAddVehicleForm = React.lazy(() =>
|
||||||
const FleetAddCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Add"));
|
import("../Fleets/Status/Vehicles/Add")
|
||||||
const FleetUpdateCANFilterForm = React.lazy(() => import("../Fleets/Status/CANFilters/Update"));
|
);
|
||||||
|
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 Home = React.lazy(() => import("../Home"));
|
||||||
const Manifests = React.lazy(() => import("../Manifest/List"));
|
const Manifests = React.lazy(() => import("../Manifest/List"));
|
||||||
const ManifestDeploy = React.lazy(() => import("../Manifest/Deploy"));
|
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 PageNotFound = React.lazy(() => import("../404"));
|
||||||
const SSOForm = React.lazy(() => import("../SSOForm"));
|
const SSOForm = React.lazy(() => import("../SSOForm"));
|
||||||
const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
|
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 SiteRoutes = () => {
|
||||||
const { token, groups } = useUserContext();
|
const { token, groups } = useUserContext();
|
||||||
@@ -191,6 +198,14 @@ const SiteRoutes = () => {
|
|||||||
groups={groups}
|
groups={groups}
|
||||||
roles={[Roles.CREATE]}
|
roles={[Roles.CREATE]}
|
||||||
/>
|
/>
|
||||||
|
<AuthRoute
|
||||||
|
path="/tools/certificates/add"
|
||||||
|
render={() => <CertificateCreate />}
|
||||||
|
type={TYPES.PROTECTED}
|
||||||
|
token={token}
|
||||||
|
groups={groups}
|
||||||
|
roles={[Roles.CERTIFICATES]}
|
||||||
|
/>
|
||||||
<PageNotFound />
|
<PageNotFound />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
17
src/services/certificatesAPI.js
Normal file
17
src/services/certificatesAPI.js
Normal 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;
|
||||||
@@ -3,8 +3,9 @@ import { parsePayload } from "./jwt";
|
|||||||
export const Roles = {
|
export const Roles = {
|
||||||
READ: "a729bbd4-2038-4649-9127-16782bb1e701",
|
READ: "a729bbd4-2038-4649-9127-16782bb1e701",
|
||||||
CREATE: "efcc3025-e2d8-4212-8227-805c7be39d2c",
|
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) => {
|
export const hasRoleToken = (roles, token) => {
|
||||||
if (!roles || roles.length === 0) return true;
|
if (!roles || roles.length === 0) return true;
|
||||||
@@ -14,7 +15,7 @@ export const hasRoleToken = (roles, token) => {
|
|||||||
if (!groups) return false;
|
if (!groups) return false;
|
||||||
|
|
||||||
return hasRole(roles, groups);
|
return hasRole(roles, groups);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getGroups = (token) => {
|
export const getGroups = (token) => {
|
||||||
const payload = parsePayload(token);
|
const payload = parsePayload(token);
|
||||||
@@ -22,7 +23,7 @@ export const getGroups = (token) => {
|
|||||||
if (!payload || !payload["custom:groups"]) return null;
|
if (!payload || !payload["custom:groups"]) return null;
|
||||||
|
|
||||||
return payload["custom:groups"];
|
return payload["custom:groups"];
|
||||||
}
|
};
|
||||||
|
|
||||||
export const hasRole = (roles, groups) => {
|
export const hasRole = (roles, groups) => {
|
||||||
if (!roles || roles.length === 0) return true;
|
if (!roles || roles.length === 0) return true;
|
||||||
@@ -33,4 +34,4 @@ export const hasRole = (roles, groups) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 = {
|
export const TEST_AUTH_OBJECT = {
|
||||||
idToken: {
|
idToken: {
|
||||||
jwtToken: TEST_TOKEN,
|
jwtToken: TEST_TOKEN,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const TEST_EXPECTED_GROUPS =
|
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]";
|
||||||
|
|||||||
Reference in New Issue
Block a user