diff --git a/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.test.tsx b/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.test.tsx index cd63b56..88d294a 100644 --- a/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.test.tsx +++ b/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.test.tsx @@ -182,8 +182,12 @@ describe('OverviewTab', () => { ); }); - it('geschuetzte PFX: Passwort eingeben und erneut pruefen', async () => { - mockAnalyze.mockResolvedValueOnce({ items: [LEAF], locked: ['a.pfx'], ignored: [] }); + it('geschuetzte PFX ohne Schluessel im Paket: Passwort eingeben und erneut pruefen', async () => { + mockAnalyze.mockResolvedValueOnce({ + items: [{ ...LEAF, matchId: null }], + locked: ['a.pfx'], + ignored: [], + }); const pfx = new File(['x'], 'a.pfx'); await upload([pfx]); @@ -194,6 +198,17 @@ describe('OverviewTab', () => { expect(await screen.findAllByTestId('bundle-item')).toHaveLength(2); }); + it('geschuetzte PFX, aber Zertifikat und Schluessel liegen schon vor: ruhiger Hinweis, Passwort optional', async () => { + mockAnalyze.mockResolvedValue({ items: [LEAF, INTER, KEY], locked: ['a.pfx'], ignored: [] }); + await upload([new File(['x'], 'paket.zip')]); + + const notice = await screen.findByTestId('locked-notice'); + expect(within(notice).getByText(/lockedNotNeeded/)).toBeInTheDocument(); + expect(within(notice).queryByLabelText('lockedPassword')).not.toBeInTheDocument(); + fireEvent.click(within(notice).getByText('unlockAnyway')); + expect(within(notice).getByLabelText('lockedPassword')).toBeInTheDocument(); + }); + it('Fehler beim Pruefen wird angezeigt', async () => { mockAnalyze.mockRejectedValue(new Error('400')); await upload([new File(['x'], 'a.txt')]); diff --git a/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx b/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx index e3eef62..3975c7d 100644 --- a/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx +++ b/apps/web/src/app/(portal)/modules/cert-manager/components/OverviewTab.tsx @@ -54,6 +54,7 @@ export function OverviewTab() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); + const [unlockOpen, setUnlockOpen] = useState(false); async function analyze(next: File[], pw: string) { if (next.length === 0) { @@ -159,30 +160,20 @@ export function OverviewTab() { {error &&

{error}

} {result && result.locked.length > 0 && ( -
-

- {t('locked', { files: result.locked.join(', ') })} -

-
- setPassword(e.target.value)} - placeholder={t('lockedPassword')} - aria-label={t('lockedPassword')} - autoComplete="off" - className="w-64 rounded border border-border bg-background px-3 py-1.5 text-sm" - /> - -
-
+ i.kind === 'certificate' && i.matchId !== null)} + open={unlockOpen} + onOpen={() => setUnlockOpen(true)} + password={password} + onPassword={setPassword} + loading={loading} + onUnlock={() => void analyze(files, password)} + /> )} {result && result.items.length === 0 && !loading && ( @@ -206,6 +197,62 @@ export function OverviewTab() { ); } +function LockedNotice(props: { + files: string[]; + notNeeded: boolean; + open: boolean; + onOpen: () => void; + password: string; + onPassword: (value: string) => void; + loading: boolean; + onUnlock: () => void; +}) { + const t = useTranslations('certManager.overview'); + const showInput = !props.notNeeded || props.open; + return ( +
+

+ {t(props.notNeeded ? 'lockedNotNeeded' : 'locked', { files: props.files.join(', ') })} +

+ {!showInput && ( + + )} + {showInput && ( +
+ props.onPassword(e.target.value)} + placeholder={t('lockedPassword')} + aria-label={t('lockedPassword')} + autoComplete="off" + className="w-64 rounded border border-border bg-background px-3 py-1.5 text-sm" + /> + +
+ )} +
+ ); +} + function BundleItemCard({ item, byId }: { item: BundleItem; byId: Record }) { const t = useTranslations('certManager.overview'); const [pfxOpen, setPfxOpen] = useState(false); diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 3e6b325..2553897 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -1239,8 +1239,10 @@ "analyzing": "Dateien werden geprüft …", "error": "Die Dateien konnten nicht geprüft werden. Bitte prüfen Sie, ob es Zertifikatsdateien sind.", "locked": "Geschützt: {files}. Geben Sie das Passwort ein, um auch diesen Inhalt zu lesen.", + "lockedNotNeeded": "{files} ist mit einem Passwort geschützt. Sie brauchen es nicht: Zertifikat und privater Schlüssel liegen schon als einzelne Dateien vor und sind unten aufgeführt. Eine neue PFX-Datei mit eigenem Passwort können Sie beim Serverzertifikat erstellen.", "lockedPassword": "Passwort der geschützten Datei", "unlock": "Entsperren", + "unlockAnyway": "Trotzdem mit Passwort öffnen", "nothingFound": "In den Dateien wurde kein Zertifikat, Schlüssel und keine Zertifikatsanfrage gefunden.", "ignored": "Nicht verwendet (kein Zertifikat erkannt): {files}", "type": { diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index ef066eb..e9f7a15 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -1239,8 +1239,10 @@ "analyzing": "Checking files …", "error": "The files could not be checked. Please make sure they are certificate files.", "locked": "Protected: {files}. Enter the password to read this content as well.", + "lockedNotNeeded": "{files} is password-protected. You do not need it: certificate and private key are already available as separate files and listed below. You can create a new PFX file with your own password at the server certificate.", "lockedPassword": "Password of the protected file", "unlock": "Unlock", + "unlockAnyway": "Open with password anyway", "nothingFound": "No certificate, key or certificate request was found in the files.", "ignored": "Not used (no certificate detected): {files}", "type": {