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 () => {
|
it('geschuetzte PFX ohne Schluessel im Paket: Passwort eingeben und erneut pruefen', async () => {
|
||||||
mockAnalyze.mockResolvedValueOnce({ items: [LEAF], locked: ['a.pfx'], ignored: [] });
|
mockAnalyze.mockResolvedValueOnce({
|
||||||
|
items: [{ ...LEAF, matchId: null }],
|
||||||
|
locked: ['a.pfx'],
|
||||||
|
ignored: [],
|
||||||
|
});
|
||||||
const pfx = new File(['x'], 'a.pfx');
|
const pfx = new File(['x'], 'a.pfx');
|
||||||
await upload([pfx]);
|
await upload([pfx]);
|
||||||
|
|
||||||
@@ -194,6 +198,17 @@ describe('OverviewTab', () => {
|
|||||||
expect(await screen.findAllByTestId('bundle-item')).toHaveLength(2);
|
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 () => {
|
it('Fehler beim Pruefen wird angezeigt', async () => {
|
||||||
mockAnalyze.mockRejectedValue(new Error('400'));
|
mockAnalyze.mockRejectedValue(new Error('400'));
|
||||||
await upload([new File(['x'], 'a.txt')]);
|
await upload([new File(['x'], 'a.txt')]);
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function OverviewTab() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [result, setResult] = useState<BundleAnalysis | null>(null);
|
const [result, setResult] = useState<BundleAnalysis | null>(null);
|
||||||
|
const [unlockOpen, setUnlockOpen] = useState(false);
|
||||||
|
|
||||||
async function analyze(next: File[], pw: string) {
|
async function analyze(next: File[], pw: string) {
|
||||||
if (next.length === 0) {
|
if (next.length === 0) {
|
||||||
@@ -159,30 +160,20 @@ export function OverviewTab() {
|
|||||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
|
||||||
{result && result.locked.length > 0 && (
|
{result && result.locked.length > 0 && (
|
||||||
<div className="space-y-2 rounded-lg border border-status-warn/50 bg-status-warn/10 p-4">
|
<LockedNotice
|
||||||
<p className="text-sm text-foreground">
|
files={result.locked}
|
||||||
{t('locked', { files: result.locked.join(', ') })}
|
// Liegen ein Zertifikat UND sein Schluessel schon einzeln vor,
|
||||||
</p>
|
// steckt in der geschuetzten PFX nichts Neues (Aussteller-ZIP vom
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
// 01.10.2026: PFX mit unbekanntem Passwort neben .pem/.key) — dann
|
||||||
<input
|
// nur ein ruhiger Hinweis, das Passwort ist optional.
|
||||||
type="password"
|
notNeeded={result.items.some((i) => i.kind === 'certificate' && i.matchId !== null)}
|
||||||
value={password}
|
open={unlockOpen}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onOpen={() => setUnlockOpen(true)}
|
||||||
placeholder={t('lockedPassword')}
|
password={password}
|
||||||
aria-label={t('lockedPassword')}
|
onPassword={setPassword}
|
||||||
autoComplete="off"
|
loading={loading}
|
||||||
className="w-64 rounded border border-border bg-background px-3 py-1.5 text-sm"
|
onUnlock={() => void analyze(files, password)}
|
||||||
/>
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
disabled={!password || loading}
|
|
||||||
onClick={() => void analyze(files, password)}
|
|
||||||
>
|
|
||||||
{t('unlock')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{result && result.items.length === 0 && !loading && (
|
{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> }) {
|
function BundleItemCard({ item, byId }: { item: BundleItem; byId: Record<string, BundleItem> }) {
|
||||||
const t = useTranslations('certManager.overview');
|
const t = useTranslations('certManager.overview');
|
||||||
const [pfxOpen, setPfxOpen] = useState(false);
|
const [pfxOpen, setPfxOpen] = useState(false);
|
||||||
|
|||||||
@@ -1239,8 +1239,10 @@
|
|||||||
"analyzing": "Dateien werden geprüft …",
|
"analyzing": "Dateien werden geprüft …",
|
||||||
"error": "Die Dateien konnten nicht geprüft werden. Bitte prüfen Sie, ob es Zertifikatsdateien sind.",
|
"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.",
|
"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",
|
"lockedPassword": "Passwort der geschützten Datei",
|
||||||
"unlock": "Entsperren",
|
"unlock": "Entsperren",
|
||||||
|
"unlockAnyway": "Trotzdem mit Passwort öffnen",
|
||||||
"nothingFound": "In den Dateien wurde kein Zertifikat, Schlüssel und keine Zertifikatsanfrage gefunden.",
|
"nothingFound": "In den Dateien wurde kein Zertifikat, Schlüssel und keine Zertifikatsanfrage gefunden.",
|
||||||
"ignored": "Nicht verwendet (kein Zertifikat erkannt): {files}",
|
"ignored": "Nicht verwendet (kein Zertifikat erkannt): {files}",
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
@@ -1239,8 +1239,10 @@
|
|||||||
"analyzing": "Checking files …",
|
"analyzing": "Checking files …",
|
||||||
"error": "The files could not be checked. Please make sure they are certificate 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.",
|
"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",
|
"lockedPassword": "Password of the protected file",
|
||||||
"unlock": "Unlock",
|
"unlock": "Unlock",
|
||||||
|
"unlockAnyway": "Open with password anyway",
|
||||||
"nothingFound": "No certificate, key or certificate request was found in the files.",
|
"nothingFound": "No certificate, key or certificate request was found in the files.",
|
||||||
"ignored": "Not used (no certificate detected): {files}",
|
"ignored": "Not used (no certificate detected): {files}",
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
Reference in New Issue
Block a user