feat: 登陆新增图片验证码功能 (#31)

This commit is contained in:
luch 2023-12-26 14:52:25 +08:00 committed by sudoooooo
parent 7b3ec44aea
commit 3ec86cbe73
12 changed files with 184 additions and 49 deletions

View File

@ -4,11 +4,10 @@
"description": "survey server template",
"main": "index.js",
"scripts": {
"copy": "mkdir -p ./build/ && cp -rf ./src/* ./build/",
"build": "tsc",
"start:stable": "npm run copy && npm run build && SERVER_ENV=stable node ./build/index.js",
"start:preonline": "npm run copy && npm run build && SERVER_ENV=preonline node ./build/index.js",
"start:online": "npm run copy && npm run build && SERVER_ENV=online node ./build/index.js",
"start:stable": "npm run build && SERVER_ENV=stable node ./build/index.js",
"start:preonline": "npm run build && SERVER_ENV=preonline node ./build/index.js",
"start:online": "npm run build && SERVER_ENV=online node ./build/index.js",
"start": "npm run start:online",
"local": "npx ts-node scripts/run-local.ts",
"dev": "npx ts-node-dev ./src/index.ts"
@ -35,7 +34,8 @@
"koa-router": "^12.0.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"mongodb": "^5.7.0"
"mongodb": "^5.7.0",
"svg-captcha": "^1.4.0"
},
"engines": {
"node": ">=14.21.0",

View File

@ -2,17 +2,29 @@ import { SurveyServer } from '../../decorator'
import { Request, Response } from 'koa'
import * as Joi from 'joi'
import { userService } from './service/userService'
import { captchaService } from './service/captchaService'
import { getValidateValue } from './utils/index'
import { CommonError } from '../../types/index'
export default class User {
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
async register({ req, res }: { req: Request, res: Response }) {
const userInfo = getValidateValue(Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
captchaId: Joi.string().required(),
captcha: Joi.string().required(),
}).validate(req.body, { allowUnknown: true }));
const userRegisterRes = await userService.register(userInfo)
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId })
if (!isCorrect) {
throw new CommonError('验证码不正确')
}
const userRegisterRes = await userService.register({
username: userInfo.username,
password: userInfo.password,
})
// 删除验证码
captchaService.deleteCaptcha({ id: userInfo.captchaId })
return {
code: 200,
data: userRegisterRes,
@ -24,8 +36,19 @@ export default class User {
const userInfo = getValidateValue(Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
captchaId: Joi.string().required(),
captcha: Joi.string().required(),
}).validate(req.body, { allowUnknown: true }));
const data = await userService.login(userInfo)
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId })
if (!isCorrect) {
throw new CommonError('验证码不正确')
}
const data = await userService.login({
username: userInfo.username,
password: userInfo.password,
})
// 删除验证码
captchaService.deleteCaptcha({ id: userInfo.captchaId })
return {
code: 200,
data,
@ -40,4 +63,21 @@ export default class User {
context, // 上下文主要是传递调用方信息使用比如traceid
}
}
@SurveyServer({ type: 'http', method: 'post', routerName: '/captcha' })
async refreshCaptcha({ req }) {
const captchaData = captchaService.createCaptcha()
const res = await captchaService.addCaptchaData({ text: captchaData.text })
if (req.body && req.body.captchaId) {
// 删除验证码
captchaService.deleteCaptcha({ id: req.body.captchaId })
}
return {
code: 200,
data: {
id: res.insertedId,
img: captchaData.data,
},
}
}
}

View File

@ -0,0 +1,40 @@
import { mongo } from '../db/mongo'
import { create } from 'svg-captcha'
class CaptchaService {
createCaptcha() {
return create({
size: 4, // 验证码长度
ignoreChars: '0o1i', // 忽略字符
noise: 3, // 干扰线数量
color: true, // 启用彩色
background: '#f0f0f0', // 背景色
})
}
async addCaptchaData({ text }) {
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
const addRes = await captchaDb.insertOne({
text,
});
return addRes;
}
async checkCaptchaIsCorrect({ captcha, id }) {
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
const captchaData = await captchaDb.findOne({
_id: mongo.getObjectIdByStr(id),
});
return captcha.toLowerCase() === captchaData?.text?.toLowerCase();
}
async deleteCaptcha({ id }) {
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
const _id = mongo.getObjectIdByStr(id)
await captchaDb.deleteOne({
_id
})
}
}
export const captchaService = new CaptchaService()

View File

@ -1,15 +1,15 @@
import { SURVEY_STATUS, CommonError } from '../../../types/index'
import * as Joi from 'joi'
export function getStatusObject({status}:{status:SURVEY_STATUS}) {
import * as Joi from 'joi'
export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
return {
status,
id: status,
date: Date.now(),
};
}
export function getValidateValue<T=any>(validationResult:Joi.ValidationResult<T>):T {
if(validationResult.error) {
throw new CommonError(validationResult.error.details.map(e=>e.message).join())
export function getValidateValue<T = any>(validationResult: Joi.ValidationResult<T>): T {
if (validationResult.error) {
throw new CommonError(validationResult.error.details.map(e => e.message).join())
}
return validationResult.value;
}

View File

@ -0,0 +1,5 @@
import axios from './base';
export const refreshCaptcha = ({ captchaId }) => {
return axios.post('/user/captcha', { captchaId });
};

View File

@ -1,15 +1,9 @@
import axios from './base';
export const register = ({ username, password }) => {
return axios.post('/user/register', {
username,
password,
});
export const register = (data) => {
return axios.post('/user/register', data);
};
export const login = ({ username, password }) => {
return axios.post('/user/login', {
username,
password,
});
export const login = (data) => {
return axios.post('/user/login', data);
};

View File

@ -12,19 +12,33 @@
</div>
<div class="login-box">
<el-form
:model="ruleForm"
:model="formData"
:rules="rules"
ref="ruleForm"
ref="formData"
label-width="100px"
class="login-form"
@submit.native.prevent
>
<el-form-item label="账号" prop="name">
<el-input v-model="ruleForm.name"></el-input>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password"></el-input>
<el-input type="password" v-model="formData.password"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="captcha">
<div class="captcha-wrapper">
<el-input
style="width: 150px"
v-model="formData.captcha"
></el-input>
<div
class="captcha-img"
@click="refreshCaptcha"
v-html="captchaImgData"
></div>
</div>
</el-form-item>
<el-form-item class="button-group">
@ -33,14 +47,14 @@
size="small"
type="primary"
class="button"
@click="submitForm('ruleForm', 'login')"
@click="submitForm('formData', 'login')"
>登录</el-button
>
<el-button
:loading="registerPending"
size="small"
class="button register-button"
@click="submitForm('ruleForm', 'register')"
@click="submitForm('formData', 'register')"
>注册</el-button
>
</el-form-item>
@ -50,14 +64,17 @@
</template>
<script>
import { login, register } from '@/management/api/user';
import { refreshCaptcha } from '@/management/api/captcha';
import { CODE_MAP } from '@/management/api/base';
export default {
name: 'loginPage',
data() {
return {
ruleForm: {
formData: {
name: '',
password: '',
captcha: '',
captchaId: '',
},
rules: {
name: [
@ -78,11 +95,22 @@ export default {
trigger: 'blur',
},
],
captcha: [
{
required: true,
message: '请输入验证码',
trigger: 'blur',
},
],
},
loginPending: false,
registerPending: false,
captchaImgData: '',
};
},
created() {
this.refreshCaptcha();
},
methods: {
submitForm(formName, type) {
this.$refs[formName].validate(async (valid) => {
@ -94,8 +122,10 @@ export default {
};
this[`${type}Pending`] = true;
const res = await submitTypes[type]({
username: this.ruleForm.name,
password: this.ruleForm.password,
username: this.formData.name,
password: this.formData.password,
captcha: this.formData.captcha,
captchaId: this.formData.captchaId,
});
this[`${type}Pending`] = false;
if (res.code !== CODE_MAP.SUCCESS) {
@ -123,6 +153,14 @@ export default {
}
});
},
async refreshCaptcha() {
const res = await refreshCaptcha({ captchaId: this.formData.captchaId });
if (res.code === 200) {
const { id, img } = res.data;
this.formData.captchaId = id;
this.captchaImgData = img;
}
},
},
};
</script>
@ -130,6 +168,7 @@ export default {
.login-page {
overflow: hidden;
height: 100vh;
.login-top {
color: #4a4c5b;
height: 56px;
@ -138,10 +177,12 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
img {
width: 90px;
}
}
.login-box {
display: flex;
align-items: center;
@ -149,28 +190,31 @@ export default {
height: 100%;
width: 100%;
}
.login-form {
border-radius: 8px;
padding: 60px 60px 60px 0;
width: 500px;
height: 300px;
background: #fff;
box-shadow: 4px 0 20px 0 rgba(82, 82, 102, 0.15);
margin-top: -150px;
.button-group {
margin-top: 40px;
}
.button {
width: 160px;
height: 40px;
font-size: 14px;
}
.register-button {
border-color: #faa600;
color: #faa600;
margin-left: 20px;
}
}
.tips {
color: #999;
position: fixed;
@ -179,5 +223,17 @@ export default {
margin-left: 50%;
transform: translateX(-50%);
}
.captcha-wrapper {
display: flex;
align-items: center;
.captcha-img {
height: 40px;
cursor: pointer;
::v-deep > svg {
max-height: 40px;
}
}
}
}
</style>

View File

@ -3,7 +3,7 @@ import { getBannerData } from '@/management/api/skin.js';
export default {
async getBannerData({ state, commit }) {
if (state.bannerList && state.bannerList.length > 0) {
return
return;
}
const res = await getBannerData();
if (res.code === 200) {

View File

@ -3,10 +3,10 @@ import { cloneDeep as _cloneDeep, get as _get } from 'lodash';
import { getSurveyById } from '@/management/api/survey';
export default {
init({ state, dispatch }) {
async init({ state, dispatch }) {
const metaData = _get(state, 'schema.metaData');
if (!metaData || metaData._id !== state.surveyId) {
dispatch('getSchemaFromRemote');
await dispatch('getSchemaFromRemote');
}
dispatch('resetState');
},

View File

@ -45,7 +45,7 @@
</div>
</template>
<script>
import { get as _get} from 'lodash';
import { get as _get } from 'lodash';
import { formatLink } from '../utils/index.js';
export default {
@ -73,11 +73,11 @@ export default {
window.open(formatLink(jumpLink));
},
play() {
const video = document.getElementById("video");
const video = document.getElementById('video');
document.querySelector('.play-icon').style.display = 'none';
document.querySelector(".video-modal").style.display = 'none';
document.querySelector('.video-modal').style.display = 'none';
video.play();
},
},
},
};
</script>

View File

@ -1,12 +1,12 @@
<template>
<div class="container">
<div v-if="logoImage" class="logo-wrapper">
<img
:style="{ width: !isMobile ? '20%' : logoImageWidth || '20%' }"
:src="logoImage"
/>
<div v-if="logoImage" class="logo-wrapper">
<img
:style="{ width: !isMobile ? '20%' : logoImageWidth || '20%' }"
:src="logoImage"
/>
</div>
</div>
</div>
</template>
<script>
export default {
@ -25,10 +25,11 @@ export default {
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.container{
.container {
display: flex;
justify-content: center;
}
.logo-wrapper {
max-width: 300px;
text-align: center;

View File

@ -20,7 +20,6 @@ import materialGroup from './materialGroup';
export default {
name: 'mainRenderer',
computed: {
// ...mapState('questionData'),
questionData() {
return this.$store.state.questionData;
},