system消息通知:修复已读消息bug;permission:接口权限完成。
parent
c9a34da1a5
commit
2514d98173
15
README.md
15
README.md
|
@ -1,6 +1,6 @@
|
|||
# Django-Vue-Admin
|
||||
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://pypi.org/project/django-simpleui/#history) [](https://python.org/) [](https://nodejs.org/zh-cn/download/releases/)[](https://pypi.org/project/django-simpleui/)
|
||||
[](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [](https://pypi.org/project/django-simpleui/#history) [](https://python.org/) 
|
||||
|
||||
|
||||
|
||||
|
@ -68,9 +68,6 @@ git clone https://gitee.com/liqianglog/django-vue-admin.git
|
|||
cd dvadmin-ui
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 建议不要直接使用cnpm安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题。
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
|
||||
# 启动服务
|
||||
|
@ -95,7 +92,12 @@ npm run build:prod
|
|||
~~~bash
|
||||
1. 进入项目目录 cd dvadmin-backend
|
||||
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
|
||||
|
||||
3. 在 env.py 中配置数据库信息
|
||||
mysql数据库版本建议:5.7以上
|
||||
mysql数据库字符集:utf8mb4
|
||||
mysql数据库排序规则:utf8mb4_0900_ai_ci
|
||||
|
||||
4. 安装依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
5. 执行迁移命令:
|
||||
|
@ -104,10 +106,13 @@ npm run build:prod
|
|||
6. 初始化数据
|
||||
python3 manage.py init
|
||||
7. 启动项目
|
||||
python3 manage.py runserver 0.0.0.0:8000
|
||||
python3 manage.py runserver 127.0.0.1:8000
|
||||
|
||||
定时任务启动命令:
|
||||
celery -A application worker -B --loglevel=info
|
||||
注:
|
||||
Windows 运行celery 需要安装 pip install eventlet
|
||||
celery -A application worker -P eventlet --loglevel=info
|
||||
|
||||
初始账号:admin 密码:123456
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from conf.env import *
|
|||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0,os.path.join(BASE_DIR,'apps'))
|
||||
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
|
@ -54,6 +54,7 @@ INSTALLED_APPS = [
|
|||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'vadmin.op_drf.middleware.PermissionModeMiddleware', # 权限中间件
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
@ -202,7 +203,7 @@ LOGGING = {
|
|||
'loggers': {
|
||||
# default日志
|
||||
'': {
|
||||
'handlers': ['console','error','file'],
|
||||
'handlers': ['console', 'error', 'file'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
# 数据库相关日志
|
||||
|
@ -300,11 +301,11 @@ USERNAME_FIELD = 'username'
|
|||
# ************** 登录验证码配置 ************** #
|
||||
# ================================================= #
|
||||
CAPTCHA_STATE = CAPTCHA_STATE
|
||||
#字母验证码
|
||||
# 字母验证码
|
||||
CAPTCHA_IMAGE_SIZE = (160, 60) # 设置 captcha 图片大小
|
||||
CAPTCHA_LENGTH = 4 # 字符个数
|
||||
CAPTCHA_TIMEOUT = 1 # 超时(minutes)
|
||||
#加减乘除验证码
|
||||
# 加减乘除验证码
|
||||
CAPTCHA_OUTPUT_FORMAT = '%(image)s %(text_field)s %(hidden_field)s '
|
||||
CAPTCHA_FONT_SIZE = 40 # 字体大小
|
||||
CAPTCHA_FOREGROUND_COLOR = '#0033FF' # 前景色
|
||||
|
@ -312,7 +313,7 @@ CAPTCHA_BACKGROUND_COLOR = '#F5F7F4' # 背景色
|
|||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
# 'captcha.helpers.noise_arcs', # 线
|
||||
# 'captcha.helpers.noise_dots', # 点
|
||||
)
|
||||
)
|
||||
# CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge'
|
||||
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge'
|
||||
|
||||
|
@ -320,5 +321,10 @@ API_LOG_ENABLE = True
|
|||
# API_LOG_METHODS = 'ALL' # ['POST', 'DELETE']
|
||||
# API_LOG_METHODS = ['POST', 'DELETE'] # ['POST', 'DELETE']
|
||||
BROKER_URL = f'redis://:{REDIS_PASSWORD if REDIS_PASSWORD else ""}@{os.getenv("REDIS_HOST") or REDIS_HOST}:' \
|
||||
f'{REDIS_PORT}/{locals().get("CELERY_DB",2)}' #Broker使用Redis
|
||||
CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' #Backend数据库
|
||||
f'{REDIS_PORT}/{locals().get("CELERY_DB", 2)}' # Broker使用Redis
|
||||
CELERYBEAT_SCHEDULER = 'django_celery_beat.schedulers.DatabaseScheduler' # Backend数据库
|
||||
# ================================================= #
|
||||
# ************** 其他配置 ************** #
|
||||
# ================================================= #
|
||||
# 接口权限
|
||||
INTERFACE_PERMISSION = {locals().get("INTERFACE_PERMISSION", False)}
|
||||
|
|
|
@ -22,7 +22,7 @@ from django.urls import re_path, include
|
|||
from django.views.static import serve
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.vadmin.op_drf.response import SuccessResponse
|
||||
from vadmin.utils.response import SuccessResponse
|
||||
|
||||
|
||||
class CaptchaRefresh(APIView):
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
"""
|
||||
django中间件
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
from apps.vadmin.permission.models import Menu
|
||||
from apps.vadmin.system.models import OperationLog
|
||||
from ..utils.request_util import get_request_ip, get_request_data, get_request_path, get_browser, get_os, \
|
||||
get_login_location
|
||||
get_login_location, get_request_canonical_path, get_request_user
|
||||
from ..utils.response import ErrorJsonResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
|
@ -77,3 +83,78 @@ class PermissionModeMiddleware(MiddlewareMixin):
|
|||
"""
|
||||
权限模式拦截判断
|
||||
"""
|
||||
|
||||
def process_request(self, request):
|
||||
"""
|
||||
判断环境变量中,是否为演示模式(正常可忽略此判断)
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
white_list = ['/admin/logout/', '/admin/login/']
|
||||
if os.getenv('DEMO_ENV') and not request.method == 'GET' and request.path not in white_list:
|
||||
return ErrorJsonResponse(data={}, msg=f'演示模式,不允许操作!')
|
||||
|
||||
def has_interface_permission(self, request, method, view_path, user=None):
|
||||
"""
|
||||
接口权限验证,优先级:
|
||||
(1)接口是否接入权限管理, 是:继续; 否:通过
|
||||
(2)认证的user是否superuser, 是:通过; 否:继续
|
||||
(3)user的角色有该接口权限, 是:通过, 否:不通过
|
||||
|
||||
auth_code含义: auth_code >=0, 表示接口认证通过; auth_code < 0, 表示无接口访问权限, 具体含义如下
|
||||
-1:
|
||||
-10: 该请求已认证的用户没有这个接口的访问权限
|
||||
0:
|
||||
1: 白名单
|
||||
10: 该接口没有录入权限系统, 放行 请求中认证的用户为超级管理员, 直接放行
|
||||
20: 请求中认证的用户是superuser放行
|
||||
30: 请求中认证的用户对应的角色中,某个角色包含了该接口的访问权限, 放行
|
||||
1. 先获取所有录入系统的接口
|
||||
2 判断此用户是否为 superuser
|
||||
3. 获取此用户所请求的接口
|
||||
4. 获取此用户关联角色所有有权限的接口
|
||||
|
||||
:param interface: 接口模型
|
||||
:param path: 接口路径
|
||||
:param method: 请求方法
|
||||
:param project: 接口所属项目
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
interface_dict = Menu.get_interface_dict()
|
||||
# (1) 接口是否接入权限管理, 是:继续; 否:通过
|
||||
if not view_path in interface_dict.get(method, []):
|
||||
return 10
|
||||
# (2)认证的user是否superuser, 是:通过; 否:继续
|
||||
if user.is_superuser or (hasattr(user, 'role') and user.role.filter(status='1', admin=True).count()):
|
||||
return 20
|
||||
# (3)user的角色有该接口权限, 是:通过, 否:不通过
|
||||
if view_path in user.get_user_interface_dict:
|
||||
return 30
|
||||
return -10
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
if not settings.INTERFACE_PERMISSION:
|
||||
return
|
||||
user = get_request_user(request)
|
||||
|
||||
if user and not isinstance(user, AnonymousUser):
|
||||
method = request.method.upper()
|
||||
if method == 'GET': # GET 不设置接口权限
|
||||
return
|
||||
view_path = get_request_canonical_path(request, *view_args, **view_kwargs)
|
||||
auth_code = self.has_interface_permission(request, method, view_path, user)
|
||||
logger.info(f"[{user.username}] {method}:{view_path}, 权限认证:{auth_code}")
|
||||
if auth_code >= 0:
|
||||
return
|
||||
return ErrorJsonResponse(data={}, msg=f'无接口访问权限!')
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""
|
||||
主要请求处理完之后记录
|
||||
:param request:
|
||||
:param response:
|
||||
:return:
|
||||
"""
|
||||
return response
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.db.models import IntegerField, ForeignKey, CharField, CASCADE
|
||||
from django.core.cache import cache
|
||||
from django.db.models import IntegerField, ForeignKey, CharField, CASCADE, Q
|
||||
|
||||
from ...op_drf.models import CoreModel
|
||||
|
||||
|
@ -34,6 +35,31 @@ class Menu(CoreModel):
|
|||
visible = CharField(max_length=8, verbose_name="显示状态")
|
||||
isCache = CharField(max_length=8, verbose_name="是否缓存")
|
||||
|
||||
@classmethod
|
||||
def get_interface_dict(cls):
|
||||
"""
|
||||
获取所有接口列表
|
||||
:return:
|
||||
"""
|
||||
interface_dict = cache.get('permission_interface_dict', {})
|
||||
if not interface_dict:
|
||||
for ele in Menu.objects.filter(~Q(interface_path=''), ~Q(interface_path=None), status='1', ).values(
|
||||
'interface_path', 'interface_method'):
|
||||
if ele.get('interface_method') in interface_dict:
|
||||
interface_dict[ele.get('interface_method', '')].append(ele.get('interface_path'))
|
||||
else:
|
||||
interface_dict[ele.get('interface_method', '')] = [ele.get('interface_path')]
|
||||
cache.set('permission_interface_dict', interface_dict, 84600)
|
||||
return interface_dict
|
||||
|
||||
@classmethod
|
||||
def delete_cache(cls):
|
||||
"""
|
||||
清空缓存中的接口列表
|
||||
:return:
|
||||
"""
|
||||
cache.delete('permission_interface_dict')
|
||||
|
||||
class Meta:
|
||||
verbose_name = '菜单管理'
|
||||
verbose_name_plural = verbose_name
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import UserManager, AbstractUser
|
||||
from django.core.cache import cache
|
||||
from django.db.models import IntegerField, ForeignKey, CharField, TextField, ManyToManyField, CASCADE
|
||||
|
||||
from ...op_drf.fields import CreateDateTimeField, UpdateDateTimeField
|
||||
|
@ -28,6 +29,29 @@ class UserProfile(AbstractUser):
|
|||
create_datetime = CreateDateTimeField()
|
||||
update_datetime = UpdateDateTimeField()
|
||||
|
||||
@property
|
||||
def get_user_interface_dict(self):
|
||||
interface_dict = cache.get(f'permission_interface_dict{self.username}', {})
|
||||
if not interface_dict:
|
||||
for ele in self.role.filter(status='1', menu__status='1').values('menu__interface_path',
|
||||
'menu__interface_method').distinct():
|
||||
interface_path = ele.get('menu__interface_path')
|
||||
if interface_path is None or interface_path == '':
|
||||
continue
|
||||
if ele.get('menu__interface_method') in interface_dict:
|
||||
interface_dict[ele.get('menu__interface_method', '')].append(interface_path)
|
||||
else:
|
||||
interface_dict[ele.get('menu__interface_method', '')] = [interface_path]
|
||||
cache.set(f'permission_interface_dict_{self.username}', interface_dict, 84600)
|
||||
return interface_dict
|
||||
|
||||
@property
|
||||
def delete_cache(self):
|
||||
"""
|
||||
清空缓存中的接口列表
|
||||
:return:
|
||||
"""
|
||||
return cache.delete(f'permission_interface_dict_{self.username}')
|
||||
class Meta:
|
||||
verbose_name = '用户管理'
|
||||
verbose_name_plural = verbose_name
|
||||
|
|
|
@ -87,11 +87,9 @@ class CommonPermission(CustomPermission):
|
|||
return int(instance.dept_belong_id) in list(set(dept_list))
|
||||
|
||||
def has_permission(self, request: Request, view: APIView):
|
||||
"""判断是否为演示模式"""
|
||||
return True
|
||||
|
||||
def has_object_permission(self, request: Request, view: APIView, instance):
|
||||
self.message = f"没有此数据操作权限!"
|
||||
res = self.check_queryset(request, instance)
|
||||
print(res)
|
||||
return res
|
||||
|
|
|
@ -37,6 +37,10 @@ class MenuCreateUpdateSerializer(CustomModelSerializer):
|
|||
# raise APIException(message=f'仅Manger能创建/更新角色为公共角色')
|
||||
return super().validate(attrs)
|
||||
|
||||
def save(self, **kwargs):
|
||||
Menu.delete_cache()
|
||||
return super().save(**kwargs)
|
||||
|
||||
class Meta:
|
||||
model = Menu
|
||||
fields = '__all__'
|
||||
|
@ -91,7 +95,7 @@ class DeptTreeSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = ('id', 'label', 'parentId','status')
|
||||
fields = ('id', 'label', 'parentId', 'status')
|
||||
|
||||
|
||||
# ================================================= #
|
||||
|
|
|
@ -25,6 +25,7 @@ class GetUserProfileView(APIView):
|
|||
user_dict = UserProfileSerializer(request.user).data
|
||||
permissions_list = ['*:*:*'] if user_dict.get('admin') else Menu.objects.filter(
|
||||
role__userprofile=request.user).values_list('perms', flat=True)
|
||||
delete_cache = request.user.delete_cache
|
||||
return SuccessResponse({
|
||||
'permissions': [ele for ele in permissions_list if ele],
|
||||
'roles': Role.objects.filter(userprofile=request.user).values_list('roleKey', flat=True),
|
||||
|
|
|
@ -22,9 +22,9 @@ from django.urls import re_path, include
|
|||
from rest_framework.documentation import include_docs_urls
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .op_drf.response import SuccessResponse
|
||||
from .permission.views import GetUserProfileView, GetRouters
|
||||
from .utils.login import LoginView, LogoutView
|
||||
from .utils.response import SuccessResponse
|
||||
|
||||
|
||||
class CaptchaRefresh(APIView):
|
||||
|
|
|
@ -8,10 +8,10 @@ from django.contrib.auth.models import AbstractBaseUser
|
|||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
from django.urls.resolvers import ResolverMatch
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.settings import api_settings as drf_settings
|
||||
from user_agents import parse
|
||||
|
||||
from apps.vadmin.utils.authentication import OpAuthJwtAuthentication
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -26,18 +26,8 @@ def get_request_user(request, authenticate=True):
|
|||
user: AbstractBaseUser = getattr(request, 'user', None)
|
||||
if user and user.is_authenticated:
|
||||
return user
|
||||
authentication: BaseAuthentication = None
|
||||
for authentication_class in drf_settings.DEFAULT_AUTHENTICATION_CLASSES:
|
||||
try:
|
||||
authentication = authentication_class()
|
||||
user_auth_tuple = authentication.authenticate(request)
|
||||
if user_auth_tuple is not None:
|
||||
user, token = user_auth_tuple
|
||||
if authenticate:
|
||||
request.user = user
|
||||
return user
|
||||
except Exception:
|
||||
pass
|
||||
user, tokrn = OpAuthJwtAuthentication().authenticate(request)
|
||||
print(22, user)
|
||||
return user or AnonymousUser()
|
||||
|
||||
|
||||
|
@ -127,9 +117,11 @@ def get_request_canonical_path(request, *args, **kwargs):
|
|||
for value in resolver_match.args:
|
||||
path = path.replace(f"/{value}", "/{id}")
|
||||
for key, value in resolver_match.kwargs.items():
|
||||
path = path.replace(f"/{value}", f"/{{{key}}}")
|
||||
if key == 'pk':
|
||||
pass
|
||||
path = path.replace(f"/{value}", f"/{{id}}")
|
||||
continue
|
||||
path = path.replace(f"/{value}", f"/{{{key}}}")
|
||||
|
||||
return path
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
常用的Response以及Django的Response、DRF的Response
|
||||
"""
|
||||
from django.http.response import DjangoJSONEncoder
|
||||
from django.http.response import DjangoJSONEncoder, JsonResponse
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
|
@ -56,3 +56,36 @@ class ErrorResponse(Response):
|
|||
|
||||
def __str__(self):
|
||||
return str(self.std_data)
|
||||
|
||||
|
||||
class SuccessJsonResponse(JsonResponse):
|
||||
"""
|
||||
标准JsonResponse, SuccessJsonResponse(data)SuccessJsonResponse(data=data)
|
||||
(1)仅SuccessResponse无法使用时才能推荐使用SuccessJsonResponse
|
||||
"""
|
||||
|
||||
def __init__(self, data, msg='success', encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs):
|
||||
std_data = {
|
||||
"code": 200,
|
||||
"data": data,
|
||||
"msg": msg,
|
||||
"status": 'success'
|
||||
}
|
||||
super().__init__(std_data, encoder, safe, json_dumps_params, **kwargs)
|
||||
|
||||
|
||||
class ErrorJsonResponse(JsonResponse):
|
||||
"""
|
||||
标准JsonResponse, 仅ErrorResponse无法使用时才能使用ErrorJsonResponse
|
||||
(1)默认错误码返回2001, 也可以指定其他返回码:ErrorJsonResponse(code=xxx)
|
||||
"""
|
||||
|
||||
def __init__(self, data, msg='error', code=201, encoder=OpDRFJSONEncoder, safe=True, json_dumps_params=None,
|
||||
**kwargs):
|
||||
std_data = {
|
||||
"code": code,
|
||||
"data": data,
|
||||
"msg": msg,
|
||||
"status": 'error'
|
||||
}
|
||||
super().__init__(std_data, encoder, safe, json_dumps_params, **kwargs)
|
||||
|
|
|
@ -39,3 +39,5 @@ CAPTCHA_STATE = True
|
|||
# 操作日志配置
|
||||
API_LOG_ENABLE = True
|
||||
API_LOG_METHODS = ['POST', 'DELETE', 'PUT'] # 'ALL' or ['POST', 'DELETE']
|
||||
# 接口权限
|
||||
INTERFACE_PERMISSION = True
|
||||
|
|
|
@ -39,9 +39,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getToken } from "@/utils/auth";
|
||||
import {getToken} from "@/utils/auth";
|
||||
|
||||
export default {
|
||||
export default {
|
||||
props: {
|
||||
// 值
|
||||
value: [String, Object, Array],
|
||||
|
@ -135,8 +135,12 @@ export default {
|
|||
},
|
||||
// 上传成功回调
|
||||
handleUploadSuccess(res, file) {
|
||||
if (res.code === 200) {
|
||||
this.$message.success("上传成功");
|
||||
this.$emit("input", res.url);
|
||||
this.$emit("input", res.data.file);
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
},
|
||||
// 删除文件
|
||||
handleDelete(index) {
|
||||
|
|
Loading…
Reference in New Issue