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