Skip to content

Commit 2fd4f6f

Browse files
committed
refactor(webui): improved CertManager layout & design
1 parent 630ca3f commit 2fd4f6f

3 files changed

Lines changed: 111 additions & 63 deletions

File tree

‎data/src/lib/components/AppMisc.svelte‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
} from "$lib/types/api";
1515
import { diff } from "$lib/utils/objDiff";
1616
import HardwareConfig from "$lib/components/HardwareConfig.svelte";
17-
import HttpsCertManager from "$lib/components/CertManager.svelte";
17+
import CertManager from "$lib/components/CertManager.svelte";
1818
1919
let { misc, eth, nfcPresets, nfcConnected = $bindable(false), error } = $props();
2020
@@ -518,12 +518,11 @@
518518
</div>
519519
</div>
520520
</div>
521-
<HttpsCertManager
521+
<CertManager
522522
certType={CertificateType.HTTPS_SERVER_CERT}
523523
keyType={CertificateType.HTTPS_PRIVATE_KEY}
524524
caType={CertificateType.HTTPS_CA_CERT}
525-
title="Current Certificate"
526-
description="Upload PEM-encoded certificate, private key, and optional CA certificate (for mTLS)."
525+
mode="https"
527526
/>
528527
{/if}
529528
</div>

‎data/src/lib/components/AppMqtt.svelte‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@
544544
certType={CertificateType.MQTT_CLIENT}
545545
keyType={CertificateType.MQTT_PRIVATE_KEY}
546546
caType={CertificateType.MQTT_CA}
547-
title="Current Certificate"
547+
mode="mqtt"
548548
/>
549549
</div>
550550
{/if}

‎data/src/lib/components/CertManager.svelte‎

Lines changed: 107 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,21 @@
1313
keyType: CertificateType;
1414
/** Certificate type for the CA certificate (optional) */
1515
caType?: CertificateType;
16-
/** Section title */
17-
title?: string;
18-
/** Optional description text */
19-
description?: string;
16+
/** Mode determines certificate requirements - mqtt (CA required) or https (cert+key required) */
17+
mode?: 'mqtt' | 'https';
2018
}
2119
2220
let {
2321
certType,
2422
keyType,
2523
caType,
26-
title = 'Current Certificate',
27-
description,
24+
mode = 'https',
2825
}: Props = $props();
2926
27+
let certLabel = $derived(mode === 'mqtt' ? 'Client Certificate' : 'Certificate File');
28+
let keyLabel = $derived(mode === 'mqtt' ? 'Client Private Key' : 'Private Key');
29+
let caLabel = $derived(mode === 'mqtt' ? 'CA Certificate (Required)' : 'CA Certificate File (Optional)');
30+
3031
let selectedCertFile: File | null = $state(null);
3132
let selectedKeyFile: File | null = $state(null);
3233
let selectedCaFile: File | null = $state(null);
@@ -85,7 +86,6 @@
8586
8687
const VALID_CERT_EXTENSIONS = ['.pem', '.crt', '.cer'];
8788
const VALID_KEY_EXTENSIONS = ['.pem', '.key'];
88-
const VALID_CA_EXTENSIONS = ['.pem', '.crt', '.cer'];
8989
9090
function isValidCertFile(file: File): boolean {
9191
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
@@ -123,7 +123,7 @@
123123
124124
function isValidCaFile(file: File): boolean {
125125
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
126-
return VALID_CA_EXTENSIONS.includes(ext);
126+
return VALID_CERT_EXTENSIONS.includes(ext);
127127
}
128128
129129
function handleCaFileChange(event: Event) {
@@ -145,36 +145,57 @@
145145
});
146146
147147
async function handleUpload() {
148-
if (!selectedCertFile || !selectedKeyFile) {
149-
notifications.addError('Please select both certificate and private key files');
150-
return;
148+
if (mode === 'mqtt') {
149+
if (!selectedCaFile) {
150+
notifications.addError('Please select a CA certificate file for server verification');
151+
return;
152+
}
153+
if (selectedCertFile && !selectedKeyFile) {
154+
notifications.addError('Please select a private key file to match the client certificate');
155+
return;
156+
}
157+
if (selectedKeyFile && !selectedCertFile) {
158+
notifications.addError('Please select a client certificate file to match the private key');
159+
return;
160+
}
161+
} else {
162+
if (!selectedCertFile || !selectedKeyFile) {
163+
notifications.addError('Please select both certificate and private key files');
164+
return;
165+
}
151166
}
152167
153168
isUploading = true;
154-
uploadProgress.cert = 10;
155-
uploadProgress.key = 10;
169+
uploadProgress.cert = selectedCertFile ? 10 : 0;
170+
uploadProgress.key = selectedKeyFile ? 10 : 0;
171+
uploadProgress.ca = selectedCaFile ? 10 : 0;
156172
157173
try {
158-
const certContent = await selectedCertFile.text();
159-
uploadProgress.cert = 50;
160-
uploadProgress.key = 50;
161-
162-
const keyContent = await selectedKeyFile.text();
163-
164-
const certResult = await uploadCertificate(certType, certContent);
165-
uploadProgress.cert = 90;
166-
uploadProgress.key = 90;
167-
168-
const keyResult = await uploadCertificate(keyType, keyContent);
169-
174+
let certResult = { success: true };
175+
let keyResult = { success: true };
170176
let caResult = { success: true };
177+
171178
if (selectedCaFile && caType !== undefined) {
172179
uploadProgress.ca = 50;
173180
const caContent = await selectedCaFile.text();
174181
caResult = await uploadCertificate(caType, caContent);
175182
uploadProgress.ca = 90;
176183
}
177184
185+
if (selectedCertFile) {
186+
uploadProgress.cert = 50;
187+
const certContent = await selectedCertFile.text();
188+
certResult = await uploadCertificate(certType, certContent);
189+
uploadProgress.cert = 90;
190+
}
191+
192+
if (selectedKeyFile) {
193+
uploadProgress.key = 50;
194+
const keyContent = await selectedKeyFile.text();
195+
keyResult = await uploadCertificate(keyType, keyContent);
196+
uploadProgress.key = 90;
197+
}
198+
178199
if (certResult.success && keyResult.success && caResult.success) {
179200
uploadProgress.cert = 100;
180201
uploadProgress.key = 100;
@@ -218,7 +239,6 @@
218239
return;
219240
}
220241
221-
// Also delete CA cert if it exists
222242
if (caType !== undefined) {
223243
await deleteCertificate(caType);
224244
}
@@ -247,7 +267,7 @@
247267
<!-- Certificate Status Display -->
248268
<div class="py-3 px-3 bg-base-100 rounded-lg">
249269
<div class="flex items-center justify-between mb-3">
250-
<h4 class="text-sm font-medium">{title}</h4>
270+
<h4 class="text-sm font-medium">Certificates</h4>
251271
{#if hasCustomCert}
252272
<button
253273
type="button"
@@ -265,12 +285,14 @@
265285
{/if}
266286
</div>
267287

268-
{#if description}
269-
<p class="text-xs text-base-content/60 mb-3">{description}</p>
270-
{/if}
271-
272288
{#if certInfo}
273289
<div class="space-y-2 text-xs">
290+
<div class="flex items-center gap-2">
291+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-info" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
292+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
293+
</svg>
294+
<span class="font-medium text-info">{mode === "https" ? "Server" : "Client"} Certificate Present</span>
295+
</div>
274296
{#if certInfo.issuer}
275297
<div class="flex flex-col">
276298
<span class="text-base-content/60">Issuer</span>
@@ -288,7 +310,7 @@
288310
{#if certInfo.serial}
289311
<div class="flex flex-col">
290312
<span class="text-base-content/60">Serial number</span>
291-
<span class="font-mono truncate" title={certInfo.serial}>{certInfo.serial}</span>
313+
<span class="font-mono break-words" title={certInfo.serial}>{certInfo.serial}</span>
292314
</div>
293315
{/if}
294316

@@ -358,19 +380,32 @@
358380
</div>
359381
{#if caCertInfo.issuer}
360382
<div class="flex flex-col">
361-
<span class="text-base-content/60">CA Issuer</span>
383+
<span class="text-base-content/60">Issuer</span>
362384
<span class="font-mono truncate" title={caCertInfo.issuer}>{caCertInfo.issuer}</span>
363385
</div>
364386
{/if}
365387
{#if caCertInfo.subject}
366388
<div class="flex flex-col">
367-
<span class="text-base-content/60">CA Subject</span>
389+
<span class="text-base-content/60">Subject</span>
368390
<span class="font-mono truncate" title={caCertInfo.subject}>{caCertInfo.subject}</span>
369391
</div>
370392
{/if}
393+
{#if caCertInfo.serial}
394+
<div class="flex flex-col">
395+
<span class="text-base-content/60">Serial number</span>
396+
<span class="font-mono break-words" title={caCertInfo.serial}>{caCertInfo.serial}</span>
397+
</div>
398+
{/if}
399+
400+
{#if caCertInfo.fingerprint}
401+
<div class="flex flex-col">
402+
<span class="text-base-content/60">SHA256 Fingerprint</span>
403+
<span class="font-mono break-words" title={caCertInfo.fingerprint}>{caCertInfo.fingerprint}</span>
404+
</div>
405+
{/if}
371406
{#if caCertInfo.expiration?.from && caCertInfo.expiration?.to}
372407
<div class="flex flex-col">
373-
<span class="text-base-content/60">CA Valid Period</span>
408+
<span class="text-base-content/60">Valid Period</span>
374409
<span class="font-mono">
375410
{formatDate(caCertInfo.expiration.from)} - {formatDate(caCertInfo.expiration.to)}
376411
</span>
@@ -420,21 +455,42 @@
420455
<!-- File Upload Section -->
421456
<div class="space-y-3 py-2 px-3 bg-base-100 rounded-lg">
422457
<h4 class="text-sm font-medium">Upload Certificate</h4>
423-
<p class="text-xs text-base-content/60">Upload PEM-encoded certificate, private key, and optional CA certificate (for mTLS).</p>
424-
458+
<!-- Help Text -->
459+
<div class="border-l-4 border-info bg-gradient-to-r from-info/10 to-transparent rounded-r-lg p-3 shadow-sm">
460+
<div class="flex items-start gap-2">
461+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-info mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
462+
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
463+
</svg>
464+
<div class="space-y-1.5">
465+
<p class="text-xs font-semibold text-base-content">Certificate Requirements</p>
466+
<ul class="text-xs text-base-content/75 list-disc list-inside space-y-0.5">
467+
<li>Must be in PEM format</li>
468+
<li>Supported keys: RSA (2048+ bits) and SECP256R1 (P-256)</li>
469+
{#if mode === 'mqtt'}
470+
<li><strong>CA certificate</strong> is required for verifying the MQTT broker's identity</li>
471+
<li><strong>Client certificate + key</strong> are only needed for mutual TLS (mTLS) authentication</li>
472+
{:else}
473+
<li><strong>Server certificate + private key</strong> are required for HTTPS operation</li>
474+
<li><strong>CA certificate</strong> is only needed if requiring client certificate validation (mTLS)</li>
475+
{/if}
476+
</ul>
477+
</div>
478+
</div>
479+
</div>
425480
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
426481
<!-- Certificate File Input -->
427482
<div class="form-control">
428-
<label class="label" for="https-cert-file">
429-
<span class="label-text text-xs">Certificate File (.pem, .crt, .cer)</span>
483+
<label class="label" for="cert-file-input">
484+
<span class="label-text text-xs">{certLabel} (.pem, .crt, .cer){mode === 'mqtt' ? ' - Optional' : ''}</span>
430485
</label>
431486
<input
432-
id="https-cert-file"
487+
id="cert-file-input"
433488
type="file"
434489
accept={VALID_CERT_EXTENSIONS.join(",")}
435490
onchange={handleCertFileChange}
436491
bind:this={certInputRef}
437492
class="file-input file-input-sm file-input-bordered w-full text-xs"
493+
disabled={isUploading}
438494
/>
439495
{#if selectedCertFile}
440496
<p class="text-xs text-success mt-1">Selected: {selectedCertFile.name}</p>
@@ -446,16 +502,17 @@
446502

447503
<!-- Private Key File Input -->
448504
<div class="form-control">
449-
<label class="label" for="https-key-file">
450-
<span class="label-text text-xs">Private Key File (.pem, .key)</span>
505+
<label class="label" for="key-file-input">
506+
<span class="label-text text-xs">{keyLabel} (.pem, .key){mode === 'mqtt' ? ' - Optional' : ''}</span>
451507
</label>
452508
<input
453-
id="https-key-file"
509+
id="key-file-input"
454510
type="file"
455511
accept={VALID_KEY_EXTENSIONS.join(",")}
456512
onchange={handleKeyFileChange}
457513
bind:this={keyInputRef}
458514
class="file-input file-input-sm file-input-bordered w-full text-xs"
515+
disabled={isUploading}
459516
/>
460517
{#if selectedKeyFile}
461518
<p class="text-xs text-success mt-1">Selected: {selectedKeyFile.name}</p>
@@ -466,18 +523,19 @@
466523
</div>
467524
</div>
468525

469-
<!-- CA Certificate File Input -->
526+
<!-- CA Certificate File Input - Full width for both modes -->
470527
<div class="form-control mt-3">
471-
<label class="label" for="https-ca-file">
472-
<span class="label-text text-xs">CA Certificate File (.pem, .crt, .cer) - Optional</span>
528+
<label class="label" for="ca-file-input">
529+
<span class="label-text text-xs">{caLabel} (.pem, .crt, .cer)</span>
473530
</label>
474531
<input
475-
id="https-ca-file"
532+
id="ca-file-input"
476533
type="file"
477-
accept={VALID_CA_EXTENSIONS.join(",")}
534+
accept={VALID_CERT_EXTENSIONS.join(",")}
478535
onchange={handleCaFileChange}
479536
bind:this={caInputRef}
480537
class="file-input file-input-sm file-input-bordered w-full text-xs"
538+
disabled={isUploading}
481539
/>
482540
{#if selectedCaFile}
483541
<p class="text-xs text-success mt-1">Selected: {selectedCaFile.name}</p>
@@ -491,7 +549,7 @@
491549
<button
492550
type="button"
493551
class="btn btn-sm btn-primary"
494-
disabled={!selectedCertFile || !selectedKeyFile || isUploading}
552+
disabled={isUploading || (mode === 'mqtt' ? !selectedCaFile : (!selectedCertFile || !selectedKeyFile))}
495553
onclick={handleUpload}
496554
>
497555
{#if isUploading}
@@ -505,13 +563,4 @@
505563
{/if}
506564
</button>
507565
</div>
508-
509-
<!-- Help Text -->
510-
<div class="text-xs text-base-content/60 py-2 px-3 bg-base-100 rounded-lg">
511-
<ul class="list-disc list-inside space-y-1">
512-
<li>Certificate must be in PEM format</li>
513-
<li>Supported keys: RSA (2048+ bits) and SECP256R1</li>
514-
<li>CA certificate is optional and only used for client certificate validation (mTLS)</li>
515-
</ul>
516-
</div>
517566
</div>

0 commit comments

Comments
 (0)