This commit is contained in:
caoqianming 2020-09-23 08:40:13 +08:00
parent 89482b4aed
commit ea28be2f5f
15 changed files with 444 additions and 105 deletions

View File

@ -9,3 +9,18 @@ export default {
name: 'App' name: 'App'
} }
</script> </script>
<style >
.el-table--medium td,   .el-table--medium th {
padding: 2px 0;
}
.el-form-item {
margin-bottom: 16px;
}
.el-card__body {
padding: 10px;
}
.el-card__header {
padding: 10px;
}
</style>

9
client/src/api/msg.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
export function sendMsg(data) {
return request({
url: '/system/sendmsg/',
method: 'post',
data
})
}

View File

@ -8,6 +8,15 @@ export function login(data) {
}) })
} }
export function login2(data) {
//验证码登陆
return request({
url: '/token2/',
method: 'post',
data
})
}
export function logout() { export function logout() {
return request({ return request({
url: '/token/black/', url: '/token/black/',

View File

@ -90,10 +90,10 @@ export default {
} }
</script> </script>
<style scoped> <style>
.pagination-container { .pagination-container {
background: #fff; background: #fff;
padding: 32px 16px; padding: 0px 2px;
} }
.pagination-container.hidden { .pagination-container.hidden {
display: none; display: none;

View File

@ -1,4 +1,4 @@
import { login, logout, getInfo } from '@/api/user' import { login, logout, getInfo, login2 } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router' import { resetRouter } from '@/router'
@ -47,7 +47,21 @@ const actions = {
}) })
}) })
}, },
// user code login
login2({ commit }, msginfo) {
const { mail, msg } = msginfo
return new Promise((resolve, reject) => {
login2({ mail: mail.trim(), msg: msg }).then(response => {
const { data } = response
commit('SET_TOKEN', data.access)
setToken(data.access)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info // get user info
getInfo({ commit, state }) { getInfo({ commit, state }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -61,5 +61,43 @@ div:focus {
// main-container global css // main-container global css
.app-container { .app-container {
padding: 20px; padding: 10px;
}
.el-table--medium td,   .el-table--medium th {
padding: 2px 0;
}
.el-form-item {
margin-bottom: 16px;
}
.el-card, .el-message {
border-radius: 0px;
overflow: hidden;
}
.el-card__body {
padding: 6px;
}
.el-card__header {
padding: 6px;
}
.el-tabs--border-card>.el-tabs__content {
padding: 6px;
}
.el-dialog__header {
padding: 10px 10px 6px;
}
.el-dialog__body {
padding: 8px 12px;
}
// .el-form-item--medium .el-form-item__label {
// line-height: 16px;
// }
.el-form--label-top .el-form-item__label {
line-height: 16px;
}
.el-button+.el-button {
margin-left: 1px;
}
.el-button {
border-radius: 0px;
} }

View File

@ -45,22 +45,30 @@ service.interceptors.response.use(
const res = response.data const res = response.data
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 401) { if (res.code === 401) {
MessageBox.confirm('您已退出,您可以停留在本页或者重新登陆.', '确认退出', { if(res.msg.indexOf('No active account')!=-1){
confirmButtonText: '重新登陆', Message({
cancelButtonText: '取消', message: '用户名或密码错误',
type: 'warning' type: 'error',
}).then(() => { duration: 3 * 1000
store.dispatch('user/resetToken').then(() => {
location.reload()
}) })
}) }else{
MessageBox.confirm('认证失败,请重新登陆.', '确认退出', {
confirmButtonText: '重新登陆',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
} else if (res.code >= 400) { } else if (res.code >= 400) {
Message({ Message({
message: res.msg || '请求出错', message: res.msg || '请求出错',
type: 'error', type: 'error',
duration: 5 * 1000 duration: 3 * 1000
}) })
return Promise.reject(new Error(res.msg || '请求出错')) return Promise.reject(new Error(res.msg || '请求出错'))
} else { } else {
return res return res

View File

@ -1,62 +1,125 @@
<template> <template>
<div class="login-container"> <div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> <div class="login-form">
<div class="title-container"> <div class="title-container">
<h3 class="title">CTC能力检索</h3> <h3 class="title">ctc能力检索</h3>
</div> </div>
<el-tabs v-model="activeName" :stretch="true">
<el-tab-pane label="验证码登陆" name="msg">
<el-form
ref="loginForm2"
:model="loginForm2"
:rules="loginRules2"
auto-complete="on"
label-position="left"
>
<el-form-item prop="mail">
<span class="svg-container">
<svg-icon icon-class="email" />
</span>
<el-input
ref="mail"
v-model="loginForm2.mail"
placeholder="邮箱号"
name="mail"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<el-form-item prop="username"> <el-form-item prop="msg">
<span class="svg-container"> <span class="svg-container">
<svg-icon icon-class="user" /> <svg-icon icon-class="message" />
</span> </span>
<el-input <el-input
ref="username" ref="msg"
v-model="loginForm.username" v-model="loginForm2.msg"
placeholder="账号" type="text"
name="username" placeholder="验证码"
type="text" name="msg"
tabindex="1" tabindex="2"
auto-complete="on" auto-complete="on"
/> @keyup.enter.native="handleLogin"
</el-form-item> />
<span class="show-pwd" @click="sendMsg">
<el-button :disabled="disabled" plain type="primary">
<template>{{buttonmsg}}</template>
</el-button>
<!-- <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> -->
</span>
</el-form-item>
<el-button
:loading="loading2"
type="primary"
style="width:100%;margin-bottom:30px;"
@click.native.prevent="handleLogin2"
>验证登陆</el-button>
</el-form>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="密码"
name="password"
tabindex="2"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登陆</el-button> </el-tab-pane>
<el-tab-pane label="账户密码登陆" name="pwd">
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
auto-complete="on"
label-position="left"
>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user" />
</span>
<el-input
ref="username"
v-model="loginForm.username"
placeholder="账号"
name="username"
type="text"
tabindex="1"
auto-complete="on"
/>
</el-form-item>
<!-- <div class="tips"> <el-form-item prop="password">
<span style="margin-right:20px;">username: admin</span> <span class="svg-container">
<span> password: any</span> <svg-icon icon-class="password" />
</div> --> </span>
<el-input
</el-form> :key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="密码"
name="password"
tabindex="2"
auto-complete="on"
@keyup.enter.native="handleLogin"
/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
</span>
</el-form-item>
<el-button
:loading="loading"
type="primary"
style="width:100%;margin-bottom:30px;"
@click.native.prevent="handleLogin"
>登陆</el-button>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
</div> </div>
</template> </template>
<script> <script>
// import { validUsername } from '@/utils/validate' // import { validUsername } from '@/utils/validate'
import { sendMsg } from '@/api/msg'
import { login2 } from "@/api/user"
export default { export default {
name: 'Login', name: "Login",
data() { data() {
// const validateUsername = (rule, value, callback) => { // const validateUsername = (rule, value, callback) => {
// if (!validUsername(value)) { // if (!validUsername(value)) {
@ -67,70 +130,147 @@ export default {
// } // }
const validatePassword = (rule, value, callback) => { const validatePassword = (rule, value, callback) => {
if (value.length < 4) { if (value.length < 4) {
callback(new Error('密码长度小于4位!')) callback(new Error("密码长度小于4位!"));
} else { } else {
callback() callback();
} }
} };
return { return {
timer:null,
disabled:false,
buttonmsg:'发送验证码',
count:20,
activeName: "pwd",
loginForm: { loginForm: {
username: '', username: "",
password: '' password: "",
},
loginForm2: {
mail: "",
msg: "",
}, },
loginRules: { loginRules: {
username: [{ required: true, trigger: 'blur', message: "请输入账户" }], username: [{ required: true, trigger: "blur", message: "请输入账户" }],
password: [{ required: true, trigger: 'blur', validator: validatePassword, message: "请输入密码" }] password: [
{
required: true,
trigger: "blur",
validator: validatePassword,
message: "请输入密码",
},
],
}, },
loginRules2: {
mail: [{ required: true, trigger: "blur", message: "请输入邮箱号" }],
msg: [
{
required: true,
trigger: "blur",
message: "请输入验证码",
},
],
},
loading2:false,
loading: false, loading: false,
passwordType: 'password', passwordType: "password",
redirect: undefined redirect: undefined,
} };
}, },
watch: { watch: {
$route: { $route: {
handler: function(route) { handler: function (route) {
this.redirect = route.query && route.query.redirect this.redirect = route.query && route.query.redirect;
}, },
immediate: true immediate: true,
} },
}, },
methods: { methods: {
showPwd() { showPwd() {
if (this.passwordType === 'password') { if (this.passwordType === "password") {
this.passwordType = '' this.passwordType = "";
} else { } else {
this.passwordType = 'password' this.passwordType = "password";
} }
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.password.focus() this.$refs.password.focus();
}) });
}, },
handleLogin() { sendMsg(){
this.$refs.loginForm.validate(valid => { if(this.loginForm2.mail.length){
if (valid) { this.getTimer()
this.loading = true sendMsg({'mail':this.loginForm2.mail}).then(res=>{
this.$store.dispatch('user/login', this.loginForm).then(() => { this.$message.success('验证码已发送至该邮箱,请注意查收')
this.$router.push({ path: this.redirect || '/' }) }).catch(e=>{})
this.loading = false }else{
}).catch(() => { this.$message.warning('请输入邮箱号')
this.loading = false }
}) },
getTimer(){
const TIME_COUNT = 20;
if (!this.timer) {
this.count = TIME_COUNT;
this.disabled = true;
this.timer = setInterval(() => {
if (this.count > 0 && this.count <= TIME_COUNT) {
this.count--;
this.buttonmsg = this.count
} else { } else {
console.log('error submit!!') this.disabled = false;
return false clearInterval(this.timer);
this.timer = null;
this.buttonmsg = '发送验证码'
} }
}) }, 1000)
} }
} },
} handleLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.$store
.dispatch("user/login", this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
})
.catch(() => {
this.loading = false;
});
} else {
console.log("error submit!!");
return false;
}
});
},
handleLogin2() {
this.$refs.loginForm2.validate((valid) => {
if (valid) {
this.loading2 = true;
this.$store
.dispatch("user/login2", this.loginForm2)
.then(() => {
this.$router.push({ path: this.redirect || "/" });
this.loading2 = false;
})
.catch(() => {
this.loading2 = false;
});
} else {
console.log("error submit!!");
return false;
}
});
},
},
};
</script> </script>
<style lang="scss"> <style lang="scss">
/* 修复input 背景不协调 和光标变色 */ /* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
$bg:#283443; $bg: #283443;
$light_gray:#fff; $light_gray: #fff;
$cursor: #fff; $cursor: #fff;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) { @supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
@ -169,13 +309,19 @@ $cursor: #fff;
border-radius: 5px; border-radius: 5px;
color: #454545; color: #454545;
} }
.el-tabs__item {
color: #fff;
}
.el-tabs__item.is-active {
color: #409eff;
}
} }
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
$bg:#2d3a4b; $bg: #2d3a4b;
$dark_gray:#889aa4; $dark_gray: #889aa4;
$light_gray:#eee; $light_gray: #eee;
.login-container { .login-container {
min-height: 100%; min-height: 100%;

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.7 on 2020-09-22 08:52
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('system', '0003_auto_20200528_1716'),
]
operations = [
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('mail', models.CharField(max_length=200, verbose_name='发送邮箱')),
('msg', models.CharField(max_length=200, verbose_name='验证码')),
],
options={
'abstract': False,
},
),
]

View File

@ -216,3 +216,7 @@ class File(CommonAModel):
def __str__(self): def __str__(self):
return self.name return self.name
class Message(BaseModel):
mail = models.CharField('发送邮箱', max_length=200)
msg = models.CharField('验证码', max_length=200)

View File

@ -4,6 +4,7 @@ from rest_framework import serializers
from .models import Organization, Permission, Role, User, Position, DictType, Dict, File from .models import Organization, Permission, Role, User, Position, DictType, Dict, File
class FileSerializer(serializers.ModelSerializer): class FileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = File model = File

View File

@ -1,5 +1,5 @@
from django.urls import path, include from django.urls import path, include
from .views import UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet from .views import UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet, sendMsg
from rest_framework import routers from rest_framework import routers
@ -13,5 +13,6 @@ router.register('dicttype', DictTypeViewSet, basename="dicttype")
router.register('dict', DictViewSet, basename="dict") router.register('dict', DictViewSet, basename="dict")
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
path('sendmsg/', sendMsg.as_view()),
path('test/', TestView.as_view()) path('test/', TestView.as_view())
] ]

View File

@ -1,10 +1,12 @@
import logging import logging
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.models import UserManager
from django.core.cache import cache from django.core.cache import cache
from django.http import request
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action, authentication_classes, permission_classes
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin, from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
ListModelMixin, RetrieveModelMixin, ListModelMixin, RetrieveModelMixin,
@ -20,7 +22,7 @@ from utils.queryset import get_child_queryset2
from .filters import UserFilter from .filters import UserFilter
from .models import (Dict, DictType, Organization, Permission, Position, Role, from .models import (Dict, DictType, Organization, Permission, Position, Role,
User, File) User, File, Message)
from .permission import RbacPermission, get_permission_list from .permission import RbacPermission, get_permission_list
from .permission_data import RbacFilterSet from .permission_data import RbacFilterSet
from .serializers import (DictSerializer, DictTypeSerializer, from .serializers import (DictSerializer, DictTypeSerializer,
@ -40,6 +42,69 @@ class LogoutView(APIView):
def get(self, request, *args, **kwargs): # 可将token加入黑名单 def get(self, request, *args, **kwargs): # 可将token加入黑名单
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
from email.header import Header
import random
from rest_framework_simplejwt.tokens import RefreshToken
def get_tokens_for_user(user):
refresh = RefreshToken.for_user(user)
return {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
class Login2View(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
mail = request.data['mail']
msg = request.data['msg']
if not User.objects.filter(username=mail).exists():
return Response('账户不存在', status=status.HTTP_400_BAD_REQUEST)
if Message.objects.filter(mail=mail).last().msg == msg:
user = User.objects.get(username=mail)
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST)
class sendMsg(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
code = random.randint(1000,9999)
my_sender = 'caoqianming@ctc.ac.cn'
my_user = request.data['mail']
my_pass = '9093QQww'
if not User.objects.filter(username=my_user).exists():
return Response('该账户不存在', status=status.HTTP_400_BAD_REQUEST)
try:
# 邮件内容
msg=MIMEText('ctc能力检索本次验证码为' + str(code),'plain','utf-8')
# 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['From']=formataddr(["国检集团能力检索",my_sender])
# 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['To']=formataddr(["国检集团",my_user])
# 邮件的主题
msg['Subject'] = Header(str(code), 'utf-8').encode()
# SMTP服务器腾讯企业邮箱端口是465腾讯邮箱支持SSL(不强制) 不支持TLS
# qq邮箱smtp服务器地址:smtp.qq.com,端口号456
# 163邮箱smtp服务器地址smtp.163.com端口号25
server=smtplib.SMTP_SSL("smtp.exmail.qq.com", 465)
# 登录服务器,括号中对应的是发件人邮箱账号、邮箱密码
server.login(my_sender, my_pass)
# 发送邮件,括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.sendmail(my_sender,[my_user,],msg.as_string())
# 关闭连接
server.quit()
Message.objects.create(mail=my_user, msg=code)
except:
return Response('验证码发送失败', status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_200_OK)
class DictTypeViewSet(ModelViewSet): class DictTypeViewSet(ModelViewSet):
""" """

Binary file not shown.

View File

@ -22,13 +22,14 @@ from rest_framework.documentation import include_docs_urls
from rest_framework_simplejwt.views import (TokenObtainPairView, from rest_framework_simplejwt.views import (TokenObtainPairView,
TokenRefreshView) TokenRefreshView)
from apps.system.views import FileViewSet, LogoutView from apps.system.views import FileViewSet, LogoutView, Login2View
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register('file', FileViewSet, basename="file") router.register('file', FileViewSet, basename="file")
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token2/', Login2View.as_view(), name='token_obtain_2'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/black/', LogoutView.as_view(), name='token_black'), path('token/black/', LogoutView.as_view(), name='token_black'),
path('system/', include('apps.system.urls')), path('system/', include('apps.system.urls')),