Passwort-Komplexität + Portal-Credentials-UX
validatePasswordComplexity (12 Zeichen, Groß/Klein/Zahl/Sonderzeichen) zentral in passwordGenerator.ts; jetzt erzwungen in setPortalPassword, confirmPasswordReset, register, createUser, updateUser. Neue Endpoints: - POST /customers/:id/portal/password/generate → 16-Zeichen Zufallspasswort - POST /customers/:id/portal/send-credentials → Versand per Mail (nur wenn portalEnabled aktiv) Frontend (CustomerDetail): Generate-Button vor Setzen, Send-Credentials nach gesetztem Passwort, Live-Komplexitäts-Hint (✓/○) während Eingabe, alert() durch Toast-Notifications ersetzt. Live-verifiziert: schwaches Passwort → 400 mit Detail-Fehler, komplexes Passwort → 200, Generator liefert 16-Zeichen-Passwort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1796,6 +1796,38 @@ function ContractsTab({
|
||||
);
|
||||
}
|
||||
|
||||
// Passwort-Komplexität – muss zur Backend-Regel in
|
||||
// backend/src/utils/passwordGenerator.ts:validatePasswordComplexity passen.
|
||||
function passwordMeetsComplexity(pw: string): boolean {
|
||||
return (
|
||||
pw.length >= 12 &&
|
||||
/[a-z]/.test(pw) &&
|
||||
/[A-Z]/.test(pw) &&
|
||||
/[0-9]/.test(pw) &&
|
||||
/[^A-Za-z0-9]/.test(pw)
|
||||
);
|
||||
}
|
||||
|
||||
// Live-Hinweis welche Komplexitäts-Anforderungen noch fehlen
|
||||
function PasswordComplexityHint({ password }: { password: string }) {
|
||||
const checks = [
|
||||
{ ok: password.length >= 12, label: '≥ 12 Zeichen' },
|
||||
{ ok: /[a-z]/.test(password), label: 'Kleinbuchstabe' },
|
||||
{ ok: /[A-Z]/.test(password), label: 'Großbuchstabe' },
|
||||
{ ok: /[0-9]/.test(password), label: 'Ziffer' },
|
||||
{ ok: /[^A-Za-z0-9]/.test(password), label: 'Sonderzeichen' },
|
||||
];
|
||||
return (
|
||||
<ul className="mt-2 text-xs space-y-0.5">
|
||||
{checks.map((c) => (
|
||||
<li key={c.label} className={c.ok ? 'text-green-600' : 'text-gray-500'}>
|
||||
{c.ok ? '✓' : '○'} {c.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// Gespeichertes Passwort anzeigen
|
||||
function StoredPasswordDisplay({ customerId }: { customerId: number }) {
|
||||
const [showStoredPassword, setShowStoredPassword] = useState(false);
|
||||
@@ -1898,10 +1930,35 @@ function PortalTab({
|
||||
onSuccess: () => {
|
||||
setNewPassword('');
|
||||
queryClient.invalidateQueries({ queryKey: ['customer-portal', customerId] });
|
||||
alert('Passwort wurde gesetzt');
|
||||
toast.success('Passwort wurde gesetzt');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
alert(error.message);
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
// Passwort generieren (16 Zeichen, komplex) – ins Input-Feld füllen
|
||||
const generatePasswordMutation = useMutation({
|
||||
mutationFn: () => customerApi.generatePortalPassword(customerId),
|
||||
onSuccess: (res) => {
|
||||
const generated = res.data?.password || '';
|
||||
setNewPassword(generated);
|
||||
setShowPassword(true);
|
||||
toast.success('Komplexes Passwort generiert – jetzt „Setzen" klicken.');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
// Zugangsdaten per E-Mail an den Kunden senden
|
||||
const sendCredentialsMutation = useMutation({
|
||||
mutationFn: () => customerApi.sendPortalCredentials(customerId),
|
||||
onSuccess: (res) => {
|
||||
toast.success(res.message || 'Zugangsdaten gesendet');
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2003,7 +2060,7 @@ function PortalTab({
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
placeholder="Mind. 12 Zeichen, Groß/Klein/Zahl/Sonderzeichen"
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
<button
|
||||
@@ -2014,15 +2071,48 @@ function PortalTab({
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => generatePasswordMutation.mutate()}
|
||||
disabled={!canEdit || generatePasswordMutation.isPending}
|
||||
title='Komplexes Passwort generieren (16 Zeichen, Groß/Klein/Zahl/Sonderzeichen). Wird ins Feld geschrieben – danach "Setzen" klicken.'
|
||||
>
|
||||
{generatePasswordMutation.isPending ? 'Generieren...' : 'Generieren'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setPasswordMutation.mutate(newPassword)}
|
||||
disabled={!canEdit || newPassword.length < 6 || setPasswordMutation.isPending}
|
||||
disabled={!canEdit || !passwordMeetsComplexity(newPassword) || setPasswordMutation.isPending}
|
||||
title={passwordMeetsComplexity(newPassword) ? 'Passwort speichern' : 'Komplexität nicht erfüllt'}
|
||||
>
|
||||
{setPasswordMutation.isPending ? 'Speichern...' : 'Setzen'}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Komplexitäts-Hinweise: zeigt live welche Anforderungen erfüllt sind */}
|
||||
{newPassword.length > 0 && !passwordMeetsComplexity(newPassword) && (
|
||||
<PasswordComplexityHint password={newPassword} />
|
||||
)}
|
||||
{portal?.hasPassword && (
|
||||
<StoredPasswordDisplay customerId={customerId} />
|
||||
<>
|
||||
<StoredPasswordDisplay customerId={customerId} />
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (confirm(
|
||||
'Aktuelles Portal-Passwort und Login-URL per E-Mail an den Kunden senden?\n\n' +
|
||||
'Hinweis: Das Passwort wird im Klartext in der E-Mail enthalten sein.'
|
||||
)) {
|
||||
sendCredentialsMutation.mutate();
|
||||
}
|
||||
}}
|
||||
disabled={!canEdit || sendCredentialsMutation.isPending}
|
||||
title="Login-URL + E-Mail + Passwort an die Kunden-E-Mail versenden"
|
||||
>
|
||||
{sendCredentialsMutation.isPending ? 'Sende...' : 'Zugangsdaten per E-Mail versenden'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -168,6 +168,14 @@ export const customerApi = {
|
||||
const res = await api.get<ApiResponse<{ password: string | null }>>(`/customers/${customerId}/portal/password`);
|
||||
return res.data;
|
||||
},
|
||||
generatePortalPassword: async (customerId: number) => {
|
||||
const res = await api.post<ApiResponse<{ password: string }>>(`/customers/${customerId}/portal/password/generate`);
|
||||
return res.data;
|
||||
},
|
||||
sendPortalCredentials: async (customerId: number) => {
|
||||
const res = await api.post<ApiResponse<void>>(`/customers/${customerId}/portal/send-credentials`);
|
||||
return res.data;
|
||||
},
|
||||
// Vertreter-Verwaltung
|
||||
getRepresentatives: async (customerId: number) => {
|
||||
const res = await api.get<ApiResponse<CustomerRepresentative[]>>(`/customers/${customerId}/representatives`);
|
||||
|
||||
Reference in New Issue
Block a user