From e58a5507a9e01e76e29f919cbbee59dd4c5a20e4 Mon Sep 17 00:00:00 2001 From: shaohuzhang1 Date: Mon, 14 Apr 2025 20:11:23 +0800 Subject: [PATCH] init project --- apps/common/auth/__init__.py | 9 + apps/common/auth/authenticate.py | 96 +++++++++ apps/common/auth/handle/auth_base_handle.py | 19 ++ apps/common/auth/handle/impl/user_token.py | 36 ++++ apps/common/constants/authentication_type.py | 16 ++ apps/common/constants/cache_version.py | 14 ++ apps/common/constants/permission_constants.py | 117 +++++++++++ apps/common/exception/app_exception.py | 83 ++++++++ apps/common/exception/handle_exception.py | 101 +++++++++ apps/common/mixins/api_mixin.py | 38 ++++ apps/common/result/__init__.py | 10 + apps/common/result/api.py | 56 +++++ apps/common/result/result.py | 51 +++++ apps/common/utils/common.py | 21 ++ apps/locales/en_US/LC_MESSAGES/django.po | 104 ++++++++++ apps/locales/zh_CN/LC_MESSAGES/django.po | 104 ++++++++++ apps/locales/zh_Hant/LC_MESSAGES/django.po | 104 ++++++++++ apps/manage.py | 22 ++ apps/maxkb/__init__.py | 0 apps/maxkb/asgi.py | 16 ++ apps/maxkb/conf.py | 196 ++++++++++++++++++ apps/maxkb/const.py | 12 ++ apps/maxkb/settings/__init__.py | 10 + apps/maxkb/settings/auth.py | 13 ++ apps/maxkb/settings/base.py | 151 ++++++++++++++ apps/maxkb/urls.py | 41 ++++ apps/maxkb/wsgi.py | 16 ++ apps/users/__init__.py | 0 apps/users/admin.py | 3 + apps/users/api/__init__.py | 9 + apps/users/api/login.py | 42 ++++ apps/users/api/user.py | 23 ++ apps/users/apps.py | 6 + apps/users/migrations/0001_initial.py | 48 +++++ apps/users/migrations/__init__.py | 0 apps/users/models/__init__.py | 9 + apps/users/models/user.py | 38 ++++ apps/users/serializers/login.py | 51 +++++ apps/users/serializers/user.py | 37 ++++ apps/users/tests.py | 3 + apps/users/urls.py | 9 + apps/users/views/__init__.py | 10 + apps/users/views/login.py | 27 +++ apps/users/views/user.py | 29 +++ main.py | 123 +++++++++++ pyproject.toml | 24 +++ ui/.editorconfig | 9 + ui/.gitattributes | 1 + ui/.gitignore | 30 +++ ui/.prettierrc.json | 6 + ui/README.md | 39 ++++ ui/env.d.ts | 1 + ui/eslint.config.ts | 22 ++ ui/index.html | 13 ++ ui/package.json | 38 ++++ ui/public/favicon.ico | Bin 0 -> 4286 bytes ui/src/App.vue | 85 ++++++++ ui/src/assets/base.css | 86 ++++++++ ui/src/assets/logo.svg | 1 + ui/src/assets/main.css | 35 ++++ ui/src/components/HelloWorld.vue | 41 ++++ ui/src/components/TheWelcome.vue | 94 +++++++++ ui/src/components/WelcomeItem.vue | 87 ++++++++ ui/src/components/icons/IconCommunity.vue | 7 + ui/src/components/icons/IconDocumentation.vue | 7 + ui/src/components/icons/IconEcosystem.vue | 7 + ui/src/components/icons/IconSupport.vue | 7 + ui/src/components/icons/IconTooling.vue | 19 ++ ui/src/main.ts | 14 ++ ui/src/router/index.ts | 23 ++ ui/src/stores/counter.ts | 12 ++ ui/src/views/AboutView.vue | 15 ++ ui/src/views/HomeView.vue | 9 + ui/tsconfig.app.json | 12 ++ ui/tsconfig.json | 11 + ui/tsconfig.node.json | 19 ++ ui/vite.config.ts | 20 ++ 77 files changed, 2717 insertions(+) create mode 100644 apps/common/auth/__init__.py create mode 100644 apps/common/auth/authenticate.py create mode 100644 apps/common/auth/handle/auth_base_handle.py create mode 100644 apps/common/auth/handle/impl/user_token.py create mode 100644 apps/common/constants/authentication_type.py create mode 100644 apps/common/constants/cache_version.py create mode 100644 apps/common/constants/permission_constants.py create mode 100644 apps/common/exception/app_exception.py create mode 100644 apps/common/exception/handle_exception.py create mode 100644 apps/common/mixins/api_mixin.py create mode 100644 apps/common/result/__init__.py create mode 100644 apps/common/result/api.py create mode 100644 apps/common/result/result.py create mode 100644 apps/common/utils/common.py create mode 100644 apps/locales/en_US/LC_MESSAGES/django.po create mode 100644 apps/locales/zh_CN/LC_MESSAGES/django.po create mode 100644 apps/locales/zh_Hant/LC_MESSAGES/django.po create mode 100644 apps/manage.py create mode 100644 apps/maxkb/__init__.py create mode 100644 apps/maxkb/asgi.py create mode 100644 apps/maxkb/conf.py create mode 100644 apps/maxkb/const.py create mode 100644 apps/maxkb/settings/__init__.py create mode 100644 apps/maxkb/settings/auth.py create mode 100644 apps/maxkb/settings/base.py create mode 100644 apps/maxkb/urls.py create mode 100644 apps/maxkb/wsgi.py create mode 100644 apps/users/__init__.py create mode 100644 apps/users/admin.py create mode 100644 apps/users/api/__init__.py create mode 100644 apps/users/api/login.py create mode 100644 apps/users/api/user.py create mode 100644 apps/users/apps.py create mode 100644 apps/users/migrations/0001_initial.py create mode 100644 apps/users/migrations/__init__.py create mode 100644 apps/users/models/__init__.py create mode 100644 apps/users/models/user.py create mode 100644 apps/users/serializers/login.py create mode 100644 apps/users/serializers/user.py create mode 100644 apps/users/tests.py create mode 100644 apps/users/urls.py create mode 100644 apps/users/views/__init__.py create mode 100644 apps/users/views/login.py create mode 100644 apps/users/views/user.py create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 ui/.editorconfig create mode 100644 ui/.gitattributes create mode 100644 ui/.gitignore create mode 100644 ui/.prettierrc.json create mode 100644 ui/README.md create mode 100644 ui/env.d.ts create mode 100644 ui/eslint.config.ts create mode 100644 ui/index.html create mode 100644 ui/package.json create mode 100644 ui/public/favicon.ico create mode 100644 ui/src/App.vue create mode 100644 ui/src/assets/base.css create mode 100644 ui/src/assets/logo.svg create mode 100644 ui/src/assets/main.css create mode 100644 ui/src/components/HelloWorld.vue create mode 100644 ui/src/components/TheWelcome.vue create mode 100644 ui/src/components/WelcomeItem.vue create mode 100644 ui/src/components/icons/IconCommunity.vue create mode 100644 ui/src/components/icons/IconDocumentation.vue create mode 100644 ui/src/components/icons/IconEcosystem.vue create mode 100644 ui/src/components/icons/IconSupport.vue create mode 100644 ui/src/components/icons/IconTooling.vue create mode 100644 ui/src/main.ts create mode 100644 ui/src/router/index.ts create mode 100644 ui/src/stores/counter.ts create mode 100644 ui/src/views/AboutView.vue create mode 100644 ui/src/views/HomeView.vue create mode 100644 ui/tsconfig.app.json create mode 100644 ui/tsconfig.json create mode 100644 ui/tsconfig.node.json create mode 100644 ui/vite.config.ts diff --git a/apps/common/auth/__init__.py b/apps/common/auth/__init__.py new file mode 100644 index 000000000..aea9eccd1 --- /dev/null +++ b/apps/common/auth/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/14 10:44 + @desc: +""" +from .authenticate import * diff --git a/apps/common/auth/authenticate.py b/apps/common/auth/authenticate.py new file mode 100644 index 000000000..ac200ab35 --- /dev/null +++ b/apps/common/auth/authenticate.py @@ -0,0 +1,96 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎虎 + @file: authenticate.py + @date:2023/9/4 11:16 + @desc: 认证类 +""" +import traceback +from importlib import import_module + +from django.conf import settings +from django.core import cache +from django.core import signing +from rest_framework.authentication import TokenAuthentication + +from common.exception.app_exception import AppAuthenticationFailed, AppEmbedIdentityFailed, AppChatNumOutOfBoundsFailed, \ + ChatException, AppApiException +from django.utils.translation import gettext_lazy as _ + +token_cache = cache.caches['default'] + + +class AnonymousAuthentication(TokenAuthentication): + def authenticate(self, request): + return None, None + + +def new_instance_by_class_path(class_path: str): + parts = class_path.rpartition('.') + package_path = parts[0] + class_name = parts[2] + module = import_module(package_path) + HandlerClass = getattr(module, class_name) + return HandlerClass() + + +handles = [new_instance_by_class_path(class_path) for class_path in settings.AUTH_HANDLES] + + +class TokenDetails: + token_details = None + is_load = False + + def __init__(self, token: str): + self.token = token + + def get_token_details(self): + if self.token_details is None and not self.is_load: + try: + self.token_details = signing.loads(self.token) + except Exception as e: + self.is_load = True + return self.token_details + + +class OpenAIKeyAuth(TokenAuthentication): + def authenticate(self, request): + auth = request.META.get('HTTP_AUTHORIZATION') + auth = auth.replace('Bearer ', '') + # 未认证 + if auth is None: + raise AppAuthenticationFailed(1003, _('Not logged in, please log in first')) + try: + token_details = TokenDetails(auth) + for handle in handles: + if handle.support(request, auth, token_details.get_token_details): + return handle.handle(request, auth, token_details.get_token_details) + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user')) + except Exception as e: + traceback.format_exc() + if isinstance(e, AppEmbedIdentityFailed) or isinstance(e, AppChatNumOutOfBoundsFailed) or isinstance(e, + AppApiException): + raise e + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user')) + + +class TokenAuth(TokenAuthentication): + # 重新 authenticate 方法,自定义认证规则 + def authenticate(self, request): + auth = request.META.get('HTTP_AUTHORIZATION') + # 未认证 + if auth is None: + raise AppAuthenticationFailed(1003, _('Not logged in, please log in first')) + try: + token_details = TokenDetails(auth) + for handle in handles: + if handle.support(request, auth, token_details.get_token_details): + return handle.handle(request, auth, token_details.get_token_details) + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user')) + except Exception as e: + traceback.format_exc() + if isinstance(e, AppEmbedIdentityFailed) or isinstance(e, AppChatNumOutOfBoundsFailed) or isinstance(e, + AppApiException): + raise e + raise AppAuthenticationFailed(1002, _('Authentication information is incorrect! illegal user')) diff --git a/apps/common/auth/handle/auth_base_handle.py b/apps/common/auth/handle/auth_base_handle.py new file mode 100644 index 000000000..a448efd47 --- /dev/null +++ b/apps/common/auth/handle/auth_base_handle.py @@ -0,0 +1,19 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎虎 + @file: authenticate.py + @date:2024/3/14 03:02 + @desc: 认证处理器 +""" +from abc import ABC, abstractmethod + + +class AuthBaseHandle(ABC): + @abstractmethod + def support(self, request, token: str, get_token_details): + pass + + @abstractmethod + def handle(self, request, token: str, get_token_details): + pass diff --git a/apps/common/auth/handle/impl/user_token.py b/apps/common/auth/handle/impl/user_token.py new file mode 100644 index 000000000..36c4be25a --- /dev/null +++ b/apps/common/auth/handle/impl/user_token.py @@ -0,0 +1,36 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎虎 + @file: authenticate.py + @date:2024/3/14 03:02 + @desc: 用户认证 +""" +from django.db.models import QuerySet +from common.auth.handle.auth_base_handle import AuthBaseHandle +from common.constants.authentication_type import AuthenticationType +from common.constants.cache_version import Cache_Version +from common.constants.permission_constants import Auth, RoleConstants +from common.exception.app_exception import AppAuthenticationFailed +from users.models import User +from django.core.cache import cache +from django.utils.translation import gettext_lazy as _ + + +class UserToken(AuthBaseHandle): + def support(self, request, token: str, get_token_details): + auth_details = get_token_details() + if auth_details is None: + return False + return True + + def handle(self, request, token: str, get_token_details): + cache_token = cache.get(token, version=Cache_Version.TOKEN) + if cache_token is None: + raise AppAuthenticationFailed(1002, _('Login expired')) + auth_details = get_token_details() + user = QuerySet(User).get(id=auth_details['id']) + role = RoleConstants[user.role] + return user, Auth([], [], + client_id=str(user.id), + client_type=AuthenticationType.SYSTEM_USER.value, current_role=role) diff --git a/apps/common/constants/authentication_type.py b/apps/common/constants/authentication_type.py new file mode 100644 index 000000000..f9c04ce12 --- /dev/null +++ b/apps/common/constants/authentication_type.py @@ -0,0 +1,16 @@ +# coding=utf-8 +""" + @project: maxkb + @Author:虎虎 + @file: authentication_type.py + @date:2023/11/14 20:03 + @desc: +""" +from enum import Enum + + +class AuthenticationType(Enum): + # 系统用户 + SYSTEM_USER = "SYSTEM_USER" + # 对话用户 + CHAT_USER = "CHAT_USER" diff --git a/apps/common/constants/cache_version.py b/apps/common/constants/cache_version.py new file mode 100644 index 000000000..83ab6af2c --- /dev/null +++ b/apps/common/constants/cache_version.py @@ -0,0 +1,14 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: cache_version.py + @date:2025/4/14 19:09 + @desc: +""" +from enum import Enum + + +class Cache_Version(Enum): + # 系统用户 + TOKEN = "TOKEN" diff --git a/apps/common/constants/permission_constants.py b/apps/common/constants/permission_constants.py new file mode 100644 index 000000000..895f17434 --- /dev/null +++ b/apps/common/constants/permission_constants.py @@ -0,0 +1,117 @@ +""" + @project: qabot + @Author:虎虎 + @file: permission_constants.py + @date:2023/9/13 18:23 + @desc: 权限,角色 常量 +""" +from enum import Enum +from typing import List + +from django.utils.translation import gettext_lazy as _ + + +class Group(Enum): + """ + 权限组 一个组一般对应前端一个菜单 + """ + USER = "USER" + + +class Operate(Enum): + """ + 一个权限组的操作权限 + """ + READ = 'READ' + EDIT = "EDIT" + CREATE = "CREATE" + DELETE = "DELETE" + + +class RoleGroup(Enum): + # 系统用户 + SYSTEM_USER = "SYSTEM_USER" + # 对话用户 + CHAT_USER = "CHAT_USER" + + +class Role: + def __init__(self, name: str, decs: str, group: RoleGroup): + self.name = name + self.decs = decs + self.group = group + + +class RoleConstants(Enum): + ADMIN = Role(_("ADMIN"), _('Super administrator'), RoleGroup.SYSTEM_USER) + + +class Permission: + """ + 权限信息 + """ + + def __init__(self, group: Group, operate: Operate, roles=None, dynamic_tag=None): + if roles is None: + roles = [] + self.group = group + self.operate = operate + self.roleList = roles + self.dynamic_tag = dynamic_tag + + def __str__(self): + return self.group.value + ":" + self.operate.value + ( + (":" + self.dynamic_tag) if self.dynamic_tag is not None else '') + + def __eq__(self, other): + return str(self) == str(other) + + +class PermissionConstants(Enum): + """ + 权限枚举 + """ + USER_READ = Permission(group=Group.USER, operate=Operate.READ, roles=[RoleConstants.ADMIN]) + USER_EDIT = Permission(group=Group.USER, operate=Operate.EDIT, roles=[RoleConstants.ADMIN]) + USER_DELETE = Permission(group=Group.USER, operate=Operate.DELETE, roles=[RoleConstants.ADMIN]) + + +def get_permission_list_by_role(role: RoleConstants): + """ + 根据角色 获取角色对应的权限 + :param role: 角色 + :return: 权限 + """ + return list(map(lambda k: PermissionConstants[k], + list(filter(lambda k: PermissionConstants[k].value.roleList.__contains__(role), + PermissionConstants.__members__)))) + + +class Auth: + """ + 用于存储当前用户的角色和权限 + """ + + def __init__(self, role_list: List[RoleConstants], permission_list: List[PermissionConstants | Permission] + , client_id, client_type, current_role: RoleConstants, **keywords): + self.role_list = role_list + self.permission_list = permission_list + self.client_id = client_id + self.client_type = client_type + self.keywords = keywords + self.current_role = current_role + + +class CompareConstants(Enum): + # 或者 + OR = "OR" + # 并且 + AND = "AND" + + +class ViewPermission: + def __init__(self, roleList: List[RoleConstants], permissionList: List[PermissionConstants | object], + compare=CompareConstants.OR): + self.roleList = roleList + self.permissionList = permissionList + self.compare = compare diff --git a/apps/common/exception/app_exception.py b/apps/common/exception/app_exception.py new file mode 100644 index 000000000..1de0a4b70 --- /dev/null +++ b/apps/common/exception/app_exception.py @@ -0,0 +1,83 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎虎 + @file: app_exception.py + @date:2023/9/4 14:04 + @desc: +""" +from rest_framework import status + + +class AppApiException(Exception): + """ + 项目内异常 + """ + status_code = status.HTTP_200_OK + + def __init__(self, code, message): + self.code = code + self.message = message + + +class NotFound404(AppApiException): + """ + 未认证(未登录)异常 + """ + status_code = status.HTTP_404_NOT_FOUND + + def __init__(self, code, message): + self.code = code + self.message = message + + +class AppAuthenticationFailed(AppApiException): + """ + 未认证(未登录)异常 + """ + status_code = status.HTTP_401_UNAUTHORIZED + + def __init__(self, code, message): + self.code = code + self.message = message + + +class AppUnauthorizedFailed(AppApiException): + """ + 未授权(没有权限)异常 + """ + status_code = status.HTTP_403_FORBIDDEN + + def __init__(self, code, message): + self.code = code + self.message = message + + +class AppEmbedIdentityFailed(AppApiException): + """ + 嵌入cookie异常 + """ + status_code = 460 + + def __init__(self, code, message): + self.code = code + self.message = message + + +class AppChatNumOutOfBoundsFailed(AppApiException): + """ + 访问次数超过今日访问量 + """ + status_code = 461 + + def __init__(self, code, message): + self.code = code + self.message = message + + +class ChatException(AppApiException): + status_code = 500 + + def __init__(self, code, message): + self.code = code + self.message = message diff --git a/apps/common/exception/handle_exception.py b/apps/common/exception/handle_exception.py new file mode 100644 index 000000000..195ff01e7 --- /dev/null +++ b/apps/common/exception/handle_exception.py @@ -0,0 +1,101 @@ +# coding=utf-8 +""" + @project: qabot + @Author:虎虎 + @file: handle_exception.py + @date:2023/9/5 19:29 + @desc: +""" +import logging +import traceback + +from rest_framework.exceptions import ValidationError, ErrorDetail, APIException +from rest_framework.views import exception_handler + +from common import result +from common.exception.app_exception import AppApiException + +from django.utils.translation import gettext_lazy as _ + + +def to_result(key, args, parent_key=None): + """ + 将校验异常 args转换为统一数据 + :param key: 校验key + :param args: 校验异常参数 + :param parent_key 父key + :return: 接口响应对象 + """ + error_detail = list(filter( + lambda d: True if isinstance(d, ErrorDetail) else True if isinstance(d, dict) and len( + d.keys()) > 0 else False, + (args[0] if len(args) > 0 else {key: [ErrorDetail(_('Unknown exception'), code='unknown')]}).get(key)))[0] + + if isinstance(error_detail, dict): + return list(map(lambda k: to_result(k, args=[error_detail], + parent_key=key if parent_key is None else parent_key + '.' + key), + error_detail.keys() if len(error_detail) > 0 else []))[0] + + return result.Result(500 if isinstance(error_detail.code, str) else error_detail.code, + message=f"【{key if parent_key is None else parent_key + '.' + key}】为必填参数" if str( + error_detail) == "This field is required." else error_detail) + + +def validation_error_to_result(exc: ValidationError): + """ + 校验异常转响应对象 + :param exc: 校验异常 + :return: 接口响应对象 + """ + try: + v = find_err_detail(exc.detail) + if v is None: + return result.error(str(exc.detail)) + return result.error(str(v)) + except Exception as e: + return result.error(str(exc.detail)) + + +def find_err_detail(exc_detail): + if isinstance(exc_detail, ErrorDetail): + return exc_detail + if isinstance(exc_detail, dict): + keys = exc_detail.keys() + for key in keys: + _label = get_label(key, exc_detail) + _value = exc_detail[key] + if isinstance(_value, list): + return f"{_label}:{find_err_detail(_value)}" + if isinstance(_value, ErrorDetail): + return f"{_label}:{find_err_detail(_value)}" + if isinstance(_value, dict) and len(_value.keys()) > 0: + return find_err_detail(_value) + if isinstance(exc_detail, list): + for v in exc_detail: + r = find_err_detail(v) + if r is not None: + return r + + +def get_label(key, exc_detail): + try: + return exc_detail.serializer.fields[key].label + except Exception as e: + return key + + +def handle_exception(exc, context): + exception_class = exc.__class__ + # 先调用REST framework默认的异常处理方法获得标准错误响应对象 + response = exception_handler(exc, context) + # 在此处补充自定义的异常处理 + if issubclass(exception_class, ValidationError): + return validation_error_to_result(exc) + if issubclass(exception_class, AppApiException): + return result.Result(exc.code, exc.message, response_status=exc.status_code) + if issubclass(exception_class, APIException): + return result.error(exc.detail) + if response is None: + logging.getLogger("max_kb_error").error(f'{str(exc)}:{traceback.format_exc()}') + return result.error(str(exc)) + return response diff --git a/apps/common/mixins/api_mixin.py b/apps/common/mixins/api_mixin.py new file mode 100644 index 000000000..ece449b3b --- /dev/null +++ b/apps/common/mixins/api_mixin.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: ApiMixin.py + @date:2025/4/14 18:03 + @desc: +""" + + +class APIMixin: + @staticmethod + def get_request(): + return None + + @staticmethod + def get_response(): + return None + + @staticmethod + def get_parameters(): + """ + return OpenApiParameter( + # 参数的名称是done + name="done", + # 对参数的备注 + description="是否完成", + # 指定参数的类型 + type=OpenApiTypes.BOOL, + location=OpenApiParameter.QUERY, + # 指定必须给 + required=True, + # 指定枚举项 + enum=[True, False], + ) + + """ + return None diff --git a/apps/common/result/__init__.py b/apps/common/result/__init__.py new file mode 100644 index 000000000..2aa4574cf --- /dev/null +++ b/apps/common/result/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py + @date:2025/4/14 15:45 + @desc: +""" +from .api import * +from .result import * diff --git a/apps/common/result/api.py b/apps/common/result/api.py new file mode 100644 index 000000000..24685a77d --- /dev/null +++ b/apps/common/result/api.py @@ -0,0 +1,56 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: api.py + @date:2025/4/14 15:20 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + + +class DefaultResultSerializer(serializers.Serializer): + """ + 响应结果 + """ + code = serializers.IntegerField(required=True, help_text=_('response code'), label=_('response code')) + message = serializers.CharField(required=False, default="success", help_text=_('error prompt'), + label=_('error prompt')) + data = serializers.BooleanField(required=False, default=True) + + +class ResultSerializer(serializers.Serializer): + """ + 响应结果 + """ + code = serializers.IntegerField(required=True, help_text=_('response code'), label=_('response code')) + message = serializers.CharField(required=False, default="success", help_text=_('error prompt'), + label=_('error prompt')) + + def get_data(self): + pass + + def __init__(self, **kwargs): + self.fields['data'] = self.get_data() + super().__init__(**kwargs) + + +class PageDataResponse(serializers.Serializer): + """ + 分页数据 + """ + total = serializers.IntegerField(required=True, label=_('total number of data')) + current = serializers.IntegerField(required=True, label=_('current page')) + size = serializers.IntegerField(required=True, label=_('page size')) + + def __init__(self, records, **kwargs): + self.fields['records'] = records + super().__init__(**kwargs) + + +class ResultPageSerializer(ResultSerializer): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields['data'] = PageDataResponse(self.get_data()) diff --git a/apps/common/result/result.py b/apps/common/result/result.py new file mode 100644 index 000000000..f8eb0cae9 --- /dev/null +++ b/apps/common/result/result.py @@ -0,0 +1,51 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: result.py + @date:2025/4/14 15:18 + @desc: +""" +from typing import List + +from django.http import JsonResponse +from django.utils.translation import gettext_lazy as _ +from rest_framework import status + + +class Page(dict): + """ + 分页对象 + """ + + def __init__(self, total: int, records: List, current_page: int, page_size: int, **kwargs): + super().__init__(**{'total': total, 'records': records, 'current': current_page, 'size': page_size}) + + +class Result(JsonResponse): + charset = 'utf-8' + """ + 接口统一返回对象 + """ + + def __init__(self, code=200, message=_('Success'), data=None, response_status=status.HTTP_200_OK, **kwargs): + back_info_dict = {"code": code, "message": message, 'data': data} + super().__init__(data=back_info_dict, status=response_status, **kwargs) + + +def success(data, **kwargs): + """ + 获取一个成功的响应对象 + :param data: 接口响应数据 + :return: 请求响应对象 + """ + return Result(data=data, **kwargs) + + +def error(message, **kwargs): + """ + 获取一个失败的响应对象 + :param message: 错误提示 + :return: 接口响应对象 + """ + return Result(code=500, message=message, **kwargs) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py new file mode 100644 index 000000000..e8c510353 --- /dev/null +++ b/apps/common/utils/common.py @@ -0,0 +1,21 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: common.py + @date:2025/4/14 18:23 + @desc: +""" +import hashlib + + +def password_encrypt(row_password): + """ + 密码 md5加密 + :param row_password: 密码 + :return: 加密后密码 + """ + md5 = hashlib.md5() # 2,实例化md5() 方法 + md5.update(row_password.encode()) # 3,对字符串的字节类型加密 + result = md5.hexdigest() # 4,加密 + return result diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po new file mode 100644 index 000000000..d28549afd --- /dev/null +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -0,0 +1,104 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-04-14 20:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: .\apps\common\auth\authenticate.py:63 .\apps\common\auth\authenticate.py:84 +msgid "Not logged in, please log in first" +msgstr "" + +#: .\apps\common\auth\authenticate.py:69 .\apps\common\auth\authenticate.py:75 +#: .\apps\common\auth\authenticate.py:90 .\apps\common\auth\authenticate.py:96 +msgid "Authentication information is incorrect! illegal user" +msgstr "" + +#: .\apps\common\auth\handle\impl\user_token.py:30 +msgid "Login expired" +msgstr "" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "ADMIN" +msgstr "" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "Super administrator" +msgstr "" + +#: .\apps\common\exception\handle_exception.py:32 +msgid "Unknown exception" +msgstr "" + +#: .\apps\common\result\api.py:17 .\apps\common\result\api.py:27 +msgid "response code" +msgstr "" + +#: .\apps\common\result\api.py:18 .\apps\common\result\api.py:19 +#: .\apps\common\result\api.py:28 .\apps\common\result\api.py:29 +msgid "error prompt" +msgstr "" + +#: .\apps\common\result\api.py:43 +msgid "total number of data" +msgstr "" + +#: .\apps\common\result\api.py:44 +msgid "current page" +msgstr "" + +#: .\apps\common\result\api.py:45 +msgid "page size" +msgstr "" + +#: .\apps\common\result\result.py:31 +msgid "Success" +msgstr "" + +#: .\apps\maxkb\settings\base.py:80 +msgid "Intelligent customer service platform" +msgstr "" + +#: .\apps\users\serializers\login.py:23 +msgid "Username" +msgstr "" + +#: .\apps\users\serializers\login.py:24 +msgid "Password" +msgstr "" + +#: .\apps\users\serializers\login.py:31 +msgid "token" +msgstr "" + +#: .\apps\users\serializers\login.py:43 +msgid "The username or password is incorrect" +msgstr "" + +#: .\apps\users\serializers\login.py:45 +msgid "The user has been disabled, please contact the administrator!" +msgstr "" + +#: .\apps\users\views\login.py:21 .\apps\users\views\login.py:22 +msgid "Log in" +msgstr "" + +#: .\apps\users\views\login.py:23 .\apps\users\views\user.py:26 +msgid "User management" +msgstr "" + +#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 +msgid "Get current user information" +msgstr "" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 000000000..3e3f18828 --- /dev/null +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,104 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-04-14 19:50+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: .\apps\common\auth\authenticate.py:63 .\apps\common\auth\authenticate.py:84 +msgid "Not logged in, please log in first" +msgstr "未登录,请先登录" + +#: .\apps\common\auth\authenticate.py:69 .\apps\common\auth\authenticate.py:75 +#: .\apps\common\auth\authenticate.py:90 .\apps\common\auth\authenticate.py:96 +msgid "Authentication information is incorrect! illegal user" +msgstr "身份验证信息不正确!非法用户" + +#: .\apps\common\auth\handle\impl\user_token.py:30 +msgid "Login expired" +msgstr "登录已过期" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "ADMIN" +msgstr "管理员" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "Super administrator" +msgstr "超级管理员" + +#: .\apps\common\exception\handle_exception.py:32 +msgid "Unknown exception" +msgstr "未知错误" + +#: .\apps\common\result\api.py:17 .\apps\common\result\api.py:27 +msgid "response code" +msgstr "响应码" + +#: .\apps\common\result\api.py:18 .\apps\common\result\api.py:19 +#: .\apps\common\result\api.py:28 .\apps\common\result\api.py:29 +msgid "error prompt" +msgstr "错误提示" + +#: .\apps\common\result\api.py:43 +msgid "total number of data" +msgstr "总数据" + +#: .\apps\common\result\api.py:44 +msgid "current page" +msgstr "当前页" + +#: .\apps\common\result\api.py:45 +msgid "page size" +msgstr "每页大小" + +#: .\apps\common\result\result.py:31 +msgid "Success" +msgstr "成功" + +#: .\apps\maxkb\settings\base.py:80 +msgid "Intelligent customer service platform" +msgstr "智能客服平台" + +#: .\apps\users\serializers\login.py:23 +msgid "Username" +msgstr "用户名" + +#: .\apps\users\serializers\login.py:24 +msgid "Password" +msgstr "密码" + +#: .\apps\users\serializers\login.py:31 +msgid "token" +msgstr "令牌" + +#: .\apps\users\serializers\login.py:43 +msgid "The username or password is incorrect" +msgstr "用户名或密码不正确" + +#: .\apps\users\serializers\login.py:45 +msgid "The user has been disabled, please contact the administrator!" +msgstr "用户已被禁用,请联系管理员!" + +#: .\apps\users\views\login.py:21 .\apps\users\views\login.py:22 +msgid "Log in" +msgstr "登录" + +#: .\apps\users\views\login.py:23 .\apps\users\views\user.py:26 +msgid "User management" +msgstr "用户管理" + +#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 +msgid "Get current user information" +msgstr "获取当前用户信息" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 000000000..e7b7b3ec5 --- /dev/null +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,104 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-04-14 20:02+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +#: .\apps\common\auth\authenticate.py:63 .\apps\common\auth\authenticate.py:84 +msgid "Not logged in, please log in first" +msgstr "未登錄,請先登錄" + +#: .\apps\common\auth\authenticate.py:69 .\apps\common\auth\authenticate.py:75 +#: .\apps\common\auth\authenticate.py:90 .\apps\common\auth\authenticate.py:96 +msgid "Authentication information is incorrect! illegal user" +msgstr "身份驗證資訊不正確! 非法用戶" + +#: .\apps\common\auth\handle\impl\user_token.py:30 +msgid "Login expired" +msgstr "登入已過期" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "ADMIN" +msgstr "管理員" + +#: .\apps\common\constants\permission_constants.py:46 +msgid "Super administrator" +msgstr "超級管理員" + +#: .\apps\common\exception\handle_exception.py:32 +msgid "Unknown exception" +msgstr "未知錯誤" + +#: .\apps\common\result\api.py:17 .\apps\common\result\api.py:27 +msgid "response code" +msgstr "響應碼" + +#: .\apps\common\result\api.py:18 .\apps\common\result\api.py:19 +#: .\apps\common\result\api.py:28 .\apps\common\result\api.py:29 +msgid "error prompt" +msgstr "錯誤提示" + +#: .\apps\common\result\api.py:43 +msgid "total number of data" +msgstr "總數據" + +#: .\apps\common\result\api.py:44 +msgid "current page" +msgstr "當前頁" + +#: .\apps\common\result\api.py:45 +msgid "page size" +msgstr "每頁大小" + +#: .\apps\common\result\result.py:31 +msgid "Success" +msgstr "成功" + +#: .\apps\maxkb\settings\base.py:80 +msgid "Intelligent customer service platform" +msgstr "智慧客服平臺" + +#: .\apps\users\serializers\login.py:23 +msgid "Username" +msgstr "用戶名" + +#: .\apps\users\serializers\login.py:24 +msgid "Password" +msgstr "密碼" + +#: .\apps\users\serializers\login.py:31 +msgid "token" +msgstr "權杖" + +#: .\apps\users\serializers\login.py:43 +msgid "The username or password is incorrect" +msgstr "用戶名或密碼不正確" + +#: .\apps\users\serializers\login.py:45 +msgid "The user has been disabled, please contact the administrator!" +msgstr "用戶已被禁用,請聯系管理員!" + +#: .\apps\users\views\login.py:21 .\apps\users\views\login.py:22 +msgid "Log in" +msgstr "登入" + +#: .\apps\users\views\login.py:23 .\apps\users\views\user.py:26 +msgid "User management" +msgstr "用戶管理" + +#: .\apps\users\views\user.py:24 .\apps\users\views\user.py:25 +msgid "Get current user information" +msgstr "獲取當前用戶資訊" diff --git a/apps/manage.py b/apps/manage.py new file mode 100644 index 000000000..54ec3299b --- /dev/null +++ b/apps/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'maxkb.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/apps/maxkb/__init__.py b/apps/maxkb/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/maxkb/asgi.py b/apps/maxkb/asgi.py new file mode 100644 index 000000000..63ab7f75f --- /dev/null +++ b/apps/maxkb/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for maxkb project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'maxkb.settings') + +application = get_asgi_application() diff --git a/apps/maxkb/conf.py b/apps/maxkb/conf.py new file mode 100644 index 000000000..1ca7c7f44 --- /dev/null +++ b/apps/maxkb/conf.py @@ -0,0 +1,196 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: conf.py + @date:2025/4/11 16:58 + @desc: +""" +import errno +import logging +import os + +import yaml + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.dirname(BASE_DIR) +logger = logging.getLogger('smartdoc.conf') + + +class Config(dict): + defaults = { + # 数据库相关配置 + "DB_HOST": "127.0.0.1", + "DB_PORT": 5432, + "DB_USER": "root", + "DB_PASSWORD": "Password123@postgres", + "DB_ENGINE": "dj_db_conn_pool.backends.postgresql", + "DB_MAX_OVERFLOW": 80, + # 语言 + 'LANGUAGE_CODE': 'zh-CN', + "DEBUG": False, + # redis 目前先支持单机 哨兵的配置后期加上 + 'REDIS_HOST': '127.0.0.1', + # 端口 + 'REDIS_PORT': 6379, + # 密码 + 'REDIS_PASSWORD': 'Password123@postgres', + # 库 + 'REDIS_DB': 0, + # 最大连接数 + 'REDIS_MAX_CONNECTIONS': 100 + } + + def get_debug(self) -> bool: + return self.get('DEBUG') if 'DEBUG' in self else True + + def get_time_zone(self) -> str: + return self.get('TIME_ZONE') if 'TIME_ZONE' in self else 'Asia/Shanghai' + + def get_db_setting(self) -> dict: + return { + "NAME": self.get('DB_NAME'), + "HOST": self.get('DB_HOST'), + "PORT": self.get('DB_PORT'), + "USER": self.get('DB_USER'), + "PASSWORD": self.get('DB_PASSWORD'), + "ENGINE": self.get('DB_ENGINE'), + "POOL_OPTIONS": { + "POOL_SIZE": 20, + "MAX_OVERFLOW": int(self.get('DB_MAX_OVERFLOW')) + } + } + + def get_cache_setting(self): + return { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': f'redis://{self.get("REDIS_HOST")}:{self.get("REDIS_PORT")}', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + "DB": self.get("REDIS_DB"), + "PASSWORD": self.get("REDIS_PASSWORD"), + "CONNECTION_POOL_KWARGS": {"max_connections": self.get("REDIS_MAX_CONNECTIONS")} + }, + }, + } + + def get_language_code(self): + return self.get('LANGUAGE_CODE', 'zh-CN') + + def __init__(self, *args): + super().__init__(*args) + + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) + + def __getitem__(self, item): + return self.get(item) + + def __getattr__(self, item): + return self.get(item) + + +class ConfigManager: + config_class = Config + + def __init__(self, root_path=None): + self.root_path = root_path + self.config = self.config_class() + for key in self.config_class.defaults: + self.config[key] = self.config_class.defaults[key] + + def from_mapping(self, *mapping, **kwargs): + """Updates the config like :meth:`update` ignoring items with non-upper + keys. + + .. versionadded:: 0.11 + """ + mappings = [] + if len(mapping) == 1: + if hasattr(mapping[0], 'items'): + mappings.append(mapping[0].items()) + else: + mappings.append(mapping[0]) + elif len(mapping) > 1: + raise TypeError( + 'expected at most 1 positional argument, got %d' % len(mapping) + ) + mappings.append(kwargs.items()) + for mapping in mappings: + for (key, value) in mapping: + if key.isupper(): + self.config[key] = value + return True + + def from_yaml(self, filename, silent=False): + if self.root_path: + filename = os.path.join(self.root_path, filename) + try: + with open(filename, 'rt', encoding='utf8') as f: + obj = yaml.safe_load(f) + except IOError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + e.strerror = 'Unable to load configuration file (%s)' % e.strerror + raise + if obj: + return self.from_mapping(obj) + return True + + def load_from_yml(self): + for i in ['config_example.yml', 'config.yaml', 'config.yml']: + if not os.path.isfile(os.path.join(self.root_path, i)): + continue + loaded = self.from_yaml(i) + if loaded: + return True + msg = f""" + + Error: No config file found. + + You can run `cp config_example.yml {self.root_path}/config.yml`, and edit it. + + """ + raise ImportError(msg) + + def load_from_env(self): + keys = os.environ.keys() + config = {key.replace('MAXKB_', ''): os.environ.get(key) for key in keys if key.startswith('MAXKB_')} + if len(config.keys()) <= 0: + msg = f""" + + Error: No config env found. + + Please set environment variables + MAXKB_CONFIG_TYPE: 配置文件读取方式 FILE: 使用配置文件配置 ENV: 使用ENV配置 + MAXKB_DB_NAME: 数据库名称 + MAXKB_DB_HOST: 数据库主机 + MAXKB_DB_PORT: 数据库端口 + MAXKB_DB_USER: 数据库用户名 + MAXKB_DB_PASSWORD: 数据库密码 + + MAXKB_REDIS_HOST:缓存数据库主机 + MAXKB_REDIS_PORT:缓存数据库端口 + MAXKB_REDIS_PASSWORD:缓存数据库密码 + MAXKB_REDIS_DB:缓存数据库 + MAXKB_REDIS_MAX_CONNECTIONS:缓存数据库最大连接数 + """ + raise ImportError(msg) + self.from_mapping(config) + return True + + @classmethod + def load_user_config(cls, root_path=None, config_class=None): + config_class = config_class or Config + cls.config_class = config_class + if not root_path: + root_path = PROJECT_DIR + manager = cls(root_path=root_path) + config_type = os.environ.get('MAXKB_CONFIG_TYPE') + if config_type is None or config_type != 'ENV': + manager.load_from_yml() + else: + manager.load_from_env() + config = manager.config + return config diff --git a/apps/maxkb/const.py b/apps/maxkb/const.py new file mode 100644 index 000000000..ad06ad817 --- /dev/null +++ b/apps/maxkb/const.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# +import os + +from .conf import ConfigManager + +__all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG'] + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.dirname(BASE_DIR) +VERSION = '2.0.0' +CONFIG = ConfigManager.load_user_config(root_path=os.path.abspath('/opt/maxkb/conf')) diff --git a/apps/maxkb/settings/__init__.py b/apps/maxkb/settings/__init__.py new file mode 100644 index 000000000..43753b734 --- /dev/null +++ b/apps/maxkb/settings/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/11 16:39 + @desc: +""" +from .base import * +from .auth import * diff --git a/apps/maxkb/settings/auth.py b/apps/maxkb/settings/auth.py new file mode 100644 index 000000000..2b0fc7e1d --- /dev/null +++ b/apps/maxkb/settings/auth.py @@ -0,0 +1,13 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: auth.py + @date:2024/7/9 18:47 + @desc: +""" +USER_TOKEN_AUTH = 'common.auth.handle.impl.user_token.UserToken' + +AUTH_HANDLES = [ + USER_TOKEN_AUTH, +] diff --git a/apps/maxkb/settings/base.py b/apps/maxkb/settings/base.py new file mode 100644 index 000000000..e38468c12 --- /dev/null +++ b/apps/maxkb/settings/base.py @@ -0,0 +1,151 @@ +""" +Django settings for maxkb project. + +Generated by 'django-admin startproject' using Django 4.2.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path +from ..const import CONFIG, PROJECT_DIR +import os +from django.utils.translation import gettext_lazy as _ + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-zm^1_^i5)3gp^&0io6zg72&z!a*d=9kf9o2%uft+27l)+t(#3e' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = CONFIG.get_debug() + +ALLOWED_HOSTS = ['*'] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'drf_spectacular', + 'drf_spectacular_sidecar', + 'users.apps.UsersConfig', + 'common' +] + +MIDDLEWARE = [ + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +] + +REST_FRAMEWORK = { + 'EXCEPTION_HANDLER': 'common.exception.handle_exception.handle_exception', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_AUTHENTICATION_CLASSES': ['common.auth.authenticate.AnonymousAuthentication'] +} +STATICFILES_DIRS = [(os.path.join(PROJECT_DIR, 'ui', 'dist'))] + +STATIC_ROOT = os.path.join(BASE_DIR.parent, 'static') +ROOT_URLCONF = 'maxkb.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ["apps/static/ui"], + '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', + ], + }, + }, +] +SPECTACULAR_SETTINGS = { + 'TITLE': 'MaxKB API', + 'DESCRIPTION': _('Intelligent customer service platform'), + 'VERSION': 'v2', + 'SERVE_INCLUDE_SCHEMA': False, + # OTHER SETTINGS + 'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead + 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', + 'REDOC_DIST': 'SIDECAR', + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'AUTHORIZATION', + 'in': 'header', + } + } +} +WSGI_APPLICATION = 'maxkb.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = {'default': CONFIG.get_db_setting()} + +CACHES = CONFIG.get_cache_setting() + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = CONFIG.get("LANGUAGE_CODE") + +TIME_ZONE = CONFIG.get_time_zone() + +USE_I18N = True + +USE_TZ = True +# 支持的语言 +LANGUAGES = [ + ('en', 'English'), + ('zh', '中文简体'), + ('zh-hant', '中文繁体') +] +# 翻译文件路径 +LOCALE_PATHS = [ + os.path.join(BASE_DIR.parent, 'locales') +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/apps/maxkb/urls.py b/apps/maxkb/urls.py new file mode 100644 index 000000000..8d56f0e6a --- /dev/null +++ b/apps/maxkb/urls.py @@ -0,0 +1,41 @@ +""" +URL configuration for maxkb project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path, re_path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from rest_framework import permissions +from common.auth import AnonymousAuthentication +from django.views import static + +from maxkb import settings + +SpectacularSwaggerView.permission_classes = [permissions.AllowAny] +SpectacularSwaggerView.authentication_classes = [AnonymousAuthentication] +SpectacularAPIView.permission_classes = [permissions.AllowAny] +SpectacularAPIView.authentication_classes = [AnonymousAuthentication] +SpectacularRedocView.permission_classes = [permissions.AllowAny] +SpectacularRedocView.authentication_classes = [AnonymousAuthentication] +urlpatterns = [ + path("api/", include("users.urls")), +] +urlpatterns += [ + path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由,下面两个ui也是根据这个配置文件来生成的 + path('doc/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # swagger-ui的路由 + path('redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # redoc的路由 +] +urlpatterns.append( + re_path(r'^static/(?P.*)$', static.serve, {'document_root': settings.STATIC_ROOT}, name='static'), +) diff --git a/apps/maxkb/wsgi.py b/apps/maxkb/wsgi.py new file mode 100644 index 000000000..74479b816 --- /dev/null +++ b/apps/maxkb/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for maxkb project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'maxkb.settings') + +application = get_wsgi_application() diff --git a/apps/users/__init__.py b/apps/users/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/users/admin.py b/apps/users/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/users/api/__init__.py b/apps/users/api/__init__.py new file mode 100644 index 000000000..577bf6a2c --- /dev/null +++ b/apps/users/api/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/14 10:28 + @desc: +""" +from .login import * diff --git a/apps/users/api/login.py b/apps/users/api/login.py new file mode 100644 index 000000000..6086192d1 --- /dev/null +++ b/apps/users/api/login.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: login.py + @date:2025/4/14 10:30 + @desc: +""" + +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from users.serializers.login import LoginResponse, LoginRequest + + +class ApiLoginResponse(ResultSerializer): + def get_data(self): + return LoginResponse() + + +""" +Request 和Response 都可以使用此方法 +使用serializers.Serializer +class LoginRequest(serializers.Serializer): + username = serializers.CharField(required=True, max_length=64, help_text=_("Username"), label=_("Username")) + password = serializers.CharField(required=True, max_length=128, label=_("Password")) +使用serializers.ModelSerializer Request不要使用serializers.ModelSerializer的方式 +class LoginRequest(serializers.ModelSerializer): + class Meta: + model = User + fields = ['username', 'password'] + +""" + + +class LoginAPI(APIMixin): + @staticmethod + def get_request(): + return LoginRequest + + @staticmethod + def get_response(): + return ApiLoginResponse diff --git a/apps/users/api/user.py b/apps/users/api/user.py new file mode 100644 index 000000000..6350669ab --- /dev/null +++ b/apps/users/api/user.py @@ -0,0 +1,23 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 19:23 + @desc: +""" +from common.mixins.api_mixin import APIMixin +from common.result import ResultSerializer +from users.serializers.user import UserProfileResponse + + +class ApiUserProfileResponse(ResultSerializer): + def get_data(self): + return UserProfileResponse() + + +class UserProfileAPI(APIMixin): + + @staticmethod + def get_response(): + return ApiUserProfileResponse diff --git a/apps/users/apps.py b/apps/users/apps.py new file mode 100644 index 000000000..72b140106 --- /dev/null +++ b/apps/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/apps/users/migrations/0001_initial.py b/apps/users/migrations/0001_initial.py new file mode 100644 index 000000000..2d44acd94 --- /dev/null +++ b/apps/users/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2 on 2025-04-14 02:22 + +import uuid +from django.db import migrations, models + +from common.constants.permission_constants import RoleConstants +from common.utils.common import password_encrypt + + +def insert_default_data(apps, schema_editor): + UserModel = apps.get_model('users', 'User') + UserModel.objects.create(id='f0dd8f71-e4ee-11ee-8c84-a8a1595801ab', email='', username='admin', + nick_name="系统管理员", + password=password_encrypt('MaxKB@123..'), + role=RoleConstants.ADMIN.name, + is_active=True) + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False, + verbose_name='主键id')), + ('email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='邮箱')), + ('phone', models.CharField(default='', max_length=20, verbose_name='电话')), + ('nick_name', models.CharField(default='', max_length=150, verbose_name='昵称')), + ('username', models.CharField(max_length=150, unique=True, verbose_name='用户名')), + ('password', models.CharField(max_length=150, verbose_name='密码')), + ('role', models.CharField(max_length=150, verbose_name='角色')), + ('source', models.CharField(default='LOCAL', max_length=10, verbose_name='来源')), + ('is_active', models.BooleanField(default=True)), + ('language', models.CharField(default=None, max_length=10, null=True, verbose_name='语言')), + ('create_time', models.DateTimeField(auto_now_add=True, null=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, null=True, verbose_name='修改时间')), + ], + options={ + 'db_table': 'user', + }, + ), + migrations.RunPython(insert_default_data) + ] diff --git a/apps/users/migrations/__init__.py b/apps/users/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/users/models/__init__.py b/apps/users/models/__init__.py new file mode 100644 index 000000000..5708f6a24 --- /dev/null +++ b/apps/users/models/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/14 10:20 + @desc: +""" +from .user import * diff --git a/apps/users/models/user.py b/apps/users/models/user.py new file mode 100644 index 000000000..ea608c15c --- /dev/null +++ b/apps/users/models/user.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 10:20 + @desc: +""" +import uuid + +from django.db import models + +from common.utils.common import password_encrypt + + +class User(models.Model): + id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid1, editable=False, verbose_name="主键id") + email = models.EmailField(unique=True, null=True, blank=True, verbose_name="邮箱") + phone = models.CharField(max_length=20, verbose_name="电话", default="") + nick_name = models.CharField(max_length=150, verbose_name="昵称", default="") + username = models.CharField(max_length=150, unique=True, verbose_name="用户名") + password = models.CharField(max_length=150, verbose_name="密码") + role = models.CharField(max_length=150, verbose_name="角色") + source = models.CharField(max_length=10, verbose_name="来源", default="LOCAL") + is_active = models.BooleanField(default=True) + language = models.CharField(max_length=10, verbose_name="语言", null=True, default=None) + create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True, null=True) + update_time = models.DateTimeField(verbose_name="修改时间", auto_now=True, null=True) + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = [] + + class Meta: + db_table = "user" + + def set_password(self, row_password): + self.password = password_encrypt(row_password) + self._password = row_password diff --git a/apps/users/serializers/login.py b/apps/users/serializers/login.py new file mode 100644 index 000000000..190e8ed6f --- /dev/null +++ b/apps/users/serializers/login.py @@ -0,0 +1,51 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: login.py + @date:2025/4/14 11:08 + @desc: +""" +from django.core import signing +from django.core.cache import cache +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.constants.authentication_type import AuthenticationType +from common.constants.cache_version import Cache_Version +from common.exception.app_exception import AppApiException +from common.utils.common import password_encrypt +from users.models import User + + +class LoginRequest(serializers.Serializer): + username = serializers.CharField(required=True, max_length=64, help_text=_("Username"), label=_("Username")) + password = serializers.CharField(required=True, max_length=128, label=_("Password")) + + +class LoginResponse(serializers.Serializer): + """ + 登录响应对象 + """ + token = serializers.CharField(required=True, label=_("token")) + + +class LoginSerializer(serializers.Serializer): + + @staticmethod + def login(instance): + LoginRequest(data=instance).is_valid(raise_exception=True) + username = instance.get('username') + password = instance.get('password') + user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first() + if user is None: + raise AppApiException(500, _('The username or password is incorrect')) + if not user.is_active: + raise AppApiException(1005, _("The user has been disabled, please contact the administrator!")) + token = signing.dumps({'username': user.username, + 'id': str(user.id), + 'email': user.email, + 'type': AuthenticationType.SYSTEM_USER.value}) + cache.set(token, user, version=Cache_Version.TOKEN) + return {'token': token} diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py new file mode 100644 index 000000000..4c64f4917 --- /dev/null +++ b/apps/users/serializers/user.py @@ -0,0 +1,37 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 19:18 + @desc: +""" +from rest_framework import serializers + +from users.models import User + + +class UserProfileResponse(serializers.ModelSerializer): + is_edit_password = serializers.BooleanField(required=True, label="是否修改密码") + permissions = serializers.ListField(required=True, label="权限") + + class Meta: + model = User + fields = ['id', 'username', 'email', 'role', 'permissions', 'language', 'is_edit_password'] + + +class UserProfileSerializer(serializers.Serializer): + @staticmethod + def profile(user: User): + """ + 获取用户详情 + :param user: 用户对象 + :return: + """ + return {'id': user.id, + 'username': user.username, + 'email': user.email, + 'role': user.role, + 'permissions': [str(p) for p in []], + 'is_edit_password': user.password == 'd880e722c47a34d8e9fce789fc62389d' if user.role == 'ADMIN' else False, + 'language': user.language} diff --git a/apps/users/tests.py b/apps/users/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/users/urls.py b/apps/users/urls.py new file mode 100644 index 000000000..761942ab7 --- /dev/null +++ b/apps/users/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from . import views + +app_name = "user" +urlpatterns = [ + path('user/login', views.LoginView.as_view(), name='login'), + path('user/profile', views.UserProfileView.as_view(), name="user_profile") +] diff --git a/apps/users/views/__init__.py b/apps/users/views/__init__.py new file mode 100644 index 000000000..9ef4e79ce --- /dev/null +++ b/apps/users/views/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: __init__.py.py + @date:2025/4/14 10:20 + @desc: +""" +from .login import * +from .user import * diff --git a/apps/users/views/login.py b/apps/users/views/login.py new file mode 100644 index 000000000..fced2dc13 --- /dev/null +++ b/apps/users/views/login.py @@ -0,0 +1,27 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 10:22 + @desc: +""" +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.request import Request +from rest_framework.views import APIView + +from common import result +from users.api.login import LoginAPI +from users.serializers.login import LoginSerializer + + +class LoginView(APIView): + @extend_schema(methods=['POST'], + description=_("Log in"), + operation_id=_("Log in"), + tags=[_("User management")], + request=LoginAPI.get_request(), + responses=LoginAPI.get_response()) + def post(self, request: Request): + return result.success(LoginSerializer().login(request.data)) diff --git a/apps/users/views/user.py b/apps/users/views/user.py new file mode 100644 index 000000000..84e94ea74 --- /dev/null +++ b/apps/users/views/user.py @@ -0,0 +1,29 @@ +# coding=utf-8 +""" + @project: MaxKB + @Author:虎虎 + @file: user.py + @date:2025/4/14 19:25 + @desc: +""" +from drf_spectacular.utils import extend_schema +from rest_framework.views import APIView +from django.utils.translation import gettext_lazy as _ +from rest_framework.request import Request + +from common.auth import TokenAuth +from common.result import result +from users.api.user import UserProfileAPI +from users.serializers.user import UserProfileSerializer + + +class UserProfileView(APIView): + authentication_classes = [TokenAuth] + + @extend_schema(methods=['GET'], + description=_("Get current user information"), + operation_id=_("Get current user information"), + tags=[_("User management")], + responses=UserProfileAPI.get_response()) + def get(self, request: Request): + return result.success(UserProfileSerializer().profile(request.user)) diff --git a/main.py b/main.py new file mode 100644 index 000000000..1835fa146 --- /dev/null +++ b/main.py @@ -0,0 +1,123 @@ +import argparse +import logging +import os +import sys +import time + +import django +from django.core import management + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +APP_DIR = os.path.join(BASE_DIR, 'apps') + +os.chdir(BASE_DIR) +sys.path.insert(0, APP_DIR) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "maxkb.settings") +django.setup() + + +def collect_static(): + """ + 收集静态文件到指定目录 + 本项目主要是将前端vue/dist的前端项目放到静态目录下面 + :return: + """ + logging.info("Collect static files") + try: + management.call_command('collectstatic', '--no-input', '-c', verbosity=0, interactive=False) + logging.info("Collect static files done") + except: + pass + + +def perform_db_migrate(): + """ + 初始化数据库表 + """ + logging.info("Check database structure change ...") + logging.info("Migrate model change to database ...") + try: + management.call_command('migrate') + except Exception as e: + logging.error('Perform migrate failed, exit', exc_info=True) + sys.exit(11) + + +def start_services(): + services = args.services if isinstance(args.services, list) else [args.services] + start_args = [] + if args.daemon: + start_args.append('--daemon') + if args.force: + start_args.append('--force') + if args.worker: + start_args.extend(['--worker', str(args.worker)]) + else: + worker = os.environ.get('CORE_WORKER') + if isinstance(worker, str) and worker.isdigit(): + start_args.extend(['--worker', worker]) + + try: + management.call_command(action, *services, *start_args) + except KeyboardInterrupt: + logging.info('Cancel ...') + time.sleep(2) + except Exception as exc: + logging.error("Start service error {}: {}".format(services, exc)) + time.sleep(2) + + +def dev(): + services = args.services if isinstance(args.services, list) else args.services + if services.__contains__('web'): + management.call_command('runserver', "0.0.0.0:8080") + elif services.__contains__('celery'): + management.call_command('celery', 'celery') + elif services.__contains__('local_model'): + os.environ.setdefault('SERVER_NAME', 'local_model') + from maxkb.const import CONFIG + bind = f'{CONFIG.get("LOCAL_MODEL_HOST")}:{CONFIG.get("LOCAL_MODEL_PORT")}' + management.call_command('runserver', bind) + + +if __name__ == '__main__': + os.environ['HF_HOME'] = '/opt/maxkb/model/base' + parser = argparse.ArgumentParser( + description=""" + qabot service control tools; + + Example: \r\n + + %(prog)s start all -d; + """ + ) + parser.add_argument( + 'action', type=str, + choices=("start", "dev", "upgrade_db", "collect_static"), + help="Action to run" + ) + args, e = parser.parse_known_args() + parser.add_argument( + "services", type=str, default='all' if args.action == 'start' else 'web', nargs="*", + choices=("all", "web", "task") if args.action == 'start' else ("web", "celery", 'local_model'), + help="The service to start", + ) + + parser.add_argument('-d', '--daemon', nargs="?", const=True) + parser.add_argument('-w', '--worker', type=int, nargs="?") + parser.add_argument('-f', '--force', nargs="?", const=True) + args = parser.parse_args() + action = args.action + if action == "upgrade_db": + perform_db_migrate() + elif action == "collect_static": + collect_static() + elif action == 'dev': + collect_static() + perform_db_migrate() + dev() + else: + collect_static() + perform_db_migrate() + start_services() + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..40eb554d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "maxkb" +version = "2.1.0" +description = "智能知识库问答系统" +authors = ["shaohuzhang1 "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +django = "^5.2" +drf-spectacular = {extras = ["sidecar"], version = "^0.28.0"} +django-redis = "^5.4.0" +psycopg2-binary = "^2.9.10" +django-db-connection-pool = "^1.2.5" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[[tool.poetry.source]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +priority = "explicit" \ No newline at end of file diff --git a/ui/.editorconfig b/ui/.editorconfig new file mode 100644 index 000000000..5a5809dbe --- /dev/null +++ b/ui/.editorconfig @@ -0,0 +1,9 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +end_of_line = lf +max_line_length = 100 diff --git a/ui/.gitattributes b/ui/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/ui/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..8ee54e8d3 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json new file mode 100644 index 000000000..29a2402ef --- /dev/null +++ b/ui/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 000000000..a5c11ae69 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,39 @@ +# ui + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/ui/env.d.ts b/ui/env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/ui/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/eslint.config.ts b/ui/eslint.config.ts new file mode 100644 index 000000000..20475f81e --- /dev/null +++ b/ui/eslint.config.ts @@ -0,0 +1,22 @@ +import { globalIgnores } from 'eslint/config' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import pluginVue from 'eslint-plugin-vue' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +// To allow more languages other than `ts` in `.vue` files, uncomment the following lines: +// import { configureVueProject } from '@vue/eslint-config-typescript' +// configureVueProject({ scriptLangs: ['ts', 'tsx'] }) +// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup + +export default defineConfigWithVueTs( + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + pluginVue.configs['flat/essential'], + vueTsConfigs.recommended, + skipFormatting, +) diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 000000000..9e5fc8f06 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 000000000..770322189 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,38 @@ +{ + "name": "ui", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "pinia": "^3.0.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.1", + "@types/node": "^22.14.0", + "@vitejs/plugin-vue": "^5.2.3", + "@vitejs/plugin-vue-jsx": "^4.1.2", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.5.0", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.22.0", + "eslint-plugin-vue": "~10.0.0", + "jiti": "^2.4.2", + "npm-run-all2": "^7.0.2", + "prettier": "3.5.3", + "typescript": "~5.8.0", + "vite": "^6.2.4", + "vite-plugin-vue-devtools": "^7.7.2", + "vue-tsc": "^2.2.8" + } +} diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/ui/src/App.vue b/ui/src/App.vue new file mode 100644 index 000000000..7905b0516 --- /dev/null +++ b/ui/src/App.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/ui/src/assets/base.css b/ui/src/assets/base.css new file mode 100644 index 000000000..8816868a4 --- /dev/null +++ b/ui/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/ui/src/assets/logo.svg b/ui/src/assets/logo.svg new file mode 100644 index 000000000..756566035 --- /dev/null +++ b/ui/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/ui/src/assets/main.css b/ui/src/assets/main.css new file mode 100644 index 000000000..36fb845b5 --- /dev/null +++ b/ui/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/ui/src/components/HelloWorld.vue b/ui/src/components/HelloWorld.vue new file mode 100644 index 000000000..d174cf8e1 --- /dev/null +++ b/ui/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/ui/src/components/TheWelcome.vue b/ui/src/components/TheWelcome.vue new file mode 100644 index 000000000..6092dff14 --- /dev/null +++ b/ui/src/components/TheWelcome.vue @@ -0,0 +1,94 @@ + + + diff --git a/ui/src/components/WelcomeItem.vue b/ui/src/components/WelcomeItem.vue new file mode 100644 index 000000000..6d7086aea --- /dev/null +++ b/ui/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/ui/src/components/icons/IconCommunity.vue b/ui/src/components/icons/IconCommunity.vue new file mode 100644 index 000000000..2dc8b0552 --- /dev/null +++ b/ui/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/ui/src/components/icons/IconDocumentation.vue b/ui/src/components/icons/IconDocumentation.vue new file mode 100644 index 000000000..6d4791cfb --- /dev/null +++ b/ui/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/ui/src/components/icons/IconEcosystem.vue b/ui/src/components/icons/IconEcosystem.vue new file mode 100644 index 000000000..c3a4f078c --- /dev/null +++ b/ui/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/ui/src/components/icons/IconSupport.vue b/ui/src/components/icons/IconSupport.vue new file mode 100644 index 000000000..7452834d3 --- /dev/null +++ b/ui/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/ui/src/components/icons/IconTooling.vue b/ui/src/components/icons/IconTooling.vue new file mode 100644 index 000000000..660598d7c --- /dev/null +++ b/ui/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/ui/src/main.ts b/ui/src/main.ts new file mode 100644 index 000000000..5dcad83c3 --- /dev/null +++ b/ui/src/main.ts @@ -0,0 +1,14 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts new file mode 100644 index 000000000..3e49915ca --- /dev/null +++ b/ui/src/router/index.ts @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView, + }, + { + path: '/about', + name: 'about', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/AboutView.vue'), + }, + ], +}) + +export default router diff --git a/ui/src/stores/counter.ts b/ui/src/stores/counter.ts new file mode 100644 index 000000000..b6757ba57 --- /dev/null +++ b/ui/src/stores/counter.ts @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/ui/src/views/AboutView.vue b/ui/src/views/AboutView.vue new file mode 100644 index 000000000..756ad2a17 --- /dev/null +++ b/ui/src/views/AboutView.vue @@ -0,0 +1,15 @@ + + + diff --git a/ui/src/views/HomeView.vue b/ui/src/views/HomeView.vue new file mode 100644 index 000000000..d5c0217e4 --- /dev/null +++ b/ui/src/views/HomeView.vue @@ -0,0 +1,9 @@ + + + diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json new file mode 100644 index 000000000..913b8f279 --- /dev/null +++ b/ui/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 000000000..66b5e5703 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json new file mode 100644 index 000000000..a83dfc9d4 --- /dev/null +++ b/ui/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 000000000..d49d70842 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,20 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueJsx(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +})