From 7707b1cf5ea46a4e079a6eb20f1e539ab31ca8fb Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 17:28:50 +0800 Subject: [PATCH] fix(web): require password confirmation in auth forms Require users to enter new passwords twice in the registration and password change forms so typos are caught before credentials are stored. --- easytier-web/frontend-lib/src/locales/cn.yaml | 1 + easytier-web/frontend-lib/src/locales/en.yaml | 1 + .../src/components/ChangePassword.vue | 27 ++++++++++++++- .../frontend/src/components/Login.vue | 33 ++++++++++++++++++- 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 203f4929..c713d37b 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -366,6 +366,7 @@ web: password_empty: 密码不能为空 password_min_length: 密码至少需要 8 位 password_too_weak: 密码强度不足 + password_mismatch: 两次输入的密码不一致 password_strength_hint: 密码至少 8 位,且需包含大小写字母、数字、特殊字符中的至少 2 类 enable: 开启 disable: 关闭 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index 1e017af8..4b751ccf 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -366,6 +366,7 @@ web: password_empty: Password cannot be empty password_min_length: Password must be at least 8 characters long password_too_weak: Password is too weak + password_mismatch: Passwords do not match password_strength_hint: Password must be at least 8 characters and include at least 2 of uppercase letters, lowercase letters, numbers, or special characters enable: Enable disable: Disable diff --git a/easytier-web/frontend/src/components/ChangePassword.vue b/easytier-web/frontend/src/components/ChangePassword.vue index 8de920bd..dd805933 100644 --- a/easytier-web/frontend/src/components/ChangePassword.vue +++ b/easytier-web/frontend/src/components/ChangePassword.vue @@ -13,10 +13,12 @@ const dialogRef = inject('dialogRef'); const api = computed(() => dialogRef.value.data.api); const password = ref(''); +const confirmPassword = ref(''); const toast = useToast(); const router = useRouter(); const { t } = useI18n(); const passwordValidation = computed(() => validatePasswordStrength(password.value)); +const passwordMatches = computed(() => password.value === confirmPassword.value); const passwordErrorMessage = computed(() => { if (password.value.length === 0 || passwordValidation.value.valid) { return ''; @@ -24,6 +26,14 @@ const passwordErrorMessage = computed(() => { return t(passwordValidation.value.reasonKey!); }); +const confirmPasswordErrorMessage = computed(() => { + if (confirmPassword.value.length === 0 || passwordMatches.value) { + return ''; + } + + return t('web.common.password_mismatch'); +}); +const canSubmit = computed(() => passwordValidation.value.valid && passwordMatches.value); const changePassword = async () => { if (!passwordValidation.value.valid) { @@ -36,6 +46,16 @@ const changePassword = async () => { return; } + if (!passwordMatches.value) { + toast.add({ + severity: 'warn', + summary: t('web.common.warning'), + detail: t('web.common.password_mismatch'), + life: 3000, + }); + return; + } + try { await api.value.change_password(password.value); toast.add({ @@ -69,14 +89,19 @@ const changePassword = async () => {
+ {{ t('web.common.password_strength_hint') }} {{ passwordErrorMessage }} + + {{ confirmPasswordErrorMessage }} +
diff --git a/easytier-web/frontend/src/components/Login.vue b/easytier-web/frontend/src/components/Login.vue index 6420351e..d93dae99 100644 --- a/easytier-web/frontend/src/components/Login.vue +++ b/easytier-web/frontend/src/components/Login.vue @@ -24,9 +24,11 @@ const username = ref(''); const password = ref(''); const registerUsername = ref(''); const registerPassword = ref(''); +const registerConfirmPassword = ref(''); const captcha = ref(''); const captchaSrc = computed(() => api.value.captcha_url()); const registerPasswordValidation = computed(() => validatePasswordStrength(registerPassword.value)); +const registerPasswordsMatch = computed(() => registerPassword.value === registerConfirmPassword.value); const registerPasswordErrorMessage = computed(() => { if (registerPassword.value.length === 0 || registerPasswordValidation.value.valid) { return ''; @@ -34,6 +36,14 @@ const registerPasswordErrorMessage = computed(() => { return t(registerPasswordValidation.value.reasonKey!); }); +const registerConfirmPasswordErrorMessage = computed(() => { + if (registerConfirmPassword.value.length === 0 || registerPasswordsMatch.value) { + return ''; + } + + return t('web.common.password_mismatch'); +}); +const canRegister = computed(() => registerPasswordValidation.value.valid && registerPasswordsMatch.value); const onSubmit = async () => { @@ -64,6 +74,16 @@ const onRegister = async () => { return; } + if (!registerPasswordsMatch.value) { + toast.add({ + severity: 'warn', + summary: t('web.common.warning'), + detail: t('web.common.password_mismatch'), + life: 3000, + }); + return; + } + saveApiHost(apiHost.value); const credential: Credential = { username: registerUsername.value, password: registerPassword.value }; const registerReq: RegisterData = { credentials: credential, captcha: captcha.value }; @@ -184,6 +204,17 @@ onBeforeUnmount(() => { {{ registerPasswordErrorMessage }} +
+ + + + {{ registerConfirmPasswordErrorMessage }} + +
@@ -191,7 +222,7 @@ onBeforeUnmount(() => {