fix(auth): new passwords need at least 8 characters (FLAW-09)
auth.ValidatePassword checks every way a password is set: setup, registration, profile, admin create and reset, reset by mail. Same rule in the browser before sending (lib/password.ts), with our own German message instead of the browser popup. Existing shorter passwords still log in. Profile and admin reset no longer trim the password, which made passwords with surrounding spaces unusable for login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8989d0c44
commit
b5f926bcf7
13 files changed
+142
-26
No files matched your search
@@ -15,6 +15,7 @@ import { AuthStatus, SharingMode, User } from '../../lib/types';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { ToggleSwitch } from '../../components/ToggleSwitch';
|
||||
import { LegalLinks } from '../../components/LegalLinks';
|
||||
import { passwordProblem } from '../../lib/password';
|
||||
|
||||
interface SetupAndAuthViewProps {
|
||||
authStatus: AuthStatus;
|
||||
@@ -74,6 +75,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
const handleResetPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const resetProblem = passwordProblem(newPassword);
|
||||
if (resetProblem) {
|
||||
setError(resetProblem);
|
||||
triggerHaptic(25);
|
||||
return;
|
||||
}
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
setError('Die eingegebenen Passwörter stimmen nicht überein.');
|
||||
triggerHaptic(25);
|
||||
@@ -99,6 +106,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
const setupProblem = passwordProblem(setupPassword);
|
||||
if (setupProblem) {
|
||||
setError(setupProblem);
|
||||
triggerHaptic(25);
|
||||
return;
|
||||
}
|
||||
if (setupPassword !== setupPasswordConfirm) {
|
||||
setError('Die eingegebenen Passwörter stimmen nicht überein.');
|
||||
triggerHaptic(25);
|
||||
@@ -143,6 +156,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
const handleRegisterOrInvite = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const registerProblem = passwordProblem(password);
|
||||
if (registerProblem) {
|
||||
setError(registerProblem);
|
||||
triggerHaptic(25);
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
triggerHaptic(15);
|
||||
try {
|
||||
@@ -235,8 +254,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
type="password"
|
||||
value={setupPassword}
|
||||
onChange={(e) => setSetupPassword(e.target.value)}
|
||||
placeholder="Mindestens 4 Zeichen"
|
||||
minLength={4}
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
required
|
||||
className="w-full h-8 px-2.5 input-field"
|
||||
/>
|
||||
@@ -248,7 +266,6 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
value={setupPasswordConfirm}
|
||||
onChange={(e) => setSetupPasswordConfirm(e.target.value)}
|
||||
placeholder="Passwort erneut eingeben"
|
||||
minLength={4}
|
||||
required
|
||||
className={`w-full h-8 px-2.5 input-field ${
|
||||
setupPasswordConfirm && setupPassword !== setupPasswordConfirm
|
||||
@@ -529,7 +546,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
minLength={4}
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
@@ -542,7 +559,6 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
type="password"
|
||||
value={newPasswordConfirm}
|
||||
onChange={(e) => setNewPasswordConfirm(e.target.value)}
|
||||
minLength={4}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
className="w-full h-11 sm:h-8 px-2.5 input-field text-base sm:text-xs"
|
||||
@@ -716,8 +732,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mindestens 4 Zeichen"
|
||||
minLength={4}
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
required
|
||||
className="w-full h-8 px-2.5 input-field"
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
UserRole,
|
||||
} from '../../lib/types';
|
||||
import { api, setCurrentSharingMode, triggerHaptic } from '../../lib/api';
|
||||
import { passwordProblem } from '../../lib/password';
|
||||
import { UserAvatar } from '../../components/UserAvatar';
|
||||
import { ToggleSwitch } from '../../components/ToggleSwitch';
|
||||
import { CountBadge } from '../../components/CountBadge';
|
||||
@@ -151,6 +152,11 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatusMsg(null);
|
||||
const problem = passwordProblem(newUserPassword);
|
||||
if (problem) {
|
||||
setStatusMsg({ type: 'err', text: problem });
|
||||
return;
|
||||
}
|
||||
triggerHaptic(15);
|
||||
try {
|
||||
const created = await api.adminCreateUser({
|
||||
@@ -235,8 +241,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
const handleConfirmResetPassword = async (userId: string) => {
|
||||
if (!resetPwdVal.trim()) return;
|
||||
setStatusMsg(null);
|
||||
const problem = passwordProblem(resetPwdVal);
|
||||
if (problem) {
|
||||
setStatusMsg({ type: 'err', text: problem });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.adminResetPassword(userId, resetPwdVal.trim());
|
||||
// Not trimmed: login compares the password exactly as typed
|
||||
await api.adminResetPassword(userId, resetPwdVal);
|
||||
setResetPwdUserId(null);
|
||||
setResetPwdVal('');
|
||||
setStatusMsg({ type: 'ok', text: 'Passwort erfolgreich zurückgesetzt.' });
|
||||
@@ -555,8 +567,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
type="password"
|
||||
value={newUserPassword}
|
||||
onChange={(e) => setNewUserPassword(e.target.value)}
|
||||
placeholder="Mind. 4 Zeichen"
|
||||
minLength={4}
|
||||
placeholder="Mind. 8 Zeichen"
|
||||
required
|
||||
className="w-full min-w-0 h-8 px-2.5 input-field"
|
||||
/>
|
||||
@@ -843,7 +854,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
type="password"
|
||||
value={resetPwdVal}
|
||||
onChange={(e) => setResetPwdVal(e.target.value)}
|
||||
placeholder="Neues Passwort eingeben…"
|
||||
placeholder="Neues Passwort (mind. 8 Zeichen)…"
|
||||
className="flex-1 h-7 px-2 input-field"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { APIKey, AuthStatus, User, UserTheme } from '../../lib/types';
|
||||
import { api, clearToken, triggerHaptic } from '../../lib/api';
|
||||
import { passwordProblem } from '../../lib/password';
|
||||
import { customThemeId, ThemeId, ThemeOption } from '../../lib/theme';
|
||||
import themeTemplateCss from '../../themes/theme-template.css?raw';
|
||||
import { UserAvatar, resizeImageToDataUrl } from '../../components/UserAvatar';
|
||||
@@ -283,7 +284,13 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
avatarUrl: avatarUrl.trim(),
|
||||
};
|
||||
if (newPassword.trim()) {
|
||||
patch.password = newPassword.trim();
|
||||
const problem = passwordProblem(newPassword);
|
||||
if (problem) {
|
||||
setProfileMsg({ type: 'err', text: problem });
|
||||
return;
|
||||
}
|
||||
// Not trimmed: login compares the password exactly as typed
|
||||
patch.password = newPassword;
|
||||
}
|
||||
const updated = await api.updateSettings(patch);
|
||||
onUserUpdated(updated);
|
||||
@@ -696,7 +703,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Mindestens 4 Zeichen"
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
className="w-full h-9 px-3 input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Same rule as the server (auth.ValidatePassword): at least 8 characters, not only spaces.
|
||||
// Only for setting a password; logging in with an older, shorter one keeps working.
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
export const passwordProblem = (password: string): string | null =>
|
||||
!password.trim() || Array.from(password).length < MIN_PASSWORD_LENGTH
|
||||
? `Das Passwort muss mindestens ${MIN_PASSWORD_LENGTH} Zeichen lang sein`
|
||||
: null;
|
||||
Reference in new issue
Block a user