From 2490bb9808a4110e5808e0f2b438e2888e1c8c90 Mon Sep 17 00:00:00 2001 From: fanyang Date: Sun, 5 Apr 2026 17:24:18 +0800 Subject: [PATCH] fix(web): enforce password strength in auth forms Apply the same password policy to registration and password changes so operators cannot replace default credentials with another weak password and users see consistent guidance. --- easytier-web/frontend-lib/src/locales/cn.yaml | 4 ++ easytier-web/frontend-lib/src/locales/en.yaml | 4 ++ .../src/components/ChangePassword.vue | 22 ++++++-- .../frontend/src/components/Login.vue | 28 +++++++++- .../frontend/src/modules/password-policy.ts | 55 +++++++++++++++++++ 5 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 easytier-web/frontend/src/modules/password-policy.ts diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 9f3751b2..203f4929 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -363,6 +363,10 @@ web: success: 成功 warning: 警告 info: 提示 + password_empty: 密码不能为空 + password_min_length: 密码至少需要 8 位 + password_too_weak: 密码强度不足 + password_strength_hint: 密码至少 8 位,且需包含大小写字母、数字、特殊字符中的至少 2 类 enable: 开启 disable: 关闭 address: 地址 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index ebbc361b..1e017af8 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -363,6 +363,10 @@ web: success: Success warning: Warning info: Info + 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_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 address: Address diff --git a/easytier-web/frontend/src/components/ChangePassword.vue b/easytier-web/frontend/src/components/ChangePassword.vue index 29f1be60..8de920bd 100644 --- a/easytier-web/frontend/src/components/ChangePassword.vue +++ b/easytier-web/frontend/src/components/ChangePassword.vue @@ -6,6 +6,7 @@ import { useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; import ApiClient from '../modules/api'; import { clearMustChangePasswordFlag } from '../modules/auth-status'; +import { validatePasswordStrength } from '../modules/password-policy'; const dialogRef = inject('dialogRef'); @@ -15,14 +16,21 @@ const password = ref(''); const toast = useToast(); const router = useRouter(); const { t } = useI18n(); -const passwordIsEmpty = computed(() => password.value.trim().length === 0); +const passwordValidation = computed(() => validatePasswordStrength(password.value)); +const passwordErrorMessage = computed(() => { + if (password.value.length === 0 || passwordValidation.value.valid) { + return ''; + } + + return t(passwordValidation.value.reasonKey!); +}); const changePassword = async () => { - if (passwordIsEmpty.value) { + if (!passwordValidation.value.valid) { toast.add({ severity: 'warn', summary: t('web.common.warning'), - detail: t('web.settings.new_password_empty'), + detail: t(passwordValidation.value.reasonKey!), life: 3000, }); return; @@ -61,8 +69,14 @@ const changePassword = async () => {
+ + {{ t('web.common.password_strength_hint') }} + + + {{ passwordErrorMessage }} +
diff --git a/easytier-web/frontend/src/components/Login.vue b/easytier-web/frontend/src/components/Login.vue index 5dd33135..6420351e 100644 --- a/easytier-web/frontend/src/components/Login.vue +++ b/easytier-web/frontend/src/components/Login.vue @@ -8,6 +8,7 @@ import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules import { useI18n } from 'vue-i18n' import ApiClient, { Credential, RegisterData } from '../modules/api'; import { setMustChangePasswordFlag } from '../modules/auth-status'; +import { validatePasswordStrength } from '../modules/password-policy'; const { t } = useI18n() @@ -25,6 +26,14 @@ const registerUsername = ref(''); const registerPassword = ref(''); const captcha = ref(''); const captchaSrc = computed(() => api.value.captcha_url()); +const registerPasswordValidation = computed(() => validatePasswordStrength(registerPassword.value)); +const registerPasswordErrorMessage = computed(() => { + if (registerPassword.value.length === 0 || registerPasswordValidation.value.valid) { + return ''; + } + + return t(registerPasswordValidation.value.reasonKey!); +}); const onSubmit = async () => { @@ -45,6 +54,16 @@ const onSubmit = async () => { }; const onRegister = async () => { + if (!registerPasswordValidation.value.valid) { + toast.add({ + severity: 'warn', + summary: t('web.common.warning'), + detail: t(registerPasswordValidation.value.reasonKey!), + life: 3000, + }); + return; + } + saveApiHost(apiHost.value); const credential: Credential = { username: registerUsername.value, password: registerPassword.value }; const registerReq: RegisterData = { credentials: credential, captcha: captcha.value }; @@ -158,6 +177,12 @@ onBeforeUnmount(() => { }} + + {{ t('web.common.password_strength_hint') }} + + + {{ registerPasswordErrorMessage }} +
@@ -165,7 +190,8 @@ onBeforeUnmount(() => { Captcha
-