From ae27aa2d0cdab69d0bff88ed1b4a171139d2dbdb Mon Sep 17 00:00:00 2001 From: Angelo Date: Tue, 26 Apr 2022 14:29:03 +0800 Subject: [PATCH] feat(frontend captcha): add captcha status control in login page --- .gitignore | 2 + backend/application/settings.py | 316 +++++++++++++------------- backend/application/urls.py | 58 +++-- backend/conf/env.example.py | 49 ++-- backend/dvadmin/system/init_data.py | 6 +- backend/dvadmin/system/views/login.py | 123 +++++----- backend/dvadmin/system/views/user.py | 187 ++++++++++----- web/src/router/index.js | 4 +- web/src/views/system/login/api.js | 16 +- web/src/views/system/login/page.vue | 93 +++----- web/src/views/system/user/crud.js | 33 +-- web/src/views/system/user/index.vue | 137 +++++------ 12 files changed, 553 insertions(+), 471 deletions(-) diff --git a/.gitignore b/.gitignore index 664497b..d6fc516 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /backend/venv /backend/.idea .idea + +.history/ \ No newline at end of file diff --git a/backend/application/settings.py b/backend/application/settings.py index bee5dfc..a57e956 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -27,108 +27,111 @@ from conf.env import * # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm' +SECRET_KEY = "django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm" # 初始化plugins插件路径到环境变量中 -PLUGINS_PATH = os.path.join(BASE_DIR, 'plugins') +PLUGINS_PATH = os.path.join(BASE_DIR, "plugins") sys.path.insert(0, os.path.join(PLUGINS_PATH)) -[sys.path.insert(0, os.path.join(PLUGINS_PATH, ele)) for ele in os.listdir(PLUGINS_PATH) if - os.path.isdir(os.path.join(PLUGINS_PATH, ele)) and not ele.startswith('__')] +[ + sys.path.insert(0, os.path.join(PLUGINS_PATH, ele)) + for ele in os.listdir(PLUGINS_PATH) + if os.path.isdir(os.path.join(PLUGINS_PATH, ele)) and not ele.startswith("__") +] # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = locals().get('DEBUG', True) -ALLOWED_HOSTS = locals().get('ALLOWED_HOSTS', ['*']) +DEBUG = locals().get("DEBUG", True) +ALLOWED_HOSTS = locals().get("ALLOWED_HOSTS", ["*"]) # Application definition INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_comment_migrate', - 'rest_framework', - 'django_filters', - 'corsheaders', # 注册跨域app - 'dvadmin.system', - 'drf_yasg', - 'captcha', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_comment_migrate", + "rest_framework", + "django_filters", + "corsheaders", # 注册跨域app + "dvadmin.system", + "drf_yasg", + "captcha", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', # 跨域中间件 - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'dvadmin.utils.middleware.ApiLoggingMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", # 跨域中间件 + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "dvadmin.utils.middleware.ApiLoggingMiddleware", ] -ROOT_URLCONF = 'application.urls' +ROOT_URLCONF = "application.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'application.wsgi.application' +WSGI_APPLICATION = "application.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': DATABASE_ENGINE, - 'NAME': DATABASE_NAME, - 'USER': DATABASE_USER, - 'PASSWORD': DATABASE_PASSWORD, - 'HOST': DATABASE_HOST, - 'PORT': DATABASE_PORT, + "default": { + "ENGINE": DATABASE_ENGINE, + "NAME": DATABASE_NAME, + "USER": DATABASE_USER, + "PASSWORD": DATABASE_PASSWORD, + "HOST": DATABASE_HOST, + "PORT": DATABASE_PORT, } } -AUTH_USER_MODEL = 'system.Users' -USERNAME_FIELD = 'username' +AUTH_USER_MODEL = "system.Users" +USERNAME_FIELD = "username" # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'zh-hans' +LANGUAGE_CODE = "zh-hans" -TIME_ZONE = 'Asia/Shanghai' +TIME_ZONE = "Asia/Shanghai" USE_I18N = True @@ -139,13 +142,13 @@ USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # # 设置django的静态文件目录 STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] -MEDIA_ROOT = 'media' # 项目下的目录 +MEDIA_ROOT = "media" # 项目下的目录 MEDIA_URL = "/media/" # 跟STATIC_URL类似,指定用户可以通过这个url找到文件 # 收集静态文件,必须将 MEDIA_ROOT,STATICFILES_DIRS先注释 @@ -166,78 +169,80 @@ CORS_ALLOW_CREDENTIALS = True # 指明在跨域访问中,后端是否支持 # ================================================= # # log 配置部分BEGIN # -SERVER_LOGS_FILE = os.path.join(BASE_DIR, 'logs', 'server.log') -ERROR_LOGS_FILE = os.path.join(BASE_DIR, 'logs', 'error.log') -if not os.path.exists(os.path.join(BASE_DIR, 'logs')): - os.makedirs(os.path.join(BASE_DIR, 'logs')) +SERVER_LOGS_FILE = os.path.join(BASE_DIR, "logs", "server.log") +ERROR_LOGS_FILE = os.path.join(BASE_DIR, "logs", "error.log") +if not os.path.exists(os.path.join(BASE_DIR, "logs")): + os.makedirs(os.path.join(BASE_DIR, "logs")) # 格式:[2020-04-22 23:33:01][micoservice.apps.ready():16] [INFO] 这是一条日志: # 格式:[日期][模块.函数名称():行号] [级别] 信息 -STANDARD_LOG_FORMAT = '[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s' -CONSOLE_LOG_FORMAT = '[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s' +STANDARD_LOG_FORMAT = ( + "[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s" +) +CONSOLE_LOG_FORMAT = ( + "[%(asctime)s][%(name)s.%(funcName)s():%(lineno)d] [%(levelname)s] %(message)s" +) LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': STANDARD_LOG_FORMAT + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": {"format": STANDARD_LOG_FORMAT}, + "console": { + "format": CONSOLE_LOG_FORMAT, + "datefmt": "%Y-%m-%d %H:%M:%S", }, - 'console': { - 'format': CONSOLE_LOG_FORMAT, - 'datefmt': '%Y-%m-%d %H:%M:%S', - }, - 'file': { - 'format': CONSOLE_LOG_FORMAT, - 'datefmt': '%Y-%m-%d %H:%M:%S', + "file": { + "format": CONSOLE_LOG_FORMAT, + "datefmt": "%Y-%m-%d %H:%M:%S", }, }, - 'handlers': { - 'file': { - 'level': 'INFO', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': SERVER_LOGS_FILE, - 'maxBytes': 1024 * 1024 * 100, # 100 MB - 'backupCount': 5, # 最多备份5个 - 'formatter': 'standard', - 'encoding': 'utf-8', + "handlers": { + "file": { + "level": "INFO", + "class": "logging.handlers.RotatingFileHandler", + "filename": SERVER_LOGS_FILE, + "maxBytes": 1024 * 1024 * 100, # 100 MB + "backupCount": 5, # 最多备份5个 + "formatter": "standard", + "encoding": "utf-8", }, - 'error': { - 'level': 'ERROR', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': ERROR_LOGS_FILE, - 'maxBytes': 1024 * 1024 * 100, # 100 MB - 'backupCount': 3, # 最多备份3个 - 'formatter': 'standard', - 'encoding': 'utf-8', + "error": { + "level": "ERROR", + "class": "logging.handlers.RotatingFileHandler", + "filename": ERROR_LOGS_FILE, + "maxBytes": 1024 * 1024 * 100, # 100 MB + "backupCount": 3, # 最多备份3个 + "formatter": "standard", + "encoding": "utf-8", + }, + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "console", }, - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'console', - } }, - 'loggers': { + "loggers": { # default日志 - '': { - 'handlers': ['console', 'error', 'file'], - 'level': 'INFO', + "": { + "handlers": ["console", "error", "file"], + "level": "INFO", }, - 'django': { - 'handlers': ['console', 'error', 'file'], - 'level': 'INFO', + "django": { + "handlers": ["console", "error", "file"], + "level": "INFO", }, - 'scripts': { - 'handlers': ['console', 'error', 'file'], - 'level': 'INFO', + "scripts": { + "handlers": ["console", "error", "file"], + "level": "INFO", }, # 数据库相关日志 - 'django.db.backends': { - 'handlers': [], - 'propagate': True, - 'level': 'INFO', + "django.db.backends": { + "handlers": [], + "propagate": True, + "level": "INFO", }, - } + }, } # ================================================= # @@ -245,34 +250,32 @@ LOGGING = { # ================================================= # REST_FRAMEWORK = { - 'DATETIME_FORMAT': "%Y-%m-%d %H:%M:%S", # 日期时间格式配置 - 'DATE_FORMAT': "%Y-%m-%d", - 'DEFAULT_FILTER_BACKENDS': ( + "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置 + "DATE_FORMAT": "%Y-%m-%d", + "DEFAULT_FILTER_BACKENDS": ( # 'django_filters.rest_framework.DjangoFilterBackend', - 'dvadmin.utils.filters.CustomDjangoFilterBackend', - 'rest_framework.filters.SearchFilter', - 'rest_framework.filters.OrderingFilter', + "dvadmin.utils.filters.CustomDjangoFilterBackend", + "rest_framework.filters.SearchFilter", + "rest_framework.filters.OrderingFilter", ), - 'DEFAULT_PAGINATION_CLASS': 'dvadmin.utils.pagination.CustomPagination', # 自定义分页 - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework.authentication.SessionAuthentication', + "DEFAULT_PAGINATION_CLASS": "dvadmin.utils.pagination.CustomPagination", # 自定义分页 + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication", ), - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', # 只有经过身份认证确定用户身份才能访问 + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", # 只有经过身份认证确定用户身份才能访问 # 'rest_framework.permissions.IsAdminUser', # is_staff=True才能访问 —— 管理员(员工)权限 # 'rest_framework.permissions.AllowAny', # 允许所有 # 'rest_framework.permissions.IsAuthenticatedOrReadOnly', # 有身份 或者 只读访问(self.list,self.retrieve) ], - 'EXCEPTION_HANDLER': 'dvadmin.utils.exception.CustomExceptionHandler', # 自定义的异常处理 + "EXCEPTION_HANDLER": "dvadmin.utils.exception.CustomExceptionHandler", # 自定义的异常处理 } # ================================================= # # ******************** 登录方式配置 ******************** # # ================================================= # -AUTHENTICATION_BACKENDS = [ - 'dvadmin.utils.backends.CustomBackend' -] +AUTHENTICATION_BACKENDS = ["dvadmin.utils.backends.CustomBackend"] # ================================================= # # ****************** simplejwt配置 ***************** # # ================================================= # @@ -280,12 +283,12 @@ from datetime import timedelta SIMPLE_JWT = { # token有效时长 - 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=120), + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=120), # token刷新后的有效时间 - 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), # 设置前缀 - 'AUTH_HEADER_TYPES': ('JWT',), - 'ROTATE_REFRESH_TOKENS': True, + "AUTH_HEADER_TYPES": ("JWT",), + "ROTATE_REFRESH_TOKENS": True, } # ====================================# @@ -293,70 +296,63 @@ SIMPLE_JWT = { # ====================================# SWAGGER_SETTINGS = { # 基础样式 - 'SECURITY_DEFINITIONS': { - "basic": { - 'type': 'basic' - } - }, + "SECURITY_DEFINITIONS": {"basic": {"type": "basic"}}, # 如果需要登录才能够查看接口文档, 登录的链接使用restframework自带的. - - 'LOGIN_URL': 'apiLogin/', + "LOGIN_URL": "apiLogin/", # 'LOGIN_URL': 'rest_framework:login', - 'LOGOUT_URL': 'rest_framework:logout', + "LOGOUT_URL": "rest_framework:logout", # 'DOC_EXPANSION': None, # 'SHOW_REQUEST_HEADERS':True, # 'USE_SESSION_AUTH': True, # 'DOC_EXPANSION': 'list', # 接口文档中方法列表以首字母升序排列 - 'APIS_SORTER': 'alpha', + "APIS_SORTER": "alpha", # 如果支持json提交, 则接口文档中包含json输入框 - 'JSON_EDITOR': True, + "JSON_EDITOR": True, # 方法列表字母排序 - 'OPERATIONS_SORTER': 'alpha', - 'VALIDATOR_URL': None, - 'AUTO_SCHEMA_TYPE': 2, # 分组根据url层级分,0、1 或 2 层 - 'DEFAULT_AUTO_SCHEMA_CLASS': 'dvadmin.utils.swagger.CustomSwaggerAutoSchema', + "OPERATIONS_SORTER": "alpha", + "VALIDATOR_URL": None, + "AUTO_SCHEMA_TYPE": 2, # 分组根据url层级分,0、1 或 2 层 + "DEFAULT_AUTO_SCHEMA_CLASS": "dvadmin.utils.swagger.CustomSwaggerAutoSchema", } # ================================================= # # **************** 验证码配置 ******************* # # ================================================= # -CAPTCHA_STATE = True +CAPTCHA_STATE = locals().get("CAPTCHA_STATE", False) 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_OUTPUT_FORMAT = "%(image)s %(text_field)s %(hidden_field)s " CAPTCHA_FONT_SIZE = 40 # 字体大小 -CAPTCHA_FOREGROUND_COLOR = '#0033FF' # 前景色 -CAPTCHA_BACKGROUND_COLOR = '#F5F7F4' # 背景色 +CAPTCHA_FOREGROUND_COLOR = "#0033FF" # 前景色 +CAPTCHA_BACKGROUND_COLOR = "#F5F7F4" # 背景色 CAPTCHA_NOISE_FUNCTIONS = ( - 'captcha.helpers.noise_arcs', # 线 - 'captcha.helpers.noise_dots', # 点 + "captcha.helpers.noise_arcs", # 线 + "captcha.helpers.noise_dots", # 点 ) # CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' #字母验证码 -CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge' # 加减乘除验证码 +CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.math_challenge" # 加减乘除验证码 # ================================================= # # ******************** 其他配置 ******************** # # ================================================= # -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" API_LOG_ENABLE = True # API_LOG_METHODS = 'ALL' # ['POST', 'DELETE'] -API_LOG_METHODS = ['POST', 'UPDATE', 'DELETE', 'PUT'] # ['POST', 'DELETE'] +API_LOG_METHODS = ["POST", "UPDATE", "DELETE", "PUT"] # ['POST', 'DELETE'] API_MODEL_MAP = { "/token/": "登录模块", "/api/login/": "登录模块", "/api/plugins_market/plugins/": "插件市场", } -# 表前缀 -TABLE_PREFIX = "dvadmin_" + DJANGO_CELERY_BEAT_TZ_AWARE = False -CELERY_TIMEZONE = 'Asia/Shanghai' # celery 时区问题 +CELERY_TIMEZONE = "Asia/Shanghai" # celery 时区问题 # 静态页面压缩 -STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' -# 初始化需要执行的列表,用来初始化后执行 -INITIALIZE_RESET_LIST = [] +STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" + ALL_MODELS_OBJECTS = [] # 所有app models 对象 # dvadmin 插件 REGISTER_PLUGINS = ( diff --git a/backend/application/urls.py b/backend/application/urls.py index c068d5a..ffd55e9 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -23,13 +23,19 @@ from rest_framework_simplejwt.views import ( ) from application import settings -from dvadmin.system.views.login import LoginView, CaptchaView, ApiLogin, LogoutView +from dvadmin.system.views.login import ( + LoginView, + CaptchaStatusView, + CaptchaView, + ApiLogin, + LogoutView, +) from dvadmin.utils.swagger import CustomOpenAPISchemaGenerator schema_view = get_schema_view( openapi.Info( title="Snippets API", - default_version='v1', + default_version="v1", description="Test description", terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="contact@snippets.local"), @@ -38,20 +44,38 @@ schema_view = get_schema_view( public=True, permission_classes=(permissions.AllowAny,), generator_class=CustomOpenAPISchemaGenerator, - ) -urlpatterns = [ - re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), - name='schema-json'), - path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path(r'redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - path('api/system/', include('dvadmin.system.urls')), - path('api/login/', LoginView.as_view(), name='token_obtain_pair'), - path('api/logout/', LogoutView.as_view(), name='token_obtain_pair'), - path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), - re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - path('api/captcha/', CaptchaView.as_view()), - path('apiLogin/', ApiLogin.as_view()), -]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, - document_root=settings.STATIC_URL) +urlpatterns = ( + [ + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + path( + "", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path( + r"redoc/", + schema_view.with_ui("redoc", cache_timeout=0), + name="schema-redoc", + ), + path("api/system/", include("dvadmin.system.urls")), + path("api/login/", LoginView.as_view(), name="token_obtain_pair"), + path("api/logout/", LogoutView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + re_path( + r"^api-auth/", include("rest_framework.urls", namespace="rest_framework") + ), + path("api/captcha/", CaptchaView.as_view()), + path("api/captcha/status/", CaptchaStatusView.as_view()), + path("apiLogin/", ApiLogin.as_view()), + # 业务路由 + # re_path(r'^api/app_route/', include('apps.app_name.urls')), + ] + + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + static(settings.STATIC_URL, document_root=settings.STATIC_URL) +) diff --git a/backend/conf/env.example.py b/backend/conf/env.example.py index 9c8def9..9016d58 100644 --- a/backend/conf/env.example.py +++ b/backend/conf/env.example.py @@ -1,19 +1,8 @@ -import os - -from application.settings import BASE_DIR - # ================================================= # -# ************** 数据库 配置 ************** # +# *************** mysql数据库 配置 *************** # # ================================================= # - -# 数据库 ENGINE ,默认演示使用 sqlite3 数据库,正式环境建议使用 mysql 数据库 -# sqlite3 设置 -DATABASE_ENGINE = "django.db.backends.sqlite3" -DATABASE_NAME = os.path.join(BASE_DIR, 'db.sqlite3') - -# 使用mysql时,改为此配置 -# DATABASE_ENGINE = "django.db.backends.mysql" -# DATABASE_NAME = 'django-vue-admin' # mysql 时使用 +# 数据库地址 +DATABASE_ENGINE = "django.db.backends.mysql" # 数据库地址 改为自己数据库地址 DATABASE_HOST = "127.0.0.1" # # 数据库端口 @@ -22,17 +11,39 @@ DATABASE_PORT = 3306 DATABASE_USER = "root" # # 数据库密码 DATABASE_PASSWORD = "123456" +# 数据库名 +DATABASE_NAME = "database_name" +# 表前缀 +TABLE_PREFIX = "sys_" +APP_PREFIX = "app_" # ================================================= # -# ************** redis配置,无redis 可不进行配置 ************** # +# ******** redis配置,无redis 可不进行配置 ******** # # ================================================= # # REDIS_PASSWORD = '' # REDIS_HOST = '127.0.0.1' # REDIS_URL = f'redis://:{REDIS_PASSWORD or ""}@{REDIS_HOST}:6380' # ================================================= # -# ************** 其他 配置 ************** # +# ****************** 功能 启停 ******************* # # ================================================= # -DEBUG = True # 线上环境请设置为True +DEBUG = False +# 是否启用插件,不需要可以设置为False +ENABLE_PLUGINS = False +# 启动登录详细概略获取(通过调用api获取ip详细地址) +ENABLE_LOGIN_ANALYSIS_LOG = True +# 是否启用登录验证码,不需要可以设置为False +CAPTCHA_STATE = False +# 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消 +LOGIN_NO_CAPTCHA_AUTH = True +# ================================================= # +# ****************** 其他 配置 ******************* # +# ================================================= # + ALLOWED_HOSTS = ["*"] -LOGIN_NO_CAPTCHA_AUTH = True # 登录接口 /api/token/ 是否需要验证码认证,用于测试,正式环境建议取消 -ENABLE_LOGIN_ANALYSIS_LOG = True # 启动登录详细概略获取(通过调用api获取ip详细地址) + +# 默认密码 +DEFAULT_PASSWORD = "admin123456" + +# 初始化需要执行的列表,用来初始化后执行 +INITIALIZE_LIST = [] +INITIALIZE_RESET_LIST = [] diff --git a/backend/dvadmin/system/init_data.py b/backend/dvadmin/system/init_data.py index e7f5e5a..c43b62a 100644 --- a/backend/dvadmin/system/init_data.py +++ b/backend/dvadmin/system/init_data.py @@ -149,7 +149,7 @@ button_data = [ "update_datetime": datetime.datetime.now(), "create_datetime": datetime.datetime.now(), "name": "重置密码", - "value": "ResetPwd", + "value": "ResetPassword", "creator_id": 1, }, ] @@ -1251,7 +1251,7 @@ menu_button_data = [ "update_datetime": datetime.datetime.now(), "create_datetime": datetime.datetime.now(), "name": "重置密码", - "value": "ResetPwd", + "value": "ResetPassword", "api": "/api/system/user/reset_password/{id}/", "method": 2, "creator_id": 1, @@ -1362,7 +1362,7 @@ staff_data = [ "create_datetime": datetime.datetime.now(), "username": "admin", "email": "dvadmin@django-vue-admin.com", - "mobile": "13333333333", + "mobile": "18888888888", "avatar": "", "name": "管理员", "gender": 1, diff --git a/backend/dvadmin/system/views/login.py b/backend/dvadmin/system/views/login.py index 90bf163..553e068 100644 --- a/backend/dvadmin/system/views/login.py +++ b/backend/dvadmin/system/views/login.py @@ -1,11 +1,3 @@ -# -*- coding: utf-8 -*- - -""" -@author: 猿小天 -@contact: QQ:1638245306 -@Created on: 2021/6/2 002 14:20 -@Remark:登录视图 -""" import base64 import hashlib from datetime import datetime, timedelta @@ -24,7 +16,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView from application import settings from dvadmin.system.models import Users -from dvadmin.utils.json_response import SuccessResponse, ErrorResponse, DetailResponse +from dvadmin.utils.json_response import ErrorResponse, DetailResponse from dvadmin.utils.request_util import save_login_log from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.validator import CustomValidationError @@ -35,21 +27,33 @@ class CaptchaView(APIView): permission_classes = [] @swagger_auto_schema( - responses={ - '200': openapi.Response('获取成功') - }, + responses={"200": openapi.Response("获取成功")}, security=[], - operation_id='captcha-get', - operation_description='验证码获取', + operation_id="captcha-get", + operation_description="验证码获取", ) def get(self, request): - hashkey = CaptchaStore.generate_key() - id = CaptchaStore.objects.filter(hashkey=hashkey).first().id - imgage = captcha_image(request, hashkey) - # 将图片转换为base64 - image_base = base64.b64encode(imgage.content) - json_data = {"key": id, "image_base": "data:image/png;base64," + image_base.decode('utf-8')} - return SuccessResponse(data=json_data) + data = {} + if settings.CAPTCHA_STATE: + hashkey = CaptchaStore.generate_key() + id = CaptchaStore.objects.filter(hashkey=hashkey).first().id + imgage = captcha_image(request, hashkey) + # 将图片转换为base64 + image_base = base64.b64encode(imgage.content) + data = { + "key": id, + "image_base": "data:image/png;base64," + image_base.decode("utf-8"), + } + return DetailResponse(data=data) + + +class CaptchaStatusView(APIView): + + authentication_classes = [] + permission_classes = [] + + def get(self, request): + return DetailResponse(data={"status": settings.CAPTCHA_STATE}) class LoginSerializer(TokenObtainPairSerializer): @@ -57,53 +61,55 @@ class LoginSerializer(TokenObtainPairSerializer): 登录的序列化器: 重写djangorestframework-simplejwt的序列化器 """ - captcha = serializers.CharField(max_length=6, required=False, allow_null=True) + + captcha = serializers.CharField( + max_length=6, required=False, allow_null=True, allow_blank=True + ) class Meta: model = Users fields = "__all__" read_only_fields = ["id"] - default_error_messages = { - 'no_active_account': _('账号/密码不正确') - } + default_error_messages = {"no_active_account": _("账号/密码错误")} def validate(self, attrs): - captcha = self.initial_data.get('captcha', None) + captcha = self.initial_data.get("captcha", None) if settings.CAPTCHA_STATE: if captcha is None: raise CustomValidationError("验证码不能为空") self.image_code = CaptchaStore.objects.filter( - id=self.initial_data['captchaKey']).first() + id=self.initial_data["captchaKey"] + ).first() five_minute_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0) if self.image_code and five_minute_ago > self.image_code.expiration: self.image_code and self.image_code.delete() - raise CustomValidationError('验证码过期') + raise CustomValidationError("验证码过期") else: - if self.image_code and (self.image_code.response == captcha or self.image_code.challenge == captcha): + if self.image_code and ( + self.image_code.response == captcha + or self.image_code.challenge == captcha + ): self.image_code and self.image_code.delete() else: self.image_code and self.image_code.delete() raise CustomValidationError("图片验证码错误") data = super().validate(attrs) - data['name'] = self.user.name - data['userId'] = self.user.id - data['avatar'] = self.user.avatar - request = self.context.get('request') + data["name"] = self.user.name + data["userId"] = self.user.id + data["avatar"] = self.user.avatar + request = self.context.get("request") request.user = self.user # 记录登录日志 save_login_log(request=request) - return { - "code": 2000, - "msg": "请求成功", - "data": data - } + return {"code": 2000, "msg": "请求成功", "data": data} class LoginView(TokenObtainPairView): """ 登录接口 """ + serializer_class = LoginSerializer permission_classes = [] @@ -118,31 +124,22 @@ class LoginTokenSerializer(TokenObtainPairSerializer): fields = "__all__" read_only_fields = ["id"] - default_error_messages = { - 'no_active_account': _('账号/密码不正确') - } + default_error_messages = {"no_active_account": _("账号/密码不正确")} def validate(self, attrs): - if not getattr(settings, 'LOGIN_NO_CAPTCHA_AUTH', False): - return { - "code": 4000, - "msg": "该接口暂未开通!", - "data": None - } + if not getattr(settings, "LOGIN_NO_CAPTCHA_AUTH", False): + return {"code": 4000, "msg": "该接口暂未开通!", "data": None} data = super().validate(attrs) - data['name'] = self.user.name - data['userId'] = self.user.id - return { - "code": 2000, - "msg": "请求成功", - "data": data - } + data["name"] = self.user.name + data["userId"] = self.user.id + return {"code": 2000, "msg": "请求成功", "data": data} class LoginTokenView(TokenObtainPairView): """ 登录获取token接口 """ + serializer_class = LoginTokenSerializer permission_classes = [] @@ -154,27 +151,31 @@ class LogoutView(APIView): class ApiLoginSerializer(CustomModelSerializer): """接口文档登录-序列化器""" + username = serializers.CharField() password = serializers.CharField() class Meta: model = Users - fields = ['username', 'password'] + fields = ["username", "password"] class ApiLogin(APIView): """接口文档的登录接口""" + serializer_class = ApiLoginSerializer authentication_classes = [] permission_classes = [] def post(self, request): - username = request.data.get('username') - password = request.data.get('password') - user_obj = auth.authenticate(request, username=username, - password=hashlib.md5(password.encode(encoding='UTF-8')).hexdigest()) - if user_obj: + username = request.data.get("username") + password = request.data.get("password") + if user_obj := auth.authenticate( + request, + username=username, + password=hashlib.md5(password.encode(encoding="UTF-8")).hexdigest(), + ): login(request, user_obj) - return redirect('/') + return redirect("/") else: return ErrorResponse(msg="账号/密码错误") diff --git a/backend/dvadmin/system/views/user.py b/backend/dvadmin/system/views/user.py index 36cb938..294db61 100644 --- a/backend/dvadmin/system/views/user.py +++ b/backend/dvadmin/system/views/user.py @@ -1,13 +1,6 @@ -# -*- coding: utf-8 -*- - -""" -@author: 猿小天 -@contact: QQ:1638245306 -@Created on: 2021/6/3 003 0:30 -@Remark: 用户管理 -""" import hashlib +from application import settings from django.contrib.auth.hashers import make_password from rest_framework import serializers from rest_framework.decorators import action @@ -28,9 +21,9 @@ class UserSerializer(CustomModelSerializer): class Meta: model = Users read_only_fields = ["id"] - exclude = ['password'] + exclude = ["password"] extra_kwargs = { - 'post': {'required': False}, + "post": {"required": False}, } @@ -38,14 +31,23 @@ class UserCreateSerializer(CustomModelSerializer): """ 用户新增-序列化器 """ - username = serializers.CharField(max_length=50, - validators=[CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一")]) - password = serializers.CharField(required=False, default=make_password( - hashlib.md5('admin123456'.encode(encoding='UTF-8')).hexdigest())) + + username = serializers.CharField( + max_length=50, + validators=[ + CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一") + ], + ) + password = serializers.CharField( + required=False, + default=make_password( + hashlib.md5(settings.DEFAULT_PASSWORD.encode(encoding="UTF-8")).hexdigest() + ), + ) def save(self, **kwargs): data = super().save(**kwargs) - data.post.set(self.initial_data.get('post', [])) + data.post.set(self.initial_data.get("post", [])) return data class Meta: @@ -53,7 +55,7 @@ class UserCreateSerializer(CustomModelSerializer): fields = "__all__" read_only_fields = ["id"] extra_kwargs = { - 'post': {'required': False}, + "post": {"required": False}, } @@ -61,13 +63,24 @@ class UserUpdateSerializer(CustomModelSerializer): """ 用户修改-序列化器 """ - username = serializers.CharField(max_length=50, - validators=[CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一")]) + + username = serializers.CharField( + max_length=50, + validators=[ + CustomUniqueValidator(queryset=Users.objects.all(), message="账号必须唯一") + ], + ) password = serializers.CharField(required=False, allow_blank=True) + mobile = serializers.CharField( + max_length=50, + validators=[ + CustomUniqueValidator(queryset=Users.objects.all(), message="手机号必须唯一") + ], + ) def save(self, **kwargs): data = super().save(**kwargs) - data.post.set(self.initial_data.get('post', [])) + data.post.set(self.initial_data.get("post", [])) return data class Meta: @@ -75,7 +88,7 @@ class UserUpdateSerializer(CustomModelSerializer): read_only_fields = ["id"] fields = "__all__" extra_kwargs = { - 'post': {'required': False, 'read_only': True}, + "post": {"required": False, "read_only": True}, } @@ -83,22 +96,35 @@ class ExportUserProfileSerializer(CustomModelSerializer): """ 用户导出 序列化器 """ - last_login = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", required=False, read_only=True) - dept__deptName = serializers.CharField(source='dept.deptName', default='') - dept__owner = serializers.CharField(source='dept.owner', default='') - gender = serializers.CharField(source='get_gender_display', read_only=True) + + last_login = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S", required=False, read_only=True + ) + dept__deptName = serializers.CharField(source="dept.deptName", default="") + dept__owner = serializers.CharField(source="dept.owner", default="") + gender = serializers.CharField(source="get_gender_display", read_only=True) class Meta: model = Users - fields = ('username', 'name', 'email', 'mobile', 'gender', 'is_active', 'last_login', 'dept__deptName', - 'dept__owner') + fields = ( + "username", + "name", + "email", + "mobile", + "gender", + "is_active", + "last_login", + "dept__deptName", + "dept__owner", + ) class UserProfileImportSerializer(CustomModelSerializer): - def save(self, **kwargs): data = super().save(**kwargs) - password = hashlib.new('md5', str(self.initial_data.get('password', '')).encode(encoding='UTF-8')).hexdigest() + password = hashlib.new( + "md5", str(self.initial_data.get("password", "")).encode(encoding="UTF-8") + ).hexdigest() data.set_password(password) data.save() return data @@ -106,15 +132,22 @@ class UserProfileImportSerializer(CustomModelSerializer): def run_validation(self, data={}): # 把excel 数据进行格式转换 if type(data) is dict: - data['role'] = str(data['role']).split(',') - data['dept_id'] = str(data['dept']).split(',') - data['gender'] = {'男': '1', '女': '0', '未知': '2'}.get(data['gender']) - data['is_active'] = {'启用': True, '禁用': False}.get(data['is_active']) + data["role"] = str(data["role"]).split(",") + data["dept_id"] = str(data["dept"]).split(",") + data["gender"] = {"男": "1", "女": "0", "未知": "2"}.get(data["gender"]) + data["is_active"] = {"启用": True, "禁用": False}.get(data["is_active"]) return super().run_validation(data) class Meta: model = Users - exclude = ('password', 'post', 'user_permissions', 'groups', 'is_superuser', 'date_joined') + exclude = ( + "password", + "post", + "user_permissions", + "groups", + "is_superuser", + "date_joined", + ) class UserViewSet(CustomModelViewSet): @@ -126,11 +159,12 @@ class UserViewSet(CustomModelViewSet): retrieve:单例 destroy:删除 """ + queryset = Users.objects.exclude(is_superuser=1).all() serializer_class = UserSerializer create_serializer_class = UserCreateSerializer update_serializer_class = UserUpdateSerializer - filter_fields = ['name', 'username', 'gender', 'is_active', 'dept', 'user_type'] + filter_fields = ["name", "username", "gender", "is_active", "dept", "user_type"] # filter_fields = { # 'name': ['icontains'], # 'username': ['icontains'], @@ -138,17 +172,35 @@ class UserViewSet(CustomModelViewSet): # 'is_active': ['icontains'], # 'dept': ['exact'], # } - search_fields = ['username', 'name', 'gender', 'dept__name', 'role__name'] + search_fields = ["username", "name", "gender", "dept__name", "role__name"] # 导出 - export_field_label = ['用户账号', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态', '最后登录时间', '部门名称', '部门负责人'] + export_field_label = [ + "用户账号", + "用户名称", + "用户邮箱", + "手机号码", + "用户性别", + "帐号状态", + "最后登录时间", + "部门名称", + "部门负责人", + ] export_serializer_class = ExportUserProfileSerializer # 导入 import_serializer_class = UserProfileImportSerializer - import_field_dict = {'username': '登录账号', 'name': '用户名称', 'email': '用户邮箱', 'mobile': '手机号码', - 'gender': '用户性别(男/女/未知)', - 'is_active': '帐号状态(启用/禁用)', 'password': '登录密码', 'dept': '部门ID', 'role': '角色ID'} + import_field_dict = { + "username": "登录账号", + "name": "用户名称", + "email": "用户邮箱", + "mobile": "手机号码", + "gender": "用户性别(男/女/未知)", + "is_active": "帐号状态(启用/禁用)", + "password": "登录密码", + "dept": "部门ID", + "role": "角色ID", + } - @action(methods=['GET'], detail=True, permission_classes=[IsAuthenticated]) + @action(methods=["GET"], detail=True, permission_classes=[IsAuthenticated]) def user_info(self, request): """获取当前用户信息""" user = request.user @@ -157,25 +209,25 @@ class UserViewSet(CustomModelViewSet): "mobile": user.mobile, "gender": user.gender, "email": user.email, - 'avatar':user.avatar + "avatar": user.avatar, } return DetailResponse(data=result, msg="获取成功") - @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) + @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def update_user_info(self, request): """修改当前用户信息""" user = request.user Users.objects.filter(id=user.id).update(**request.data) return DetailResponse(data=None, msg="修改成功") - @action(methods=['PUT'], detail=True, permission_classes=[IsAuthenticated]) + @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) def change_password(self, request, *args, **kwargs): """密码修改""" - instance = Users.objects.filter(id=kwargs.get('pk')).first() + instance = Users.objects.filter(id=kwargs.get("pk")).first() data = request.data - old_pwd = data.get('oldPassword') - new_pwd = data.get('newPassword') - new_pwd2 = data.get('newPassword2') + old_pwd = data.get("oldPassword") + new_pwd = data.get("newPassword") + new_pwd2 = data.get("newPassword2") if instance: if new_pwd != new_pwd2: return ErrorResponse(msg="两次密码不匹配") @@ -188,21 +240,32 @@ class UserViewSet(CustomModelViewSet): else: return ErrorResponse(msg="未获取到用户") - @action(methods=['PUT'], detail=True) - def reset_password(self, request, pk): - """ - 密码重置 - """ - instance = Users.objects.filter(id=pk).first() - data = request.data - new_pwd = data.get('newPassword') - new_pwd2 = data.get('newPassword2') + @action(methods=["PUT"], detail=True, permission_classes=[IsAuthenticated]) + def reset_password(self, request, *args, **kwargs): + """重置密码""" + instance = Users.objects.filter(id=kwargs.get("pk")).first() if instance: - if new_pwd != new_pwd2: - return ErrorResponse(msg="两次密码不匹配") - else: - instance.password = make_password(new_pwd) - instance.save() - return DetailResponse(data=None, msg="修改成功") + instance.set_password(settings.DEFAULT_PASSWORD) + instance.save() + return DetailResponse(data=None, msg="密码重置成功") else: return ErrorResponse(msg="未获取到用户") + + # @action(methods=['PUT'], detail=True) + # def reset_password(self, request, pk): + # """ + # 密码重置 + # """ + # instance = Users.objects.filter(id=pk).first() + # data = request.data + # new_pwd = data.get('newPassword') + # new_pwd2 = data.get('newPassword2') + # if instance: + # if new_pwd != new_pwd2: + # return ErrorResponse(msg="两次密码不匹配") + # else: + # instance.password = make_password(new_pwd) + # instance.save() + # return DetailResponse(data=None, msg="修改成功") + # else: + # return ErrorResponse(msg="未获取到用户") diff --git a/web/src/router/index.js b/web/src/router/index.js index ba1761a..3a0afc3 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -64,7 +64,9 @@ router.beforeEach(async (to, from, next) => { // 处理路由 得到每一级的路由设置 store.commit('d2admin/page/init', routes) - router.addRoutes(routes) + // router.addRoutes(routes) + routes.forEach(route => router.addRoute(route)) + const menu = handleAsideMenu(ret) const aside = handleAsideMenu(ret.filter(value => value.visible === true)) store.commit('d2admin/menu/asideSet', aside) // 设置侧边栏菜单 diff --git a/web/src/views/system/login/api.js b/web/src/views/system/login/api.js index 9f62d5b..f0dbc67 100644 --- a/web/src/views/system/login/api.js +++ b/web/src/views/system/login/api.js @@ -1,12 +1,3 @@ -/* - * @创建文件时间: 2021-06-02 10:33:33 - * @Auther: 猿小天 - * @最后修改人: 猿小天 - * @最后修改时间: 2021-08-12 22:53:38 - * 联系Qq:1638245306 - * @文件介绍: 登录的接口 - */ - import { request } from '@/api/service' export function SYS_USER_LOGIN (data) { @@ -31,3 +22,10 @@ export function getCaptcha () { method: 'get' }) } + +export function getCaptchaStatus () { + return request({ + url: 'api/captcha/status/', + method: 'get' + }) +} diff --git a/web/src/views/system/login/page.vue b/web/src/views/system/login/page.vue index fc095eb..5b263bd 100644 --- a/web/src/views/system/login/page.vue +++ b/web/src/views/system/login/page.vue @@ -5,26 +5,13 @@
  • - +