diff options
Diffstat (limited to 'packages/renderer/src/components/errorPage/CertificateDetails.tsx')
-rw-r--r-- | packages/renderer/src/components/errorPage/CertificateDetails.tsx | 186 |
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 | |||
21 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | ||
22 | import Accordion from '@mui/material/Accordion'; | ||
23 | import AccordionDetails from '@mui/material/AccordionDetails'; | ||
24 | import AccordionSummary from '@mui/material/AccordionSummary'; | ||
25 | import Box from '@mui/material/Box'; | ||
26 | import Tab from '@mui/material/Tab'; | ||
27 | import Tabs from '@mui/material/Tabs'; | ||
28 | import Typography from '@mui/material/Typography'; | ||
29 | import { styled } from '@mui/material/styles'; | ||
30 | import type { Certificate } from '@sophie/shared'; | ||
31 | import { observer } from 'mobx-react-lite'; | ||
32 | import React, { useState } from 'react'; | ||
33 | import { useTranslation } from 'react-i18next'; | ||
34 | |||
35 | import type Service from '../../stores/Service.js'; | ||
36 | |||
37 | import SingleCertificateDetails from './SingleCertificateDetails.js'; | ||
38 | import TrustCertificateDialog from './TrustCertificateDialog.js'; | ||
39 | |||
40 | const SUMMARY_ID = 'Sophie-CertificateDetails-header'; | ||
41 | const DETAILS_ID = 'Sophie-CertificateDetails-content'; | ||
42 | |||
43 | function 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 | |||
53 | const DetailsBox = styled(Box)(({ theme }) => ({ | ||
54 | padding: `0 ${theme.spacing(2)}`, | ||
55 | })); | ||
56 | |||
57 | function 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 | |||
186 | export default observer(CertificateDetails); | ||