diff --git a/server/src/enums/exceptionCode.ts b/server/src/enums/exceptionCode.ts index 85cffaa7..6e139ad9 100644 --- a/server/src/enums/exceptionCode.ts +++ b/server/src/enums/exceptionCode.ts @@ -6,6 +6,7 @@ export enum EXCEPTION_CODE { USER_EXISTS = 2001, // 用户已存在 USER_NOT_EXISTS = 2002, // 用户不存在 USER_PASSWORD_WRONG = 2003, // 用户名或密码错误 + PASSWORD_INVALID = 2004, // 密码无效 NO_SURVEY_PERMISSION = 3001, // 没有问卷权限 SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错 SURVEY_TYPE_ERROR = 3003, // 问卷类型错误 diff --git a/server/src/modules/auth/__test/auth.controller.spec.ts b/server/src/modules/auth/__test/auth.controller.spec.ts index 9c705944..eb7e0ca7 100644 --- a/server/src/modules/auth/__test/auth.controller.spec.ts +++ b/server/src/modules/auth/__test/auth.controller.spec.ts @@ -82,6 +82,19 @@ describe('AuthController', () => { new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT), ); }); + + it('should throw HttpException with PASSWORD_INVALID code when password is invalid', async () => { + const mockUserInfo = { + username: 'testUser', + password: '无效的密码abc123', + captchaId: 'testCaptchaId', + captcha: 'testCaptcha', + }; + + await expect(controller.register(mockUserInfo)).rejects.toThrow( + new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID), + ); + }); }); describe('login', () => { diff --git a/server/src/modules/auth/controllers/auth.controller.ts b/server/src/modules/auth/controllers/auth.controller.ts index 9d5d6d08..764e4d81 100644 --- a/server/src/modules/auth/controllers/auth.controller.ts +++ b/server/src/modules/auth/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, HttpCode } from '@nestjs/common'; +import { Controller, Post, Body, HttpCode, Get, Query } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { UserService } from '../services/user.service'; import { CaptchaService } from '../services/captcha.service'; @@ -7,6 +7,9 @@ import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { create } from 'svg-captcha'; import { ApiTags } from '@nestjs/swagger'; + +const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/; + @ApiTags('auth') @Controller('/api/auth') export class AuthController { @@ -28,6 +31,24 @@ export class AuthController { captcha: string; }, ) { + if (!userInfo.password) { + throw new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID); + } + + if (userInfo.password.length < 6 || userInfo.password.length > 16) { + throw new HttpException( + '密码长度在 6 到 16 个字符', + EXCEPTION_CODE.PASSWORD_INVALID, + ); + } + + if (!passwordReg.test(userInfo.password)) { + throw new HttpException( + '密码只能输入数字、字母、特殊字符', + EXCEPTION_CODE.PASSWORD_INVALID, + ); + } + const isCorrect = await this.captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId, @@ -162,4 +183,35 @@ export class AuthController { }, }; } + + /** + * 密码强度 + */ + @Get('register/password/strength') + @HttpCode(200) + async getPasswordStrength(@Query('password') password: string) { + const numberReg = /[0-9]/.test(password); + const letterReg = /[a-zA-Z]/.test(password); + const symbolReg = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password); + // 包含三种、且长度大于8 + if (numberReg && letterReg && symbolReg && password.length >= 8) { + return { + code: 200, + data: 'Strong', + }; + } + + // 满足任意两种 + if ([numberReg, letterReg, symbolReg].filter(Boolean).length >= 2) { + return { + code: 200, + data: 'Medium', + }; + } + + return { + code: 200, + data: 'Weak', + }; + } } diff --git a/web/src/management/api/auth.js b/web/src/management/api/auth.js index d654488a..502d4ae5 100644 --- a/web/src/management/api/auth.js +++ b/web/src/management/api/auth.js @@ -7,3 +7,12 @@ export const register = (data) => { export const login = (data) => { return axios.post('/auth/login', data) } + +/** 获取密码强度 */ +export const getPasswordStrength = (password) => { + return axios.get('/auth/register/password/strength', { + params: { + password + } + }) +} diff --git a/web/src/management/pages/login/LoginPage.vue b/web/src/management/pages/login/LoginPage.vue index 6564c9bf..3ebe51e5 100644 --- a/web/src/management/pages/login/LoginPage.vue +++ b/web/src/management/pages/login/LoginPage.vue @@ -27,6 +27,15 @@ + + + +
@@ -62,7 +71,7 @@ import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import 'element-plus/theme-chalk/src/message.scss' -import { login, register } from '@/management/api/auth' +import { getPasswordStrength, login, register } from '@/management/api/auth' import { refreshCaptcha as refreshCaptchaApi } from '@/management/api/captcha' import { CODE_MAP } from '@/management/api/base' import { useUserStore } from '@/management/stores/user' @@ -89,6 +98,55 @@ const formData = reactive({ captchaId: '' }) +// 每个滑块不同强度的颜色,索引0对应第一个滑块 +const strengthColor = reactive([ + { + Strong: '#67C23A', + Medium: '#ebb563', + Weak: '#f78989' + }, + { + Strong: '#67C23A', + Medium: '#ebb563', + Weak: '#2a598a' + }, + { + Strong: '#67C23A', + Medium: '#2a598a', + Weak: '#2a598a' + } +]) + +// 密码内容校验 +const passwordValidator = (_: any, value: any, callback: any) => { + if (!value) { + callback(new Error('请输入密码')) + passwordStrength.value = undefined + return + } + + if (value.length < 6 || value.length > 16) { + callback(new Error('长度在 6 到 16 个字符')) + passwordStrength.value = undefined + return + } + + if (!/^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/.test(value)) { + callback(new Error('只能输入数字、字母、特殊字符')) + passwordStrength.value = undefined + return + } + passwordStrengthHandle(value) + callback() +} + +const passwordStrengthHandle = async (value: string) => { + const res: any = await getPasswordStrength(value) + if (res.code === CODE_MAP.SUCCESS) { + passwordStrength.value = res.data + } +} + const rules = { name: [ { required: true, message: '请输入账号', trigger: 'blur' }, @@ -100,11 +158,8 @@ const rules = { } ], password: [ - { required: true, message: '请输入密码', trigger: 'blur' }, { - min: 8, - max: 16, - message: '长度在 8 到 16 个字符', + validator: passwordValidator, trigger: 'blur' } ], @@ -128,6 +183,7 @@ const pending = reactive({ const captchaImgData = ref('') const formDataRef = ref(null) +const passwordStrength = ref<'Strong' | 'Medium' | 'Weak'>() const submitForm = (type: 'login' | 'register') => { formDataRef.value.validate(async (valid: boolean) => { @@ -258,5 +314,16 @@ const refreshCaptcha = async () => { } } } + + .strength { + display: inline-block; + width: 20%; + height: 6px; + border-radius: 8px; + background: red; + &:not(:first-child) { + margin-left: 10px; + } + } }