fix(cert-manager): geschuetzte PFX nur noch als ruhiger Hinweis, wenn Zertifikat und Schluessel schon vorliegen
Die Aussteller-ZIP enthaelt neben .pem/.key eine PFX mit einem Passwort, das niemand kennt (Windows certutil: ERROR_INVALID_PASSWORD bei leerem Passwort). Die Uebersicht verlangte dafuer prominent ein Passwort, obwohl Zertifikat und Schluessel einzeln vorliegen. Jetzt: neutraler Hinweis, dass das Passwort nicht gebraucht wird, Passwortfeld nur auf Wunsch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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')]);
|
||||
|
||||
@@ -54,6 +54,7 @@ export function OverviewTab() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<BundleAnalysis | null>(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 && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
{result && result.locked.length > 0 && (
|
||||
<div className="space-y-2 rounded-lg border border-status-warn/50 bg-status-warn/10 p-4">
|
||||
<p className="text-sm text-foreground">
|
||||
{t('locked', { files: result.locked.join(', ') })}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={!password || loading}
|
||||
onClick={() => void analyze(files, password)}
|
||||
>
|
||||
{t('unlock')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LockedNotice
|
||||
files={result.locked}
|
||||
// Liegen ein Zertifikat UND sein Schluessel schon einzeln vor,
|
||||
// steckt in der geschuetzten PFX nichts Neues (Aussteller-ZIP vom
|
||||
// 01.10.2026: PFX mit unbekanntem Passwort neben .pem/.key) — dann
|
||||
// nur ein ruhiger Hinweis, das Passwort ist optional.
|
||||
notNeeded={result.items.some((i) => 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 (
|
||||
<div
|
||||
className={`space-y-2 rounded-lg border p-4 ${
|
||||
props.notNeeded ? 'border-border bg-muted/30' : 'border-status-warn/50 bg-status-warn/10'
|
||||
}`}
|
||||
data-testid="locked-notice"
|
||||
>
|
||||
<p className="text-sm text-foreground">
|
||||
{t(props.notNeeded ? 'lockedNotNeeded' : 'locked', { files: props.files.join(', ') })}
|
||||
</p>
|
||||
{!showInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onOpen}
|
||||
className="text-xs text-muted-foreground underline"
|
||||
>
|
||||
{t('unlockAnyway')}
|
||||
</button>
|
||||
)}
|
||||
{showInput && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={props.password}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={!props.password || props.loading}
|
||||
onClick={props.onUnlock}
|
||||
>
|
||||
{t('unlock')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BundleItemCard({ item, byId }: { item: BundleItem; byId: Record<string, BundleItem> }) {
|
||||
const t = useTranslations('certManager.overview');
|
||||
const [pfxOpen, setPfxOpen] = useState(false);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user