版本升级
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:
parent
7fbdcb3b0f
commit
ebcdac7796
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -1,2 +1,8 @@
|
||||
@import './var.css';
|
||||
@import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
|
||||
// 解决抽屉弹出时,body宽度变化的问题
|
||||
.el-popup-parent--hidden {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
# 是否开启保存每次请求日志到本地
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
||||
"""
|
||||
|
@ -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)
|
||||
):
|
||||
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:
|
||||
return ErrorResponse(msg="请使用正确的登录方式")
|
||||
raise ValueError("无效参数")
|
||||
|
||||
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
|
||||
raise ValueError(result.msg)
|
||||
|
||||
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)
|
||||
|
||||
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": result.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
|
||||
"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, result.status, request, resp)
|
||||
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)
|
||||
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:
|
||||
msg = "无效Code"
|
||||
await VadminLoginRecord.create_login_record(db, data, False, request, {"message": msg})
|
||||
return ErrorResponse(msg=msg)
|
||||
raise ValueError("无效Code")
|
||||
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 = "手机号不存在!"
|
||||
raise ValueError("此手机号不存在")
|
||||
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)
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user