diff --git a/kinit-admin/package.json b/kinit-admin/package.json index 0cc1ca1..e1c5c25 100644 --- a/kinit-admin/package.json +++ b/kinit-admin/package.json @@ -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" }, diff --git a/kinit-admin/src/components/Descriptions/src/Descriptions.vue b/kinit-admin/src/components/Descriptions/src/Descriptions.vue index a1c7a07..4b70992 100644 --- a/kinit-admin/src/components/Descriptions/src/Descriptions.vue +++ b/kinit-admin/src/components/Descriptions/src/Descriptions.vue @@ -107,7 +107,13 @@ const toggleClick = () => { v-bind="getBindItemValue(item)" > + + diff --git a/kinit-admin/src/hooks/web/useCrudSchemas.ts b/kinit-admin/src/hooks/web/useCrudSchemas.ts index 1cc2025..5acf078 100644 --- a/kinit-admin/src/hooks/web/useCrudSchemas.ts +++ b/kinit-admin/src/hooks/web/useCrudSchemas.ts @@ -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 { diff --git a/kinit-admin/src/styles/index.less b/kinit-admin/src/styles/index.less index c023316..c5122f3 100644 --- a/kinit-admin/src/styles/index.less +++ b/kinit-admin/src/styles/index.less @@ -1,2 +1,8 @@ @import './var.css'; @import 'element-plus/theme-chalk/dark/css-vars.css'; + +// 解决抽屉弹出时,body宽度变化的问题 +.el-popup-parent--hidden { + width: 100% !important; +} + diff --git a/kinit-api/application/settings.py b/kinit-api/application/settings.py index d1ee2a6..b9e44cc 100644 --- a/kinit-api/application/settings.py +++ b/kinit-api/application/settings.py @@ -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 # 是否开启保存每次请求日志到本地 diff --git a/kinit-api/apps/vadmin/auth/crud.py b/kinit-api/apps/vadmin/auth/crud.py index 0b29ba3..cfbea00 100644 --- a/kinit-api/apps/vadmin/auth/crud.py +++ b/kinit-api/apps/vadmin/auth/crud.py @@ -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: diff --git a/kinit-api/apps/vadmin/auth/utils/current.py b/kinit-api/apps/vadmin/auth/utils/current.py index ccc71d7..4c98a0a 100644 --- a/kinit-api/apps/vadmin/auth/utils/current.py +++ b/kinit-api/apps/vadmin/auth/utils/current.py @@ -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): """ diff --git a/kinit-api/apps/vadmin/auth/utils/login.py b/kinit-api/apps/vadmin/auth/utils/login.py index e3e5b7f..70ee680 100644 --- a/kinit-api/apps/vadmin/auth/utils/login.py +++ b/kinit-api/apps/vadmin/auth/utils/login.py @@ -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 diff --git a/kinit-api/apps/vadmin/auth/utils/validation/login.py b/kinit-api/apps/vadmin/auth/utils/validation/login.py index 74b3b79..9c4b251 100644 --- a/kinit-api/apps/vadmin/auth/utils/validation/login.py +++ b/kinit-api/apps/vadmin/auth/utils/validation/login.py @@ -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) diff --git a/kinit-uni/store/modules/auth.js b/kinit-uni/store/modules/auth.js index d8c91a2..4c0acc3 100644 --- a/kinit-uni/store/modules/auth.js +++ b/kinit-uni/store/modules/auth.js @@ -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)