mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-04-22 16:17:23 +08:00
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.
This commit is contained in:
@@ -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: 地址
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<any>('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 () => {
|
||||
<div class="flex flex-col space-y-4">
|
||||
<Password v-model="password" :placeholder="t('web.settings.new_password')" :feedback="false"
|
||||
toggleMask />
|
||||
<small class="text-surface-500 dark:text-surface-400">
|
||||
{{ t('web.common.password_strength_hint') }}
|
||||
</small>
|
||||
<small v-if="passwordErrorMessage" class="text-red-500 dark:text-red-400">
|
||||
{{ passwordErrorMessage }}
|
||||
</small>
|
||||
<Button @click="changePassword" :label="t('web.common.confirm')"
|
||||
:disabled="passwordIsEmpty" />
|
||||
:disabled="!passwordValidation.valid" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
@@ -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(() => {
|
||||
}}</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
:feedback="false" class="w-full" />
|
||||
<small class="text-surface-500 dark:text-surface-400">
|
||||
{{ t('web.common.password_strength_hint') }}
|
||||
</small>
|
||||
<small v-if="registerPasswordErrorMessage" class="block text-red-500 dark:text-red-400">
|
||||
{{ registerPasswordErrorMessage }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
|
||||
@@ -165,7 +190,8 @@ onBeforeUnmount(() => {
|
||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :label="t('web.login.register')" type="submit" class="w-full" />
|
||||
<Button :label="t('web.login.register')" type="submit" class="w-full"
|
||||
:disabled="!registerPasswordValidation.valid" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :label="t('web.login.back_to_login')" type="button" class="w-full"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export type PasswordValidationReasonKey =
|
||||
| 'web.common.password_empty'
|
||||
| 'web.common.password_min_length'
|
||||
| 'web.common.password_too_weak';
|
||||
|
||||
export interface PasswordValidationResult {
|
||||
valid: boolean;
|
||||
reasonKey?: PasswordValidationReasonKey;
|
||||
}
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
|
||||
export const countPasswordClasses = (password: string) => {
|
||||
let count = 0;
|
||||
|
||||
if (/[a-z]/.test(password)) {
|
||||
count += 1;
|
||||
}
|
||||
if (/[A-Z]/.test(password)) {
|
||||
count += 1;
|
||||
}
|
||||
if (/\d/.test(password)) {
|
||||
count += 1;
|
||||
}
|
||||
if (/[^A-Za-z0-9\s]/.test(password)) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
export const validatePasswordStrength = (password: string): PasswordValidationResult => {
|
||||
if (password.trim().length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
reasonKey: 'web.common.password_empty',
|
||||
};
|
||||
}
|
||||
|
||||
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||
return {
|
||||
valid: false,
|
||||
reasonKey: 'web.common.password_min_length',
|
||||
};
|
||||
}
|
||||
|
||||
if (countPasswordClasses(password) < 2) {
|
||||
return {
|
||||
valid: false,
|
||||
reasonKey: 'web.common.password_too_weak',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
Reference in New Issue
Block a user