aboutsummaryrefslogtreecommitdiffstats
path: root/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx')
-rw-r--r--packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx371
1 files changed, 371 insertions, 0 deletions
diff --git a/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx b/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx
new file mode 100644
index 0000000..0cbcac4
--- /dev/null
+++ b/packages/renderer/src/components/errorPage/SingleCertificateDetails.tsx
@@ -0,0 +1,371 @@
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 AttachmentIcon from '@mui/icons-material/Attachment';
22import Link from '@mui/material/Link';
23import { styled } from '@mui/material/styles';
24import type { Certificate, CertificatePrincipal } from '@sophie/shared';
25import type { TFunction } from 'i18next';
26import { observer } from 'mobx-react-lite';
27import React, { type ReactNode } from 'react';
28import { useTranslation } from 'react-i18next';
29
30const SingleCertificateDetailsRoot = styled('table')(({ theme }) => ({
31 width: '100%',
32 borderSpacing: `${theme.spacing(2)} 0`,
33}));
34
35const Header = styled('th')({
36 textAlign: 'right',
37 whiteSpace: 'nowrap',
38 verticalAlign: 'top',
39});
40
41const SectionHeader = styled(Header)(({ theme }) => ({
42 padding: `${theme.spacing(2)} 0`,
43 fontWeight: theme.typography.fontWeightBold,
44}));
45
46const SubHeader = styled(Header)(({ theme }) => ({
47 color: theme.palette.text.secondary,
48 fontWeight: theme.typography.fontWeightMedium,
49}));
50
51const Cell = styled('td')({
52 verticalAlign: 'top',
53});
54
55function Section({ id, label }: { id: string; label: string }): JSX.Element {
56 return (
57 <tr>
58 <SectionHeader id={id} scope="colgroup">
59 {label}
60 </SectionHeader>
61 <Cell aria-hidden="true" />
62 </tr>
63 );
64}
65
66function Row({
67 id,
68 label,
69 headers,
70 children,
71}: {
72 id: string;
73 label: string;
74 headers: string;
75 children: ReactNode;
76}): JSX.Element {
77 return (
78 <tr>
79 <SubHeader id={id} scope="row">
80 {label}
81 </SubHeader>
82 <Cell headers={`${headers} ${id}`}>{children}</Cell>
83 </tr>
84 );
85}
86
87function OptionalRow({
88 id,
89 label,
90 value,
91 headers,
92}: {
93 id: string;
94 label: string;
95 value: string;
96 headers: string;
97}): JSX.Element | null {
98 if (value === '') {
99 // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering.
100 return null;
101 }
102
103 return (
104 <Row id={id} label={label} headers={headers}>
105 {value}
106 </Row>
107 );
108}
109
110function DateTimeRow({
111 id,
112 label,
113 value,
114 locales,
115 headers,
116}: {
117 id: string;
118 label: string;
119 value: number;
120 locales: readonly string[];
121 headers: string;
122}): JSX.Element {
123 // Electron provides `validStart` and `validExpiry` in seconds,
124 // but `Date` expects milliseconds.
125 const date = new Date(value * 1000);
126 const dateStr = new Intl.DateTimeFormat([...locales], {
127 dateStyle: 'long',
128 timeStyle: 'long',
129 }).format(date);
130
131 return (
132 <Row id={id} label={label} headers={headers}>
133 {dateStr}
134 </Row>
135 );
136}
137
138function Principal({
139 id,
140 label,
141 principal: {
142 country,
143 state,
144 locality,
145 organizations,
146 organizationUnits,
147 commonName,
148 },
149 t,
150 onIssuerClick,
151}: {
152 id: string;
153 label: string;
154 principal: CertificatePrincipal;
155 t: TFunction;
156 onIssuerClick?: (() => void) | undefined;
157}): JSX.Element {
158 return (
159 <>
160 <Section id={id} label={label} />
161 <OptionalRow
162 id={`${id}-country`}
163 label={t<string>('country')}
164 value={country}
165 headers={id}
166 />
167 <OptionalRow
168 id={`${id}-state`}
169 label={t<string>('state')}
170 value={state}
171 headers={id}
172 />
173 <OptionalRow
174 id={`${id}-locality`}
175 label={t<string>('locality')}
176 value={locality}
177 headers={id}
178 />
179 {/*
180 eslint-disable react/no-array-index-key --
181 These entries can freely contain repetitions, so we can only tell them apart by index.
182 */}
183 {organizations.map((value, i) => (
184 <OptionalRow
185 key={i}
186 id={`${id}-organization-${i}`}
187 label={t<string>('organization')}
188 value={value}
189 headers={id}
190 />
191 ))}
192 {organizationUnits.map((value, i) => (
193 <OptionalRow
194 key={i}
195 id={`${id}-organizationUnit-${i}`}
196 label={t<string>('organizationUnit')}
197 value={value}
198 headers={id}
199 />
200 ))}
201 {/* eslint-enable react/no-array-index-key */}
202 {commonName !== '' && (
203 <Row
204 id={`${id}-commonName`}
205 label={t<string>('commonName')}
206 headers={id}
207 >
208 {onIssuerClick === undefined ? (
209 commonName
210 ) : (
211 // eslint-disable-next-line jsx-a11y/anchor-is-valid -- This is a `button`.
212 <Link
213 component="button"
214 variant="body1"
215 onClick={onIssuerClick}
216 sx={{ verticalAlign: 'baseline' }}
217 >
218 {commonName}
219 </Link>
220 )}
221 </Row>
222 )}
223 </>
224 );
225}
226
227Principal.defaultProps = {
228 onIssuerClick: undefined,
229};
230
231function parseFingerprint(fingerprint: string): {
232 algorithm: string | undefined;
233 formattedValue: string;
234} {
235 const separatorIndex = fingerprint.indexOf('/');
236 if (separatorIndex <= 0) {
237 return {
238 algorithm: undefined,
239 formattedValue: fingerprint,
240 };
241 }
242 return {
243 algorithm: fingerprint.slice(0, separatorIndex).toUpperCase(),
244 formattedValue: fingerprint.slice(separatorIndex + 1),
245 };
246}
247
248function Fingerprint({
249 id,
250 label,
251 fallbackAlgorithmLabel,
252 value,
253}: {
254 id: string;
255 label: string;
256 fallbackAlgorithmLabel: string;
257 value: string;
258}) {
259 const { algorithm, formattedValue } = parseFingerprint(value);
260 return (
261 <>
262 <Section id={id} label={label} />
263 <OptionalRow
264 id={`${id}-fingerprint`}
265 label={algorithm ?? fallbackAlgorithmLabel}
266 headers={id}
267 value={formattedValue}
268 />
269 </>
270 );
271}
272
273function SingleCertificateDetails({
274 detailsId,
275 certificate: {
276 subject,
277 issuer,
278 validStart,
279 validExpiry,
280 fingerprint,
281 serialNumber,
282 },
283 onIssuerClick,
284 onDownloadClick,
285}: {
286 detailsId: string;
287 certificate: Certificate;
288 onIssuerClick?: (() => void) | undefined;
289 onDownloadClick: () => void;
290}): JSX.Element {
291 const {
292 t,
293 i18n: { languages },
294 } = useTranslation(undefined, {
295 keyPrefix: 'error.certificateError.details',
296 });
297
298 const id = `${detailsId}-${fingerprint}-certificate`;
299
300 return (
301 <SingleCertificateDetailsRoot>
302 <tbody>
303 <Principal
304 id={`${id}-subject`}
305 label={t<string>('subject')}
306 principal={subject}
307 t={t}
308 />
309 <Principal
310 id={`${id}-issuer`}
311 label={t<string>('issuer')}
312 principal={issuer}
313 t={t}
314 onIssuerClick={onIssuerClick}
315 />
316 <Section id={`${id}-validity`} label={t<string>('validity')} />
317 <DateTimeRow
318 id={`${id}-validity-validStart`}
319 label={t<string>('validStart')}
320 value={validStart}
321 locales={languages}
322 headers={`${id}-validity`}
323 />
324 <DateTimeRow
325 id={`${id}-validity-validExpiry`}
326 label={t<string>('validExpiry')}
327 value={validExpiry}
328 locales={languages}
329 headers={`${id}-validity`}
330 />
331 <Section
332 id={`${id}-miscellaneous`}
333 label={t<string>('miscellaneous')}
334 />
335 <OptionalRow
336 id={`${id}-miscellaneous-serialNumber`}
337 label={t<string>('serialNumber')}
338 value={serialNumber}
339 headers={`${id}-miscellaneous`}
340 />
341 <Row
342 id={`${id}-miscellaneous-download`}
343 label={t<string>('download')}
344 headers={`${id}-miscellaneous`}
345 >
346 {/* eslint-disable-next-line jsx-a11y/anchor-is-valid -- This is a `button`. */}
347 <Link
348 component="button"
349 variant="body1"
350 onClick={onDownloadClick}
351 sx={{ verticalAlign: 'baseline' }}
352 >
353 <AttachmentIcon fontSize="inherit" /> {t('downloadPEM')}
354 </Link>
355 </Row>
356 <Fingerprint
357 id={`${id}-fingerprint`}
358 label={t<string>('fingerprint')}
359 fallbackAlgorithmLabel={t<string>('fingerprintUnknown')}
360 value={fingerprint}
361 />
362 </tbody>
363 </SingleCertificateDetailsRoot>
364 );
365}
366
367SingleCertificateDetails.defaultProps = {
368 onIssuerClick: undefined,
369};
370
371export default observer(SingleCertificateDetails);