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

This commit is contained in:
luch 2023-12-26 14:52:25 +08:00 committed by GitHub
parent 2df12109a5
commit 8dbcca68a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 184 additions and 49 deletions

View File

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

View File

@ -2,17 +2,29 @@ import { SurveyServer } from '../../decorator'
import { Request, Response } from 'koa' import { Request, Response } from 'koa'
import * as Joi from 'joi' import * as Joi from 'joi'
import { userService } from './service/userService' import { userService } from './service/userService'
import { captchaService } from './service/captchaService'
import { getValidateValue } from './utils/index' import { getValidateValue } from './utils/index'
import { CommonError } from '../../types/index'
export default class User { export default class User {
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' }) @SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
async register({ req, res }: { req: Request, res: Response }) { async register({ req, res }: { req: Request, res: Response }) {
const userInfo = getValidateValue(Joi.object({ const userInfo = getValidateValue(Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
password: Joi.string().required(), password: Joi.string().required(),
captchaId: Joi.string().required(),
captcha: Joi.string().required(),
}).validate(req.body, { allowUnknown: true })); }).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 { return {
code: 200, code: 200,
data: userRegisterRes, data: userRegisterRes,
@ -24,8 +36,19 @@ export default class User {
const userInfo = getValidateValue(Joi.object({ const userInfo = getValidateValue(Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
password: Joi.string().required(), password: Joi.string().required(),
captchaId: Joi.string().required(),
captcha: Joi.string().required(),
}).validate(req.body, { allowUnknown: true })); }).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 { return {
code: 200, code: 200,
data, data,
@ -40,4 +63,21 @@ export default class User {
context, // 上下文主要是传递调用方信息使用比如traceid 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 { SURVEY_STATUS, CommonError } from '../../../types/index'
import * as Joi from 'joi' import * as Joi from 'joi'
export function getStatusObject({status}:{status:SURVEY_STATUS}) { export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
return { return {
status, status,
id: status, id: status,
date: Date.now(), date: Date.now(),
}; };
} }
export function getValidateValue<T=any>(validationResult:Joi.ValidationResult<T>):T { export function getValidateValue<T = any>(validationResult: Joi.ValidationResult<T>): T {
if(validationResult.error) { if (validationResult.error) {
throw new CommonError(validationResult.error.details.map(e=>e.message).join()) throw new CommonError(validationResult.error.details.map(e => e.message).join())
} }
return validationResult.value; 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'; import axios from './base';
export const register = ({ username, password }) => { export const register = (data) => {
return axios.post('/user/register', { return axios.post('/user/register', data);
username,
password,
});
}; };
export const login = ({ username, password }) => { export const login = (data) => {
return axios.post('/user/login', { return axios.post('/user/login', data);
username,
password,
});
}; };

View File

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

View File

@ -3,7 +3,7 @@ import { getBannerData } from '@/management/api/skin.js';
export default { export default {
async getBannerData({ state, commit }) { async getBannerData({ state, commit }) {
if (state.bannerList && state.bannerList.length > 0) { if (state.bannerList && state.bannerList.length > 0) {
return return;
} }
const res = await getBannerData(); const res = await getBannerData();
if (res.code === 200) { 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'; import { getSurveyById } from '@/management/api/survey';
export default { export default {
init({ state, dispatch }) { async init({ state, dispatch }) {
const metaData = _get(state, 'schema.metaData'); const metaData = _get(state, 'schema.metaData');
if (!metaData || metaData._id !== state.surveyId) { if (!metaData || metaData._id !== state.surveyId) {
dispatch('getSchemaFromRemote'); await dispatch('getSchemaFromRemote');
} }
dispatch('resetState'); dispatch('resetState');
}, },

View File

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

View File

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

View File

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