feat: 登陆新增图片验证码功能 (#31)
This commit is contained in:
parent
7b3ec44aea
commit
3ec86cbe73
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
40
server/src/apps/user/service/captchaService.ts
Normal file
40
server/src/apps/user/service/captchaService.ts
Normal 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()
|
@ -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;
|
||||
}
|
5
web/src/management/api/captcha.js
Normal file
5
web/src/management/api/captcha.js
Normal file
@ -0,0 +1,5 @@
|
||||
import axios from './base';
|
||||
|
||||
export const refreshCaptcha = ({ captchaId }) => {
|
||||
return axios.post('/user/captcha', { captchaId });
|
||||
};
|
@ -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);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -20,7 +20,6 @@ import materialGroup from './materialGroup';
|
||||
export default {
|
||||
name: 'mainRenderer',
|
||||
computed: {
|
||||
// ...mapState('questionData'),
|
||||
questionData() {
|
||||
return this.$store.state.questionData;
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user