版本升级

1. 新增(kinit-api):防止密码破解,新增输入5次错误密码,账号锁定,使用过期时间为1天
2. 新增(kinit-api):新增 OpenAuth 开放认证方式
3. 新增(kinit-api,kinit-uni):创建用户时,如果没有头像,则添加默认头像链接
4. 优化(kinit-api):登录接口优化
5. 新增(kinit-api):新增Count计数类
6. 更新(kinit-admin):前端升级到vue-element-plus-admin最新版本1.9.4
This commit is contained in:
ktianc 2023-03-05 22:55:24 +08:00
parent 7fbdcb3b0f
commit ebcdac7796
12 changed files with 183 additions and 97 deletions

View File

@ -1,6 +1,6 @@
{
"name": "vue-element-plus-admin",
"version": "1.9.2",
"version": "1.9.4",
"description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
"author": "Archer <502431556@qq.com>",
"private": false,
@ -25,28 +25,28 @@
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@iconify/iconify": "^3.0.1",
"@iconify/iconify": "^3.1.0",
"@kjgl77/datav-vue3": "^1.4.2",
"@vueuse/core": "^9.10.0",
"@vueuse/core": "^9.13.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^2.1.0",
"@zxcvbn-ts/core": "^2.2.1",
"animate.css": "^4.1.1",
"axios": "^1.2.2",
"axios": "^1.3.4",
"echarts": "^5.4.1",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.2.28",
"element-plus": "2.2.32",
"intro.js": "^6.0.0",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"pinia": "2.0.29",
"pinia": "^2.0.32",
"qrcode": "^1.5.1",
"qs": "^6.11.0",
"url": "^0.11.0",
"vue": "3.2.45",
"vue": "3.2.47",
"vue-i18n": "9.2.2",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2",
@ -55,48 +55,48 @@
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
"@iconify/json": "^2.2.7",
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/json": "^2.2.29",
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0",
"@types/intro.js": "^5.1.1",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.11.18",
"@types/node": "^18.14.2",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@vitejs/plugin-legacy": "^3.0.1",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"@vitejs/plugin-legacy": "^4.0.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.13",
"consola": "^2.15.3",
"eslint": "^8.32.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-define-config": "^1.14.0",
"eslint-define-config": "^1.15.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.9.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.0",
"plop": "^3.1.1",
"lint-staged": "^13.1.2",
"plop": "^3.1.2",
"postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.8.3",
"rimraf": "^4.0.7",
"rollup": "^3.10.0",
"stylelint": "^14.16.1",
"prettier": "^2.8.4",
"rimraf": "^4.1.2",
"rollup": "^3.17.3",
"stylelint": "^15.2.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-recommended": "^9.0.0",
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^6.0.1",
"terser": "^5.16.1",
"typescript": "4.9.4",
"vite": "4.0.4",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-recommended": "^10.0.1",
"stylelint-config-standard": "^30.0.1",
"stylelint-order": "^6.0.2",
"terser": "^5.16.5",
"typescript": "4.9.5",
"vite": "4.1.4",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-mock": "^2.9.6",
@ -105,7 +105,7 @@
"vite-plugin-style-import": "2.0.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-windicss": "^1.8.10",
"vue-tsc": "^1.0.24",
"vue-tsc": "^1.2.0",
"windicss": "^3.5.6",
"windicss-analysis": "^0.3.5"
},

View File

@ -107,7 +107,13 @@ const toggleClick = () => {
v-bind="getBindItemValue(item)"
>
<template #label>
<slot :name="`${item.field}-label`" :label="item.label">{{ item.label }}</slot>
<slot
:name="`${item.field}-label`"
:row="{
label: item.label
}"
>{{ item.label }}</slot
>
</template>
<template #default>

View File

@ -412,7 +412,10 @@ watch(
{
icon: 'ant-design:close-outlined',
label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(selectedTag!)
}
},
{
divided: true,

View File

@ -39,3 +39,9 @@ const themeChange = (val: boolean) => {
@change="themeChange"
/>
</template>
<style lang="less" scoped>
:deep(.el-switch__core .el-switch__inner .is-icon) {
overflow: visible;
}
</style>

View File

@ -214,7 +214,6 @@ const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): For
for (const task of formRequestTask) {
task()
}
console.log(formSchema)
return formSchema
}
@ -243,7 +242,7 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
// 给options添加国际化
const filterOptions = (options: Recordable, labelField?: string) => {
return options.map((v: Recordable) => {
return options?.map((v: Recordable) => {
if (labelField) {
v['labelField'] = t(v.labelField)
} else {

View File

@ -1,2 +1,8 @@
@import './var.css';
@import 'element-plus/theme-chalk/dark/css-vars.css';
// 解决抽屉弹出时body宽度变化的问题
.el-popup-parent--hidden {
width: 100% !important;
}

View File

@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
"""
系统版本
"""
VERSION = "1.6.0"
VERSION = "1.6.1"
"""安全警告: 不要在生产中打开调试运行!"""
DEBUG = True
@ -105,6 +105,10 @@ EVENTS = [
"""
# 默认密码,"0" 默认为手机号后六位
DEFAULT_PASSWORD = "0"
# 默认头像
DEFAULT_AVATAR = "https://vv-reserve.oss-cn-hangzhou.aliyuncs.com/avatar/2023-01-27/1674820804e81e7631.png"
# 默认登陆时最大输入密码或验证码错误次数
DEFAULT_AUTH_ERROR_MAX_NUMBER = 5
# 是否开启保存登录日志
LOGIN_LOG_RECORD = not DEBUG
# 是否开启保存每次请求日志到本地

View File

@ -60,6 +60,7 @@ class UserDal(DalBase):
raise CustomException("手机号已存在!", code=status.HTTP_ERROR)
password = data.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
data.password = self.model.get_password_hash(password)
data.avatar = data.avatar if data.avatar else settings.DEFAULT_AVATAR
obj = self.model(**data.dict(exclude={'role_ids'}))
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
for role in roles:

View File

@ -6,6 +6,8 @@
# @desc : 获取认证后的信息工具
from typing import List, Optional
from jose import jwt, JWTError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from apps.vadmin.auth.crud import UserDal
@ -33,6 +35,61 @@ def get_user_permissions(user: VadminUser) -> set:
return permissions
class OpenAuth(AuthValidation):
"""
开放认证无认证也可以访问
认证了以后可以获取到用户信息无认证则获取不到
"""
@classmethod
def validate_token(cls, token: str | None, db: AsyncSession) -> str | None:
"""
验证用户 token没有则返回 None
"""
if not token:
return None
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
telephone: str = payload.get("sub")
if telephone is None:
return None
except JWTError:
return None
return telephone
@classmethod
async def validate_user(cls, request: Request, user: VadminUser, db: AsyncSession) -> Auth:
"""
验证用户信息
"""
if user is None:
return Auth(db=db)
elif not user.is_active:
return Auth(db=db)
request.scope["telephone"] = user.telephone
try:
request.scope["body"] = await request.body()
except RuntimeError:
request.scope["body"] = "获取失败"
return Auth(user=user, db=db)
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
db: AsyncSession = Depends(db_getter)
):
"""
每次调用依赖此类的接口会执行该方法
"""
telephone = self.validate_token(token, db)
if telephone:
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
return await self.validate_user(request, user, db)
return Auth(db=db)
class AllUserAuth(AuthValidation):
"""

View File

@ -39,77 +39,66 @@ from core.data_types import Telephone
app = APIRouter()
@app.post("/login/", summary="手机号密码登录")
@app.post("/login/", summary="手机号密码登录", description="员工登录通道限制最多输错次数达到最大值后将is_active=False")
async def login_for_access_token(
request: Request,
data: LoginForm,
manage: LoginManage = Depends(),
db: AsyncSession = Depends(db_getter)
):
if data.method == "0":
result = await manage.password_login(data, db, request)
elif data.method == "1":
result = await manage.sms_login(data, db, request)
else:
return ErrorResponse(msg="请使用正确的登录方式")
if not result.status:
resp = {"message": result.msg}
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
return ErrorResponse(msg=result.msg)
user = result.user
try:
if data.method == "0":
result = await manage.password_login(data, db, request)
elif data.method == "1":
result = await manage.sms_login(data, db, request)
else:
raise ValueError("无效参数")
if data.platform in ["0", "1"] and not user.is_staff:
msg = "此手机号无登录权限"
await VadminLoginRecord.create_login_record(db, data, result.status, request, {"message": msg})
return ErrorResponse(msg=msg)
if not result.status:
raise ValueError(result.msg)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
resp = {
"access_token": access_token,
"token_type": "bearer",
"is_reset_password": user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, result.status, request, resp)
return SuccessResponse(resp)
token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
token = LoginManage.create_access_token(data={"sub": result.user.telephone}, expires_delta=token_expires)
resp = {
"access_token": token,
"token_type": "bearer",
"is_reset_password": result.user.is_reset_password,
"is_wx_server_openid": result.user.is_wx_server_openid
}
await VadminLoginRecord.create_login_record(db, data, True, request, resp)
return SuccessResponse(resp)
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
@app.post("/wx/login/", summary="微信服务端一键登录")
@app.post("/wx/login/", summary="微信服务端一键登录", description="员工登录通道")
async def wx_login_for_access_token(request: Request, data: WXLoginForm, db: AsyncSession = Depends(db_getter)):
if data.platform not in ["0", "1"]:
msg = "错误平台"
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
return ErrorResponse(msg=msg)
wx = WXOAuth(request.app.state.redis, 0)
telephone = await wx.parsing_phone_number(data.code)
if not telephone:
msg = "无效Code"
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
return ErrorResponse(msg=msg)
data.telephone = telephone
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
msg = None
if not user:
# 手机号不存在,创建新用户
# model = UserIn(name=generate_string(), telephone=Telephone(telephone))
# user = await UserDal(db).create_data(model, v_return_obj=True)
msg = "手机号不存在!"
elif not user.is_active:
msg = "此手机号已被冻结!"
elif data.platform in ["0", "1"] and not user.is_staff:
msg = "此手机号无登录权限"
if msg:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
return ErrorResponse(msg=msg)
try:
if data.platform != "1" or data.method != "2":
raise ValueError("无效参数")
wx = WXOAuth(request.app.state.redis, 0)
telephone = await wx.parsing_phone_number(data.code)
if not telephone:
raise ValueError("无效Code")
data.telephone = telephone
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True)
if not user:
raise ValueError("此手机号不存在")
elif not user.is_active:
raise ValueError("此手机号已被冻结")
except ValueError as e:
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": str(e)})
return ErrorResponse(msg=str(e))
# 更新登录时间
await user.update_login_info(db, request.client.host)
# 登录成功创建 token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=token_expires)
resp = {
"access_token": access_token,
"access_token": token,
"token_type": "bearer",
"is_reset_password": user.is_reset_password,
"is_wx_server_openid": user.is_wx_server_openid

View File

@ -9,10 +9,13 @@
from fastapi import Request, Depends
from pydantic import BaseModel, validator
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER
from apps.vadmin.auth import models, crud, schemas
from core.database import db_getter
from core.validator import vali_telephone
from typing import Optional
from utils.count import Count
class LoginForm(BaseModel):
@ -52,8 +55,8 @@ class LoginValidation:
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
self.result = LoginResult()
if data.platform not in ["0", "1"]:
self.result.msg = "错误平台"
if data.platform not in ["0", "1"] or data.method not in ["0", "1"]:
self.result.msg = "无效参数"
return self.result
user = await crud.UserDal(db).get_data(telephone=data.telephone, v_return_none=True)
if not user:
@ -62,11 +65,23 @@ class LoginValidation:
result = await self.func(self, data=data, user=user, request=request)
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
count = Count(request.app.state.redis, count_key)
if not result.status:
self.result.msg = result.msg
number = await count.add(ex=86400)
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
await count.reset()
# 如果等于最大次数,那么就将用户 is_active=False
user.is_active = False
await db.flush()
elif not user.is_active:
self.result.msg = "此手机号已被冻结!"
elif user:
elif data.platform in ["0", "1"] and not user.is_staff:
self.result.msg = "此手机号无权限!"
else:
await count.delete()
self.result.msg = "OK"
self.result.status = True
self.result.user = schemas.UserSimpleOut.from_orm(user)

View File

@ -107,7 +107,7 @@ const actions = {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.data
const avatar = (user == null || user.avatar == "" || user.avatar == null) ? require("@/static/images/avatar.jpg") : user.avatar
const avatar = (user == null || user.avatar == "" || user.avatar == null) ? "https://vv-reserve.oss-cn-hangzhou.aliyuncs.com/avatar/2023-01-27/1674820804e81e7631.png" : user.avatar
const name = (user == null || user.name == "" || user.name == null) ? "" : user.name
commit('SET_ROLES', user.roles.map((item) => item.name) || ['ROLE_DEFAULT'])
commit('SET_PERMISSIONS', user.permissions)