├── .devcontainer ├── devcontainer.json └── docker-compose.yml ├── .github ├── release-config.yml └── workflows │ ├── build-base-image.yml │ ├── build-image.yml │ ├── issue-close-require.yml │ ├── issue-close.yml │ ├── issue-comment.yml │ ├── issue-open.yml │ └── release-drafter.yml ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── Dockerfile-base ├── Dockerfile-dev ├── LICENSE ├── README.md ├── captcha ├── __init__.py ├── admin.py ├── apps.py ├── fonts │ ├── COPYRIGHT.TXT │ ├── README.TXT │ └── Vera.ttf ├── helpers.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── captcha_clean.py │ │ └── captcha_create_pool.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tasks.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── common ├── __init__.py ├── api │ ├── __init__.py │ └── common.py ├── apps.py ├── base │ ├── magic.py │ └── utils.py ├── cache │ ├── channel.py │ ├── redis.py │ ├── state.py │ └── storage.py ├── celery │ ├── decorator.py │ ├── flower.py │ ├── heatbeat.py │ ├── logger.py │ └── utils.py ├── core │ ├── auth.py │ ├── config.py │ ├── db │ │ ├── router.py │ │ └── utils.py │ ├── exception.py │ ├── fields.py │ ├── filter.py │ ├── middleware.py │ ├── models.py │ ├── modelset.py │ ├── pagination.py │ ├── permission.py │ ├── response.py │ ├── routers.py │ ├── serializers.py │ ├── throttle.py │ ├── utils.py │ └── validators.py ├── decorators.py ├── drf │ ├── const.py │ ├── metadata.py │ ├── parsers │ │ ├── __init__.py │ │ ├── axios_form_data.py │ │ ├── base.py │ │ ├── csv.py │ │ └── excel.py │ ├── renders │ │ ├── __init__.py │ │ ├── base.py │ │ ├── csv.py │ │ └── excel.py │ └── utils.py ├── fields │ ├── char.py │ ├── image.py │ └── utils.py ├── local.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── download_ip_db.py │ │ ├── expire_caches.py │ │ ├── restart.py │ │ ├── services │ │ ├── __init__.py │ │ ├── command.py │ │ ├── hands.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── beat.py │ │ │ ├── celery_base.py │ │ │ ├── celery_default.py │ │ │ ├── flower.py │ │ │ └── gunicorn.py │ │ └── utils.py │ │ ├── start.py │ │ ├── status.py │ │ └── stop.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── notifications.py ├── sdk │ └── sms │ │ ├── alibaba.py │ │ ├── base.py │ │ ├── endpoint.py │ │ └── exceptions.py ├── serializers.py ├── signal_handlers.py ├── signals.py ├── startup.py ├── swagger │ ├── utils.py │ └── views.py ├── tasks.py ├── templates │ ├── monitor │ │ └── msg_terminal_performance.html │ └── notify │ │ └── msg_task.html ├── urls.py └── utils │ ├── __init__.py │ ├── common.py │ ├── connection.py │ ├── country.py │ ├── file.py │ ├── ip │ ├── __init__.py │ ├── geoip │ │ ├── __init__.py │ │ └── utils.py │ ├── ipip │ │ ├── __init__.py │ │ └── utils.py │ └── utils.py │ ├── media.py │ ├── pending.py │ ├── random.py │ ├── request.py │ ├── timezone.py │ ├── token.py │ └── verify_code.py ├── config_example.yml ├── data ├── logs │ └── task │ │ └── .gitkeep └── upload │ └── .gitkeep ├── demo ├── __init__.py ├── admin.py ├── apps.py ├── config.py ├── migrations │ └── __init__.py ├── models.py ├── serializers │ └── book.py ├── tests.py ├── urls.py └── views.py ├── docker-compose.yml ├── docs ├── data-permission.md ├── field-permission.md └── imgs │ ├── data-permission │ ├── add-data-permission-rules.png │ └── add-data-permission.png │ └── field-permission │ ├── add-role.png │ └── add-user-menu.png ├── entrypoint.sh ├── loadjson ├── datapermission.json ├── deptinfo.json ├── fieldpermission.json ├── menu.json ├── menumeta.json ├── modellabelfield.json ├── setting.json ├── systemconfig.json ├── userinfo.json └── userrole.json ├── locale ├── en │ └── LC_MESSAGES │ │ └── django.po └── zh │ └── LC_MESSAGES │ └── django.po ├── manage.py ├── message ├── __init__.py ├── apps.py ├── base.py ├── notify.py ├── routing.py └── utils.py ├── notifications ├── __init__.py ├── admin.py ├── apps.py ├── backends │ ├── __init__.py │ ├── base.py │ ├── email.py │ ├── site_msg.py │ └── sms.py ├── message.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── message.py │ └── notification.py ├── notifications.py ├── serializers │ ├── __init__.py │ ├── message.py │ └── notifications.py ├── signal_handlers.py ├── tests.py ├── urls.py └── views │ ├── __init__.py │ ├── message.py │ ├── notifications.py │ └── user_site_msg.py ├── requirements.txt ├── server ├── __init__.py ├── asgi.py ├── celery.py ├── conf.py ├── const.py ├── logging.py ├── middleware.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── custom.py │ ├── libs.py │ ├── logging.py │ └── setting.py ├── urls.py ├── utils.py └── wsgi.py ├── settings ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ └── __init__.py ├── models.py ├── serializers │ ├── __init__.py │ ├── basic.py │ ├── email.py │ ├── security.py │ ├── setting.py │ └── sms.py ├── signal_handlers.py ├── tests.py ├── urls.py ├── utils │ ├── __init__.py │ ├── password.py │ └── security.py └── views │ ├── __init__.py │ ├── basic.py │ ├── block_ip.py │ ├── email.py │ ├── security.py │ ├── settings.py │ └── sms.py ├── system ├── __init__.py ├── admin.py ├── apps.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── dump_init_json.py │ │ ├── expire_config_caches.py │ │ ├── load_init_json.py │ │ └── sync_model_field.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_operationlog_exec_time_operationlog_request_uuid.py │ ├── 0003_userloginlog_channel_name_and_more.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── abstract.py │ ├── config.py │ ├── department.py │ ├── field.py │ ├── log.py │ ├── menu.py │ ├── permission.py │ ├── role.py │ ├── upload.py │ └── user.py ├── notifications.py ├── serializers │ ├── __init__.py │ ├── config.py │ ├── department.py │ ├── field.py │ ├── log.py │ ├── menu.py │ ├── permission.py │ ├── role.py │ ├── route.py │ ├── upload.py │ ├── user.py │ └── userinfo.py ├── signal.py ├── signal_handler.py ├── tasks.py ├── templates │ ├── msg_verify_code.html │ └── notify │ │ ├── msg_different_city.html │ │ └── msg_rest_password_success.html ├── urls.py ├── utils │ ├── auth.py │ ├── ctasks.py │ ├── menu.py │ ├── modelfield.py │ └── modelset.py └── views │ ├── __init__.py │ ├── admin │ ├── __init__.py │ ├── config.py │ ├── dept.py │ ├── file.py │ ├── loginlog.py │ ├── menu.py │ ├── modelfield.py │ ├── online.py │ ├── operationlog.py │ ├── permission.py │ ├── role.py │ └── user.py │ ├── auth │ ├── __init__.py │ ├── login.py │ ├── logout.py │ ├── register.py │ ├── reset.py │ ├── rule.py │ ├── token.py │ └── verify_code.py │ ├── configs.py │ ├── dashboard.py │ ├── routes.py │ ├── search │ ├── __init__.py │ ├── dept.py │ ├── menu.py │ ├── role.py │ └── user.py │ └── user │ ├── __init__.py │ ├── login_log.py │ └── userinfo.py ├── tmp └── .gitkeep ├── utils ├── check_celery.sh ├── clean_migrations.sh ├── init_data.py ├── install_centos_docker.sh └── nginx.conf └── xadmin.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose 3 | { 4 | "name": "Existing Docker Compose (Extend)", 5 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 6 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 7 | "dockerComposeFile": [ 8 | "../docker-compose.yml", 9 | "docker-compose.yml" 10 | ], 11 | // The 'service' property is the name of the service for the container that VS Code should 12 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 13 | "service": "server", 14 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 15 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 16 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 17 | // Features to add to the dev container. More info: https://containers.dev/features. 18 | // "features": {}, 19 | 20 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 21 | // "forwardPorts": [], 22 | 23 | // Uncomment the next line if you want start specific services in your Docker Compose config. 24 | // "runServices": [], 25 | 26 | // Uncomment the next line if you want to keep your containers running after VS Code shuts down. 27 | // "shutdownAction": "none", 28 | 29 | // Uncomment the next line to run commands after the container is created. 30 | // "postCreateCommand": "cat /etc/os-release", 31 | 32 | // Configure tool-specific properties. 33 | // "customizations": {}, 34 | 35 | // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. 36 | "remoteUser": "root" 37 | } 38 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | # Update this to the name of the service you want to work with in your docker-compose.yml file 4 | server: 5 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 6 | # folder. Note that the path of the Dockerfile and context is relative to the *primary* 7 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 8 | # array). The sample below assumes your primary file is in the root of your project. 9 | # 10 | # build: 11 | # context: . 12 | # dockerfile: .devcontainer/Dockerfile 13 | 14 | volumes: 15 | # Update this to wherever you want VS Code to mount the folder of your project 16 | - ..:/workspaces:cached 17 | 18 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 19 | # cap_add: 20 | # - SYS_PTRACE 21 | # security_opt: 22 | # - seccomp:unconfined 23 | 24 | # Overrides default command so things don't shut down after the process ends. 25 | command: sleep infinity 26 | 27 | -------------------------------------------------------------------------------- /.github/release-config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🌱 新功能 Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - 'feat' 9 | - '新功能' 10 | - title: '🚀 性能优化 Optimization' 11 | labels: 12 | - 'perf' 13 | - 'opt' 14 | - 'refactor' 15 | - 'Optimization' 16 | - '优化' 17 | - title: '🐛 Bug修复 Bug Fixes' 18 | labels: 19 | - 'fix' 20 | - 'bugfix' 21 | - 'bug' 22 | - title: '🧰 其它 Maintenance' 23 | labels: 24 | - 'chore' 25 | - 'docs' 26 | exclude-labels: 27 | - 'no' 28 | - '无需处理' 29 | - 'wontfix' 30 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 31 | version-resolver: 32 | major: 33 | labels: 34 | - 'major' 35 | minor: 36 | labels: 37 | - 'minor' 38 | patch: 39 | labels: 40 | - 'patch' 41 | default: patch 42 | template: | 43 | ## 版本变化 What’s Changed 44 | 45 | $CHANGES 46 | -------------------------------------------------------------------------------- /.github/workflows/build-base-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Base Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'dev' 8 | paths: 9 | - 'requirements.txt' 10 | - 'Dockerfile-base' 11 | 12 | jobs: 13 | build-and-push-base-image: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Login to DockerHub 27 | uses: docker/login-action@v3 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | 32 | - name: Extract date 33 | id: vars 34 | run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV 35 | 36 | - name: Extract repository name 37 | id: repo 38 | run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV 39 | 40 | - name: Build and push multi-arch image 41 | uses: docker/build-push-action@v6 42 | with: 43 | platforms: linux/amd64,linux/arm64 44 | push: true 45 | file: Dockerfile-base 46 | tags: nineaiyu/${{ env.REPO }}-base:${{ env.IMAGE_TAG }} 47 | 48 | - name: Update Dockerfile 49 | run: | 50 | sed -i 's@nineaiyu/.* AS stage-build@nineaiyu/${{ env.REPO }}-base:${{ env.IMAGE_TAG }} AS stage-build@' Dockerfile 51 | 52 | - name: Commit changes 53 | run: | 54 | git config --global user.name 'github-actions[bot]' 55 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 56 | git add Dockerfile 57 | git commit -m "perf: Update Dockerfile with new base image tag" 58 | git push origin ${{ github.event.pull_request.head.ref }} 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build-and-push-image: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v3 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - name: Extract repository name 30 | id: repo 31 | run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV 32 | 33 | - name: Extract metadata (tags, labels) for Docker 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: nineaiyu/${{ env.REPO }} 38 | tags: | 39 | type=semver,pattern={{version}} 40 | 41 | - name: Build and push multi-arch image 42 | uses: docker/build-push-action@v6 43 | with: 44 | build-args: VERSION=${{ steps.meta.outputs.version }} 45 | platforms: linux/amd64,linux/arm64 46 | push: true 47 | file: Dockerfile 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | -------------------------------------------------------------------------------- /.github/workflows/issue-close-require.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Require 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | issue-close-require: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: need reproduce 12 | uses: actions-cool/issues-helper@v3 13 | with: 14 | actions: 'close-issues' 15 | labels: '⏳ Pending feedback' 16 | inactive-day: 30 17 | body: | 18 | You haven't provided feedback for over 30 days. 19 | We will close this issue. If you have any further needs, you can reopen it or submit a new issue. 20 | 您超过 30 天未反馈信息,我们将关闭该 issue,如有需求您可以重新打开或者提交新的 issue。 21 | -------------------------------------------------------------------------------- /.github/workflows/issue-close.yml: -------------------------------------------------------------------------------- 1 | name: Issue Close Check 2 | 3 | on: 4 | issues: 5 | types: [ closed ] 6 | 7 | jobs: 8 | issue-close-remove-labels: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Remove labels 12 | uses: actions-cool/issues-helper@v3 13 | if: ${{ !github.event.issue.pull_request }} 14 | with: 15 | actions: 'remove-labels' 16 | labels: '🔔 Pending processing,⏳ Pending feedback' -------------------------------------------------------------------------------- /.github/workflows/issue-comment.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [ created ] 4 | 5 | name: Add issues workflow labels 6 | 7 | jobs: 8 | add-label-if-is-author: 9 | runs-on: ubuntu-latest 10 | if: (github.event.issue.user.id == github.event.comment.user.id) && !github.event.issue.pull_request && (github.event.issue.state == 'open') 11 | steps: 12 | - name: Add require handle label 13 | uses: actions-cool/issues-helper@v3 14 | with: 15 | actions: 'add-labels' 16 | labels: '🔔 Pending processing' 17 | 18 | - name: Remove require reply label 19 | uses: actions-cool/issues-helper@v3 20 | with: 21 | actions: 'remove-labels' 22 | labels: '⏳ Pending feedback' 23 | 24 | add-label-if-is-member: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Get Organization name 31 | id: org_name 32 | run: echo "data=$(echo '${{ github.repository }}' | cut -d '/' -f 1)" >> $GITHUB_OUTPUT 33 | 34 | - name: Get Organization public members 35 | uses: octokit/request-action@v2.x 36 | id: members 37 | with: 38 | route: GET /orgs/${{ steps.org_name.outputs.data }}/public_members 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Process public members data 43 | # 将 members 中的数据转化为 login 字段的拼接字符串 44 | id: member_names 45 | run: echo "data=$(echo '${{ steps.members.outputs.data }}' | jq '[.[].login] | join(",")')" >> $GITHUB_OUTPUT 46 | 47 | 48 | - run: "echo members: '${{ steps.members.outputs.data }}'" 49 | - run: "echo member names: '${{ steps.member_names.outputs.data }}'" 50 | - run: "echo comment user: '${{ github.event.comment.user.login }}'" 51 | - run: "echo contains? : '${{ contains(steps.member_names.outputs.data, github.event.comment.user.login) }}'" 52 | 53 | - name: Add require replay label 54 | if: contains(steps.member_names.outputs.data, github.event.comment.user.login) 55 | uses: actions-cool/issues-helper@v3 56 | with: 57 | actions: 'add-labels' 58 | labels: '⏳ Pending feedback' 59 | 60 | - name: Remove require handle label 61 | if: contains(steps.member_names.outputs.data, github.event.comment.user.login) 62 | uses: actions-cool/issues-helper@v3 63 | with: 64 | actions: 'remove-labels' 65 | labels: '🔔 Pending processing' 66 | -------------------------------------------------------------------------------- /.github/workflows/issue-open.yml: -------------------------------------------------------------------------------- 1 | name: Issue Open Check 2 | 3 | on: 4 | issues: 5 | types: [ opened ] 6 | 7 | jobs: 8 | issue-open-add-labels: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Add labels 12 | uses: actions-cool/issues-helper@v3 13 | if: ${{ !github.event.issue.pull_request }} 14 | with: 15 | actions: 'add-labels' 16 | labels: '🔔 Pending processing' -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | create-release: 10 | name: Create Release 11 | runs-on: ubuntu-latest 12 | outputs: 13 | upload_url: ${{ steps.create_release.outputs.upload_url }} 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Get version 19 | id: get_version 20 | run: | 21 | TAG=$(basename ${GITHUB_REF}) 22 | echo "TAG=$TAG" >> $GITHUB_OUTPUT 23 | wget https://raw.githubusercontent.com/nineaiyu/xadmin-installer/master/quick_start.sh 24 | VERSION=${TAG/v/} 25 | echo "TAG=${VERSION}" >> $GITHUB_ENV 26 | sed -i "s@VERSION=.*@VERSION=v${VERSION}@g" quick_start.sh 27 | 28 | - name: Create Release 29 | id: create_release 30 | uses: release-drafter/release-drafter@v6 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | publish: true 35 | config-name: release-config.yml 36 | version: ${{ steps.get_version.outputs.TAG }} 37 | tag: ${{ steps.get_version.outputs.TAG }} 38 | 39 | - name: Sleep time 40 | run: | 41 | sleep 2 42 | 43 | - name: Release Upload Assets 44 | uses: softprops/action-gh-release@v2 45 | if: startsWith(github.ref, 'refs/tags/') 46 | with: 47 | files: | 48 | quick_start.sh 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **/__pycache__/* 3 | *.mo 4 | __pycache__/* 5 | data/* 6 | tmp/* 7 | static/* 8 | upload/* 9 | common/utils/ip/geoip/GeoLite2-City.mmdb 10 | common/utils/ip/ipip/ipipfree.ipdb 11 | config.yml 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Python Debugger: Django", 5 | "type": "debugpy", 6 | "request": "launch", 7 | "program": "${workspaceFolder}/manage.py", 8 | "args": [ 9 | "runserver", 10 | "0.0.0.0:8896" 11 | ], 12 | "django": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nineaiyu/xadmin-server-base:20250401_063806 AS stage-build 2 | ARG VERSION 3 | 4 | WORKDIR /data/xadmin-server 5 | 6 | COPY . . 7 | 8 | RUN echo > config.yml \ 9 | && \ 10 | if [ -n "${VERSION}" ]; then \ 11 | sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" server/const.py; \ 12 | fi 13 | 14 | FROM python:3.13.2-slim 15 | 16 | ENV LANG=en_US.UTF-8 \ 17 | PATH=/data/py3/bin:$PATH 18 | 19 | ARG APT_MIRROR=http://deb.debian.org 20 | 21 | ARG DEPENDENCIES=" \ 22 | gettext \ 23 | curl \ 24 | libmariadb-dev" 25 | 26 | RUN set -ex \ 27 | && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources \ 28 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 29 | && apt-get update > /dev/null \ 30 | && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ 31 | && echo "no" | dpkg-reconfigure dash \ 32 | && apt-get clean all \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | COPY --from=stage-build /data /data 36 | COPY --from=stage-build /usr/local/bin /usr/local/bin 37 | 38 | #RUN addgroup --system --gid 1001 nginx \ 39 | # && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 1001 nginx 40 | 41 | WORKDIR /data/xadmin-server 42 | 43 | VOLUME /data/xadmin-server/data 44 | 45 | #USER 1001 46 | 47 | ENTRYPOINT ["/bin/bash", "entrypoint.sh"] 48 | 49 | EXPOSE 8896 50 | 51 | STOPSIGNAL SIGQUIT 52 | 53 | CMD ["start", "all"] 54 | -------------------------------------------------------------------------------- /Dockerfile-base: -------------------------------------------------------------------------------- 1 | FROM python:3.13.2-slim 2 | 3 | # Install APT dependencies 4 | ARG DEPENDENCIES=" \ 5 | curl \ 6 | g++ \ 7 | make \ 8 | pkg-config \ 9 | libmariadb-dev \ 10 | gettext" 11 | 12 | ARG APT_MIRROR=http://deb.debian.org 13 | 14 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ 15 | --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \ 16 | set -ex \ 17 | && rm -f /etc/apt/apt.conf.d/docker-clean \ 18 | && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ 19 | && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources \ 20 | && apt-get update > /dev/null \ 21 | && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ 22 | && echo "no" | dpkg-reconfigure dash 23 | 24 | 25 | # install pip 26 | WORKDIR /data/ 27 | 28 | ARG PIP_MIRROR=https://pypi.org/simple 29 | 30 | RUN --mount=type=cache,target=/root/.cache,id=core \ 31 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 32 | set -ex \ 33 | && python3 -m venv /data/py3 \ 34 | && . /data/py3/bin/activate \ 35 | && pip install -U setuptools pip --ignore-installed -i ${PIP_MIRROR} \ 36 | && pip install --no-cache-dir -r requirements.txt -i ${PIP_MIRROR} -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM registry.cn-beijing.aliyuncs.com/nineaiyu/python:3.13.2-slim 2 | 3 | # add pip cn mirrors 4 | ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple 5 | ARG APT_MIRROR=http://mirrors.tuna.tsinghua.edu.cn 6 | 7 | # Install APT dependencies 8 | ARG DEPENDENCIES=" \ 9 | curl \ 10 | g++ \ 11 | make \ 12 | pkg-config \ 13 | libmariadb-dev \ 14 | gettext" 15 | 16 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ 17 | --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \ 18 | set -ex \ 19 | && rm -f /etc/apt/apt.conf.d/docker-clean \ 20 | && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \ 21 | && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources \ 22 | && apt-get update > /dev/null \ 23 | && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ 24 | && echo "no" | dpkg-reconfigure dash 25 | 26 | 27 | # install pip 28 | 29 | RUN --mount=type=cache,target=/root/.cache,id=core \ 30 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 31 | set -ex \ 32 | && pip install -U setuptools pip --ignore-installed -i ${PIP_MIRROR} \ 33 | && pip install --no-cache-dir -r requirements.txt -i ${PIP_MIRROR} 34 | 35 | WORKDIR /data/xadmin-server/ 36 | 37 | ENTRYPOINT ["/bin/bash", "entrypoint.sh"] 38 | 39 | EXPOSE 8896 40 | 41 | STOPSIGNAL SIGQUIT 42 | 43 | CMD ["start", "all"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nineven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xadmin-server 2 | 3 | xadmin-基于Django+vue3的rbac权限管理系统 4 | 5 | 前端 [xadmin-client](https://github.com/nineaiyu/xadmin-client) 6 | 7 | ### 在线预览 8 | 9 | [https://xadmin.dvcloud.xin/](https://xadmin.dvcloud.xin/) 10 | 账号密码:admin/admin123 11 | 12 | ## 开发部署文档 13 | 14 | [https://docs.dvcloud.xin/](https://docs.dvcloud.xin/) 15 | 16 | ## [Centos 9 Stream 安装部署](https://docs.dvcloud.xin/guide/installation-local.html) 17 | 18 | ## [Docker 容器化部署](https://docs.dvcloud.xin/guide/installation-docker.html) 19 | 20 | # 附录 21 | 22 | ⚠️ Windows上面无法正常运行celery flower,导致任务监控无法正常使用,请使用Linux环境开发部署 23 | 24 | ## 启动程序(启动之前必须配置好Redis和数据库) 25 | 26 | ### A.一键执行命令【不支持windows平台,如果是Windows,请使用 手动执行命令】 27 | 28 | ```shell 29 | python manage.py start all -d # -d 参数是后台运行,如果去掉,则前台运行 30 | ``` 31 | 32 | ### B.手动执行命令 33 | 34 | #### 1.api服务 35 | 36 | ```shell 37 | python manage.py runserver 0.0.0.0:8896 38 | ``` 39 | 40 | #### 2.定时任务 41 | 42 | ```shell 43 | python -m celery -A server beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler --max-interval 60 44 | python -m celery -A server worker -P threads -l INFO -c 10 -Q celery --heartbeat-interval 10 -n celery@%h --without-mingle 45 | ``` 46 | 47 | #### 3.任务监控[windows可能会异常] 48 | 49 | ```shell 50 | python -m celery -A server flower -logging=info --url_prefix=api/flower --auto_refresh=False --address=0.0.0.0 --port=5566 51 | ``` 52 | 53 | ## 捐赠or鼓励 54 | 55 | 如果你觉得这个项目帮助到了你,你可以[star](https://github.com/nineaiyu/xadmin-server)表示鼓励,也可以帮作者买一杯果汁🍹表示鼓励。 56 | 57 | | 微信 | 支付宝 | 58 | |----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| 59 | | | | 60 | -------------------------------------------------------------------------------- /captcha/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/captcha/__init__.py -------------------------------------------------------------------------------- /captcha/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | from django.contrib import admin 3 | 4 | from captcha.models import CaptchaStore 5 | 6 | admin.register(CaptchaStore) 7 | -------------------------------------------------------------------------------- /captcha/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CaptchaConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'captcha' 7 | -------------------------------------------------------------------------------- /captcha/fonts/README.TXT: -------------------------------------------------------------------------------- 1 | Contained herin is the Bitstream Vera font family. 2 | 3 | The Copyright information is found in the COPYRIGHT.TXT file (along 4 | with being incorporated into the fonts themselves). 5 | 6 | The releases notes are found in the file "RELEASENOTES.TXT". 7 | 8 | We hope you enjoy Vera! 9 | 10 | Bitstream, Inc. 11 | The Gnome Project 12 | -------------------------------------------------------------------------------- /captcha/fonts/Vera.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/captcha/fonts/Vera.ttf -------------------------------------------------------------------------------- /captcha/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/captcha/management/__init__.py -------------------------------------------------------------------------------- /captcha/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/captcha/management/commands/__init__.py -------------------------------------------------------------------------------- /captcha/management/commands/captcha_clean.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management.base import BaseCommand 4 | from django.utils import timezone 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Clean up expired captcha hashkeys." 9 | 10 | def handle(self, **options): 11 | from captcha.models import CaptchaStore 12 | 13 | verbose = int(options.get("verbosity")) 14 | expired_keys = CaptchaStore.objects.filter( 15 | expiration__lte=timezone.now() 16 | ).count() 17 | if verbose >= 1: 18 | print("Currently %d expired hashkeys" % expired_keys) 19 | try: 20 | CaptchaStore.remove_expired() 21 | except Exception: 22 | if verbose >= 1: 23 | print("Unable to delete expired hashkeys.") 24 | sys.exit(1) 25 | if verbose >= 1: 26 | if expired_keys > 0: 27 | print("%d expired hashkeys removed." % expired_keys) 28 | else: 29 | print("No keys to remove.") 30 | -------------------------------------------------------------------------------- /captcha/management/commands/captcha_create_pool.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.db import transaction 3 | 4 | from captcha.models import CaptchaStore 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Create a pool of random captchas." 9 | 10 | def add_arguments(self, parser): 11 | parser.add_argument( 12 | "--pool-size", 13 | type=int, 14 | default=1000, 15 | help="Number of new captchas to create, default=1000", 16 | ) 17 | parser.add_argument( 18 | "--cleanup-expired", 19 | action="store_true", 20 | default=True, 21 | help="Cleanup expired captchas after creating new ones", 22 | ) 23 | 24 | @transaction.atomic 25 | def handle(self, **options): 26 | verbose = int(options.get("verbosity")) 27 | count = options.get("pool_size") 28 | CaptchaStore.create_pool(count) 29 | verbose and self.stdout.write("Created %d new captchas\n" % count) 30 | options.get("cleanup_expired") and CaptchaStore.remove_expired() 31 | options.get("cleanup_expired") and verbose and self.stdout.write( 32 | "Expired captchas cleaned up\n" 33 | ) 34 | -------------------------------------------------------------------------------- /captcha/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-23 06:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='CaptchaStore', 15 | fields=[ 16 | ('id', models.AutoField(primary_key=True, serialize=False)), 17 | ('challenge', models.CharField(max_length=32)), 18 | ('response', models.CharField(max_length=32)), 19 | ('hashkey', models.CharField(max_length=40, unique=True)), 20 | ('expiration', models.DateTimeField()), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /captcha/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/captcha/migrations/__init__.py -------------------------------------------------------------------------------- /captcha/tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : tasks 5 | # author : ly_13 6 | # date : 9/15/2024 7 | from celery import shared_task 8 | 9 | from captcha.models import CaptchaStore 10 | from common.celery.decorator import register_as_period_task 11 | 12 | 13 | @shared_task 14 | @register_as_period_task(crontab='12 2 * * *') 15 | def auto_clean_expired_captcha_job(): 16 | CaptchaStore.remove_expired() 17 | -------------------------------------------------------------------------------- /captcha/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /captcha/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from captcha import views 4 | 5 | urlpatterns = [ 6 | re_path( 7 | r"image/(?P\w+)/$", 8 | views.captcha_image, 9 | name="captcha-image", 10 | kwargs={"scale": 1}, 11 | ), 12 | re_path( 13 | r"image/(?P\w+)@2/$", 14 | views.captcha_image, 15 | name="captcha-image-2x", 16 | kwargs={"scale": 2}, 17 | ), 18 | re_path(r"audio/(?P\w+).wav$", views.captcha_audio, name="captcha-audio"), 19 | re_path(r"refresh/$", views.captcha_refresh, name="captcha-refresh"), 20 | ] 21 | -------------------------------------------------------------------------------- /captcha/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : util 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.utils import timezone 9 | 10 | from captcha.helpers import captcha_image_url 11 | from captcha.models import CaptchaStore 12 | from common.utils import get_logger 13 | 14 | logger = get_logger(__name__) 15 | 16 | 17 | class CaptchaAuth(object): 18 | def __init__(self, captcha_key='', request=None): 19 | self.captcha_key = captcha_key 20 | self.request = request 21 | 22 | def __get_captcha_obj(self): 23 | return CaptchaStore.objects.filter(hashkey=self.captcha_key).first() 24 | 25 | def generate(self): 26 | self.captcha_key = CaptchaStore.generate_key() 27 | captcha_image = captcha_image_url(self.captcha_key) 28 | if self.request: 29 | captcha_image = self.request.build_absolute_uri(captcha_image) 30 | captcha_obj = self.__get_captcha_obj() 31 | code_length = 0 32 | if captcha_obj: 33 | code_length = len(captcha_obj.response) 34 | return {"captcha_image": captcha_image, "captcha_key": self.captcha_key, "length": code_length} 35 | 36 | def valid(self, verify_code): 37 | try: 38 | CaptchaStore.objects.get( 39 | response=verify_code.strip(" ").lower(), hashkey=self.captcha_key, expiration__gt=timezone.now() 40 | ).delete() 41 | except CaptchaStore.DoesNotExist: 42 | return False 43 | return True 44 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/__init__.py -------------------------------------------------------------------------------- /common/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/api/__init__.py -------------------------------------------------------------------------------- /common/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import sys 4 | import threading 5 | import time 6 | 7 | from django.apps import AppConfig 8 | 9 | 10 | class CommonConfig(AppConfig): 11 | default_auto_field = 'django.db.models.BigAutoField' 12 | name = 'common' 13 | 14 | def ready(self): 15 | from .celery import heatbeat # noqa 16 | from . import signal_handlers # noqa 17 | from . import tasks # noqa 18 | from .swagger.utils import OpenApiAuthenticationScheme, OpenApiPrimaryKeyRelatedField # noqa 19 | from .signals import django_ready 20 | excludes = ['migrate', 'compilemessages', 'makemigrations', 'stop'] 21 | for i in excludes: 22 | if i in sys.argv: 23 | return 24 | super().ready() 25 | 26 | def background_task(): 27 | time.sleep(0.1) 28 | django_ready.send(CommonConfig) 29 | 30 | threading.Thread(target=background_task, daemon=True).start() 31 | -------------------------------------------------------------------------------- /common/cache/channel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : channel 5 | # author : ly_13 6 | # date : 3/29/2025 7 | import time 8 | 9 | from channels_redis.core import RedisChannelLayer as _RedisChannelLayer 10 | 11 | 12 | class RedisChannelLayer(_RedisChannelLayer): 13 | layer_expire = 30 # 需要心跳方式发送在线状态,否则将channel移除 14 | 15 | async def group_discard(self, group, channel): 16 | """ 17 | Removes the channel from the named group if it is in the group; 18 | does nothing otherwise (does not error) 19 | """ 20 | assert self.valid_channel_name(channel), "Channel name not valid" 21 | connection, key = await self.auto_expire_layers(group) 22 | await connection.zrem(key, channel) 23 | 24 | async def auto_expire_layers(self, group): 25 | assert self.valid_group_name(group), "Group name not valid" 26 | key = self._group_key(group) 27 | connection = self.connection(self.consistent_hash(group)) 28 | 29 | # Discard old channels based on group_expiry 30 | await connection.zremrangebyscore( 31 | key, min=0, max=int(time.time()) - self.layer_expire 32 | ) 33 | 34 | return connection, key 35 | 36 | async def get_layers(self, group): 37 | connection, key = await self.auto_expire_layers(group) 38 | return [x.decode("utf8") for x in await connection.zrange(key, 0, -1)] 39 | 40 | async def update_active_layers(self, group, channel): 41 | connection, key = await self.auto_expire_layers(group) 42 | await connection.zadd(key, {channel: time.time()}) 43 | await connection.expire(key, self.group_expiry) 44 | 45 | async def get_groups(self): 46 | groups = [] 47 | group = self._group_key("*") 48 | for index in range(self.ring_size): 49 | connection = self.connection(index) 50 | cursor = 0 51 | while True: 52 | cursor, keys = await connection.scan(cursor, match=group) 53 | for key in keys: 54 | groups.append(key.decode("utf8").split(":")[-1]) 55 | if cursor == 0: 56 | break 57 | return groups 58 | -------------------------------------------------------------------------------- /common/cache/state.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : state 5 | # author : ly_13 6 | # date : 6/2/2023 7 | 8 | import time 9 | 10 | from django.core.cache import cache 11 | 12 | from common.utils import get_logger 13 | 14 | logger = get_logger(__name__) 15 | 16 | 17 | class CacheBaseState(object): 18 | 19 | def __init__(self, key, value=time.time(), timeout=3600 * 24): 20 | self.key = f"CacheBaseState_{self.__class__.__name__}_{key}" 21 | self.value = value 22 | self.timeout = timeout 23 | self.active = False 24 | 25 | def get_state(self): 26 | return cache.get(self.key) 27 | 28 | def del_state(self): 29 | return cache.delete(self.key) 30 | 31 | def __enter__(self): 32 | if cache.get(self.key): 33 | return False 34 | else: 35 | cache.set(self.key, self.value, self.timeout) 36 | self.active = True 37 | return True 38 | 39 | def __exit__(self, exc_type, exc_val, exc_tb): 40 | if self.active: 41 | cache.delete(self.key) 42 | logger.info(f"cache base state __exit__ {exc_type}, {exc_val}, {exc_tb}") 43 | 44 | 45 | class SyncDriveSizeState(CacheBaseState): 46 | ... 47 | 48 | 49 | class GetDriveAuthCache(CacheBaseState): 50 | ... 51 | -------------------------------------------------------------------------------- /common/celery/flower.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin_server 4 | # filename : flower 5 | # author : ly_13 6 | # date : 6/29/2023 7 | import base64 8 | 9 | from django.conf import settings 10 | from django.http import HttpResponse 11 | from django.utils.translation import gettext_lazy as _ 12 | from django.views.decorators.clickjacking import xframe_options_exempt 13 | from drf_spectacular.utils import extend_schema 14 | from proxy.views import proxy_view 15 | from rest_framework.generics import GenericAPIView 16 | 17 | from common.utils import get_logger 18 | 19 | logger = get_logger(__name__) 20 | 21 | flower_url = f'{settings.CELERY_FLOWER_HOST}:{settings.CELERY_FLOWER_PORT}' 22 | 23 | 24 | class CeleryFlowerAPIView(GenericAPIView): 25 | """celery 定时任务""" 26 | 27 | @extend_schema(exclude=True) 28 | @xframe_options_exempt 29 | def get(self, request, path): 30 | """获取{cls}""" 31 | remote_url = 'http://{}/api/flower/{}'.format(flower_url, path) 32 | try: 33 | basic_auth = base64.b64encode(settings.CELERY_FLOWER_AUTH.encode('utf-8')).decode('utf-8') 34 | response = proxy_view(request, remote_url, { 35 | 'headers': { 36 | 'Authorization': f"Basic {basic_auth}" 37 | } 38 | }) 39 | except Exception as e: 40 | logger.warning(f"celery flower service unavailable. {e}") 41 | msg = _("

Celery flower service unavailable. Please contact the administrator

") 42 | response = HttpResponse(msg) 43 | return response 44 | 45 | @extend_schema(exclude=True) 46 | @xframe_options_exempt 47 | def post(self, request, path): 48 | """操作{cls}""" 49 | return self.get(request, path) 50 | -------------------------------------------------------------------------------- /common/celery/heatbeat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : heatbeat 5 | # author : ly_13 6 | # date : 10/23/2024 7 | import os.path 8 | import tempfile 9 | from pathlib import Path 10 | 11 | from celery.signals import heartbeat_sent, worker_ready, worker_shutdown 12 | 13 | temp_dir = tempfile.gettempdir() 14 | 15 | 16 | @heartbeat_sent.connect 17 | def heartbeat(sender, **kwargs): 18 | worker_name = sender.eventer.hostname.split('@')[0] 19 | heartbeat_path = Path(os.path.join(temp_dir, f'worker_heartbeat_{worker_name}')) 20 | heartbeat_path.touch() 21 | 22 | 23 | @worker_ready.connect 24 | def worker_ready(sender, **kwargs): 25 | worker_name = sender.hostname.split('@')[0] 26 | ready_path = Path(os.path.join(temp_dir, f'worker_ready_{worker_name}')) 27 | ready_path.touch() 28 | 29 | 30 | @worker_shutdown.connect 31 | def worker_shutdown(sender, **kwargs): 32 | worker_name = sender.hostname.split('@')[0] 33 | for signal in ['ready', 'heartbeat']: 34 | path = Path(os.path.join(temp_dir, f'worker_{signal}_{worker_name}')) 35 | path.unlink(missing_ok=True) 36 | -------------------------------------------------------------------------------- /common/core/auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : system 5 | # author : ly_13 6 | # date : 6/2/2023 7 | import functools 8 | import hashlib 9 | 10 | from django.http.cookie import parse_cookie 11 | from django.utils.translation import gettext_lazy as _ 12 | from rest_framework.exceptions import NotAuthenticated 13 | from rest_framework_simplejwt.authentication import JWTAuthentication 14 | from rest_framework_simplejwt.exceptions import TokenError 15 | from rest_framework_simplejwt.tokens import AccessToken 16 | 17 | from common.cache.storage import BlackAccessTokenCache 18 | 19 | 20 | def auth_required(view_func): 21 | @functools.wraps(view_func) 22 | def wrapper(view, request, *args, **kwargs): 23 | if request.user and request.user.is_authenticated: 24 | return view_func(view, request, *args, **kwargs) 25 | raise NotAuthenticated(_("Unauthorized authentication")) 26 | 27 | return wrapper 28 | 29 | 30 | class ServerAccessToken(AccessToken): 31 | """ 32 | 自定义的token方法是为了登出的时候,将 access token 禁用 33 | """ 34 | 35 | def verify(self): 36 | user_id = self.payload.get('user_id') 37 | if BlackAccessTokenCache(user_id, hashlib.md5(self.token).hexdigest()).get_storage_cache(): 38 | raise TokenError(_("Token is invalid or expired")) 39 | super().verify() 40 | 41 | 42 | class GetUserFromAccessToken(AccessToken): 43 | token_type = "refresh" 44 | 45 | 46 | class CookieJWTAuthentication(JWTAuthentication): 47 | """ 48 | 支持cookie认证,是为了可以访问 django-proxy 的页面,比如 flower 49 | """ 50 | 51 | def get_header(self, request): 52 | header = super().get_header(request) 53 | if not header: 54 | cookies = request.META.get('HTTP_COOKIE') 55 | if cookies: 56 | cookie_dict = parse_cookie(cookies) 57 | if cookie_dict and cookie_dict.get('X-Token'): 58 | header = f"Bearer {cookie_dict.get('X-Token')}".encode('utf-8') 59 | return header 60 | -------------------------------------------------------------------------------- /common/core/db/router.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : router 5 | # author : ly_13 6 | # date : 12/18/2023 7 | 8 | # https://docs.djangoproject.com/zh-hans/5.0/topics/db/multi-db/#automatic-database-routing 9 | class DBRouter: 10 | """ 11 | A router to control all database operations on models 12 | """ 13 | 14 | def db_for_read(self, model, **hints): 15 | """ 16 | 建议用于读取“模型”类型对象的数据库。 17 | 如果数据库操作可以提供有助于选择数据库的任何附加信息,它将在 hints 中提供。这里 below 提供了有效提示的详细信息。 18 | 如果没有建议,则返回 None 。 19 | """ 20 | # if model._meta.app_label == "auth": 21 | # return "auth_db" 22 | return None 23 | 24 | def db_for_write(self, model, **hints): 25 | """ 26 | 建议用于写入模型类型对象的数据库。 27 | 如果数据库操作可以提供有助于选择数据库的任何附加信息,它将在 hints 中提供。这里 below 提供了有效提示的详细信息。 28 | 如果没有建议,则返回 None 。 29 | """ 30 | # if model._meta.app_label in ["auth", "contenttypes"]: 31 | # return "auth_db" 32 | return None 33 | 34 | def allow_relation(self, obj1, obj2, **hints): 35 | """ 36 | 如果允许 obj1 和 obj2 之间的关系,返回 True 。如果阻止关系,返回 False ,或如果路由没意见,则返回 None。 37 | 这纯粹是一种验证操作,由外键和多对多操作决定是否应该允许关系。 38 | 如果没有路由有意见(比如所有路由返回 None),则只允许同一个数据库内的关系。 39 | """ 40 | return None 41 | 42 | def allow_migrate(self, db, app_label, model_name=None, **hints): 43 | """ 44 | 决定是否允许迁移操作在别名为 db 的数据库上运行。如果操作运行,那么返回 True ,如果没有运行则返回 False ,或路由没有意见则返回 None 。 45 | app_label 参数是要迁移的应用程序的标签。 46 | model_name 由大部分迁移操作设置来要迁移的模型的 model._meta.model_name (模型 __name__ 的小写版本) 的值。 47 | 对于 RunPython 和 RunSQL 操作的值是 None ,除非它们提示要提供它。 48 | """ 49 | # if model._meta.app_label in ["auth", "contenttypes"]: 50 | # return db == "auth_db" 51 | return None 52 | -------------------------------------------------------------------------------- /common/core/pagination.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : pagination 5 | # author : ly_13 6 | # date : 6/16/2023 7 | # -*- coding: utf-8 -*- 8 | 9 | 10 | from collections import OrderedDict 11 | 12 | from drf_spectacular.plumbing import build_object_type, build_basic_type 13 | from drf_spectacular.types import OpenApiTypes 14 | from rest_framework.pagination import PageNumberPagination 15 | from rest_framework.response import Response 16 | 17 | 18 | class PageNumber(PageNumberPagination): 19 | page_size = 20 # 每页显示多少条 20 | page_size_query_param = 'size' # URL中每页显示条数的参数 21 | page_query_param = 'page' # URL中页码的参数 22 | max_page_size = 100 # 返回最大数据条数 23 | 24 | def get_paginated_response(self, data): 25 | return Response(OrderedDict([ 26 | ('total', self.page.paginator.count), 27 | # ('next', self.get_next_link()), 28 | # ('previous', self.get_previous_link()), 29 | ('results', data) 30 | ])) 31 | 32 | def get_paginated_response_schema(self, schema): 33 | return build_object_type( 34 | properties={ 35 | 'code': build_basic_type(OpenApiTypes.NUMBER), 36 | 'detail': build_basic_type(OpenApiTypes.STR), 37 | 'data': build_object_type( 38 | properties={ 39 | 'total': build_basic_type(OpenApiTypes.NUMBER), 40 | 'results': schema 41 | } 42 | ), 43 | } 44 | ) 45 | 46 | 47 | class DynamicPageNumber(object): 48 | def __init__(self, max_page_size=100, page_size=20): 49 | self.max_page_size = max_page_size 50 | self.page_size = page_size 51 | 52 | def __call__(self, *args, **kwargs): 53 | instance = PageNumber() 54 | instance.max_page_size = self.max_page_size 55 | instance.page_size = self.page_size 56 | return instance 57 | -------------------------------------------------------------------------------- /common/core/response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : response 5 | # author : ly_13 6 | # date : 6/2/2023 7 | import datetime 8 | 9 | from django.utils.translation import gettext_lazy as _ 10 | from rest_framework.response import Response 11 | 12 | from server.utils import get_current_request 13 | 14 | 15 | class ApiResponse(Response): 16 | def __init__(self, code=1000, detail=None, data=None, status=None, headers=None, content_type=None, **kwargs): 17 | dic = { 18 | 'code': code, 19 | 'detail': detail if detail else (_("Operation successful") if code == 1000 else _("Operation failed")), 20 | 'requestId': str(getattr(get_current_request(), 'request_uuid', "")), 21 | 'timestamp': str(datetime.datetime.now()), 22 | } 23 | if data is not None: 24 | dic['data'] = data 25 | dic.update(kwargs) 26 | self._data = data 27 | # 对象来调用对象的绑定方法,会自动传值 28 | super().__init__(data=dic, status=status, headers=headers, content_type=content_type) 29 | 30 | # 类来调用对象的绑定方法,这个方法就是一个普通函数,有几个参数就要传几个参数 31 | # Response.__init__(data=dic,status=status,headers=headers,content_type=content_type) 32 | -------------------------------------------------------------------------------- /common/core/routers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : routers 5 | # author : ly_13 6 | # date : 7/31/2024 7 | from rest_framework.routers import SimpleRouter, Route, DynamicRoute 8 | 9 | 10 | class NoDetailRouter(SimpleRouter): 11 | routes = [ 12 | # List route. 13 | Route( 14 | url=r'^{prefix}{trailing_slash}$', 15 | mapping={ 16 | 'post': 'create', 17 | 'get': 'retrieve', 18 | 'put': 'update', 19 | 'patch': 'partial_update', 20 | 'delete': 'destroy' 21 | }, 22 | name='{basename}-detail', 23 | detail=False, 24 | initkwargs={'suffix': 'Instance'} 25 | ), 26 | # Dynamically generated list routes. Generated using 27 | # @action(detail=False) decorator on methods of the viewset. 28 | DynamicRoute( 29 | url=r'^{prefix}/{url_path}{trailing_slash}$', 30 | name='{basename}-{url_name}', 31 | detail=False, 32 | initkwargs={} 33 | ), 34 | ] 35 | 36 | def __init__(self, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | -------------------------------------------------------------------------------- /common/core/throttle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : throttle 5 | # author : ly_13 6 | # date : 6/2/2023 7 | 8 | 9 | from rest_framework.throttling import UserRateThrottle, AnonRateThrottle 10 | 11 | 12 | class RegisterThrottle(AnonRateThrottle): 13 | scope = "register" 14 | 15 | 16 | class ResetPasswordThrottle(AnonRateThrottle): 17 | scope = "reset_password" 18 | 19 | 20 | class LoginThrottle(AnonRateThrottle): 21 | scope = "login" 22 | 23 | 24 | class UploadThrottle(UserRateThrottle): 25 | """上传速率限制""" 26 | scope = "upload" 27 | 28 | 29 | class Download1Throttle(UserRateThrottle): 30 | """下载速率限制""" 31 | scope = "download1" 32 | 33 | 34 | class Download2Throttle(UserRateThrottle): 35 | """下载速率限制""" 36 | scope = "download2" 37 | -------------------------------------------------------------------------------- /common/core/validators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : validators 5 | # author : ly_13 6 | # date : 8/6/2024 7 | import phonenumbers 8 | from django.utils.translation import gettext_lazy as _ 9 | from phonenumbers import NumberParseException 10 | from rest_framework import serializers 11 | 12 | 13 | class PhoneValidator: 14 | message = _('The phone number format is incorrect') 15 | 16 | def __call__(self, value): 17 | if not value: 18 | return 19 | 20 | try: 21 | phone = phonenumbers.parse(value, 'CN') 22 | valid = phonenumbers.is_valid_number(phone) 23 | except NumberParseException: 24 | valid = False 25 | 26 | if not valid: 27 | raise serializers.ValidationError(self.message) 28 | -------------------------------------------------------------------------------- /common/drf/const.py: -------------------------------------------------------------------------------- 1 | CSV_FILE_ESCAPE_CHARS = ['=', '@', '0'] 2 | -------------------------------------------------------------------------------- /common/drf/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .axios_form_data import * 2 | from .csv import * 3 | from .excel import * 4 | -------------------------------------------------------------------------------- /common/drf/parsers/csv.py: -------------------------------------------------------------------------------- 1 | # ~*~ coding: utf-8 ~*~ 2 | # 3 | 4 | import chardet 5 | import unicodecsv 6 | 7 | from .base import BaseFileParser 8 | from ..const import CSV_FILE_ESCAPE_CHARS 9 | from ..utils import lazyproperty 10 | 11 | 12 | class CSVFileParser(BaseFileParser): 13 | media_type = 'text/csv' 14 | 15 | @lazyproperty 16 | def match_escape_chars(self): 17 | chars = [] 18 | for c in CSV_FILE_ESCAPE_CHARS: 19 | dq_char = '"{}'.format(c) 20 | sg_char = "'{}".format(c) 21 | chars.append(dq_char) 22 | chars.append(sg_char) 23 | return tuple(chars) 24 | 25 | @staticmethod 26 | def _universal_newlines(stream): 27 | """ 28 | 保证在`通用换行模式`下打开文件 29 | """ 30 | for line in stream.splitlines(): 31 | yield line 32 | 33 | def __parse_row(self, row): 34 | row_escape = [] 35 | for d in row: 36 | if isinstance(d, str) and d.strip().startswith(self.match_escape_chars): 37 | d = d.lstrip("'").lstrip('"') 38 | row_escape.append(d) 39 | return row_escape 40 | 41 | def generate_rows(self, stream_data): 42 | detect_result = chardet.detect(stream_data) 43 | encoding = detect_result.get("encoding", "utf-8") 44 | lines = self._universal_newlines(stream_data) 45 | csv_reader = unicodecsv.reader(lines, encoding=encoding) 46 | for row in csv_reader: 47 | row = self.__parse_row(row) 48 | yield row 49 | -------------------------------------------------------------------------------- /common/drf/parsers/excel.py: -------------------------------------------------------------------------------- 1 | import pyexcel 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from .base import BaseFileParser 5 | 6 | 7 | class ExcelFileParser(BaseFileParser): 8 | media_type = 'text/xlsx' 9 | 10 | def generate_rows(self, stream_data): 11 | try: 12 | workbook = pyexcel.get_book(file_type='xlsx', file_content=stream_data) 13 | except Exception as e: 14 | raise Exception(_('Invalid excel file {}').format(str(e))) 15 | # 默认获取第一个工作表sheet 16 | sheet = workbook.sheet_by_index(0) 17 | rows = sheet.rows() 18 | return rows 19 | -------------------------------------------------------------------------------- /common/drf/renders/__init__.py: -------------------------------------------------------------------------------- 1 | from rest_framework import renderers 2 | 3 | from .csv import * 4 | from .excel import * 5 | 6 | 7 | class PassthroughRenderer(renderers.BaseRenderer): 8 | """ 9 | Return data as-is. View should supply a Response. 10 | """ 11 | media_type = 'application/octet-stream' 12 | format = '' 13 | 14 | def render(self, data, accepted_media_type=None, renderer_context=None): 15 | return data 16 | -------------------------------------------------------------------------------- /common/drf/renders/csv.py: -------------------------------------------------------------------------------- 1 | # ~*~ coding: utf-8 ~*~ 2 | # 3 | 4 | import codecs 5 | 6 | import unicodecsv 7 | from six import BytesIO 8 | 9 | from .base import BaseFileRenderer 10 | from ..const import CSV_FILE_ESCAPE_CHARS 11 | 12 | 13 | class CSVFileRenderer(BaseFileRenderer): 14 | media_type = 'text/csv' 15 | format = 'csv' 16 | writer = None 17 | buffer = None 18 | 19 | escape_chars = tuple(CSV_FILE_ESCAPE_CHARS) 20 | 21 | def initial_writer(self): 22 | csv_buffer = BytesIO() 23 | csv_buffer.write(codecs.BOM_UTF8) 24 | csv_writer = unicodecsv.writer(csv_buffer, encoding='utf-8') 25 | self.buffer = csv_buffer 26 | self.writer = csv_writer 27 | 28 | def __render_row(self, row): 29 | row_escape = [] 30 | for d in row: 31 | if isinstance(d, str) and d.strip().startswith(self.escape_chars): 32 | d = "'{}".format(d) 33 | row_escape.append(d) 34 | return row_escape 35 | 36 | def write_row(self, row): 37 | row = self.__render_row(row) 38 | self.writer.writerow(row) 39 | 40 | def get_rendered_value(self): 41 | value = self.buffer.getvalue() 42 | return value 43 | -------------------------------------------------------------------------------- /common/drf/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : utils 5 | # author : ly_13 6 | # date : 6/5/2024 7 | 8 | class lazyproperty: 9 | def __init__(self, func): 10 | self.func = func 11 | 12 | def __get__(self, instance, cls): 13 | if instance is None: 14 | return self 15 | else: 16 | value = self.func(instance) 17 | setattr(instance, self.func.__name__, value) 18 | return value 19 | -------------------------------------------------------------------------------- /common/fields/char.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : aes 5 | # author : ly_13 6 | # date : 1/17/2024 7 | 8 | from django.conf import settings 9 | from django.db import models 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from common.base.utils import AESCipher 13 | 14 | 15 | class AESField(models.Field): 16 | 17 | def __init__(self, *args, **kwargs): 18 | if 'prefix' in kwargs: 19 | self.prefix = kwargs['prefix'] 20 | del kwargs['prefix'] 21 | else: 22 | self.prefix = "aes:::" 23 | self.cipher = AESCipher(settings.SECRET_KEY) 24 | super(AESField, self).__init__(*args, **kwargs) 25 | 26 | def deconstruct(self): 27 | name, path, args, kwargs = super(AESField, self).deconstruct() 28 | if self.prefix != "aes:::": 29 | kwargs['prefix'] = self.prefix 30 | return name, path, args, kwargs 31 | 32 | def from_db_value(self, value, *args, **kwargs): 33 | if value is None: 34 | return value 35 | if value.startswith(self.prefix): 36 | value = value[len(self.prefix):] 37 | if isinstance(value, str): 38 | value = value.encode('utf-8') 39 | value = self.cipher.decrypt(value) 40 | return value 41 | 42 | def to_python(self, value): 43 | if value is None: 44 | return value 45 | elif value.startswith(self.prefix): 46 | value = value[len(self.prefix):] 47 | if isinstance(value, str): 48 | value = value.encode('utf-8') 49 | value = self.cipher.decrypt(value) 50 | return value 51 | 52 | def get_prep_value(self, value): 53 | if isinstance(value, str): 54 | value = value.encode('utf-8') 55 | if isinstance(value, bytes): 56 | value = self.cipher.encrypt(value) 57 | value = self.prefix + value.decode('utf-8') 58 | elif value is not None: 59 | raise TypeError(_("{} is not a valid value for AESCharField").format(value)) 60 | return value 61 | 62 | 63 | class AESCharField(AESField, models.CharField): 64 | pass 65 | 66 | 67 | class AESTextField(AESField, models.TextField): 68 | pass 69 | -------------------------------------------------------------------------------- /common/fields/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : utils 5 | # author : ly_13 6 | # date : 7/25/2024 7 | from functools import wraps 8 | 9 | from django.db.models.fields.files import FieldFile 10 | from rest_framework.fields import Field as RFField 11 | 12 | 13 | def get_file_absolute_uri(value: FieldFile, request=None, use_url=True): 14 | if not value: 15 | return None 16 | 17 | if use_url: 18 | try: 19 | url = value.url 20 | except AttributeError: 21 | return None 22 | if request is not None: 23 | return request.build_absolute_uri(url) 24 | return url 25 | 26 | return value.name 27 | 28 | 29 | def input_wrapper(func): 30 | """ 31 | 增加 input_type 参数,用于前端识别 32 | """ 33 | 34 | @wraps(func) 35 | def wrapper(*args, **kwargs) -> RFField: 36 | class Field(func): 37 | def __init__(self, *_args, **_kwargs): 38 | self.input_type = _kwargs.pop("input_type", '') 39 | super().__init__(*_args, **_kwargs) 40 | 41 | return Field(*args, **kwargs) 42 | 43 | return wrapper 44 | -------------------------------------------------------------------------------- /common/local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : local 5 | # author : ly_13 6 | # date : 10/18/2024 7 | 8 | 9 | from asgiref.local import Local 10 | 11 | thread_local = Local(thread_critical=True) 12 | 13 | 14 | def _find(attr): 15 | return getattr(thread_local, attr, None) 16 | -------------------------------------------------------------------------------- /common/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/management/__init__.py -------------------------------------------------------------------------------- /common/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/management/commands/__init__.py -------------------------------------------------------------------------------- /common/management/commands/download_ip_db.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from common.management.commands.services.hands import download_ip_db 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Download IP database' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument('-f', '--force', nargs="?", help="force download database", default=False, const=True) 11 | 12 | def handle(self, *args, **options): 13 | download_ip_db(force=options['force']) 14 | -------------------------------------------------------------------------------- /common/management/commands/expire_caches.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from common.cache.storage import RedisCacheBase 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Expire Caches' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument( 11 | "args", metavar="cache key", nargs="+", help="please input cache key or '*' for delete all keys" 12 | ) 13 | 14 | def handle(self, *args, **options): 15 | for key in args: 16 | if key.endswith("*"): 17 | RedisCacheBase(key).del_many() 18 | else: 19 | RedisCacheBase(key).del_storage_cache() 20 | -------------------------------------------------------------------------------- /common/management/commands/restart.py: -------------------------------------------------------------------------------- 1 | from .services.command import BaseActionCommand, Action 2 | 3 | 4 | class Command(BaseActionCommand): 5 | help = 'Restart services' 6 | action = Action.restart.value 7 | -------------------------------------------------------------------------------- /common/management/commands/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/management/commands/services/__init__.py -------------------------------------------------------------------------------- /common/management/commands/services/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .beat import * 2 | from .celery_default import * 3 | from .flower import * 4 | from .gunicorn import * 5 | -------------------------------------------------------------------------------- /common/management/commands/services/services/beat.py: -------------------------------------------------------------------------------- 1 | from .base import BaseService 2 | from ..hands import * 3 | 4 | __all__ = ['BeatService'] 5 | 6 | 7 | class BeatService(BaseService): 8 | 9 | def __init__(self, **kwargs): 10 | super().__init__(**kwargs) 11 | 12 | @property 13 | def cmd(self): 14 | scheduler = "django_celery_beat.schedulers:DatabaseScheduler" 15 | print("\n- Start Beat as Periodic Task Scheduler") 16 | cmd = [ 17 | 'celery', '-A', 18 | 'server', 'beat', 19 | '-l', 'INFO', 20 | '--scheduler', scheduler, 21 | '--max-interval', '60' 22 | ] 23 | return cmd 24 | 25 | @property 26 | def cwd(self): 27 | return APPS_DIR 28 | -------------------------------------------------------------------------------- /common/management/commands/services/services/celery_base.py: -------------------------------------------------------------------------------- 1 | from .base import BaseService 2 | from ..hands import * 3 | 4 | 5 | class CeleryBaseService(BaseService): 6 | 7 | def __init__(self, queue, num=10, **kwargs): 8 | super().__init__(**kwargs) 9 | self.queue = queue 10 | self.num = num 11 | self.autoscale = settings.CELERY_WORKER_AUTOSCALE 12 | 13 | @property 14 | def cmd(self): 15 | print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize())) 16 | os.environ.setdefault('LC_ALL', 'C.UTF-8') 17 | os.environ.setdefault('PYTHONOPTIMIZE', '1') 18 | 19 | if os.getuid() == 0: 20 | os.environ.setdefault('C_FORCE_ROOT', '1') 21 | server_hostname = os.environ.get("SERVER_HOSTNAME") 22 | if not server_hostname: 23 | server_hostname = '%h' 24 | 25 | cmd = [ 26 | 'celery', 27 | '-A', 'server', 28 | 'worker', 29 | '-P', 'threads', # 默认的prefork是资源隔离的,导致修改settings配置时候,无法同步数据到该线程,因此需要用 threads模式 30 | '-l', 'INFO', 31 | '-c', str(self.num), 32 | # '--autoscale', ",".join([str(x) for x in self.autoscale]), # 开启自动弹性伸缩 33 | '-Q', self.queue, 34 | '--heartbeat-interval', '10', 35 | '-n', f'{self.queue}@{server_hostname}', 36 | '--without-mingle', 37 | ] 38 | return cmd 39 | 40 | @property 41 | def cwd(self): 42 | return APPS_DIR 43 | -------------------------------------------------------------------------------- /common/management/commands/services/services/celery_default.py: -------------------------------------------------------------------------------- 1 | from .celery_base import CeleryBaseService 2 | 3 | __all__ = ['CeleryDefaultService'] 4 | 5 | 6 | class CeleryDefaultService(CeleryBaseService): 7 | 8 | def __init__(self, **kwargs): 9 | kwargs['queue'] = 'celery' 10 | super().__init__(**kwargs) 11 | -------------------------------------------------------------------------------- /common/management/commands/services/services/flower.py: -------------------------------------------------------------------------------- 1 | from .base import BaseService 2 | from ..hands import * 3 | 4 | __all__ = ['FlowerService'] 5 | 6 | 7 | class FlowerService(BaseService): 8 | # https://flower.readthedocs.io/en/latest/man.html?highlight=pool#description 9 | def __init__(self, **kwargs): 10 | super().__init__(**kwargs) 11 | 12 | @property 13 | def db_file(self): 14 | return os.path.join(LOG_DIR, 'flower.db') 15 | 16 | @property 17 | def cmd(self): 18 | print("\n- Start Flower as Task Monitor") 19 | 20 | if os.getuid() == 0: 21 | os.environ.setdefault('C_FORCE_ROOT', '1') 22 | cmd = [ 23 | 'celery', 24 | '-A', 'server', 25 | 'flower', 26 | '-logging=info', 27 | '--url_prefix=api/flower', 28 | '--auto_refresh=False', 29 | '--max_tasks=1000', 30 | '--persistent=True', 31 | '--state_save_interval=600000', 32 | f'--basic-auth={CELERY_FLOWER_AUTH}', # 注释则代表 flower 只读权限 33 | f'-db={self.db_file}', 34 | '--state_save_interval=600000', 35 | f'--address={CELERY_FLOWER_HOST}', 36 | f'--port={CELERY_FLOWER_PORT}', 37 | ] 38 | if settings.DEBUG: 39 | cmd += ['--debug'] 40 | return cmd 41 | 42 | @property 43 | def cwd(self): 44 | return APPS_DIR 45 | -------------------------------------------------------------------------------- /common/management/commands/services/services/gunicorn.py: -------------------------------------------------------------------------------- 1 | from common.startup import CoreTerminal 2 | from .base import BaseService 3 | from ..hands import * 4 | 5 | __all__ = ['GunicornService'] 6 | 7 | 8 | class GunicornService(BaseService): 9 | 10 | def __init__(self, **kwargs): 11 | self.worker = kwargs['worker_gunicorn'] 12 | super().__init__(**kwargs) 13 | 14 | @property 15 | def cmd(self): 16 | print("\n- Start Gunicorn ASGI HTTP Server") 17 | 18 | log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' 19 | bind = f'{HTTP_HOST}:{HTTP_PORT}' 20 | 21 | cmd = [ 22 | 'gunicorn', 'server.asgi:application', 23 | '-b', bind, 24 | '-k', 'uvicorn.workers.UvicornWorker', 25 | '-w', str(self.worker), 26 | '--max-requests', '10240', 27 | '--max-requests-jitter', '2048', 28 | '--graceful-timeout', '30', 29 | '--access-logformat', log_format, 30 | '--access-logfile', '-' 31 | ] 32 | if DEBUG: 33 | cmd.append('--reload') 34 | return cmd 35 | 36 | @property 37 | def cwd(self): 38 | return APPS_DIR 39 | 40 | def start_other(self): 41 | core_terminal = CoreTerminal() 42 | core_terminal.start_heartbeat_thread() 43 | -------------------------------------------------------------------------------- /common/management/commands/start.py: -------------------------------------------------------------------------------- 1 | from .services.command import BaseActionCommand, Action 2 | 3 | 4 | class Command(BaseActionCommand): 5 | help = 'Start services' 6 | action = Action.start.value 7 | -------------------------------------------------------------------------------- /common/management/commands/status.py: -------------------------------------------------------------------------------- 1 | from .services.command import BaseActionCommand, Action 2 | 3 | 4 | class Command(BaseActionCommand): 5 | help = 'Show services status' 6 | action = Action.status.value 7 | -------------------------------------------------------------------------------- /common/management/commands/stop.py: -------------------------------------------------------------------------------- 1 | from .services.command import BaseActionCommand, Action 2 | 3 | 4 | class Command(BaseActionCommand): 5 | help = 'Stop services' 6 | action = Action.stop.value 7 | -------------------------------------------------------------------------------- /common/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-23 06:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Monitor', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('cpu_load', models.FloatField(default=0, verbose_name='CPU Load')), 18 | ('cpu_percent', models.FloatField(default=0, verbose_name='CPU Percent')), 19 | ('memory_used', models.FloatField(verbose_name='Memory Used')), 20 | ('disk_used', models.FloatField(default=0, verbose_name='Disk Used')), 21 | ('boot_time', models.FloatField(default=0, verbose_name='Boot Time')), 22 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='Created time')), 23 | ], 24 | options={ 25 | 'verbose_name': 'Monitor', 26 | 'verbose_name_plural': 'Monitor', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /common/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/common/migrations/__init__.py -------------------------------------------------------------------------------- /common/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : models 5 | # author : ly_13 6 | # date : 9/14/2024 7 | 8 | from django.db import models 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | 12 | class Monitor(models.Model): 13 | cpu_load = models.FloatField(verbose_name=_("CPU Load"), default=0) 14 | cpu_percent = models.FloatField(verbose_name=_("CPU Percent"), default=0) 15 | memory_used = models.FloatField(verbose_name=_("Memory Used")) 16 | disk_used = models.FloatField(verbose_name=_("Disk Used"), default=0) 17 | boot_time = models.FloatField(verbose_name=_("Boot Time"), default=0) 18 | created_time = models.DateTimeField(auto_now_add=True, verbose_name=_("Created time")) 19 | 20 | class Meta: 21 | verbose_name = _("Monitor") 22 | verbose_name_plural = verbose_name 23 | 24 | def __str__(self): 25 | return "%s-%s" % (self.created_time, self.cpu_load) 26 | -------------------------------------------------------------------------------- /common/sdk/sms/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : base 5 | # author : ly_13 6 | # date : 8/6/2024 7 | from common.utils import get_logger 8 | 9 | logger = get_logger(__name__) 10 | 11 | 12 | class BaseSMSClient: 13 | """ 14 | 短信终端的基类 15 | """ 16 | 17 | SIGN_AND_TMPL_SETTING_FIELD_PREFIX: str 18 | 19 | @classmethod 20 | def new_from_settings(cls): 21 | raise NotImplementedError 22 | 23 | def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs): 24 | raise NotImplementedError 25 | 26 | @staticmethod 27 | def need_pre_check(): 28 | return True 29 | -------------------------------------------------------------------------------- /common/sdk/sms/endpoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : endpoint 5 | # author : ly_13 6 | # date : 8/6/2024 7 | import importlib 8 | from collections import OrderedDict 9 | 10 | from django.conf import settings 11 | from django.db.models import TextChoices 12 | from django.utils.translation import gettext_lazy as _ 13 | from rest_framework.exceptions import APIException 14 | 15 | from common.utils import get_logger 16 | from .base import BaseSMSClient 17 | 18 | logger = get_logger(__name__) 19 | 20 | 21 | class BACKENDS(TextChoices): 22 | ALIBABA = 'alibaba', _('Alibaba cloud') 23 | 24 | 25 | class SMS: 26 | client: BaseSMSClient 27 | 28 | def __init__(self, backend=None): 29 | backend = backend or settings.SMS_BACKEND 30 | if backend not in BACKENDS: 31 | raise APIException( 32 | code='sms_provider_not_support', 33 | detail=_('SMS provider not support: {}').format(backend) 34 | ) 35 | m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__) 36 | self.client = m.client.new_from_settings() 37 | 38 | def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs): 39 | return self.client.send_sms( 40 | phone_numbers=phone_numbers, 41 | sign_name=sign_name, 42 | template_code=template_code, 43 | template_param=template_param, 44 | **kwargs 45 | ) 46 | 47 | def send_verify_code(self, phone_number, code): 48 | prefix = getattr(self.client, 'SIGN_AND_TMPL_SETTING_FIELD_PREFIX', '') 49 | sign_name = getattr(settings, f'{prefix}_VERIFY_SIGN_NAME', None) 50 | template_code = getattr(settings, f'{prefix}_VERIFY_TEMPLATE_CODE', None) 51 | 52 | if self.client.need_pre_check() and not (sign_name and template_code): 53 | raise APIException( 54 | code='verify_code_sign_tmpl_invalid', 55 | detail=_('SMS verification code signature or template invalid') 56 | ) 57 | return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code)) 58 | -------------------------------------------------------------------------------- /common/sdk/sms/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : exceptions 5 | # author : ly_13 6 | # date : 8/6/2024 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from rest_framework.exceptions import APIException 10 | 11 | 12 | class CodeExpired(APIException): 13 | default_code = 'verify_code_expired' 14 | default_detail = _('The verification code has expired. Please resend it') 15 | 16 | 17 | class CodeError(APIException): 18 | default_code = 'verify_code_error' 19 | default_detail = _('The verification code is incorrect') 20 | 21 | 22 | class CodeSendTooFrequently(APIException): 23 | default_code = 'code_send_too_frequently' 24 | default_detail = _('Please wait {} seconds before sending') 25 | 26 | def __init__(self, ttl): 27 | super().__init__(detail=self.default_detail.format(ttl)) 28 | 29 | 30 | class CodeSendOverRate(APIException): 31 | default_code = 'code_send_over_rate' 32 | default_detail = _('Please wait {} seconds before sending') 33 | 34 | def __init__(self, ttl): 35 | super().__init__(detail=self.default_detail.format(ttl)) 36 | -------------------------------------------------------------------------------- /common/serializers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : serializers 5 | # author : ly_13 6 | # date : 9/14/2024 7 | from rest_framework import serializers 8 | 9 | from common.models import Monitor 10 | 11 | 12 | class MonitorSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = Monitor 15 | fields = ['cpu_load', 'cpu_percent', 'memory_used', 'disk_used', 'boot_time', 'created_time'] 16 | extra_kwargs = { 17 | "cpu_load": {'default': 0}, 18 | "cpu_percent": {'default': 0}, 19 | "memory_used": {'default': 0}, 20 | "disk_used": {'default': 0}, 21 | "boot_time": {'default': 0}, 22 | } 23 | -------------------------------------------------------------------------------- /common/signals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : signals 5 | # author : ly_13 6 | # date : 7/31/2024 7 | from django.dispatch import Signal 8 | 9 | django_ready = Signal() 10 | -------------------------------------------------------------------------------- /common/startup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : startup 5 | # author : ly_13 6 | # date : 9/14/2024 7 | 8 | import os 9 | import socket 10 | import threading 11 | import time 12 | 13 | from django.conf import settings 14 | 15 | from common.core.db.utils import close_old_connections 16 | from common.decorators import Singleton 17 | from common.serializers import MonitorSerializer 18 | from common.utils import get_cpu_load, get_memory_usage, get_disk_usage, get_boot_time, get_cpu_percent 19 | 20 | 21 | class BaseTerminal(object): 22 | 23 | def __init__(self, suffix_name, _type): 24 | server_hostname = os.environ.get('SERVER_HOSTNAME') or '' 25 | hostname = socket.gethostname() 26 | if server_hostname: 27 | name = f'[{suffix_name}]-{server_hostname}' 28 | else: 29 | name = f'[{suffix_name}]-{hostname}' 30 | self.name = name 31 | self.interval = 30 32 | self.remote_addr = self.get_remote_addr(hostname) 33 | self.type = _type 34 | 35 | @staticmethod 36 | def get_remote_addr(hostname): 37 | try: 38 | return socket.gethostbyname(hostname) 39 | except socket.gaierror: 40 | return '127.0.0.1' 41 | 42 | def start_heartbeat_thread(self): 43 | print(f'- Start heartbeat thread => ({self.name})') 44 | t = threading.Thread(target=self.start_heartbeat, daemon=True) 45 | t.start() 46 | 47 | def start_heartbeat(self): 48 | while True: 49 | heartbeat_data = { 50 | 'cpu_load': get_cpu_load(), 51 | 'cpu_percent': get_cpu_percent(), 52 | 'memory_used': get_memory_usage(), 53 | 'disk_used': get_disk_usage(path=settings.PROJECT_DIR), 54 | 'boot_time': get_boot_time(), 55 | } 56 | status_serializer = MonitorSerializer(data=heartbeat_data) 57 | status_serializer.is_valid() 58 | 59 | try: 60 | status_serializer.save() 61 | time.sleep(self.interval) 62 | except Exception: 63 | print("Save status error, close old connections") 64 | close_old_connections() 65 | finally: 66 | time.sleep(self.interval) 67 | 68 | 69 | @Singleton 70 | class CoreTerminal(BaseTerminal): 71 | 72 | def __init__(self): 73 | super().__init__(suffix_name='Core', _type='core') 74 | -------------------------------------------------------------------------------- /common/swagger/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : views 5 | # author : ly_13 6 | # date : 8/12/2024 7 | 8 | from django.contrib.auth import login, logout 9 | from django.shortcuts import redirect 10 | from django.utils.translation import gettext_lazy as _ 11 | from django.views.decorators.clickjacking import xframe_options_exempt 12 | from drf_spectacular.utils import extend_schema 13 | from rest_framework.generics import GenericAPIView 14 | from rest_framework_simplejwt.serializers import TokenObtainSerializer 15 | 16 | from common.core.response import ApiResponse 17 | 18 | 19 | class ApiLogin(GenericAPIView): 20 | """接口文档的登录接口""" 21 | permission_classes = () 22 | serializer_class = TokenObtainSerializer 23 | 24 | @extend_schema(exclude=True) 25 | @xframe_options_exempt 26 | def post(self, request, *args, **kwargs): 27 | 28 | serializer = self.get_serializer(data=request.data) 29 | try: 30 | serializer.is_valid(raise_exception=True) 31 | login(request, serializer.user) 32 | except Exception as e: 33 | return ApiResponse(detail=_("Incorrect username/password")) 34 | response = redirect(request.query_params.get("next", "/api-docs/swagger/")) 35 | return response 36 | 37 | @extend_schema(exclude=True) 38 | @xframe_options_exempt 39 | def get(self, request, *args, **kwargs): 40 | if request.user.is_authenticated: 41 | return redirect(to="/api-docs/swagger/") 42 | return ApiResponse(detail=_("Please enter your account information to log in")) 43 | 44 | 45 | class ApiLogout(GenericAPIView): 46 | permission_classes = [] 47 | 48 | @extend_schema(exclude=True) 49 | @xframe_options_exempt 50 | def get(self, request, *args, **kwargs): 51 | logout(request) 52 | return redirect("/api-docs/login/") 53 | -------------------------------------------------------------------------------- /common/templates/monitor/msg_terminal_performance.html: -------------------------------------------------------------------------------- 1 |
2 | {% for term, errors in terms_with_errors %} 3 |

{{ term.name }}

4 |
    5 | {% for error in errors %} 6 |
  • {{ error }}
  • 7 | {% endfor %} 8 |
9 | {% endfor %} 10 |
-------------------------------------------------------------------------------- /common/templates/notify/msg_task.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

3 | {% trans 'Hello' %} {{ name }}, 4 |

5 |

6 | {% trans 'Task name' %}: {{ task_name }}
7 | {% trans 'Your Task is finished' %}
8 | {% if state %} 9 | {{ status }} 10 | {% else %} 11 | {{ status }} 12 | {% endif %} 13 |

14 | 15 | - 16 | {% for task in tasks %} 17 |

18 | {% trans 'Task Id' %}: {{ task.task_id }}
19 | {% trans 'Task start date' %}: {{ task.start_time }}
20 | {% trans 'Task end date' %}: {{ task.end_time }}
21 | {% trans 'Task result' %}: {{ task.result }} 22 |

23 |
24 | {% endfor %} 25 | -------------------------------------------------------------------------------- /common/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : urls 5 | # author : ly_13 6 | # date : 6/6/2023 7 | from django.urls import re_path 8 | 9 | from common.api.common import ResourcesIDCacheAPIView, CountryListAPIView, HealthCheckAPIView 10 | 11 | app_name = "common" 12 | 13 | urlpatterns = [ 14 | re_path('^resources/cache$', ResourcesIDCacheAPIView.as_view(), name='resources-cache'), 15 | re_path('^countries$', CountryListAPIView.as_view(), name='countries'), 16 | re_path('^api/health', HealthCheckAPIView.as_view(), name='health'), 17 | ] 18 | -------------------------------------------------------------------------------- /common/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | from .random import * 3 | -------------------------------------------------------------------------------- /common/utils/country.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : country 5 | # author : ly_13 6 | # date : 8/6/2024 7 | import gettext 8 | 9 | import phonenumbers 10 | import pycountry 11 | from django.utils.translation import gettext_lazy as _ 12 | from phonenumbers import PhoneMetadata 13 | 14 | 15 | def get_country_phone_codes(): 16 | phone_codes = [] 17 | for region_code in phonenumbers.SUPPORTED_REGIONS: 18 | phone_metadata = PhoneMetadata.metadata_for_region(region_code) 19 | if phone_metadata: 20 | phone_codes.append((region_code, phone_metadata.country_code)) 21 | return phone_codes 22 | 23 | 24 | def get_country(region_code): 25 | country = pycountry.countries.get(alpha_2=region_code) 26 | if country: 27 | return country 28 | else: 29 | return None 30 | 31 | 32 | def get_country_phone_choices(locales=None): 33 | codes = get_country_phone_codes() 34 | choices = [] 35 | german = None 36 | if locales: 37 | german = gettext.translation( 38 | "iso3166-1", pycountry.LOCALES_DIR, languages=[locales] 39 | ) 40 | for code, phone in codes: 41 | country = get_country(code) 42 | if not country: 43 | continue 44 | country_name = country.name 45 | flag = country.flag 46 | 47 | if country.name == 'China': 48 | country_name = _('China') 49 | 50 | if code == 'TW': 51 | country_name = 'Taiwan' 52 | flag = get_country('CN').flag 53 | choices.append({ 54 | 'name': german.gettext(country_name) if german else country_name, 55 | 'phone_code': f'+{phone}', 56 | 'flag': flag, 57 | 'code': code, 58 | }) 59 | 60 | choices.sort(key=lambda x: x['name']) 61 | return choices 62 | 63 | 64 | COUNTRY_CALLING_CODES = get_country_phone_choices() 65 | COUNTRY_CALLING_CODES_ZH = get_country_phone_choices("zh") 66 | -------------------------------------------------------------------------------- /common/utils/file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : file 5 | # author : ly_13 6 | # date : 8/27/2024 7 | import requests 8 | 9 | 10 | def download_file(src, path): 11 | with requests.get(src, stream=True) as r: 12 | r.raise_for_status() 13 | with open(path, 'wb') as f: 14 | for chunk in r.iter_content(chunk_size=8192): 15 | f.write(chunk) 16 | -------------------------------------------------------------------------------- /common/utils/ip/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | -------------------------------------------------------------------------------- /common/utils/ip/geoip/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import * 2 | -------------------------------------------------------------------------------- /common/utils/ip/geoip/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | import ipaddress 4 | import os 5 | 6 | import geoip2.database 7 | from django.conf import settings 8 | from django.utils.translation import gettext_lazy as _ 9 | from geoip2.errors import GeoIP2Error 10 | 11 | __all__ = ['get_ip_city_by_geoip'] 12 | reader = None 13 | 14 | 15 | def get_ip_city_by_geoip(ip): 16 | global reader 17 | try: 18 | if reader is None: 19 | path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb') 20 | reader = geoip2.database.Reader(path) 21 | is_private = ipaddress.ip_address(ip.strip()).is_private 22 | if is_private: 23 | return _('LAN') 24 | except ValueError: 25 | return _("Invalid ip") 26 | except Exception: 27 | return _("Unknown") 28 | try: 29 | response = reader.city(ip) 30 | except GeoIP2Error: 31 | return _("Unknown") 32 | 33 | city_names = response.city.names or {} 34 | lang = settings.LANGUAGE_CODE[:2] 35 | if lang == 'zh': 36 | lang = 'zh-CN' 37 | city = city_names.get(lang, _("Unknown")) 38 | return city 39 | -------------------------------------------------------------------------------- /common/utils/ip/ipip/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | from .utils import * 4 | -------------------------------------------------------------------------------- /common/utils/ip/ipip/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | import os 4 | 5 | import ipdb 6 | 7 | __all__ = ['get_ip_city_by_ipip'] 8 | ipip_db = None 9 | 10 | 11 | def get_ip_city_by_ipip(ip): 12 | global ipip_db 13 | try: 14 | if ipip_db is None: 15 | ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb') 16 | ipip_db = ipdb.City(ipip_db_path) 17 | info = ipip_db.find_info(ip, 'CN') 18 | except ValueError: 19 | return None 20 | except Exception: 21 | return None 22 | if not info: 23 | raise None 24 | return {'city': info.city_name, 'country': info.country_name} 25 | -------------------------------------------------------------------------------- /common/utils/timezone.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.utils import timezone as dj_timezone 4 | 5 | 6 | def as_current_tz(dt: datetime): 7 | return dt.astimezone(dj_timezone.get_current_timezone()) 8 | 9 | 10 | def utc_now(): 11 | return dj_timezone.now() 12 | 13 | 14 | def local_now(): 15 | return dj_timezone.localtime(dj_timezone.now()) 16 | 17 | 18 | def local_now_display(fmt='%Y-%m-%d %H:%M:%S'): 19 | return local_now().strftime(fmt) 20 | 21 | 22 | def local_now_filename(): 23 | return local_now().strftime('%Y%m%d-%H%M%S') 24 | 25 | 26 | def local_now_date_display(fmt='%Y-%m-%d'): 27 | return local_now().strftime(fmt) 28 | 29 | 30 | def local_zero_hour(fmt='%Y-%m-%d'): 31 | return datetime.strptime(local_now().strftime(fmt), fmt) 32 | -------------------------------------------------------------------------------- /config_example.yml: -------------------------------------------------------------------------------- 1 | # SECURITY WARNING: keep the secret key used in production secret! 2 | # 加密密钥 生产服必须保证唯一性,你必须保证这个值的安全,否则攻击者可以用它来生成自己的签名值 3 | # $ cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 49;echo 4 | SECRET_KEY: 5 | 6 | # Development env open this, when error occur display the full process track, Production disable it 7 | # DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志,正式服要禁用,开发阶段,需要取消该注释,否则会导致前端ws连接失败 8 | # DEBUG: true 9 | 10 | # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/zh-hans/5.0/topics/logging/ 11 | # 日志级别 12 | # LOG_LEVEL: DEBUG 13 | 14 | # 用于DEBUG模式下,输出sql日志 15 | # DEBUG_DEV: true 16 | 17 | # Database setting, Support sqlite3, mysql, postgres .... 18 | # 数据库设置 19 | # ### 更多数据库配置,参考官方文档:https://docs.djangoproject.com/zh-hans/5.0/ref/databases/ 20 | # 创建竖数据库sql 21 | # create database xadmin default character set utf8mb4 COLLATE utf8mb4_bin; 22 | # grant all on xadmin.* to server@'127.0.0.1' identified by 'KGzKjZpWBp4R4RSa'; 23 | 24 | # SQLite setting: 25 | # 使用单文件sqlite数据库 26 | # DB_ENGINE: sqlite3 27 | # DB_DATABASE: 28 | # MySQL or postgres setting like: 29 | # DB_ENGINE can set mysql, oracle, postgresql, sqlite3 30 | 31 | # 使用 Mariadb 作为数据库 32 | #DB_ENGINE: mysql 33 | #DB_HOST: mysql 34 | #DB_PORT: 3306 35 | 36 | # 使用 postgresql 作为数据库[默认数据库] 37 | DB_ENGINE: postgresql 38 | DB_HOST: postgresql 39 | DB_PORT: 5432 40 | 41 | DB_USER: server 42 | DB_DATABASE: xadmin 43 | #DB_PASSWORD: KGzKjZpWBp4R4RSa 44 | 45 | 46 | # Use Redis as broker for celery and web socket 47 | # Redis配置 48 | REDIS_HOST: redis 49 | REDIS_PORT: 6379 50 | #REDIS_PASSWORD: nineven 51 | #DEFAULT_CACHE_ID: 1 52 | #CHANNEL_LAYERS_CACHE_ID: 2 53 | #CELERY_BROKER_CACHE_ID: 3 54 | 55 | # When Django start it will bind this host and port 56 | # ./manage.py runserver 127.0.0.1:8896 57 | # 运行时绑定端口 58 | HTTP_BIND_HOST: 0.0.0.0 59 | HTTP_LISTEN_PORT: 8896 60 | GUNICORN_MAX_WORKER: 4 61 | 62 | # 需要将创建的应用写到里面 63 | XADMIN_APPS: -------------------------------------------------------------------------------- /data/logs/task/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/upload/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/demo/__init__.py -------------------------------------------------------------------------------- /demo/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /demo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'demo' 7 | -------------------------------------------------------------------------------- /demo/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : config 5 | # author : ly_13 6 | # date : 6/12/2024 7 | 8 | 9 | from django.urls import path, include 10 | 11 | # 路由配置,当添加APP完成时候,会自动注入路由到总服务 12 | URLPATTERNS = [ 13 | path('api/demo/', include('demo.urls')), 14 | ] 15 | # 请求白名单,支持正则表达式,可参考settings.py里面的 PERMISSION_WHITE_URL 16 | PERMISSION_WHITE_REURL = [] 17 | -------------------------------------------------------------------------------- /demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /demo/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : urls 5 | # author : ly_13 6 | # date : 6/6/2023 7 | from rest_framework.routers import SimpleRouter 8 | 9 | from demo.views import BookViewSet 10 | 11 | app_name = 'demo' 12 | 13 | router = SimpleRouter(False) # 设置为 False ,为了去掉url后面的斜线 14 | 15 | router.register('book', BookViewSet, basename='book') 16 | 17 | urlpatterns = [ 18 | ] 19 | urlpatterns += router.urls 20 | -------------------------------------------------------------------------------- /demo/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | 3 | 4 | from django_filters import rest_framework as filters 5 | from rest_framework.decorators import action 6 | 7 | from common.core.filter import BaseFilterSet 8 | from common.core.modelset import BaseModelSet, ImportExportDataAction 9 | from common.core.pagination import DynamicPageNumber 10 | from common.core.response import ApiResponse 11 | from common.utils import get_logger 12 | from demo.models import Book 13 | from demo.serializers.book import BookSerializer 14 | 15 | logger = get_logger(__name__) 16 | 17 | 18 | class BookViewSetFilter(BaseFilterSet): 19 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 20 | author = filters.CharFilter(field_name='author', lookup_expr='icontains') 21 | publisher = filters.CharFilter(field_name='publisher', lookup_expr='icontains') 22 | 23 | class Meta: 24 | model = Book 25 | fields = ['name', 'isbn', 'author', 'publisher', 'is_active', 'publication_date', 'price', 26 | 'created_time'] # fields用于前端自动生成的搜索表单 27 | 28 | 29 | class BookViewSet(BaseModelSet, ImportExportDataAction): 30 | """书籍""" # 这个 书籍 的注释得写, 否则菜单中可能会显示null,访问日志记录中也可能显示异常 31 | 32 | queryset = Book.objects.all() 33 | serializer_class = BookSerializer 34 | ordering_fields = ['created_time'] 35 | filterset_class = BookViewSetFilter 36 | pagination_class = DynamicPageNumber(1000) # 表示最大分页数据1000条,如果注释,则默认最大100条数据 37 | 38 | @action(methods=['post'], detail=True) 39 | def push(self, request, *args, **kwargs): 40 | """推送到其他服务""" # 这个 推送到其他服务 的注释得写, 否则菜单中可能会显示null,访问日志记录中也可能显示异常 41 | 42 | # 自定义一个请求为post的 push 路由行为,执行自定义操作, action装饰器有好多参数,可以查看源码自行分析 43 | instance = self.get_object() 44 | return ApiResponse(detail=f"{instance.name} 推送成功") 45 | -------------------------------------------------------------------------------- /docs/data-permission.md: -------------------------------------------------------------------------------- 1 | ## 数据权限控制 2 | 3 | 原理: 数据权限是通过 ```queryset.filter``` 来实现。 4 | 5 | 在 ```settings.py``` 定义了一个全局的```DEFAULT_FILTER_BACKENDS```, 6 | 具体方法```common.core.filter.BaseDataPermissionFilter``` 7 | 8 | 具体的实现方式参考```common.core.filter.get_filter_queryset``` 9 | 10 | 如果自定义方法使用全局的filter,可以通过下面获取queryset对象 11 | 12 | ```python 13 | filter_queryset = self.filter_queryset(self.get_queryset()) 14 | ``` 15 | 16 | 1. 权限模式, 且模式表示数据需要同时满足规则列表中的每条规则,或模式即满足任意一条规则即可 17 | 2. 若存在菜单权限,则该权限仅针对所选择的菜单权限生效 18 | 19 | ## 如何使用?本次使用是查询指定条件用户的数据权限 20 | 21 | ### 1. 在前端页面菜单中,添加数据权限,然后选择菜单为查询用户,规则选择 22 | 23 | ![add-data-permission.png](imgs/data-permission/add-data-permission.png) 24 | 25 | ![add-data-permission-rules.png](imgs/data-permission/add-data-permission-rules.png) 26 | 27 | 将该数据权限分配给用户即可 -------------------------------------------------------------------------------- /docs/imgs/data-permission/add-data-permission-rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/docs/imgs/data-permission/add-data-permission-rules.png -------------------------------------------------------------------------------- /docs/imgs/data-permission/add-data-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/docs/imgs/data-permission/add-data-permission.png -------------------------------------------------------------------------------- /docs/imgs/field-permission/add-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/docs/imgs/field-permission/add-role.png -------------------------------------------------------------------------------- /docs/imgs/field-permission/add-user-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/docs/imgs/field-permission/add-user-menu.png -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function cleanup() 3 | { 4 | local pids 5 | pids=$(jobs -p) 6 | if [[ "${pids}" != "" ]]; then 7 | kill "${pids}" >/dev/null 2>/dev/null 8 | fi 9 | } 10 | 11 | action="${1-start}" 12 | service="${2-all}" 13 | 14 | trap cleanup EXIT 15 | 16 | rm -f /data/xadmin-server/tmp/*.pid 17 | 18 | if [[ "${action:0:1}" == "/" ]];then 19 | "$@" 20 | elif [[ "$action" == "bash" || "$action" == "sh" ]];then 21 | bash 22 | elif [[ "$action" == "sleep" ]];then 23 | echo "Sleep 365 days" 24 | sleep 365d 25 | else 26 | python manage.py "${action}" "${service}" 27 | fi 28 | 29 | -------------------------------------------------------------------------------- /loadjson/setting.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /loadjson/userinfo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "system.userinfo", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$720000$Do0cosVxbLzxwVi3I5HEwF$zdwmPpcZg9Frj8Df6sVkPx6ECus6mWaEF3Lt25hglTs=", 7 | "last_login": null, 8 | "is_superuser": true, 9 | "username": "isummer", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "nineven@qq.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-07-19T05:33:55.341Z", 16 | "created_time": "2024-07-19T05:33:55.758Z", 17 | "updated_time": "2024-07-19T05:33:55.758Z", 18 | "description": null, 19 | "creator": null, 20 | "modifier": null, 21 | "dept_belong": null, 22 | "mode_type": 0, 23 | "avatar": "", 24 | "nickname": "", 25 | "gender": 0, 26 | "phone": "", 27 | "dept": null, 28 | "groups": [], 29 | "user_permissions": [], 30 | "roles": [], 31 | "rules": [] 32 | } 33 | }, 34 | { 35 | "model": "system.userinfo", 36 | "pk": 2, 37 | "fields": { 38 | "password": "pbkdf2_sha256$720000$Jit275P4xwt3isqGjNaFNU$SntnpI6m0NGVGC+cljtFxp2XU+EYYJ5K4XA4OcvhlH4=", 39 | "last_login": "2024-07-19T05:52:30.014Z", 40 | "is_superuser": false, 41 | "username": "admin", 42 | "first_name": "", 43 | "last_name": "", 44 | "email": "", 45 | "is_staff": false, 46 | "is_active": true, 47 | "date_joined": "2024-07-19T05:40:43.121Z", 48 | "created_time": "2024-07-19T05:40:43.121Z", 49 | "updated_time": "2024-07-19T05:52:26.638Z", 50 | "description": null, 51 | "creator": 1, 52 | "modifier": 1, 53 | "dept_belong": null, 54 | "mode_type": 0, 55 | "avatar": "", 56 | "nickname": "", 57 | "gender": 0, 58 | "phone": "", 59 | "dept": "93b4c4df-f65b-4347-9d20-4785b09a9ca3", 60 | "groups": [], 61 | "user_permissions": [], 62 | "roles": [], 63 | "rules": [ 64 | "541738f4-be19-4ef7-9d39-38178caaae20", 65 | "db58769c-040f-41f2-874e-9be9fbd93790" 66 | ] 67 | } 68 | } 69 | ] -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /message/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/message/__init__.py -------------------------------------------------------------------------------- /message/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MessageConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'message' 7 | -------------------------------------------------------------------------------- /message/routing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : routing 5 | # author : ly_13 6 | # date : 6/2/2023 7 | 8 | from django.urls import re_path 9 | 10 | from . import notify 11 | 12 | app_name = 'message' 13 | 14 | urlpatterns = [ 15 | re_path(r"ws/message/(?P[\w+|\-?]+)+/(?P\w+)$", notify.MessageNotify.as_asgi()), 16 | ] 17 | -------------------------------------------------------------------------------- /notifications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/notifications/__init__.py -------------------------------------------------------------------------------- /notifications/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from notifications.models import * 5 | 6 | admin.site.register(MessageContent) 7 | admin.site.register(MessageUserRead) 8 | admin.site.register(UserMsgSubscription) 9 | admin.site.register(SystemMsgSubscription) 10 | -------------------------------------------------------------------------------- /notifications/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class NotificationsConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'notifications' 8 | verbose_name = _('App Notifications') 9 | 10 | def ready(self): 11 | from notifications.backends import BACKEND # noqa 12 | from . import signal_handlers # noqa 13 | from . import notifications # noqa 14 | super().ready() 15 | -------------------------------------------------------------------------------- /notifications/backends/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | client_name_mapper = {} 7 | 8 | 9 | class BACKEND(models.TextChoices): 10 | EMAIL = 'email', _('Email') 11 | SITE_MSG = 'site_msg', _('Site message') 12 | 13 | # DINGTALK = 'dingtalk', _('DingTalk') 14 | # SMS = 'sms', _('SMS') 15 | 16 | @property 17 | def client(self): 18 | return client_name_mapper[self] 19 | 20 | def get_account(self, user): 21 | return self.client.get_account(user) 22 | 23 | @property 24 | def is_enable(self): 25 | return self.client.is_enable() 26 | 27 | @classmethod 28 | def filter_enable_backends(cls, backends): 29 | enable_backends = [b for b in backends if cls(b).is_enable] 30 | return enable_backends 31 | 32 | 33 | for b in BACKEND: 34 | m = importlib.import_module(f'.{b}', __package__) 35 | client_name_mapper[b] = m.backend 36 | -------------------------------------------------------------------------------- /notifications/backends/base.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | class BackendBase: 5 | # User 表中的字段 6 | account_field = None 7 | 8 | # Django setting 中的字段名 9 | is_enable_field_in_settings = None 10 | 11 | def get_accounts(self, users): 12 | accounts = [] 13 | unbound_users = [] 14 | account_user_mapper = {} 15 | 16 | for user in users: 17 | account = getattr(user, self.account_field, None) 18 | if account: 19 | account_user_mapper[account] = user 20 | accounts.append(account) 21 | else: 22 | unbound_users.append(user) 23 | return accounts, unbound_users, account_user_mapper 24 | 25 | @classmethod 26 | def get_account(cls, user): 27 | return getattr(user, cls.account_field) 28 | 29 | @classmethod 30 | def is_enable(cls): 31 | enable = getattr(settings, cls.is_enable_field_in_settings) 32 | return bool(enable) 33 | -------------------------------------------------------------------------------- /notifications/backends/email.py: -------------------------------------------------------------------------------- 1 | from common.tasks import send_mail_async 2 | from .base import BackendBase 3 | 4 | 5 | class Email(BackendBase): 6 | account_field = 'email' 7 | is_enable_field_in_settings = 'EMAIL_ENABLED' 8 | 9 | def send_msg(self, users, message, subject): 10 | accounts, __, __ = self.get_accounts(users) 11 | if not accounts: 12 | return 13 | send_mail_async(subject, message, accounts, html_message=message) 14 | 15 | 16 | backend = Email 17 | -------------------------------------------------------------------------------- /notifications/backends/site_msg.py: -------------------------------------------------------------------------------- 1 | from notifications.message import SiteMessageUtil as Client 2 | from .base import BackendBase 3 | 4 | 5 | class SiteMessage(BackendBase): 6 | account_field = 'id' 7 | 8 | def send_msg(self, users, message, subject, **kwargs): 9 | accounts, __, __ = self.get_accounts(users) 10 | Client.send_msg(subject, message, user_ids=accounts, **kwargs) 11 | 12 | @classmethod 13 | def is_enable(cls): 14 | return True 15 | 16 | 17 | backend = SiteMessage 18 | -------------------------------------------------------------------------------- /notifications/backends/sms.py: -------------------------------------------------------------------------------- 1 | from common.sdk.sms.endpoint import SMS 2 | from .base import BackendBase 3 | 4 | 5 | class SMS(BackendBase): 6 | account_field = 'phone' 7 | is_enable_field_in_settings = 'SMS_ENABLED' 8 | 9 | def __init__(self): 10 | self.client = SMS() 11 | 12 | def send_msg(self, users, sign_name: str, template_code: str, template_param: dict): 13 | accounts, __, __ = self.get_accounts(users) 14 | if not accounts: 15 | return 16 | return self.client.send_sms(accounts, sign_name, template_code, template_param) 17 | 18 | 19 | backend = SMS 20 | -------------------------------------------------------------------------------- /notifications/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/notifications/migrations/__init__.py -------------------------------------------------------------------------------- /notifications/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .message import * 2 | from .notification import * 3 | -------------------------------------------------------------------------------- /notifications/models/notification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : notification 5 | # author : ly_13 6 | # date : 9/13/2024 7 | 8 | from django.db import models 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | from common.core.models import DbAuditModel 12 | 13 | 14 | class UserMsgSubscription(DbAuditModel): 15 | message_type = models.CharField(max_length=128, verbose_name=_('message type')) 16 | user = models.ForeignKey('system.UserInfo', related_name='user_msg_subscription', on_delete=models.CASCADE, 17 | verbose_name=_('User')) 18 | receive_backends = models.JSONField(default=list, verbose_name=_('receive backend')) 19 | 20 | class Meta: 21 | verbose_name = _('User message subscription') 22 | unique_together = (('user', 'message_type'),) 23 | 24 | def __str__(self): 25 | return _('{} subscription').format(self.user) 26 | 27 | 28 | class SystemMsgSubscription(DbAuditModel): 29 | message_type = models.CharField(max_length=128, unique=True, verbose_name=_('message type')) 30 | users = models.ManyToManyField('system.UserInfo', related_name='system_msg_subscriptions', verbose_name=_("User")) 31 | receive_backends = models.JSONField(default=list, verbose_name=_('receive backend')) 32 | 33 | class Meta: 34 | verbose_name = _('System message subscription') 35 | 36 | def __str__(self): 37 | return f'{self.message_type} -- {self.receive_backends}' 38 | -------------------------------------------------------------------------------- /notifications/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from .notifications import * 2 | -------------------------------------------------------------------------------- /notifications/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /notifications/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import SimpleRouter 2 | 3 | from notifications.views.message import NoticeMessageViewSet, NoticeUserReadMessageViewSet 4 | from notifications.views.notifications import SystemMsgSubscriptionViewSet, UserMsgSubscriptionViewSet 5 | from notifications.views.user_site_msg import UserSiteMessageViewSet 6 | 7 | app_name = 'notifications' 8 | 9 | router = SimpleRouter(False) 10 | 11 | # 消息通知路由 12 | router.register('notice-messages', NoticeMessageViewSet, basename='notice-messages') 13 | router.register('user-read-messages', NoticeUserReadMessageViewSet, basename='user-read-messages') 14 | router.register('site-messages', UserSiteMessageViewSet, basename='site-messages') 15 | 16 | # 消息订阅配置 17 | router.register('system-msg-subscription', SystemMsgSubscriptionViewSet, basename='system-msg-subscription') 18 | router.register('user-msg-subscription', UserMsgSubscriptionViewSet, basename='user-msg-subscription') 19 | 20 | urlpatterns = router.urls 21 | -------------------------------------------------------------------------------- /notifications/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 9/13/2024 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==5.1.7 2 | djangorestframework==3.15.2 3 | django-cors-headers==4.7.0 4 | django-filter==25.1 5 | mysqlclient==2.2.7 6 | psycopg2-binary==2.9.10 7 | django-redis==5.4.0 8 | pycryptodomex==3.22.0 9 | djangorestframework-simplejwt==5.5.0 10 | celery==5.4.0 11 | django-celery-beat==2.7.0 12 | django-celery-results==2.5.1 13 | flower==2.0.1 14 | python-daemon==3.1.2 15 | gunicorn==23.0.0 16 | django-proxy==1.3.0 17 | psutil==6.1.1 18 | uvicorn==0.34.0 19 | daphne==4.1.2 20 | channels==4.2.0 21 | channels-redis==4.2.1 22 | django-ranged-response==0.2.0 23 | user-agents==2.2.0 24 | aiofiles==24.1.0 25 | websockets==15.0.1 26 | django-imagekit==5.0.0 27 | pilkit==3.0 28 | drf-spectacular==0.28.0 29 | drf-spectacular-sidecar==2025.3.1 30 | openpyxl==3.2.0b1 31 | pyzipper==0.3.6 32 | unicodecsv==0.14.1 33 | chardet==5.2.0 34 | pyexcel==0.7.2 35 | pyexcel-xlsx==0.6.1 36 | alibabacloud-dysmsapi20170525==3.1.1 37 | phonenumbers==8.13.55 38 | pycountry==24.6.1 39 | geoip2==4.8.1 40 | ipip-ipdb==1.6.1 41 | requests==2.32.3 42 | html2text==2024.2.26 43 | pyotp==2.9.0 -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | # This will make sure the app is always imported when 2 | # Django starts so that shared_task will use this app. 3 | from .celery import app as celery_app 4 | 5 | __all__ = ('celery_app',) 6 | -------------------------------------------------------------------------------- /server/celery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin_server 4 | # filename : celery 5 | # author : ly_13 6 | # date : 6/29/2023 7 | 8 | import os 9 | 10 | from celery import Celery 11 | 12 | # Set the default Django settings module for the 'celery' program. 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 14 | 15 | app = Celery('server') 16 | 17 | # Using a string here means the worker doesn't have to serialize 18 | # the configuration object to child processes. 19 | # - namespace='CELERY' means all celery-related configuration keys 20 | # should have a `CELERY_` prefix. 21 | app.config_from_object('django.conf:settings', namespace='CELERY') 22 | 23 | # Load task modules from all registered Django apps. 24 | app.autodiscover_tasks() 25 | -------------------------------------------------------------------------------- /server/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | import os 4 | 5 | from .conf import ConfigManager 6 | 7 | __all__ = ['PROJECT_DIR', 'VERSION', 'CONFIG', 'LOG_DIR', 'TMP_DIR', 'CELERY_LOG_DIR'] 8 | 9 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 10 | LOG_DIR = os.path.join(PROJECT_DIR, "data", "logs") 11 | TMP_DIR = os.path.join(PROJECT_DIR, "tmp") 12 | CELERY_LOG_DIR = os.path.join(LOG_DIR, "task") 13 | VERSION = '4.2.1' 14 | CONFIG = ConfigManager.load_user_config() 15 | -------------------------------------------------------------------------------- /server/logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : logging 5 | # author : ly_13 6 | # date : 10/18/2024 7 | import logging 8 | import os 9 | from datetime import datetime, timedelta 10 | from logging.handlers import TimedRotatingFileHandler 11 | 12 | from server.utils import get_current_request 13 | 14 | 15 | class DailyTimedRotatingFileHandler(TimedRotatingFileHandler): 16 | def rotator(self, source, dest): 17 | """ Override the original method to rotate the log file daily.""" 18 | dest = self._get_rotate_dest_filename(source) 19 | if os.path.exists(source) and not os.path.exists(dest): 20 | # 存在多个服务进程时, 保证只有一个进程成功 rotate 21 | os.rename(source, dest) 22 | 23 | @staticmethod 24 | def _get_rotate_dest_filename(source): 25 | date_yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') 26 | path = [os.path.dirname(source), date_yesterday, os.path.basename(source)] 27 | filename = os.path.join(*path) 28 | os.makedirs(os.path.dirname(filename), exist_ok=True) 29 | return filename 30 | 31 | 32 | class ServerFormatter(logging.Formatter): 33 | def format(self, record): 34 | current_request = get_current_request() 35 | record.requestUser = str(current_request.user if current_request else 'SYSTEM')[:16] 36 | record.requestUuid = str(getattr(current_request, 'request_uuid', "")) 37 | return super().format(record) 38 | 39 | 40 | class ColorHandler(logging.StreamHandler): 41 | WHITE = "0" 42 | RED = "31" 43 | GREEN = "32" 44 | YELLOW = "33" 45 | BLUE = "34" 46 | PURPLE = "35" 47 | 48 | def emit(self, record): 49 | try: 50 | msg = self.format(record) 51 | level_color_map = { 52 | logging.DEBUG: self.BLUE, 53 | logging.INFO: self.GREEN, 54 | logging.WARNING: self.YELLOW, 55 | logging.ERROR: self.RED, 56 | logging.CRITICAL: self.PURPLE 57 | } 58 | 59 | csi = f"{chr(27)}[" # 控制序列引入符 60 | color = level_color_map.get(record.levelno, self.WHITE) 61 | 62 | self.stream.write(f"{csi}{color}m{msg}{csi}m\n") 63 | self.flush() 64 | except RecursionError: 65 | raise 66 | except Exception: 67 | self.handleError(record) 68 | -------------------------------------------------------------------------------- /server/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .custom import * 3 | from .libs import * 4 | from .logging import * 5 | from .setting import * 6 | -------------------------------------------------------------------------------- /server/settings/custom.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : custom 5 | # author : ly_13 6 | # date : 11/14/2024 7 | 8 | from ..const import CONFIG 9 | 10 | # 访问白名单配置,无需权限配置, key为路由,value为列表,对应的是请求方式, * 表示全部请求方式, 请求方式为大写 11 | PERMISSION_WHITE_URL = { 12 | "^/api/system/login$": ['*'], 13 | "^/api/system/logout$": ['*'], 14 | "^/api/system/userinfo$": ['GET'], 15 | "^/api/system/routes$": ['*'], 16 | "^/api/system/dashboard/": ['*'], 17 | "^/api/.*choices$": ['*'], 18 | "^/api/.*search-fields$": ['*'], 19 | "^/api/common/resources/cache$": ['*'], 20 | "^/api/notifications/site-messages/unread$": ['*'], 21 | } 22 | 23 | # 前端权限路由 忽略配置 24 | ROUTE_IGNORE_URL = [ 25 | "^/api/system/.*choices$", # 每个方法都有该路由,则忽略即可 26 | "^/api/.*search-fields$", # 每个方法都有该路由,则忽略即可 27 | "^/api/.*search-columns$", # 该路由使用list权限字段,无需重新配置 28 | "^/api/settings/.*search-columns$", # 该路由使用list权限字段,无需重新配置 29 | "^/api/system/dashboard/", # 忽略dashboard路由 30 | "^/api/system/captcha", # 忽略图片验证码路由 31 | ] 32 | 33 | # 访问权限配置 34 | PERMISSION_SHOW_PREFIX = [ 35 | r'api/system', 36 | r'api/settings', 37 | r'api/notifications', 38 | r'api/flower', 39 | r'api-docs', 40 | ] 41 | # 数据权限配置 42 | PERMISSION_DATA_AUTH_APPS = [ 43 | 'system', 44 | 'settings', 45 | 'notifications' 46 | ] 47 | 48 | API_LOG_ENABLE = CONFIG.API_LOG_ENABLE 49 | API_LOG_METHODS = CONFIG.API_LOG_METHODS # 'ALL' 50 | 51 | # 忽略日志记录, 支持model 或者 request_path, 不支持正则 52 | API_LOG_IGNORE = CONFIG.API_LOG_IGNORE 53 | 54 | # 在操作日志中详细记录的请求模块映射 55 | API_MODEL_MAP = CONFIG.API_MODEL_MAP 56 | -------------------------------------------------------------------------------- /server/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : utils 5 | # author : ly_13 6 | # date : 10/18/2024 7 | 8 | from django.conf import settings 9 | from django.db import connection 10 | from django.db.backends.utils import truncate_name 11 | from django.db.models.signals import class_prepared 12 | 13 | from common.local import thread_local 14 | 15 | 16 | def set_current_request(request): 17 | setattr(thread_local, 'current_request', request) 18 | 19 | 20 | def _find(attr): 21 | return getattr(thread_local, attr, None) 22 | 23 | 24 | def get_current_request(): 25 | return _find('current_request') 26 | 27 | 28 | def add_db_prefix(sender, **kwargs): 29 | prefix = settings.DB_PREFIX 30 | meta = sender._meta 31 | if not meta.managed: 32 | return 33 | if isinstance(prefix, dict): 34 | app_label = meta.app_label.lower() 35 | if meta.label_lower in prefix: 36 | prefix = prefix[meta.label_lower] 37 | elif meta.label in prefix: 38 | prefix = prefix[meta.label] 39 | elif app_label in prefix: 40 | prefix = prefix[app_label] 41 | else: 42 | prefix = prefix.get("", None) 43 | if prefix and not meta.db_table.startswith(prefix): 44 | meta.db_table = truncate_name("%s%s" % (prefix, meta.db_table), connection.ops.max_name_length()) 45 | 46 | 47 | class_prepared.connect(add_db_prefix) 48 | -------------------------------------------------------------------------------- /server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/__init__.py -------------------------------------------------------------------------------- /settings/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /settings/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SettingsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'settings' 7 | 8 | def ready(self): 9 | from . import signal_handlers # noqa 10 | super().ready() 11 | -------------------------------------------------------------------------------- /settings/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-23 06:50 2 | 3 | import uuid 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Setting', 17 | fields=[ 18 | ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_time', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Created time')), 20 | ('updated_time', models.DateTimeField(auto_now=True, null=True, verbose_name='Updated time')), 21 | ('description', models.CharField(blank=True, max_length=256, null=True, verbose_name='Description')), 22 | ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), 23 | ('value', models.TextField(blank=True, null=True, verbose_name='Value')), 24 | ('category', models.CharField(default='default', max_length=128, verbose_name='Category')), 25 | ('encrypted', models.BooleanField(default=False, verbose_name='Encrypted')), 26 | ('is_active', models.BooleanField(default=True, verbose_name='Is active')), 27 | ], 28 | options={ 29 | 'verbose_name': 'System setting', 30 | 'verbose_name_plural': 'System setting', 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /settings/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-23 06:50 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | initial = True 10 | 11 | dependencies = [ 12 | ('settings', '0001_initial'), 13 | ('system', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='setting', 20 | name='creator', 21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 22 | related_name='+', related_query_name='creator_query', to=settings.AUTH_USER_MODEL, 23 | verbose_name='Creator'), 24 | ), 25 | migrations.AddField( 26 | model_name='setting', 27 | name='dept_belong', 28 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 29 | related_name='+', related_query_name='dept_belong_query', to='system.deptinfo', 30 | verbose_name='Data ownership department'), 31 | ), 32 | migrations.AddField( 33 | model_name='setting', 34 | name='modifier', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, 36 | related_name='+', related_query_name='modifier_query', to=settings.AUTH_USER_MODEL, 37 | verbose_name='Modifier'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /settings/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/migrations/__init__.py -------------------------------------------------------------------------------- /settings/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 8/1/2024 7 | -------------------------------------------------------------------------------- /settings/serializers/basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : basic 5 | # author : ly_13 6 | # date : 8/1/2024 7 | from django.utils.translation import gettext_lazy as _ 8 | from rest_framework import serializers 9 | 10 | from system.signal import invalid_user_cache_signal 11 | 12 | 13 | class BasicSettingSerializer(serializers.Serializer): 14 | SITE_URL = serializers.URLField( 15 | required=False, label=_("Site URL"), 16 | help_text=_( 17 | 'Site URL is the externally accessible address of the current product ' 18 | 'service and is usually used in links in system emails' 19 | ) 20 | ) 21 | 22 | FRONT_END_WEB_WATERMARK_ENABLED = serializers.BooleanField( 23 | required=False, default=True, label=_("Front-end web watermark"), 24 | help_text=_("Enable watermark for front-end web") 25 | ) 26 | 27 | PERMISSION_FIELD_ENABLED = serializers.BooleanField( 28 | required=False, default=True, label=_("Field permission"), 29 | help_text=_("Field permissions are used to authorize access to data field display") 30 | ) 31 | 32 | PERMISSION_DATA_ENABLED = serializers.BooleanField( 33 | required=False, default=True, label=_("Data permission"), 34 | help_text=_("Data permissions are used to authorize access to data") 35 | ) 36 | 37 | EXPORT_MAX_LIMIT = serializers.IntegerField( 38 | required=False, label=_('Export max limit'), 39 | help_text=_("Limit the maximum number of rows of exported data") 40 | ) 41 | 42 | @staticmethod 43 | def validate_SITE_URL(s): 44 | if not s: 45 | return 'http://127.0.0.1' 46 | return s.strip('/') 47 | 48 | def post_save(self): 49 | if set(getattr(self, '_change_fields', [])) & {'PERMISSION_FIELD_ENABLED', 'PERMISSION_DATA_ENABLED'}: 50 | invalid_user_cache_signal.send(sender=self, user_pk='*') 51 | -------------------------------------------------------------------------------- /settings/serializers/email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : email 5 | # author : ly_13 6 | # date : 8/1/2024 7 | from django.utils.translation import gettext_lazy as _ 8 | from rest_framework import serializers 9 | 10 | 11 | class EmailSettingSerializer(serializers.Serializer): 12 | EMAIL_ENABLED = serializers.BooleanField( 13 | default=False, label=_('Email'), help_text=_('Enable Email Service (Email)') 14 | ) 15 | EMAIL_HOST = serializers.CharField(max_length=1024, required=True, label=_("Host")) 16 | EMAIL_PORT = serializers.CharField(max_length=5, required=True, label=_("Port")) 17 | EMAIL_HOST_USER = serializers.CharField( 18 | max_length=128, required=True, label=_("Account"), 19 | help_text=_("The user to be used for email server authentication") 20 | ) 21 | EMAIL_HOST_PASSWORD = serializers.CharField( 22 | max_length=1024, required=False, label=_("Password"), write_only=True, 23 | help_text=_( 24 | "Password to use for the email server. It is used in conjunction with `User` when authenticating to the email server") 25 | ) 26 | EMAIL_FROM = serializers.CharField( 27 | max_length=128, allow_blank=True, required=False, label=_('Sender'), 28 | help_text=_('Sender email address (default to using the `User`)') 29 | ) 30 | 31 | EMAIL_SUBJECT_PREFIX = serializers.CharField( 32 | max_length=128, allow_blank=True, required=False, label=_('Subject prefix'), 33 | help_text=_("The subject line prefix of the sent email") 34 | ) 35 | EMAIL_USE_SSL = serializers.BooleanField( 36 | required=False, label=_('Use SSL'), 37 | help_text=_( 38 | 'Whether to use an implicit TLS (secure) connection when talking to the SMTP server. In most email documentation this type of TLS connection is referred to as SSL. It is generally used on port 465') 39 | ) 40 | EMAIL_USE_TLS = serializers.BooleanField( 41 | required=False, label=_("Use TLS"), 42 | help_text=_( 43 | 'Whether to use a TLS (secure) connection when talking to the SMTP server. This is used for explicit TLS connections, generally on port 587') 44 | ) 45 | 46 | EMAIL_RECIPIENT = serializers.EmailField( 47 | max_length=128, allow_blank=True, required=False, label=_('Recipient'), 48 | help_text=_("The recipient is used for testing the email server's connectivity") 49 | ) 50 | -------------------------------------------------------------------------------- /settings/serializers/setting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : settings 5 | # author : ly_13 6 | # date : 10/25/2024 7 | from common.core.serializers import BaseModelSerializer 8 | from settings.models import Setting 9 | 10 | 11 | class SettingSerializer(BaseModelSerializer): 12 | class Meta: 13 | model = Setting 14 | fields = ['pk', 'name', 'value', 'category', 'is_active', 'encrypted', 'created_time'] 15 | read_only_fields = ['pk'] 16 | -------------------------------------------------------------------------------- /settings/serializers/sms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : sms 5 | # author : ly_13 6 | # date : 8/6/2024 7 | import phonenumbers 8 | from django.utils.translation import gettext_lazy as _ 9 | from rest_framework import serializers 10 | 11 | from common.core.fields import PhoneField 12 | from common.core.validators import PhoneValidator 13 | from common.sdk.sms.endpoint import BACKENDS 14 | 15 | 16 | class SMSSettingSerializer(serializers.Serializer): 17 | SMS_ENABLED = serializers.BooleanField( 18 | default=False, label=_('SMS'), help_text=_('Enable Short Message Service (SMS)') 19 | ) 20 | SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('Provider'), 21 | help_text=_('Short Message Service (SMS) provider or protocol')) 22 | 23 | 24 | class BaseSMSSettingSerializer(serializers.Serializer): 25 | PREFIX_TITLE = _('SMS') 26 | 27 | SMS_TEST_PHONE = PhoneField( 28 | validators=[PhoneValidator()], required=False, allow_blank=True, allow_null=True, 29 | label=_('Phone'), help_text=_("The phone is used for testing the SMS server's connectivity") 30 | ) 31 | 32 | def post_save(self): 33 | value = self._data['SMS_TEST_PHONE'] 34 | if isinstance(value, dict): 35 | return 36 | try: 37 | phone = phonenumbers.parse(value, 'CN') 38 | value = {'code': '+%s' % phone.country_code, 'phone': phone.national_number} 39 | except phonenumbers.NumberParseException: 40 | value = {'code': '+86', 'phone': value} 41 | self._data['SMS_TEST_PHONE'] = value 42 | 43 | 44 | class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer): 45 | ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='Access Key ID') 46 | ALIBABA_ACCESS_KEY_SECRET = serializers.CharField( 47 | max_length=256, required=False, label='Access Key Secret', write_only=True 48 | ) 49 | ALIBABA_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature')) 50 | ALIBABA_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code')) 51 | 52 | 53 | class SMSBackendSerializer(serializers.Serializer): 54 | name = serializers.CharField(max_length=256, required=True, label=_('Name')) 55 | label = serializers.CharField(max_length=256, required=True, label=_('Label')) 56 | -------------------------------------------------------------------------------- /settings/signal_handlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : signal_handlers.py 5 | # author : ly_13 6 | # date : 7/31/2024 7 | 8 | from django.db.models.signals import post_save 9 | from django.dispatch import receiver 10 | from django.utils.functional import LazyObject 11 | 12 | from common.signals import django_ready 13 | from common.utils import get_logger 14 | from common.utils.connection import RedisPubSub 15 | from settings.models import Setting 16 | 17 | logger = get_logger(__name__) 18 | 19 | 20 | class SettingSubPub(LazyObject): 21 | def _setup(self): 22 | self._wrapped = RedisPubSub('settings') 23 | 24 | 25 | setting_pub_sub = SettingSubPub() 26 | 27 | 28 | @receiver(post_save, sender=Setting) 29 | def refresh_settings_on_changed(sender, instance=None, **kwargs): 30 | if not instance: 31 | return 32 | setting_pub_sub.publish((instance.name, instance.cleaned_value)) 33 | 34 | 35 | @receiver(django_ready) 36 | def on_django_ready_add_db_config(sender, **kwargs): 37 | Setting.refresh_all_settings() 38 | 39 | 40 | @receiver(django_ready) 41 | def subscribe_settings_change(sender, **kwargs): 42 | logger.debug("Start subscribe setting change") 43 | 44 | setting_pub_sub.subscribe(lambda name: Setting.refresh_item(name)) 45 | -------------------------------------------------------------------------------- /settings/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /settings/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : urls 5 | # author : ly_13 6 | # date : 6/6/2023 7 | from rest_framework.routers import SimpleRouter 8 | 9 | from common.core.routers import NoDetailRouter 10 | from settings.views.basic import BasicSettingViewSet 11 | from settings.views.block_ip import SecurityBlockIpViewSet 12 | from settings.views.email import EmailServerSettingViewSet 13 | from settings.views.security import SecurityPasswordRuleViewSet, SecurityLoginLimitViewSet, \ 14 | SecurityLoginAuthViewSet, SecurityRegisterAuthViewSet, SecurityResetPasswordAuthViewSet, \ 15 | SecurityBindEmailAuthViewSet, SecurityBindPhoneAuthViewSet, SecurityVerifyCodeViewSet, SecurityCaptchaCodeViewSet 16 | from settings.views.settings import SettingViewSet 17 | from settings.views.sms import SmsSettingViewSet, SmsConfigViewSet 18 | 19 | app_name = "settings" 20 | 21 | router = SimpleRouter(False) 22 | no_detail_router = NoDetailRouter(False) 23 | 24 | # 设置相关 25 | no_detail_router.register('email', EmailServerSettingViewSet, basename='email-server') 26 | 27 | no_detail_router.register('basic', BasicSettingViewSet, basename='basic') 28 | no_detail_router.register('password', SecurityPasswordRuleViewSet, basename='security-password') 29 | no_detail_router.register('verify', SecurityVerifyCodeViewSet, basename='verify-code') 30 | no_detail_router.register('captcha', SecurityCaptchaCodeViewSet, basename='captcha-code') 31 | 32 | no_detail_router.register('login/limit', SecurityLoginLimitViewSet, basename='security-login-limit') 33 | no_detail_router.register('login/auth', SecurityLoginAuthViewSet, basename='security-login-auth') 34 | 35 | no_detail_router.register('register/auth', SecurityRegisterAuthViewSet, basename='security-register-auth') 36 | no_detail_router.register('reset/auth', SecurityResetPasswordAuthViewSet, basename='security-reset-auth') 37 | no_detail_router.register('bind/email', SecurityBindEmailAuthViewSet, basename='security-bind-email-auth') 38 | no_detail_router.register('bind/phone', SecurityBindPhoneAuthViewSet, basename='security-bind-phone-auth') 39 | 40 | no_detail_router.register('sms', SmsSettingViewSet, basename='sms-settings') 41 | 42 | router.register('ip/block', SecurityBlockIpViewSet, basename='ip-block') 43 | router.register('setting', SettingViewSet, basename='setting') 44 | 45 | no_detail_router.register('sms/config', SmsConfigViewSet, basename='sms-config') 46 | 47 | urlpatterns = no_detail_router.urls + router.urls 48 | -------------------------------------------------------------------------------- /settings/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 7/31/2024 7 | -------------------------------------------------------------------------------- /settings/utils/password.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : password 5 | # author : ly_13 6 | # date : 8/10/2024 7 | import re 8 | 9 | from django.conf import settings 10 | 11 | 12 | def get_password_check_rules(user): 13 | check_rules = [] 14 | for rule in settings.SECURITY_PASSWORD_RULES: 15 | if user.is_superuser and rule == 'SECURITY_PASSWORD_MIN_LENGTH': 16 | rule = 'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH' 17 | value = getattr(settings, rule) 18 | if not value: 19 | continue 20 | check_rules.append({'key': rule, 'value': int(value)}) 21 | return check_rules 22 | 23 | 24 | def check_password_rules(password, is_super_admin=False): 25 | pattern = r"^" 26 | if settings.SECURITY_PASSWORD_UPPER_CASE: 27 | pattern += r'(?=.*[A-Z])' 28 | if settings.SECURITY_PASSWORD_LOWER_CASE: 29 | pattern += r'(?=.*[a-z])' 30 | if settings.SECURITY_PASSWORD_NUMBER: 31 | pattern += r'(?=.*\d)' 32 | if settings.SECURITY_PASSWORD_SPECIAL_CHAR: 33 | pattern += r'(?=.*[`~!@#$%^&*()\-=_+\[\]{}|;:\'",.<>/?])' 34 | pattern += r'[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]' 35 | if is_super_admin: 36 | min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH 37 | else: 38 | min_length = settings.SECURITY_PASSWORD_MIN_LENGTH 39 | pattern += '.{' + str(min_length - 1) + ',}$' 40 | match_obj = re.match(pattern, password) 41 | return bool(match_obj) 42 | -------------------------------------------------------------------------------- /settings/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/settings/views/__init__.py -------------------------------------------------------------------------------- /settings/views/basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : basic 5 | # author : ly_13 6 | # date : 7/31/2024 7 | 8 | from common.utils import get_logger 9 | from settings.serializers.basic import BasicSettingSerializer 10 | from settings.views.settings import BaseSettingViewSet 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | class BasicSettingViewSet(BaseSettingViewSet): 16 | """基本设置""" 17 | serializer_class = BasicSettingSerializer 18 | category = "basic" 19 | -------------------------------------------------------------------------------- /settings/views/block_ip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : block_ip 5 | # author : ly_13 6 | # date : 8/12/2024 7 | import socket 8 | import struct 9 | 10 | from django.conf import settings 11 | from django.core.cache import cache 12 | 13 | from common.core.modelset import ListDeleteModelSet 14 | from settings.models import Setting 15 | from settings.serializers.security import SecurityBlockIPSerializer 16 | from settings.utils.security import LoginIpBlockUtil 17 | 18 | 19 | class FilterIps(list): 20 | 21 | def filter(self, pk__in=None): 22 | if pk__in is None: 23 | pk__in = [] 24 | return [obj.get('ip') for obj in self.__iter__() if obj.get('pk')() in pk__in] 25 | 26 | 27 | class IpUtils(object): 28 | def __init__(self, ip): 29 | self.ip = ip 30 | 31 | def ip_to_int(self): 32 | return str(struct.unpack("!I", socket.inet_aton(self.ip))[0]) 33 | 34 | def int_to_ip(self): 35 | return socket.inet_ntoa(struct.pack("!I", int(self.ip))) 36 | 37 | 38 | class SecurityBlockIpViewSet(ListDeleteModelSet): 39 | """Ip拦截名单""" 40 | serializer_class = SecurityBlockIPSerializer 41 | queryset = Setting.objects.none() 42 | 43 | def filter_queryset(self, obj): 44 | # 为啥写函数,去没有加(), 因为只有在序列化的时候,才会判断,如果是方法就执行,减少资源浪费 45 | data = [{'ip': ip, 'pk': IpUtils(ip).ip_to_int, 'created_time': LoginIpBlockUtil(ip).get_block_info} for ip in 46 | obj] 47 | return FilterIps(data) 48 | 49 | def get_queryset(self): 50 | ips = [] 51 | prefix = LoginIpBlockUtil.BLOCK_KEY_TMPL.replace('{}', '') 52 | keys = cache.keys(f'{prefix}*') 53 | for key in keys: 54 | ips.append(key.replace(prefix, '')) 55 | 56 | white_list = settings.SECURITY_LOGIN_IP_WHITE_LIST 57 | ips = list(set(ips) - set(white_list)) 58 | ips = [ip for ip in ips if ip != '*'] 59 | return ips 60 | 61 | def get_object(self): 62 | return IpUtils(self.kwargs.get("pk")).int_to_ip() 63 | 64 | def perform_destroy(self, ip): 65 | LoginIpBlockUtil(ip).clean_block_if_need() 66 | return 1, 1 67 | -------------------------------------------------------------------------------- /settings/views/security.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : security 5 | # author : ly_13 6 | # date : 8/1/2024 7 | 8 | from common.utils import get_logger 9 | from settings.serializers.security import SecurityPasswordRuleSerializer, SecurityLoginLimitSerializer, \ 10 | SecurityLoginAuthSerializer, SecurityRegisterAuthSerializer, SecurityResetPasswordAuthSerializer, \ 11 | SecurityBindEmailAuthSerializer, SecurityBindPhoneAuthSerializer, SecurityVerifyCodeSerializer, \ 12 | SecurityCaptchaCodeSerializer 13 | from settings.views.settings import BaseSettingViewSet 14 | 15 | logger = get_logger(__name__) 16 | 17 | 18 | class SecurityPasswordRuleViewSet(BaseSettingViewSet): 19 | """密码规则""" 20 | serializer_class = SecurityPasswordRuleSerializer 21 | category = "security_password" 22 | 23 | 24 | class SecurityLoginLimitViewSet(BaseSettingViewSet): 25 | """登录限制""" 26 | serializer_class = SecurityLoginLimitSerializer 27 | category = "security_login_limit" 28 | 29 | 30 | class SecurityLoginAuthViewSet(BaseSettingViewSet): 31 | """登录安全""" 32 | serializer_class = SecurityLoginAuthSerializer 33 | category = "security_login_auth" 34 | 35 | 36 | class SecurityRegisterAuthViewSet(BaseSettingViewSet): 37 | """注册安全""" 38 | serializer_class = SecurityRegisterAuthSerializer 39 | category = "security_register_auth" 40 | 41 | 42 | class SecurityResetPasswordAuthViewSet(BaseSettingViewSet): 43 | """重置密码""" 44 | serializer_class = SecurityResetPasswordAuthSerializer 45 | category = "security_reset_password_auth" 46 | 47 | 48 | class SecurityBindEmailAuthViewSet(BaseSettingViewSet): 49 | """绑定邮件""" 50 | serializer_class = SecurityBindEmailAuthSerializer 51 | category = "security_bind_email_auth" 52 | 53 | 54 | class SecurityBindPhoneAuthViewSet(BaseSettingViewSet): 55 | """绑定手机""" 56 | serializer_class = SecurityBindPhoneAuthSerializer 57 | category = "security_bind_phone_auth" 58 | 59 | 60 | class SecurityVerifyCodeViewSet(BaseSettingViewSet): 61 | """验证码规则""" 62 | serializer_class = SecurityVerifyCodeSerializer 63 | category = "verify" 64 | 65 | 66 | class SecurityCaptchaCodeViewSet(BaseSettingViewSet): 67 | """图片验证码""" 68 | serializer_class = SecurityCaptchaCodeSerializer 69 | category = "captcha" 70 | -------------------------------------------------------------------------------- /system/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/__init__.py -------------------------------------------------------------------------------- /system/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | from django.contrib import admin 3 | 4 | # Register your models here. 5 | from system.models import * 6 | 7 | admin.site.register(UserInfo) 8 | admin.site.register(DeptInfo) 9 | admin.site.register(ModelLabelField) 10 | admin.site.register(UserLoginLog) 11 | admin.site.register(OperationLog) 12 | admin.site.register(MenuMeta) 13 | admin.site.register(Menu) 14 | admin.site.register(DataPermission) 15 | admin.site.register(FieldPermission) 16 | admin.site.register(UserRole) 17 | admin.site.register(UploadFile) 18 | admin.site.register(SystemConfig) 19 | admin.site.register(UserPersonalConfig) 20 | -------------------------------------------------------------------------------- /system/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SystemConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'system' 7 | 8 | def ready(self): 9 | from . import signal_handler # noqa 10 | super().ready() 11 | -------------------------------------------------------------------------------- /system/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/management/__init__.py -------------------------------------------------------------------------------- /system/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/management/commands/__init__.py -------------------------------------------------------------------------------- /system/management/commands/dump_init_json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : dump_init_json 5 | # author : ly_13 6 | # date : 12/25/2023 7 | import os.path 8 | 9 | from django.conf import settings 10 | from django.core import serializers 11 | from django.core.management.base import BaseCommand 12 | 13 | from settings.models import Setting 14 | from system.models import * 15 | 16 | 17 | def get_fields(model): 18 | if issubclass(model, FieldPermission): 19 | exclude_fields = ['updated_time', 'created_time'] 20 | elif issubclass(model, ModelLabelField): 21 | exclude_fields = ['updated_time'] 22 | else: 23 | exclude_fields = [] 24 | 25 | return [x.name for x in model._meta.get_fields() if x.name not in exclude_fields] 26 | 27 | 28 | class Command(BaseCommand): 29 | help = 'dump init json data' 30 | model_names = [UserRole, DeptInfo, Menu, MenuMeta, SystemConfig, DataPermission, FieldPermission, ModelLabelField, 31 | Setting] 32 | 33 | def save_json(self, queryset, filename): 34 | stream = open(filename, 'w', encoding='utf8') 35 | try: 36 | serializers.serialize( 37 | 'json', 38 | queryset, 39 | indent=2, 40 | stream=stream or self.stdout, 41 | object_count=queryset.count(), 42 | fields=get_fields(queryset.model) 43 | ) 44 | except Exception as e: 45 | print(f"{queryset.model._meta.model_name} {filename} dump failed {e}") 46 | finally: 47 | if stream: 48 | stream.close() 49 | 50 | def handle(self, *args, **options): 51 | file_root = os.path.join(settings.PROJECT_DIR, "loadjson") 52 | for model in self.model_names: 53 | self.save_json(model.objects.all().order_by('pk'), 54 | os.path.join(file_root, f"{model._meta.model_name}.json")) 55 | -------------------------------------------------------------------------------- /system/management/commands/expire_config_caches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : expire_config_caches 5 | # author : ly_13 6 | # date : 12/25/2023 7 | from django.core.management.base import BaseCommand 8 | 9 | from common.core.config import ConfigCacheBase 10 | 11 | 12 | class Command(BaseCommand): 13 | help = 'Expire config caches' 14 | 15 | def add_arguments(self, parser): 16 | parser.add_argument('key', nargs='?', type=str, default='*') 17 | 18 | def handle(self, *args, **options): 19 | ConfigCacheBase().invalid_config_cache(options.get('key', '*')) 20 | ConfigCacheBase(px='user').invalid_config_cache(options.get('key', '*')) 21 | -------------------------------------------------------------------------------- /system/management/commands/load_init_json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : load_init_json 5 | # author : ly_13 6 | # date : 12/25/2023 7 | import os.path 8 | 9 | from django.conf import settings 10 | from django.core.management.commands.loaddata import Command as LoadCommand 11 | from django.db import DEFAULT_DB_ALIAS 12 | from django.db.models.signals import ModelSignal 13 | 14 | from settings.models import Setting 15 | from system.models import * 16 | 17 | 18 | class Command(LoadCommand): 19 | help = 'load init json data' 20 | model_names = [MenuMeta, Menu, SystemConfig, DataPermission, UserRole, FieldPermission, ModelLabelField, DeptInfo, 21 | Setting] 22 | missing_args_message = None 23 | 24 | def add_arguments(self, parser): 25 | pass 26 | 27 | def handle(self, *args, **options): 28 | ModelSignal.send = lambda *args, **kwargs: [] # 忽略任何信号 29 | 30 | fixture_labels = [] 31 | file_root = os.path.join(settings.PROJECT_DIR, "loadjson") 32 | for model in self.model_names: 33 | fixture_labels.append(os.path.join(file_root, f"{model._meta.model_name}.json")) 34 | options["ignore"] = "" 35 | options["database"] = DEFAULT_DB_ALIAS 36 | options["app_label"] = "" 37 | options["exclude"] = [] 38 | options["format"] = "json" 39 | super(Command, self).handle(*fixture_labels, **options) 40 | -------------------------------------------------------------------------------- /system/management/commands/sync_model_field.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : sync_model_field 5 | # author : ly_13 6 | # date : 10/25/2024 7 | from django.core.management.base import BaseCommand 8 | 9 | from system.utils.modelfield import sync_model_field 10 | 11 | 12 | class Command(BaseCommand): 13 | help = 'Sync Model Field' 14 | 15 | def handle(self, *args, **options): 16 | sync_model_field() 17 | -------------------------------------------------------------------------------- /system/migrations/0002_operationlog_exec_time_operationlog_request_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2024-10-20 08:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('system', '0001_initial'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='operationlog', 14 | name='exec_time', 15 | field=models.FloatField(blank=True, null=True, verbose_name='Execution time'), 16 | ), 17 | migrations.AddField( 18 | model_name='operationlog', 19 | name='request_uuid', 20 | field=models.UUIDField(blank=True, null=True, verbose_name='Request ID'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /system/migrations/0003_userloginlog_channel_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-03-28 14:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ('system', '0002_operationlog_exec_time_operationlog_request_uuid'), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name='userloginlog', 14 | name='channel_name', 15 | field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Channel name'), 16 | ), 17 | migrations.AlterField( 18 | model_name='userloginlog', 19 | name='login_type', 20 | field=models.SmallIntegerField( 21 | choices=[(0, 'Username and password'), (1, 'SMS verification code'), (2, 'Email verification code'), 22 | (4, 'Wechat scan code'), (8, 'Websocket'), (9, 'Unknown')], default=0, 23 | verbose_name='Login type'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /system/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nineaiyu/xadmin-server/e0c5002da2ed22ef349f8368dba10d4edb3d24a4/system/migrations/__init__.py -------------------------------------------------------------------------------- /system/models/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | 9 | from .abstract import * 10 | from .config import * 11 | from .department import * 12 | from .field import * 13 | from .log import * 14 | from .menu import * 15 | from .permission import * 16 | from .role import * 17 | from .upload import * 18 | from .user import * 19 | -------------------------------------------------------------------------------- /system/models/abstract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : abstract 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.db import models 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | 12 | class ModeTypeAbstract(models.Model): 13 | class ModeChoices(models.IntegerChoices): 14 | OR = 0, _("Or mode") 15 | AND = 1, _("And mode") 16 | 17 | mode_type = models.SmallIntegerField(choices=ModeChoices, default=ModeChoices.OR, 18 | verbose_name=_("Data permission mode"), 19 | help_text=_( 20 | "Permission mode, and the mode indicates that the data needs to satisfy each rule in the rule list at the same time, or the mode satisfies any rule")) 21 | 22 | class Meta: 23 | abstract = True 24 | -------------------------------------------------------------------------------- /system/models/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : config 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | 9 | from django.db import models 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from common.core.models import DbAuditModel, DbUuidModel 13 | 14 | 15 | class BaseConfig(DbAuditModel): 16 | value = models.JSONField(max_length=10240, verbose_name=_("Config value")) 17 | is_active = models.BooleanField(default=True, verbose_name=_("Is active")) 18 | access = models.BooleanField(default=False, verbose_name=_("API access"), 19 | help_text=_("Allows API interfaces to access this config")) 20 | 21 | class Meta: 22 | abstract = True 23 | 24 | 25 | class SystemConfig(BaseConfig, DbUuidModel): 26 | key = models.CharField(max_length=255, unique=True, verbose_name=_("Config name")) 27 | inherit = models.BooleanField(default=False, verbose_name=_("User inherit"), 28 | help_text=_("Allows users to inherit this config")) 29 | 30 | class Meta: 31 | verbose_name = _("System config") 32 | verbose_name_plural = verbose_name 33 | 34 | def __str__(self): 35 | return "%s-%s" % (self.key, self.description) 36 | 37 | 38 | class UserPersonalConfig(BaseConfig): 39 | owner = models.ForeignKey("system.UserInfo", verbose_name=_("User"), on_delete=models.CASCADE) 40 | key = models.CharField(max_length=255, verbose_name=_("Config name")) 41 | 42 | class Meta: 43 | verbose_name = _("User config") 44 | verbose_name_plural = verbose_name 45 | unique_together = (('owner', 'key'),) 46 | 47 | def __str__(self): 48 | return "%s-%s" % (self.key, self.description) 49 | -------------------------------------------------------------------------------- /system/models/field.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : field 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.db import models 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | from common.core.models import DbAuditModel, DbUuidModel 12 | 13 | 14 | class ModelLabelField(DbAuditModel, DbUuidModel): 15 | class KeyChoices(models.TextChoices): 16 | TEXT = 'value.text', _('Text') 17 | JSON = 'value.json', _('Json') 18 | ALL = 'value.all', _('All data') 19 | DATETIME = 'value.datetime', _('Datetime') 20 | DATETIME_RANGE = 'value.datetime.range', _('Datetime range selector') 21 | DATE = 'value.date', _('Seconds to the current time') 22 | OWNER = 'value.user.id', _('My ID') 23 | OWNER_DEPARTMENT = 'value.user.dept.id', _('My department ID') 24 | OWNER_DEPARTMENTS = 'value.user.dept.ids', _('My department ID and data below the department') 25 | DEPARTMENTS = 'value.dept.ids', _('Department ID and data below the department') 26 | TABLE_USER = 'value.table.user.ids', _('Select the user ID') 27 | TABLE_MENU = 'value.table.menu.ids', _('Select menu ID') 28 | TABLE_ROLE = 'value.table.role.ids', _('Select role ID') 29 | TABLE_DEPT = 'value.table.dept.ids', _('Select department ID') 30 | 31 | class FieldChoices(models.IntegerChoices): 32 | ROLE = 0, _("Role permission") 33 | DATA = 1, _("Data permission") 34 | 35 | field_type = models.SmallIntegerField(choices=FieldChoices, default=FieldChoices.DATA, verbose_name=_("Field type")) 36 | parent = models.ForeignKey('system.ModelLabelField', on_delete=models.CASCADE, null=True, blank=True, 37 | verbose_name=_("Parent node")) 38 | name = models.CharField(verbose_name=_("Model/Field name"), max_length=128) 39 | label = models.CharField(verbose_name=_("Model/Field label"), max_length=128) 40 | 41 | class Meta: 42 | ordering = ('-created_time',) 43 | unique_together = ('name', 'parent') 44 | verbose_name = _("Model label field") 45 | verbose_name_plural = verbose_name 46 | 47 | def __str__(self): 48 | return f"{self.label}({self.name})" 49 | -------------------------------------------------------------------------------- /system/models/permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : permission 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | 9 | from django.db import models 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | from common.core.models import DbAuditModel, DbUuidModel, DbCharModel 13 | from system.models import ModeTypeAbstract 14 | 15 | 16 | class DataPermission(DbAuditModel, ModeTypeAbstract, DbUuidModel): 17 | name = models.CharField(verbose_name=_("Name"), max_length=255, unique=True) 18 | rules = models.JSONField(verbose_name=_("Rules"), max_length=10240) 19 | is_active = models.BooleanField(verbose_name=_("Is active"), default=True) 20 | menu = models.ManyToManyField("system.Menu", verbose_name=_("Menu"), blank=True, 21 | help_text=_("If a menu exists, it only applies to the selected menu permission")) 22 | 23 | class Meta: 24 | ordering = ('-created_time',) 25 | verbose_name = _("Data permission") 26 | verbose_name_plural = verbose_name 27 | 28 | def __str__(self): 29 | return f"{self.name}" 30 | 31 | 32 | class FieldPermission(DbAuditModel, DbCharModel): 33 | role = models.ForeignKey("system.UserRole", on_delete=models.CASCADE, verbose_name=_("Role")) 34 | menu = models.ForeignKey("system.Menu", on_delete=models.CASCADE, verbose_name=_("Menu")) 35 | field = models.ManyToManyField("system.ModelLabelField", verbose_name=_("Field"), blank=True) 36 | 37 | class Meta: 38 | verbose_name = _("Field permission") 39 | verbose_name_plural = verbose_name 40 | ordering = ("-created_time",) 41 | unique_together = ("role", "menu") 42 | 43 | def save(self, *args, **kwargs): 44 | self.id = f"{self.role.pk}-{self.menu.pk}" 45 | return super().save(*args, **kwargs) 46 | 47 | def __str__(self): 48 | return f"{self.pk}-{self.role.name}-{self.created_time}" 49 | -------------------------------------------------------------------------------- /system/models/role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : role 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.db import models 9 | from django.utils.translation import gettext_lazy as _ 10 | 11 | from common.core.models import DbAuditModel, DbUuidModel 12 | 13 | 14 | class UserRole(DbAuditModel, DbUuidModel): 15 | name = models.CharField(max_length=128, verbose_name=_("Role name"), unique=True) 16 | code = models.CharField(max_length=128, verbose_name=_("Role code"), unique=True) 17 | is_active = models.BooleanField(verbose_name=_("Is active"), default=True) 18 | menu = models.ManyToManyField('system.Menu', verbose_name=_("Menu"), blank=True) 19 | 20 | class Meta: 21 | verbose_name = _("User role") 22 | verbose_name_plural = verbose_name 23 | ordering = ("-created_time",) 24 | 25 | def __str__(self): 26 | return f"{self.name}({self.code})" 27 | -------------------------------------------------------------------------------- /system/models/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : upload 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | import hashlib 9 | 10 | from django.db import models 11 | from django.utils.translation import gettext_lazy as _ 12 | 13 | from common.core.models import upload_directory_path, DbAuditModel, AutoCleanFileMixin 14 | 15 | 16 | class UploadFile(AutoCleanFileMixin, DbAuditModel): 17 | filepath = models.FileField(verbose_name=_("Filepath"), null=True, blank=True, upload_to=upload_directory_path) 18 | file_url = models.URLField(verbose_name=_("Internet URL"), max_length=255, blank=True, null=True, 19 | help_text=_("Usually an address accessible to the outside Internet")) 20 | filename = models.CharField(verbose_name=_("Filename"), max_length=255) 21 | filesize = models.IntegerField(verbose_name=_("Filesize")) 22 | mime_type = models.CharField(max_length=255, verbose_name=_("Mime type")) 23 | md5sum = models.CharField(max_length=36, verbose_name=_("File md5sum")) 24 | is_tmp = models.BooleanField(verbose_name=_("Tmp file"), default=False, 25 | help_text=_("Temporary files are automatically cleared by scheduled tasks")) 26 | is_upload = models.BooleanField(verbose_name=_("Upload file"), default=False) 27 | 28 | def save(self, *args, **kwargs): 29 | self.filename = self.filename[:255] 30 | if not self.md5sum and not self.file_url: 31 | md5 = hashlib.md5() 32 | for chunk in self.filepath.chunks(): 33 | md5.update(chunk) 34 | if not self.filesize: 35 | self.filesize = self.filepath.size 36 | self.md5sum = md5.hexdigest() 37 | return super().save(*args, **kwargs) 38 | 39 | class Meta: 40 | verbose_name = _("Upload file") 41 | verbose_name_plural = verbose_name 42 | 43 | def __str__(self): 44 | return f"{self.filename}" 45 | -------------------------------------------------------------------------------- /system/models/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : user 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.contrib.auth.models import AbstractUser 9 | from django.db import models 10 | from django.utils.translation import gettext_lazy as _ 11 | from pilkit.processors import ResizeToFill 12 | 13 | from common.core.models import upload_directory_path, DbAuditModel, AutoCleanFileMixin 14 | from common.fields.image import ProcessedImageField 15 | from system.models import ModeTypeAbstract 16 | 17 | 18 | class UserInfo(AutoCleanFileMixin, DbAuditModel, AbstractUser, ModeTypeAbstract): 19 | class GenderChoices(models.IntegerChoices): 20 | UNKNOWN = 0, _("Unknown") 21 | MALE = 1, _("Male") 22 | FEMALE = 2, _("Female") 23 | 24 | avatar = ProcessedImageField(verbose_name=_("Avatar"), null=True, blank=True, 25 | upload_to=upload_directory_path, 26 | processors=[ResizeToFill(512, 512)], # 默认存储像素大小 27 | scales=[1, 2, 3, 4], # 缩略图可缩小倍数, 28 | format='png') 29 | 30 | nickname = models.CharField(verbose_name=_("Nickname"), max_length=150, blank=True) 31 | gender = models.IntegerField(choices=GenderChoices, default=GenderChoices.UNKNOWN, verbose_name=_("Gender")) 32 | phone = models.CharField(verbose_name=_("Phone"), max_length=16, default='', blank=True, db_index=True) 33 | email = models.EmailField(verbose_name=_("Email"), default='', blank=True, db_index=True) 34 | 35 | roles = models.ManyToManyField(to="system.UserRole", verbose_name=_("Role permission"), blank=True) 36 | rules = models.ManyToManyField(to="system.DataPermission", verbose_name=_("Data permission"), blank=True) 37 | dept = models.ForeignKey(to="system.DeptInfo", verbose_name=_("Department"), on_delete=models.PROTECT, blank=True, 38 | null=True, related_query_name="dept_query") 39 | 40 | class Meta: 41 | verbose_name = _("Userinfo") 42 | verbose_name_plural = verbose_name 43 | ordering = ("-date_joined",) 44 | 45 | def __str__(self): 46 | return f"{self.nickname}({self.username})" 47 | -------------------------------------------------------------------------------- /system/notifications.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from common.utils.request import get_request_ip, get_browser 5 | from common.utils.timezone import local_now_display 6 | from notifications.notifications import UserMessage 7 | 8 | 9 | class DifferentCityLoginMessage(UserMessage): 10 | category = 'AccountSecurity' 11 | category_label = _('Account Security') 12 | message_type_label = _('Different city login reminder') 13 | 14 | def __init__(self, user, ip, city): 15 | self.ip = ip 16 | self.city = city 17 | super().__init__(user) 18 | 19 | def get_html_msg(self) -> dict: 20 | now = local_now_display() 21 | subject = _('Different city login reminder') 22 | context = dict( 23 | subject=subject, 24 | name=self.user.nickname, 25 | username=self.user.username, 26 | ip=self.ip, 27 | time=now, 28 | city=self.city, 29 | ) 30 | message = render_to_string('notify/msg_different_city.html', context) 31 | return { 32 | 'subject': subject, 33 | 'message': message 34 | } 35 | 36 | @classmethod 37 | def gen_test_msg(cls): 38 | from system.models import UserInfo 39 | user = UserInfo.objects.first() 40 | ip = '8.8.8.8' 41 | city = '洛杉矶' 42 | return cls(user, ip, city) 43 | 44 | 45 | class ResetPasswordSuccessMsg(UserMessage): 46 | category = 'AccountSecurity' 47 | category_label = _('Account Security') 48 | message_type_label = _('Reset password reminder') 49 | 50 | def __init__(self, user, request): 51 | super().__init__(user) 52 | self.ip_address = get_request_ip(request) 53 | self.browser = get_browser(request) 54 | 55 | def get_html_msg(self) -> dict: 56 | user = self.user 57 | 58 | subject = _('Reset password success') 59 | context = { 60 | 'name': user.nickname, 61 | 'username': user.username, 62 | 'ip_address': self.ip_address, 63 | 'browser': self.browser, 64 | } 65 | message = render_to_string('notify/msg_rest_password_success.html', context) 66 | return { 67 | 'subject': subject, 68 | 'message': message 69 | } 70 | 71 | @classmethod 72 | def gen_test_msg(cls): 73 | pass 74 | -------------------------------------------------------------------------------- /system/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 8/10/2024 7 | -------------------------------------------------------------------------------- /system/serializers/department.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : department 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from drf_spectacular.utils import extend_schema_field 10 | from rest_framework import serializers 11 | from rest_framework.exceptions import ValidationError 12 | 13 | from common.core.serializers import BaseModelSerializer 14 | from common.utils import get_logger 15 | from system.models import DeptInfo 16 | 17 | logger = get_logger(__name__) 18 | 19 | 20 | class DeptSerializer(BaseModelSerializer): 21 | class Meta: 22 | model = DeptInfo 23 | fields = [ 24 | 'pk', 'name', 'code', 'parent', 'rank', 'is_active', 'roles', 'user_count', 'rules', 'mode_type', 25 | 'auto_bind', 'description', 'created_time' 26 | ] 27 | 28 | table_fields = [ 29 | 'name', 'pk', 'code', 'user_count', 'rank', 'mode_type', 'auto_bind', 'is_active', 'roles', 'rules', 30 | 'created_time' 31 | ] 32 | 33 | extra_kwargs = { 34 | 'roles': {'required': False, 'attrs': ['pk', 'name', 'code'], 'format': "{name}", 'many': True}, 35 | 'rules': {'required': False, 'attrs': ['pk', 'name', 'get_mode_type_display'], 'format': "{name}", 36 | 'many': True}, 37 | 'parent': {'required': False, 'attrs': ['pk', 'name', 'parent_id']}, 38 | } 39 | 40 | user_count = serializers.SerializerMethodField(read_only=True, label=_("User count")) 41 | 42 | def validate(self, attrs): 43 | # 权限需要其他接口设置,下面三个参数忽略 44 | attrs.pop('rules', None) 45 | attrs.pop('roles', None) 46 | attrs.pop('mode_type', None) 47 | # 上级部门必须存在,否则会出现数据权限问题 48 | parent = attrs.get('parent', self.instance.parent if self.instance else None) 49 | if not parent: 50 | attrs['parent'] = self.request.user.dept 51 | return attrs 52 | 53 | def update(self, instance, validated_data): 54 | parent = validated_data.get('parent') 55 | if parent and str(parent.pk) in DeptInfo.recursion_dept_info(dept_id=instance.pk): 56 | raise ValidationError(_("The superior department cannot be its own subordinate department")) 57 | return super().update(instance, validated_data) 58 | 59 | @extend_schema_field(serializers.IntegerField) 60 | def get_user_count(self, obj): 61 | return obj.userinfo_set.count() 62 | -------------------------------------------------------------------------------- /system/serializers/field.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : field 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from common.core.serializers import BaseModelSerializer 9 | from common.utils import get_logger 10 | from system.models import ModelLabelField 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | class ModelLabelFieldSerializer(BaseModelSerializer): 16 | class Meta: 17 | model = ModelLabelField 18 | fields = ['pk', 'name', 'label', 'parent', 'field_type', 'created_time', 'updated_time'] 19 | read_only_fields = [x.name for x in ModelLabelField._meta.fields] 20 | extra_kwargs = {'parent': {'attrs': ['pk', 'name', 'label'], 'read_only': True, 'format': '{label}({pk})'}} 21 | 22 | 23 | class ModelLabelFieldImportSerializer(BaseModelSerializer): 24 | class Meta: 25 | model = ModelLabelField 26 | fields = ['pk', 'name', 'label', 'parent', 'field_type', 'created_time', 'updated_time'] 27 | -------------------------------------------------------------------------------- /system/serializers/menu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : menu 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.db import transaction 9 | from django.utils.translation import gettext_lazy as _ 10 | from rest_framework import serializers 11 | 12 | from common.core.serializers import BaseModelSerializer 13 | from common.utils import get_logger 14 | from system.models import Menu, MenuMeta 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class MenuMetaSerializer(BaseModelSerializer): 20 | class Meta: 21 | model = MenuMeta 22 | exclude = ['creator', 'modifier', 'id'] 23 | read_only_fields = ['creator', 'modifier', 'dept_belong', 'id'] 24 | 25 | pk = serializers.UUIDField(source='id', read_only=True) 26 | 27 | 28 | class MenuSerializer(BaseModelSerializer): 29 | meta = MenuMetaSerializer(label=_("Menu meta")) 30 | 31 | class Meta: 32 | model = Menu 33 | fields = [ 34 | 'pk', 'name', 'rank', 'path', 'component', 'meta', 'parent', 'menu_type', 'is_active', 'model', 'method' 35 | ] 36 | # read_only_fields = ['pk'] # 用于文件导入导出时,不丢失上级节点 37 | extra_kwargs = { 38 | 'parent': {'attrs': ['pk', 'name'], 'allow_null': True, 'required': False}, 39 | 'model': {'attrs': ['pk', 'name', 'label'], 'allow_null': True, 'required': False}, 40 | } 41 | 42 | def update(self, instance, validated_data): 43 | with transaction.atomic(): 44 | serializer = MenuMetaSerializer(instance.meta, data=validated_data.pop('meta'), partial=True, 45 | context=self.context) 46 | serializer.is_valid(raise_exception=True) 47 | serializer.save() 48 | return super().update(instance, validated_data) 49 | 50 | def create(self, validated_data): 51 | with transaction.atomic(): 52 | serializer = MenuMetaSerializer(data=validated_data.pop('meta'), context=self.context) 53 | serializer.is_valid(raise_exception=True) 54 | validated_data['meta'] = serializer.save() 55 | return super().create(validated_data) 56 | -------------------------------------------------------------------------------- /system/serializers/permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : permission 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.db.models import Q 9 | from django.utils.translation import gettext_lazy as _ 10 | from rest_framework.exceptions import ValidationError 11 | 12 | from common.core.serializers import BaseModelSerializer 13 | from common.utils import get_logger 14 | from system.models import DataPermission, Menu 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | def get_menu_queryset(): 20 | queryset = Menu.objects 21 | pks = queryset.filter(menu_type=Menu.MenuChoices.PERMISSION).values_list('parent', flat=True) 22 | return queryset.filter(Q(menu_type=Menu.MenuChoices.PERMISSION) | Q(id__in=pks)).order_by('rank') 23 | 24 | 25 | class DataPermissionSerializer(BaseModelSerializer): 26 | class Meta: 27 | model = DataPermission 28 | fields = ['pk', 'name', "is_active", "mode_type", "menu", "description", 'rules', "created_time"] 29 | table_fields = ['pk', 'name', "mode_type", "is_active", "description", "created_time"] 30 | extra_kwargs = { 31 | 'menu': { 32 | 'attrs': ['pk', 'name', 'parent_id', 'meta__title'], 33 | 'many': True, 'required': False, 'queryset': get_menu_queryset() 34 | }, 35 | } 36 | 37 | def validate(self, attrs): 38 | rules = attrs.get('rules', [] if not self.instance else self.instance.rules) 39 | if not rules: 40 | raise ValidationError(_("The rule cannot be null")) 41 | if len(rules) < 2: 42 | attrs['mode_type'] = DataPermission.ModeChoices.OR 43 | return attrs 44 | -------------------------------------------------------------------------------- /system/serializers/route.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : route 5 | # author : ly_13 6 | # date : 8/16/2024 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from rest_framework import serializers 10 | from rest_framework.serializers import ModelSerializer 11 | 12 | from common.core.serializers import BaseModelSerializer 13 | from common.utils import get_logger 14 | from system.models import Menu, MenuMeta 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class RouteMetaSerializer(ModelSerializer): 20 | class Meta: 21 | model = MenuMeta 22 | fields = [ 23 | 'title', 'icon', 'showParent', 'showLink', 'extraIcon', 'keepAlive', 'frameSrc', 'frameLoading', 24 | 'transition', 'hiddenTag', 'dynamicLevel', 'fixedTag' 25 | ] 26 | 27 | showParent = serializers.BooleanField(source='is_show_parent', read_only=True, label=_("Show parent menu")) 28 | showLink = serializers.BooleanField(source='is_show_menu', read_only=True, label=_("Show menu")) 29 | extraIcon = serializers.CharField(source='r_svg_name', read_only=True, label=_("Right icon")) 30 | keepAlive = serializers.BooleanField(source='is_keepalive', read_only=True, label=_("Keepalive")) 31 | frameSrc = serializers.CharField(source='frame_url', read_only=True, label=_("Iframe URL")) 32 | frameLoading = serializers.BooleanField(source='frame_loading', read_only=True, label=_("Iframe loading")) 33 | 34 | transition = serializers.SerializerMethodField() 35 | 36 | def get_transition(self, obj): 37 | return { 38 | 'enterTransition': obj.transition_enter, 39 | 'leaveTransition': obj.transition_leave, 40 | } 41 | 42 | hiddenTag = serializers.BooleanField(source='is_hidden_tag', read_only=True, label=_("Hidden tag")) 43 | fixedTag = serializers.BooleanField(source='fixed_tag', read_only=True, label=_("Fixed tag")) 44 | dynamicLevel = serializers.IntegerField(source='dynamic_level', read_only=True, label=_("Dynamic level")) 45 | 46 | 47 | class RouteSerializer(BaseModelSerializer): 48 | class Meta: 49 | model = Menu 50 | fields = ['pk', 'name', 'rank', 'path', 'component', 'meta', 'parent'] 51 | extra_kwargs = { 52 | 'rank': {'read_only': True}, 53 | 'parent': {'attrs': ['pk', 'name'], 'allow_null': True, 'required': False}, 54 | } 55 | 56 | meta = RouteMetaSerializer(label=_("Menu meta")) # 用于前端菜单渲染 57 | -------------------------------------------------------------------------------- /system/serializers/upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : upload 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from drf_spectacular.utils import extend_schema_field 10 | from rest_framework import serializers 11 | from rest_framework.exceptions import ValidationError 12 | 13 | from common.core.serializers import BaseModelSerializer 14 | from common.fields.utils import get_file_absolute_uri 15 | from common.utils import get_logger 16 | from system.models import UploadFile 17 | 18 | logger = get_logger(__name__) 19 | 20 | 21 | class UploadFileSerializer(BaseModelSerializer): 22 | class Meta: 23 | model = UploadFile 24 | fields = ['pk', 'filename', 'filesize', 'mime_type', 'md5sum', 'file_url', 'access_url', 'is_tmp', 'is_upload'] 25 | read_only_fields = ["pk", "is_upload"] 26 | table_fields = ['pk', 'filename', 'filesize', 'mime_type', 'access_url', 'is_tmp', 'is_upload', 'md5sum'] 27 | 28 | access_url = serializers.SerializerMethodField(label=_("Access URL")) 29 | 30 | @extend_schema_field(serializers.CharField) 31 | def get_access_url(self, obj): 32 | return obj.file_url if obj.file_url else get_file_absolute_uri(obj.filepath, self.context.get('request', None)) 33 | 34 | def create(self, validated_data): 35 | if not validated_data.get('file_url'): 36 | raise ValidationError(_("Internet url cannot be null")) 37 | return super().create(validated_data) 38 | 39 | def update(self, instance, validated_data): 40 | if not validated_data.get('file_url') and not instance.is_upload: 41 | raise ValidationError('Internet url cannot be null') 42 | return super().update(instance, validated_data) 43 | -------------------------------------------------------------------------------- /system/serializers/userinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : userinfo 5 | # author : ly_13 6 | # date : 8/10/2024 7 | 8 | 9 | from django.utils.translation import gettext_lazy as _ 10 | from drf_spectacular.utils import extend_schema_field 11 | from rest_framework import serializers 12 | 13 | from common.base.utils import AESCipherV2 14 | from common.core.serializers import BaseModelSerializer 15 | from common.utils import get_logger 16 | from settings.utils.password import check_password_rules 17 | from system import models 18 | from system.models import UserInfo 19 | 20 | logger = get_logger(__name__) 21 | 22 | 23 | class UserInfoSerializer(BaseModelSerializer): 24 | class Meta: 25 | model = UserInfo 26 | write_fields = ['username', 'nickname', 'gender'] 27 | fields = write_fields + ['email', 'last_login', 'pk', 'phone', 'avatar', 'roles', 'date_joined', 'dept'] 28 | read_only_fields = list(set([x.name for x in models.UserInfo._meta.fields]) - set(write_fields)) 29 | 30 | dept = serializers.CharField(source='dept.name', read_only=True) 31 | roles = serializers.SerializerMethodField() 32 | 33 | @extend_schema_field(serializers.ListField) 34 | def get_roles(self, obj): 35 | return list(obj.roles.values_list('name', flat=True)) 36 | 37 | 38 | class ChangePasswordSerializer(serializers.Serializer): 39 | old_password = serializers.CharField( 40 | min_length=5, max_length=128, required=True, write_only=True, label=_("Old password") 41 | ) 42 | sure_password = serializers.CharField( 43 | min_length=5, max_length=128, required=True, write_only=True, label=_("Confirm password") 44 | ) 45 | 46 | def update(self, instance, validated_data): 47 | sure_password = AESCipherV2(instance.username).decrypt(validated_data.get('sure_password')) 48 | old_password = AESCipherV2(instance.username).decrypt(validated_data.get('old_password')) 49 | if not instance.check_password(old_password): 50 | raise serializers.ValidationError(_("Old password verification failed")) 51 | if not check_password_rules(sure_password, instance.is_superuser): 52 | raise serializers.ValidationError(_('Password does not match security rules')) 53 | 54 | instance.set_password(sure_password) 55 | instance.modifier = self.context.get('request').user 56 | instance.save(update_fields=['password', 'modifier']) 57 | return instance 58 | -------------------------------------------------------------------------------- /system/signal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : signal 5 | # author : ly_13 6 | # date : 10/11/2024 7 | 8 | 9 | from django.dispatch import Signal 10 | 11 | invalid_user_cache_signal = Signal() 12 | -------------------------------------------------------------------------------- /system/tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin_server 4 | # filename : tasks 5 | # author : ly_13 6 | # date : 6/29/2023 7 | 8 | from celery import shared_task 9 | 10 | from common.celery.decorator import register_as_period_task 11 | from common.utils import get_logger 12 | from system.utils.ctasks import auto_clean_operation_log, auto_clean_black_token, auto_clean_tmp_file 13 | 14 | logger = get_logger(__name__) 15 | 16 | 17 | @shared_task 18 | @register_as_period_task(crontab='2 2 * * *') 19 | def auto_clean_operation_job(): 20 | auto_clean_operation_log(clean_day=30 * 6) 21 | 22 | 23 | @shared_task 24 | @register_as_period_task(crontab='22 2 * * *') 25 | def auto_clean_black_token_job(): 26 | auto_clean_black_token(clean_day=7) 27 | 28 | 29 | @shared_task 30 | @register_as_period_task(crontab='32 2 * * *') 31 | def auto_clean_tmp_file_job(): 32 | auto_clean_tmp_file(clean_day=7) 33 | -------------------------------------------------------------------------------- /system/templates/msg_verify_code.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 |
{{ title }}
{% trans 'Hello' %} {{ username }}
{% trans 'Verify code' %}: {{ code }}
16 | {% trans 'The validity period of the verification code is' %}{{ ttl }}{% trans 'seconds' %}
{% trans 'If you have any questions, you can contact the administrator' %}
22 |
23 | -------------------------------------------------------------------------------- /system/templates/notify/msg_different_city.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

3 | {% trans 'Hello' %} {{ name }}, 4 |

5 |

6 | {% trans 'Your account has remote login behavior, please pay attention' %} 7 |

8 |

9 | {% trans 'Username' %}: {{ username }}
10 | {% trans 'Login Date' %}: {{ time }}
11 | {% trans 'Login city' %}: {{ city }}({{ ip }}) 12 |

13 | 14 | - 15 |

16 | {% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %} 17 |

-------------------------------------------------------------------------------- /system/templates/notify/msg_rest_password_success.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% trans 'Dear' %} {{ name }},

3 | 4 |

5 | {% trans 'Your password has just been successfully updated' %} 6 |

7 |

8 | {% trans 'Username' %}: {{ username }}
9 | {% trans 'IP' %}: {{ ip_address }}
10 | {% trans 'Browser' %}: {{ browser }} 11 |

12 | - 13 |

14 | {% trans 'If the password update was not initiated by you, your account may have security issues' %}
15 | {% trans 'If you have any questions, you can contact the administrator' %} 16 |

17 | -------------------------------------------------------------------------------- /system/utils/ctasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin_server 4 | # filename : ctasks 5 | # author : ly_13 6 | # date : 6/29/2023 7 | 8 | import datetime 9 | 10 | from celery.utils.log import get_task_logger 11 | from django.utils import timezone 12 | from rest_framework_simplejwt.token_blacklist.models import OutstandingToken 13 | 14 | from system.models import OperationLog, UploadFile 15 | 16 | logger = get_task_logger(__name__) 17 | 18 | 19 | def auto_clean_operation_log(clean_day=30 * 6): 20 | return OperationLog.remove_expired(clean_day) 21 | 22 | 23 | def auto_clean_black_token(clean_day=1): 24 | clean_time = timezone.now() - datetime.timedelta(days=clean_day) 25 | deleted, _rows_count = OutstandingToken.objects.filter(expires_at__lte=clean_time).delete() 26 | logger.info(f"clean {_rows_count} black token {deleted}") 27 | 28 | 29 | def auto_clean_tmp_file(clean_day=1): 30 | clean_time = timezone.now() - datetime.timedelta(days=clean_day) 31 | _rows_count = 0 32 | for instance in UploadFile.objects.filter(created_time__lte=clean_time): 33 | if instance.delete(): 34 | _rows_count += 1 35 | logger.info(f"clean {_rows_count} upload tmp file") 36 | -------------------------------------------------------------------------------- /system/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : __init__ 5 | # author : ly_13 6 | # date : 6/6/2023 7 | -------------------------------------------------------------------------------- /system/views/admin/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 3/4/2024 7 | -------------------------------------------------------------------------------- /system/views/admin/dept.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : dept 5 | # author : ly_13 6 | # date : 6/16/2023 7 | from django_filters import rest_framework as filters 8 | 9 | from common.core.filter import BaseFilterSet 10 | from common.core.modelset import BaseModelSet, ImportExportDataAction 11 | from common.core.pagination import DynamicPageNumber 12 | from common.utils import get_logger 13 | from system.models import DeptInfo 14 | from system.serializers.department import DeptSerializer 15 | from system.utils.modelset import ChangeRolePermissionAction 16 | 17 | logger = get_logger(__name__) 18 | 19 | 20 | class DeptFilter(BaseFilterSet): 21 | pk = filters.UUIDFilter(field_name='id') 22 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 23 | 24 | class Meta: 25 | model = DeptInfo 26 | fields = ['pk', 'is_active', 'code', 'mode_type', 'auto_bind', 'name', 'description'] 27 | 28 | 29 | class DeptViewSet(BaseModelSet, ChangeRolePermissionAction, ImportExportDataAction): 30 | """部门""" 31 | queryset = DeptInfo.objects.all() 32 | serializer_class = DeptSerializer 33 | pagination_class = DynamicPageNumber(1000) 34 | ordering_fields = ['created_time', 'rank'] 35 | filterset_class = DeptFilter 36 | -------------------------------------------------------------------------------- /system/views/admin/loginlog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : loginlog 5 | # author : ly_13 6 | # date : 1/3/2024 7 | 8 | 9 | from django_filters import rest_framework as filters 10 | from drf_spectacular.utils import extend_schema 11 | from rest_framework.decorators import action 12 | 13 | from common.core.filter import BaseFilterSet, PkMultipleFilter 14 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction 15 | from common.core.response import ApiResponse 16 | from common.swagger.utils import get_default_response_schema 17 | from message.utils import send_logout_msg 18 | from system.models import UserLoginLog 19 | from system.serializers.log import LoginLogSerializer 20 | 21 | 22 | class LoginLogFilter(BaseFilterSet): 23 | ipaddress = filters.CharFilter(field_name='ipaddress', lookup_expr='icontains') 24 | city = filters.CharFilter(field_name='city', lookup_expr='icontains') 25 | system = filters.CharFilter(field_name='system', lookup_expr='icontains') 26 | agent = filters.CharFilter(field_name='agent', lookup_expr='icontains') 27 | creator_id = PkMultipleFilter(input_type='api-search-user') 28 | 29 | class Meta: 30 | model = UserLoginLog 31 | fields = ['login_type', 'ipaddress', 'city', 'system', 'creator_id', 'status', 'agent', 'created_time'] 32 | 33 | 34 | class LoginLogViewSet(ListDeleteModelSet, OnlyExportDataAction): 35 | """登录日志""" 36 | queryset = UserLoginLog.objects.all() 37 | serializer_class = LoginLogSerializer 38 | 39 | ordering_fields = ['created_time'] 40 | filterset_class = LoginLogFilter 41 | 42 | @extend_schema(responses=get_default_response_schema(), request=None) 43 | @action(methods=["post"], detail=True) 44 | def logout(self, request, *args, **kwargs): 45 | """强退用户""" 46 | instance = self.get_object() 47 | send_logout_msg(instance.creator.pk, [instance.channel_name]) 48 | return ApiResponse() 49 | -------------------------------------------------------------------------------- /system/views/admin/online.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : websocket 5 | # author : ly_13 6 | # date : 3/27/2025 7 | 8 | from common.core.filter import BaseFilterSet, PkMultipleFilter 9 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction 10 | from common.core.pagination import DynamicPageNumber 11 | from message.utils import get_online_info, send_logout_msg 12 | from system.models import UserLoginLog 13 | from system.serializers.log import UserOnlineSerializer 14 | 15 | 16 | class UserOnlineFilter(BaseFilterSet): 17 | creator_id = PkMultipleFilter(input_type='api-search-user') 18 | 19 | class Meta: 20 | model = UserLoginLog 21 | fields = ['creator_id', 'channel_name'] 22 | 23 | 24 | class UserOnlineViewSet(ListDeleteModelSet, OnlyExportDataAction): 25 | """websocket在线日志""" 26 | queryset = UserLoginLog.objects.filter(login_type=UserLoginLog.LoginTypeChoices.WEBSOCKET).all() 27 | serializer_class = UserOnlineSerializer 28 | pagination_class = DynamicPageNumber(1000) 29 | filterset_class = UserOnlineFilter 30 | ordering_fields = ['created_time'] 31 | 32 | def get_queryset(self): 33 | online_user_pks, online_user_sockets = get_online_info() 34 | return self.queryset.filter(creator_id__in=online_user_pks, channel_name__in=online_user_sockets) 35 | 36 | def perform_destroy(self, instance): 37 | send_logout_msg(instance.creator_id, [instance.channel_name]) 38 | return True 39 | -------------------------------------------------------------------------------- /system/views/admin/operationlog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin_server 4 | # filename : operationlog 5 | # author : ly_13 6 | # date : 6/27/2023 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from django_filters import rest_framework as filters 10 | 11 | from common.core.filter import BaseFilterSet, PkMultipleFilter 12 | from common.core.modelset import ListDeleteModelSet, OnlyExportDataAction 13 | from system.models import OperationLog 14 | from system.serializers.log import OperationLogSerializer 15 | 16 | 17 | class OperationLogFilter(BaseFilterSet): 18 | ipaddress = filters.CharFilter(field_name='ipaddress', lookup_expr='icontains') 19 | system = filters.CharFilter(field_name='system', lookup_expr='icontains') 20 | path = filters.CharFilter(field_name='path', lookup_expr='icontains') 21 | error_status = filters.BooleanFilter(method='get_error_status', label=_('Error status')) 22 | 23 | def get_error_status(self, queryset, name, value): 24 | if value is True: 25 | return queryset.exclude(status_code=1000) 26 | return queryset.filter(status_code=1000) 27 | 28 | # 自定义的搜索模板,需要前端同时添加 userinfo 类型 29 | creator_id = PkMultipleFilter(input_type='api-search-user') 30 | 31 | class Meta: 32 | model = OperationLog 33 | fields = ['request_uuid', 'module', 'ipaddress', 'system', 'creator_id', 'status_code', 'path', 'created_time', 34 | 'error_status'] 35 | 36 | 37 | class OperationLogViewSet(ListDeleteModelSet, OnlyExportDataAction): 38 | """操作日志""" 39 | queryset = OperationLog.objects.all() 40 | serializer_class = OperationLogSerializer 41 | 42 | ordering_fields = ['created_time', 'updated_time', 'exec_time'] 43 | filterset_class = OperationLogFilter 44 | -------------------------------------------------------------------------------- /system/views/admin/permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : permission 5 | # author : ly_13 6 | # date : 6/16/2023 7 | 8 | from django_filters import rest_framework as filters 9 | 10 | from common.core.filter import BaseFilterSet 11 | from common.core.modelset import BaseModelSet, ImportExportDataAction 12 | from common.utils import get_logger 13 | from system.models import DataPermission 14 | from system.serializers.permission import DataPermissionSerializer 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class DataPermissionFilter(BaseFilterSet): 20 | pk = filters.UUIDFilter(field_name='id') 21 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 22 | 23 | class Meta: 24 | model = DataPermission 25 | fields = ['pk', 'name', 'mode_type', 'is_active', 'description'] 26 | 27 | 28 | class DataPermissionViewSet(BaseModelSet, ImportExportDataAction): 29 | """数据权限""" 30 | queryset = DataPermission.objects.all() 31 | serializer_class = DataPermissionSerializer 32 | ordering_fields = ['created_time'] 33 | filterset_class = DataPermissionFilter 34 | -------------------------------------------------------------------------------- /system/views/admin/role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : server 4 | # filename : role 5 | # author : ly_13 6 | # date : 6/19/2023 7 | 8 | from django_filters import rest_framework as filters 9 | 10 | from common.core.filter import BaseFilterSet 11 | from common.core.modelset import BaseModelSet, ImportExportDataAction 12 | from common.utils import get_logger 13 | from system.models import UserRole 14 | from system.serializers.role import RoleSerializer, ListRoleSerializer 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class RoleFilter(BaseFilterSet): 20 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 21 | code = filters.CharFilter(field_name='code', lookup_expr='icontains') 22 | 23 | class Meta: 24 | model = UserRole 25 | fields = ['name', 'code', 'is_active', 'description'] 26 | 27 | 28 | class RoleViewSet(BaseModelSet, ImportExportDataAction): 29 | """角色""" 30 | queryset = UserRole.objects.all() 31 | serializer_class = RoleSerializer 32 | list_serializer_class = ListRoleSerializer 33 | ordering_fields = ['updated_time', 'name', 'created_time'] 34 | filterset_class = RoleFilter 35 | -------------------------------------------------------------------------------- /system/views/auth/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 8/10/2024 7 | -------------------------------------------------------------------------------- /system/views/auth/logout.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : logout 5 | # author : ly_13 6 | # date : 8/8/2024 7 | import hashlib 8 | import time 9 | 10 | from django.contrib.auth import logout 11 | from drf_spectacular.plumbing import build_object_type, build_basic_type 12 | from drf_spectacular.types import OpenApiTypes 13 | from drf_spectacular.utils import extend_schema, OpenApiRequest 14 | from rest_framework.generics import GenericAPIView 15 | from rest_framework_simplejwt.tokens import RefreshToken 16 | 17 | from common.cache.storage import BlackAccessTokenCache 18 | from common.core.response import ApiResponse 19 | from common.swagger.utils import get_default_response_schema 20 | 21 | 22 | class LogoutAPIView(GenericAPIView): 23 | """用户登出""" 24 | 25 | @extend_schema( 26 | request=OpenApiRequest(build_object_type(properties={'refresh': build_basic_type(OpenApiTypes.STR)})), 27 | responses=get_default_response_schema() 28 | ) 29 | def post(self, request): 30 | """用户登出""" 31 | auth = request.auth 32 | if not auth: 33 | return ApiResponse() 34 | exp = auth.payload.get('exp') 35 | user_id = auth.payload.get('user_id') 36 | timeout = exp - time.time() 37 | BlackAccessTokenCache(user_id, hashlib.md5(auth.token).hexdigest()).set_storage_cache(1, timeout) 38 | if request.data.get('refresh'): 39 | try: 40 | token = RefreshToken(request.data.get('refresh')) 41 | token.blacklist() # 登出账户,并且将账户的access 和 refresh token 加入黑名单 42 | except Exception as e: 43 | pass 44 | logout(request) 45 | return ApiResponse() 46 | -------------------------------------------------------------------------------- /system/views/auth/rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : rule 5 | # author : ly_13 6 | # date : 8/10/2024 7 | from drf_spectacular.plumbing import build_object_type, build_basic_type, build_array_type 8 | from drf_spectacular.types import OpenApiTypes 9 | from drf_spectacular.utils import extend_schema 10 | from rest_framework.generics import GenericAPIView 11 | 12 | from common.core.response import ApiResponse 13 | from common.swagger.utils import get_default_response_schema 14 | from settings.utils.password import get_password_check_rules 15 | 16 | 17 | class PasswordRulesAPIView(GenericAPIView): 18 | """密码规则配置信息""" 19 | permission_classes = [] 20 | 21 | @extend_schema( 22 | responses=get_default_response_schema( 23 | { 24 | 'data': build_object_type( 25 | properties={ 26 | 'password_rules': build_array_type( 27 | build_object_type( 28 | properties={ 29 | 'key': build_basic_type(OpenApiTypes.STR), 30 | 'value': build_basic_type(OpenApiTypes.NUMBER), 31 | } 32 | ) 33 | ) 34 | } 35 | ) 36 | } 37 | ) 38 | ) 39 | def get(self, request): 40 | """获取密码规则配置""" 41 | return ApiResponse(data={"password_rules": get_password_check_rules(request.user)}) 42 | -------------------------------------------------------------------------------- /system/views/auth/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : token 5 | # author : ly_13 6 | # date : 8/10/2024 7 | from drf_spectacular.plumbing import build_basic_type 8 | from drf_spectacular.types import OpenApiTypes 9 | from drf_spectacular.utils import extend_schema 10 | from rest_framework.generics import GenericAPIView 11 | from rest_framework_simplejwt.views import TokenRefreshView 12 | 13 | from captcha.utils import CaptchaAuth 14 | from common.core.response import ApiResponse 15 | from common.swagger.utils import get_default_response_schema 16 | from common.utils.request import get_request_ident 17 | from common.utils.token import make_token_cache 18 | from system.utils.auth import get_token_lifetime 19 | 20 | 21 | class TempTokenAPIView(GenericAPIView): 22 | """临时Token""" 23 | permission_classes = [] 24 | authentication_classes = [] 25 | 26 | @extend_schema(responses=get_default_response_schema({'token': build_basic_type(OpenApiTypes.STR)})) 27 | def get(self, request): 28 | """获取{cls}""" 29 | token = make_token_cache(get_request_ident(request), time_limit=600, force_new=True).encode('utf-8') 30 | return ApiResponse(token=token) 31 | 32 | 33 | class CaptchaAPIView(GenericAPIView): 34 | """图片验证码""" 35 | permission_classes = [] 36 | authentication_classes = [] 37 | 38 | @extend_schema( 39 | responses=get_default_response_schema( 40 | { 41 | 'captcha_image': build_basic_type(OpenApiTypes.STR), 42 | 'captcha_key': build_basic_type(OpenApiTypes.STR), 43 | 'length': build_basic_type(OpenApiTypes.NUMBER) 44 | } 45 | ) 46 | ) 47 | def get(self, request): 48 | """获取{cls}""" 49 | return ApiResponse(**CaptchaAuth(request=request).generate()) 50 | 51 | 52 | class RefreshTokenAPIView(TokenRefreshView): 53 | """刷新Token""" 54 | 55 | def post(self, request, *args, **kwargs): 56 | data = super().post(request, *args, **kwargs).data 57 | data.update(get_token_lifetime(request.user)) 58 | return ApiResponse(data=data) 59 | -------------------------------------------------------------------------------- /system/views/routes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : routes 5 | # author : ly_13 6 | # date : 4/21/2024 7 | from drf_spectacular.utils import extend_schema 8 | from rest_framework.generics import GenericAPIView 9 | 10 | from common.base.magic import cache_response 11 | from common.base.utils import menu_list_to_tree, format_menu_data 12 | from common.core.modelset import CacheDetailResponseMixin 13 | from common.core.permission import get_user_menu_queryset 14 | from common.core.response import ApiResponse 15 | from system.models import Menu 16 | from system.serializers.route import RouteSerializer 17 | 18 | 19 | def get_auths(user): 20 | if user.is_superuser: 21 | menu_obj = Menu.objects.filter(is_active=True) 22 | else: 23 | menu_obj = get_user_menu_queryset(user) 24 | if not menu_obj: 25 | menu_obj = Menu.objects.none() 26 | return menu_obj.filter(menu_type=Menu.MenuChoices.PERMISSION).values_list('name', flat=True).distinct() 27 | 28 | 29 | class UserRoutesAPIView(GenericAPIView, CacheDetailResponseMixin): 30 | """获取菜单路由""" 31 | 32 | @extend_schema(exclude=True) 33 | @cache_response(timeout=3600 * 24, key_func='get_cache_key') 34 | def get(self, request): 35 | route_list = [] 36 | user_obj = request.user 37 | menu_type = [Menu.MenuChoices.DIRECTORY, Menu.MenuChoices.MENU] 38 | if user_obj.is_superuser: 39 | route_list = RouteSerializer(Menu.objects.filter(is_active=True, menu_type__in=menu_type).order_by('rank'), 40 | many=True, ignore_field_permission=True).data 41 | 42 | return ApiResponse(data=format_menu_data(menu_list_to_tree(route_list)), auths=get_auths(user_obj)) 43 | else: 44 | menu_queryset = get_user_menu_queryset(user_obj) 45 | if menu_queryset: 46 | route_list = RouteSerializer( 47 | menu_queryset.filter(menu_type__in=menu_type).distinct().order_by('rank'), many=True, 48 | ignore_field_permission=True).data 49 | 50 | return ApiResponse(data=format_menu_data(menu_list_to_tree(route_list)), auths=get_auths(user_obj)) 51 | -------------------------------------------------------------------------------- /system/views/search/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__ 5 | # author : ly_13 6 | # date : 7/22/2024 7 | -------------------------------------------------------------------------------- /system/views/search/dept.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : dept 5 | # author : ly_13 6 | # date : 7/22/2024 7 | 8 | from django_filters import rest_framework as filters 9 | 10 | from common.core.filter import BaseFilterSet 11 | from common.core.modelset import OnlyListModelSet 12 | from common.core.pagination import DynamicPageNumber 13 | from common.utils import get_logger 14 | from system.models import DeptInfo 15 | from system.serializers.department import DeptSerializer 16 | 17 | logger = get_logger(__name__) 18 | 19 | 20 | class SearchDeptFilter(BaseFilterSet): 21 | pk = filters.UUIDFilter(field_name='id') 22 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 23 | 24 | class Meta: 25 | model = DeptInfo 26 | fields = ['name', 'is_active', 'code', 'description'] 27 | 28 | 29 | class SearchDeptSerializer(DeptSerializer): 30 | class Meta: 31 | model = DeptInfo 32 | fields = ['name', 'pk', 'code', 'parent', 'is_active', 'user_count', 'auto_bind', 'description', 'created_time'] 33 | table_fields = ['name', 'code', 'is_active', 'user_count', 'auto_bind', 'description', 'created_time', 'pk'] 34 | read_only_fields = [x.name for x in DeptInfo._meta.fields] 35 | 36 | 37 | class SearchDeptViewSet(OnlyListModelSet): 38 | """部门搜索""" 39 | queryset = DeptInfo.objects.all() 40 | serializer_class = SearchDeptSerializer 41 | pagination_class = DynamicPageNumber(1000) 42 | ordering_fields = ['created_time', 'rank'] 43 | filterset_class = SearchDeptFilter 44 | -------------------------------------------------------------------------------- /system/views/search/menu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : menu 5 | # author : ly_13 6 | # date : 7/22/2024 7 | 8 | from django.utils.translation import gettext_lazy as _ 9 | from django_filters import rest_framework as filters 10 | from rest_framework import serializers 11 | 12 | from common.core.filter import BaseFilterSet 13 | from common.core.modelset import OnlyListModelSet 14 | from common.core.pagination import DynamicPageNumber 15 | from system.models import Menu 16 | from system.serializers.menu import MenuSerializer 17 | 18 | 19 | class SearchMenuFilter(BaseFilterSet): 20 | component = filters.CharFilter(field_name='component', lookup_expr='icontains') 21 | title = filters.CharFilter(field_name='meta__title', lookup_expr='icontains') 22 | path = filters.CharFilter(field_name='path', lookup_expr='icontains') 23 | 24 | class Meta: 25 | model = Menu 26 | fields = ['title', 'path', 'component'] 27 | 28 | 29 | class SearchMenuSerializer(MenuSerializer): 30 | class Meta: 31 | model = Menu 32 | fields = ['title', 'pk', 'rank', 'path', 'component', 'parent', 'menu_type', 'is_active', 'method'] 33 | table_fields = ['title', 'menu_type', 'path', 'component', 'is_active', 'method'] 34 | read_only_fields = [x.name for x in Menu._meta.fields] 35 | 36 | title = serializers.CharField(source='meta.title', read_only=True, label=_("Menu title")) 37 | 38 | 39 | class SearchMenuViewSet(OnlyListModelSet): 40 | """菜单搜索""" 41 | queryset = Menu.objects.order_by('rank').all() 42 | serializer_class = SearchMenuSerializer 43 | pagination_class = DynamicPageNumber(1000) 44 | ordering_fields = ['-rank', 'updated_time', 'created_time'] 45 | filterset_class = SearchMenuFilter 46 | -------------------------------------------------------------------------------- /system/views/search/role.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : role 5 | # author : ly_13 6 | # date : 7/22/2024 7 | 8 | from django_filters import rest_framework as filters 9 | 10 | from common.core.filter import BaseFilterSet 11 | from common.core.modelset import OnlyListModelSet 12 | from common.utils import get_logger 13 | from system.models import UserRole 14 | from system.serializers.role import RoleSerializer 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class SearchRoleFilter(BaseFilterSet): 20 | name = filters.CharFilter(field_name='name', lookup_expr='icontains') 21 | code = filters.CharFilter(field_name='code', lookup_expr='icontains') 22 | 23 | class Meta: 24 | model = UserRole 25 | fields = ['name', 'code', 'is_active', 'description'] 26 | 27 | 28 | class SearchRoleSerializer(RoleSerializer): 29 | class Meta: 30 | model = UserRole 31 | fields = ['pk', 'name', 'code', 'is_active', 'description', 'updated_time'] 32 | read_only_fields = [x.name for x in UserRole._meta.fields] 33 | 34 | 35 | class SearchRoleViewSet(OnlyListModelSet): 36 | """角色搜索""" 37 | queryset = UserRole.objects.all() 38 | serializer_class = SearchRoleSerializer 39 | ordering_fields = ['updated_time', 'name', 'created_time'] 40 | filterset_class = SearchRoleFilter 41 | -------------------------------------------------------------------------------- /system/views/search/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : user 5 | # author : ly_13 6 | # date : 7/22/2024 7 | 8 | from django_filters import rest_framework as filters 9 | 10 | from common.core.filter import BaseFilterSet 11 | from common.core.modelset import OnlyListModelSet 12 | from system.models import UserInfo 13 | from system.serializers.user import UserSerializer 14 | 15 | 16 | class SearchUserFilter(BaseFilterSet): 17 | username = filters.CharFilter(field_name='username', lookup_expr='icontains') 18 | nickname = filters.CharFilter(field_name='nickname', lookup_expr='icontains') 19 | phone = filters.CharFilter(field_name='phone', lookup_expr='icontains') 20 | 21 | class Meta: 22 | model = UserInfo 23 | fields = ['username', 'nickname', 'phone', 'email', 'is_active', 'gender', 'dept'] 24 | 25 | 26 | class SearchUserSerializer(UserSerializer): 27 | class Meta: 28 | model = UserInfo 29 | fields = ['pk', 'avatar', 'username', 'nickname', 'phone', 'email', 'gender', 'is_active', 'password', 'dept', 30 | 'description', 'last_login', 'date_joined'] 31 | 32 | read_only_fields = [x.name for x in UserInfo._meta.fields] 33 | 34 | table_fields = ['pk', 'avatar', 'username', 'nickname', 'gender', 'is_active', 'dept', 'phone', 35 | 'last_login', 'date_joined'] 36 | 37 | 38 | class SearchUserViewSet(OnlyListModelSet): 39 | """用户搜索""" 40 | queryset = UserInfo.objects.all() 41 | serializer_class = SearchUserSerializer 42 | 43 | ordering_fields = ['date_joined', 'last_login', 'created_time'] 44 | filterset_class = SearchUserFilter 45 | -------------------------------------------------------------------------------- /system/views/user/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : __init__.py 5 | # author : ly_13 6 | # date : 3/4/2024 7 | -------------------------------------------------------------------------------- /system/views/user/login_log.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : login_log 5 | # author : ly_13 6 | # date : 8/11/2024 7 | from rest_framework.mixins import ListModelMixin 8 | from rest_framework.viewsets import GenericViewSet 9 | 10 | from common.core.modelset import SearchColumnsAction 11 | from common.core.response import ApiResponse 12 | from system.models import UserLoginLog 13 | from system.serializers.log import UserLoginLogSerializer 14 | 15 | 16 | class UserLoginLogViewSet(ListModelMixin, SearchColumnsAction, GenericViewSet): 17 | """用户登录日志""" 18 | queryset = UserLoginLog.objects.all() 19 | serializer_class = UserLoginLogSerializer 20 | 21 | ordering_fields = ['created_time'] 22 | 23 | def get_queryset(self): 24 | return self.queryset.filter(creator=self.request.user) 25 | 26 | def list(self, request, *args, **kwargs): 27 | data = super().list(request, *args, **kwargs).data 28 | return ApiResponse(data=data) 29 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /utils/check_celery.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | set -e 5 | 6 | test -e /tmp/worker_ready_celery 7 | test -e /tmp/worker_heartbeat_celery && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_celery))) -lt 20 8 | -------------------------------------------------------------------------------- /utils/clean_migrations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | utils_dir=$(dirname "$(readlink -f "$0")") 5 | cd "$(dirname "${utils_dir}")" || exit 1 6 | 7 | for d in *;do 8 | if [ -d "$d" ] && [ -d "$d"/migrations ];then 9 | rm -f "$d"/migrations/00* 10 | fi 11 | done 12 | 13 | -------------------------------------------------------------------------------- /utils/init_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # project : xadmin-server 4 | # filename : tests 5 | # author : ly_13 6 | # date : 12/23/2023 7 | import os 8 | import sys 9 | 10 | import django 11 | 12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') 14 | django.setup() 15 | 16 | from system.models import * 17 | from django.core import management 18 | 19 | # 如果有用户存在,则不支持初始化操作 20 | try: 21 | if UserInfo.objects.exists(): 22 | print(f'User already exists') 23 | exit(-1) 24 | except Exception as e: 25 | print(e) 26 | pass 27 | 28 | # 初始化操作 29 | try: 30 | management.call_command('makemigrations', ) 31 | management.call_command('migrate', ) 32 | # management.call_command('collectstatic', ) 33 | management.call_command('compilemessages', ) 34 | management.call_command('download_ip_db', ) 35 | except Exception as e: 36 | print(f'Perform migrate failed, {e} exit') 37 | 38 | # 创建是默认管理员用户,请及时修改信息 39 | UserInfo.objects.create_superuser('xadmin', 'xadmin@dvcloud.xin', 'xAdminPwd!') 40 | 41 | management.call_command('load_init_json', ) 42 | 43 | # 加载默认用户数据,一般部署新服的时候,如果有默认数据,则可以进行加载 44 | # management.call_command('loaddata', 'loadjson/userinfo.json') 45 | -------------------------------------------------------------------------------- /utils/install_centos_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if which docker &>/dev/null ;then 4 | yum remove -y docker \ 5 | docker-client \ 6 | docker-client-latest \ 7 | docker-common \ 8 | docker-latest \ 9 | docker-latest-logrotate \ 10 | docker-logrotate \ 11 | docker-engine 12 | fi 13 | if ! which docker &>/dev/null ;then 14 | yum install -y yum-utils \ 15 | && yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo \ 16 | && sed -i 's+https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo \ 17 | && yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ 18 | && systemctl restart docker 19 | fi 20 | -------------------------------------------------------------------------------- /utils/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes auto; 4 | 5 | error_log /var/log/nginx/error.log notice; 6 | pid /var/run/nginx.pid; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | stream { 13 | log_format proxy '$remote_addr [$time_local] ' 14 | '$protocol $status $bytes_sent $bytes_received ' 15 | '$session_time "$upstream_addr" ' 16 | '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"'; 17 | 18 | access_log /var/log/nginx/tcp-access.log proxy; 19 | 20 | open_log_file_cache off; 21 | 22 | upstream server { 23 | hash $remote_addr consistent; 24 | server server:8896 weight=5 max_fails=3 fail_timeout=30s; 25 | } 26 | server { 27 | listen 8896; 28 | proxy_pass server; 29 | } 30 | 31 | } 32 | 33 | --------------------------------------------------------------------------------