aboutsummaryrefslogtreecommitdiffstats
path: root/packages/renderer/src/components/errorPage/CertificateDetails.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/renderer/src/components/errorPage/CertificateDetails.tsx')
-rw-r--r--packages/renderer/src/components/errorPage/CertificateDetails.tsx186
1 files changed, 186 insertions, 0 deletions
diff --git a/packages/renderer/src/components/errorPage/CertificateDetails.tsx b/packages/renderer/src/components/errorPage/CertificateDetails.tsx
new file mode 100644
index 0000000..04d483e
--- /dev/null
+++ b/packages/renderer/src/components/errorPage/CertificateDetails.tsx
@@ -0,0 +1,186 @@
1/*
2 * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
22import Accordion from '@mui/material/Accordion';
23import AccordionDetails from '@mui/material/AccordionDetails';
24import AccordionSummary from '@mui/material/AccordionSummary';
25import Box from '@mui/material/Box';
26import Tab from '@mui/material/Tab';
27import Tabs from '@mui/material/Tabs';
28import Typography from '@mui/material/Typography';
29import { styled } from '@mui/material/styles';
30import type { Certificate } from '@sophie/shared';
31import { observer } from 'mobx-react-lite';
32import React, { useState } from 'react';
33import { useTranslation } from 'react-i18next';
34
35import type Service from '../../stores/Service.js';
36
37import SingleCertificateDetails from './SingleCertificateDetails.js';
38import TrustCertificateDialog from './TrustCertificateDialog.js';
39
40const SUMMARY_ID = 'Sophie-CertificateDetails-header';
41const DETAILS_ID = 'Sophie-CertificateDetails-content';
42
43function getCertificateChain(endEntityCertificate: Certificate): Certificate[] {
44 const chain: Certificate[] = [];
45 let certificate: Certificate | undefined = endEntityCertificate;
46 while (certificate !== undefined) {
47 chain.push(certificate);
48 certificate = certificate.issuerCert as Certificate;
49 }
50 return chain;
51}
52
53const DetailsBox = styled(Box)(({ theme }) => ({
54 padding: `0 ${theme.spacing(2)}`,
55}));
56
57function CertificateDetails({
58 service,
59}: {
60 service: Service;
61}): JSX.Element | null {
62 const { t } = useTranslation(undefined, {
63 keyPrefix: 'error.certificateError.details',
64 });
65 const [certificateIndex, setCertificateIndex] = useState(0);
66
67 const { id: serviceId, state } = service;
68 const { type: errorType } = state;
69
70 if (errorType !== 'certificateError') {
71 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
72 return null;
73 }
74
75 const { certificate, trust } = state;
76
77 const chain = getCertificateChain(certificate);
78
79 if (chain.length === 0) {
80 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
81 return null;
82 }
83
84 const id = `${serviceId}-certificateDetails`;
85
86 const trustCertificateDialog = (
87 <TrustCertificateDialog
88 trust={trust}
89 onClick={() => service.temporarilyTrustCurrentCertificate()}
90 />
91 );
92
93 return (
94 <Accordion
95 disableGutters
96 variant="outlined"
97 sx={{ '&::before': { display: 'none' }, borderRadius: 1 }}
98 >
99 <AccordionSummary
100 id={SUMMARY_ID}
101 aria-controls={DETAILS_ID}
102 expandIcon={<ExpandMoreIcon />}
103 >
104 <Typography>{t('title')}</Typography>
105 </AccordionSummary>
106 <AccordionDetails
107 sx={{
108 px: 0,
109 pt: 0,
110 }}
111 >
112 {chain.length >= 2 ? (
113 <>
114 <Tabs
115 aria-label={t<string>('tabsLabel')}
116 value={certificateIndex < chain.length ? certificateIndex : false}
117 onChange={(_event, value) => setCertificateIndex(value as number)}
118 variant="fullWidth"
119 >
120 {chain.map((member, i) => (
121 <Tab
122 key={member.fingerprint}
123 value={i}
124 id={`${id}-tab-${i}`}
125 aria-controls={`${id}-tabpanel-${i}`}
126 label={
127 member.subjectName === ''
128 ? member.fingerprint
129 : member.subjectName
130 }
131 />
132 ))}
133 </Tabs>
134 <DetailsBox
135 sx={(theme) => ({
136 paddingTop: 1,
137 borderTop: `1px solid ${theme.palette.divider}`,
138 })}
139 >
140 {chain.map((member, i) => (
141 <div
142 key={member.fingerprint}
143 role="tabpanel"
144 hidden={certificateIndex !== i}
145 id={`${id}-tabpanel-${i}`}
146 aria-labelledby={`${id}-tab-${i}`}
147 >
148 {certificateIndex === i && (
149 <>
150 <SingleCertificateDetails
151 detailsId={id}
152 certificate={member}
153 onIssuerClick={
154 i < chain.length - 1
155 ? () => setCertificateIndex(i + 1)
156 : undefined
157 }
158 onDownloadClick={() =>
159 service.downloadCertificate(member.fingerprint)
160 }
161 />
162 {i === 0 && trustCertificateDialog}
163 </>
164 )}
165 </div>
166 ))}
167 </DetailsBox>
168 </>
169 ) : (
170 <DetailsBox>
171 <SingleCertificateDetails
172 detailsId={id}
173 certificate={chain[0]}
174 onDownloadClick={() =>
175 service.downloadCertificate(chain[0].fingerprint)
176 }
177 />
178 {trustCertificateDialog}
179 </DetailsBox>
180 )}
181 </AccordionDetails>
182 </Accordion>
183 );
184}
185
186export default observer(CertificateDetails);