chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:34 +08:00
commit 665ce57fb8
391 changed files with 780423 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.github
logs
temp
database
doc
http
image
.pytest_cache
+15
View File
@@ -0,0 +1,15 @@
# database
DB_CONNECT_URL=mysql://root:123456@127.0.0.1:3306/data
# app mode: production / development
APP_MODE=production
# allow commands eg:
ALLOW_COMMANDS=/opt/nginx/sbin/nginx -s reload
# allow user register
ENABLED_REGISTER=true
# shodan
SHODAN_API_KEY=
+67
View File
@@ -0,0 +1,67 @@
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
#
name: Deploy VitePress site to Pages
on:
# 在针对 `main` 分支的推送上运行。如果你
# 使用 `master` 分支作为默认分支,请将其更改为 `master`
push:
branches: [master]
# 允许你从 Actions 选项卡手动运行此工作流程
workflow_dispatch:
# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
concurrency:
group: pages
cancel-in-progress: false
jobs:
# 构建工作
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
- uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消注释
with:
version: 8
# - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm # 或 pnpm / yarn
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: pnpm install --no-frozen-lockfile # 或 pnpm install / yarn install / bun install
- name: Build with VitePress
run: pnpm docs:build # 或 pnpm docs:build / yarn docs:build / bun run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/.vitepress/dist
# 部署工作
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+38
View File
@@ -0,0 +1,38 @@
# https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml
name: Doc Build
on:
push:
branch: ['master']
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.8'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r doc/requirements.txt
- name: Build and publish
run: |
cd ./doc && make html && cd ../
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
branch: doc # The branch the action should deploy to.
folder: doc/build/html # The folder the action should deploy.
+71
View File
@@ -0,0 +1,71 @@
# 参考
# Build and push Docker images
# https://github.com/marketplace/actions/build-and-push-docker-images
# Docker Metadata action
# https://github.com/marketplace/actions/docker-metadata-action
# Multi-platform image with GitHub Actions
# https://docs.docker.com/build/ci/github-actions/multi-platform/
name: Docker Publish
#on:
# release:
# types: [published]
# push:
# branch: ['master']
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: mouday/domain-admin
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-web'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public'
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-mini'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public/m'
# remove .git
- name: Remove .git Dir
run: rm -rf '.git' 'domain_admin/public/.git' 'domain_admin/public/m/.git'
- name: Build Docker Image
uses: docker/build-push-action@v2
if: startsWith(github.ref, 'refs/tags/')
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+86
View File
@@ -0,0 +1,86 @@
# https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml
# https://docs.pypi.org/trusted-publishers/adding-a-publisher/
name: Upload Python Package
#on:
# release:
# types: [published]
#on:
# push:
# branch: ['master']
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/domain-admin
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
# 拉取前端最新的dist代码
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-web'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public'
- name: Checkout
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-mini'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public/m'
# remove .git
- name: Remove .git Dir
run: rm -rf '.git' 'domain_admin/public/.git' 'domain_admin/public/m/.git'
- name: Build
run: |
python setup.py sdist bdist_wheel --python-tag py2.py3
twine check dist/*
# - name: Build and publish
# if: startsWith(github.ref, 'refs/tags/')
#
# env:
# TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
# TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
# run: |
# python setup.py sdist bdist_wheel --python-tag py2.py3
# twine check dist/*
# twine upload dist/*
# Publishing with a Trusted Publisher
# https://docs.pypi.org/trusted-publishers/using-a-publisher/
# https://github.com/pypa/gh-action-pypi-publish/
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+104
View File
@@ -0,0 +1,104 @@
#https://github.com/yyx990803/release-tag
name: Create Release
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
#on:
# push:
# branch: ['master']
jobs:
build:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout Master
uses: actions/checkout@master
# 拉取web前端dist的代码
- name: Checkout Web Dist
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-web'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public'
- name: Checkout Mini Dist
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-mini'
ref: 'dist'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin/public/m'
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build Dist
run: |
python setup.py sdist bdist_wheel --python-tag py2.py3
# 拉取前端最新的master代码
- name: Checkout Web Master
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-web'
ref: 'master'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin_web'
# https://github.com/a7ul/tar-action
# https://docs.github.com/zh/actions/learn-github-actions/contexts#github-context
- name: Compress action step Web
uses: a7ul/tar-action@v1.1.0
id: compress-web
with:
command: c
cwd: ./
files: |
./domain_admin_web
outPath: ./dist/domain-admin-web-${{github.ref_name}}.tar.gz
# 拉取H5前端最新的master代码
- name: Checkout Mini Master
uses: actions/checkout@v3
with:
repository: 'mouday/domain-admin-mini'
ref: 'master'
token: ${{ secrets.GH_PAT }}
path: 'domain_admin_mini'
# remove .git
- name: Remove .git Dir
run: rm -rf '.git' 'domain_admin/public/.git' 'domain_admin/public/m/.git'
- name: Compress action step Mini
uses: a7ul/tar-action@v1.1.0
id: compress-mini
with:
command: c
cwd: ./
files: |
./domain_admin_mini
outPath: ./dist/domain-admin-mini-${{github.ref_name}}.tar.gz
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: ./dist/*
body: Please refer to [CHANGELOG.md](https://domain-admin.readthedocs.io/zh_CN/latest/manual/changelog.html) for details.
# note you'll typically need to create a personal access token
# with permissions to create releases in the other repo
+34
View File
@@ -0,0 +1,34 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
sync_with_upstream:
name: Sync with Upstream
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
- name: Checkout target repo
uses: actions/checkout@v3
- name: Sync Upstream
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
target_repo_token: ${{ secrets.GITHUB_TOKEN }}
upstream_sync_repo: dromara/domain-admin
upstream_sync_branch: master
target_sync_branch: master
test_mode: false
- name: Check for Failure
if: failure()
run: |
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
exit 1
+57
View File
@@ -0,0 +1,57 @@
# 测试
# doc: https://docs.github.com/zh/actions/automating-builds-and-tests/building-and-testing-python
# https://github.com/actions/python-versions
# https://github.com/actions/python-versions/blob/main/versions-manifest.json
name: Run tests
on:
push:
branches:
- "master"
pull_request:
branches:
- "master"
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# Run in all these versions of Python
python-version: ["3.8", "3.9", "3.10", "3.11"]
# 排除版本
exclude:
- os: ubuntu-latest
python-version: "3.5"
- os: macos-latest
python-version: "3.5"
- os: macos-latest
python-version: "3.7"
steps:
# Checkout the latest code from the repo
- name: Checkout repo
uses: actions/checkout@v3
# Setup which version of Python to use
- name: Set Up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
# Display the Python version being used
- name: Display Python version
run: python -c "import sys; print(sys.version)"
# Install pytest (you can use some other testing utility)
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements/development.txt
# Install the package using the setup.py
- name: Install package
run: python setup.py install
# Run the tests. I'm using pytest and the file is in the tests directory.
- name: Run tests
# run: pytest -c pytest.ini tests/api/test_index.py
run: python -m unittest tests/test_hello.py
Executable
+24
View File
@@ -0,0 +1,24 @@
.idea
__pycache__
*.pyc
*.sqlite
*.db
build/
domain_admin/public/
dist/
*.egg-info/
*.log
*.db-shm
*.db-wal
.env
config.yml
logs/
.deta
/temp
database
.pytest_cache
venv/
node_modules/
docs/.vitepress/dist/
docs/.vitepress/cache/
deploy-docs.sh
+35
View File
@@ -0,0 +1,35 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.7"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: doc/source/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: doc/requirements.txt
+12
View File
@@ -0,0 +1,12 @@
language: python
python:
- "3.8"
- "3.9"
# command to install dependencies
install:
- pip install -r requirements/development.txt
# command to run tests
script:
- pytest
+821
View File
@@ -0,0 +1,821 @@
- v1.6.78(2026-06-07)
- 修复多级域名问题
- v1.6.77(2026-05-24)
- fix close #220
- fix: 修复证书过期误报并优化不可达场景回退刷新 (#219)
- v1.6.76(2026-04-04)
- fix: 根域名写入为空的问题
- Merge branch 'master' of github.com:dromara/domain-admin
- fix uk
- feat: 新增单点登录功能 (#217)
- v1.6.75(2026-03-15)
- fix getDomainInfoById not permission for admin
- v1.6.74(2025-12-22)
- 增加升级脚本 for #211
- feat: implement dns-host association and auto-renew result tracking (#211)
- Update CMD to bind to IPv6 address (#210)
- v1.6.73(2025-10-19)
- fix
- v1.6.72(2025-10-19)
- 支持.lt的注册信息查询
- v1.6.71(2025-08-17)
- add log params
- add log
- fix
- fix
- fix
- v1.6.70(2025-07-04)
- refactor(issue_certificate): 重构证书续签的验证逻辑,避免重复创建订单 (#193)
- v1.6.69(2025-07-02)
- feat(证书验证): 添加验证文件部署和DNS记录部署的可选参数 (#192)
- Fix acme_v2_api.py (#188)
- v1.6.68(2025-06-04)
- 移除多余代码
- v1.6.67(2025-06-04)
- 新增 网址微信拦截状态检查
- fix doc
- check_wechat (#185)
- v1.6.66(2025-04-02)
- Support FTP cert(port 21) (#176)
- v1.6.65(2025-03-13)
- 支持纯ipv6域名做SSL证书到期检测 (#171)
- fix
- v1.6.64(2025-02-07)
- fix: DNS域名验证以及一键部署成功后,还是无法自动续期 #134 (#167)
- v1.6.63(2024-12-24)
- 新增繁体中文
- v1.6.62(2024-12-20)
- 修复.tw后缀域名无法查询 fix #159
- v1.6.61(2024-11-23)
- Cert Saver, ordered by expire_time (#153)
- v1.6.60(2024-11-17)
- 优化:主机列表/DNS列表接口密码返回为空 fix:#149
- v1.6.59(2024-11-05)
- doc: dingtalk webhook (#146)
- 修复普通用户网站监测日志查看权限
- v1.6.58(2024-11-02)
- fix 升级失败
- fix
- v1.6.57(2024-11-02)
- 新增:网站监测支持超时时间到毫秒级
- 修复证书文件导入失败的问题
- v1.6.55(2024-11-01)
- 修复多ip证书获取问题
- v1.6.54(2024-10-31)
- 修复服务部署后访问地址是域名加子路径时,导出会报错
- v1.6.53(2024-10-27)
- 修复修改域名时提示数据不存在的问题 #141
- 修复普通用户分组关联无法搜索的问题
- v1.6.52(2024-10-20)
- 通过api部署托管证书
- v1.6.51(2024-09-27)
- fix:www的域名查询不到证书过期时间信息
- 优化显示
- 增加允许注册判断
- v1.6.50(2024-09-21)
- 修改文档
- 支持用户注册
- v1.6.49(2024-09-08)
- move project to dromara
- v1.6.48(2024-08-28)
- bugfix:修复管理员提示无权限问题
- v1.6.47(2024-08-25)
- 修复zerossl无法申请的问题
- fix #126
- 版本升级
- 修改权限策略
- add zerossl
- 优化权限控制
- v1.6.46(2024-07-28)
- 优化:SSH秘钥兼容常见RSA,DSS,ECDSA,Ed5519算法 (#124)
- 完善文档
- v1.6.45(2024-07-26)
- 修复1.6.32 升级失败的问题
- v1.6.44(2024-07-25)
- 新增证书部署到阿里云CDN和DCND
- v1.6.43(2024-07-25)
- 优化参数
- 新增 证书部署到阿里云oss
- 完善文档
- v1.6.42(2024-07-22)
- fixnot found directory_url
- v1.6.41(2024-07-22)
- fix: type must be an integer
- v1.6.40(2024-07-10)
- 修复域名icp查询超时问题
- 增加证书到期邮件提醒备注字段
- 修复 webhook部署不能自动续期问题
- v1.6.39(2024-07-05)
- 修改证书导出命名格式 fix#113
- v1.6.38(2024-07-05)
- 修复通配符域名多级子域名判定错误
- v1.6.37(2024-07-03)
- 修复低版本升级到v1.6.35失败的问题
- 更新文档
- v1.6.36(2024-06-28)
- 支持dns账号:腾讯云
- v1.6.35(2024-06-27)
- 新增多种签发机构
- 修复DNS添加TXT报错提示问题
- v1.6.34(2024-06-25)
- 新增 通过账号添加dns的TXT记录
- 支持 dns验证和api部署的自动续期
- v1.6.33(2024-06-23)
- 支持DNS账号部署
- 新增DNS账户管理
- v1.6.32(2024-06-07)
- 增加电报通知
- 完善文档
- v1.6.31(2024-05-07)
- 新增 网站监控异常恢复通知事件
- v1.6.30(2024-04-23)
- 域名列表新增导入子域证书按钮
- v1.6.29(2024-04-22)
- 修复 证书申请文件验证失败问题
- remove git
- v1.6.28(2024-04-18)
- 修复 sqlite数据库域名监控无法导入的问题
- v1.6.27(2024-04-16)
- 尝试修复运行超时数据库链接断开问题
- v1.6.26(2024-04-11)
- 优化 修改秘钥字段长度
- 修复 企业微信通知配置提示isJson is not defined fix:#98
- v1.6.25(2024-04-08)
- 修复:证书一键部署失败的问题
- 修复:证书申请后自动监控过滤通配符的域名
- v1.6.24(2024-04-08)
- 修复 域名监控/域名列表排序问题
- v1.6.23(2024-04-03)
- 修复 域名监控/域名列表 自动更新字段排序无效问题
- 修复 工具箱/证书托管天数排序无效的问题
- v1.6.22(2024-04-02)
- 修复 自定义指令为空时的处理问题
- v1.6.21(2024-04-01)
- 增加 自动更新whois文件的机制 fix:#95
- 更新whois list
- 新增 支持用户自定义配置的命令
- 完善文档
- v1.6.20(2024-03-31)
- 新增 托管证书一键部署
- 完善文档
- v1.6.19(2024-03-27)
- 增加证书检测重试次数,避免错误检测
- v1.6.18(2024-03-26)
- 修复 手动编辑域名或证书到期时间,天数筛选不匹配的问题
- v1.6.17(2024-03-22)
- 新增:托管证书到期提醒 #69
- 新增 远程api部署ssl证书
- v1.6.16(2024-03-19)
- 优化:证书托管弹框点击遮罩不关闭
- v1.6.15(2024-03-05)
- 退登自动跳转到登录界面
- 完善文档
- 修复日志不滚动的问题
- 优化证书dns验证txt记录key显示,避免误导使用者
- v1.6.14(2024-02-27)
- 邮件配置时支持邮件发送测试
- 退登自动跳转到登录界面
- v1.6.13(2024-02-26)
- bugfix: 如果用户删除分组后,分组数据不存在会出现null问题
- v1.6.12(2024-02-25)
- 支持证书托管
- 新增网站监控列表批量删除
- v1.6.11(2024-02-24)
- 优化网站监控异常提示信息
- 网站监控增加错误重试次数
- 网站监控支持导入导出
- 优化耗时显示
- v1.6.10(2024-02-21)
- fix:修复分组关联域名弹框分页数量无法更改的问题
- fix:修复触发分组新增分组后无法自动勾选问题
- 增加证书列表批量操作
- fix:修复手动设置启动网站监测状态后不运行的问题
- v1.6.9(2024-02-20)
- 支持自定义主机ip地址不清空
- v1.6.8(2024-02-19)
- 完善大屏统计数据
- 优化兼容性
- 增加网站探活请求头
- v1.6.7(2024-02-06)
- 移除预安装依赖:psycopg2
- v1.6.6(2024-02-03)
- 新增域名修改时搜索已有标签
- 优化代码备注
- v1.6.5(2024-02-02)
- 修复到期邮件发送失败的bug
- v1.6.4(2024-02-01)
- fix:修复icp缓存不过期问题
- 支持域名icp信息的导入
- fix:修复timeout类型转换错误的bug
- 更新文档
- v1.6.3(2024-01-31)
- 修复监控列表删除bug
- 添加标签表
- 增加icp查询缓存
- 文件导入域名自动更新子域证书列表
- v1.6.2(2024-01-29)
- 增加首页控制台数据接口
- 修复时间显示错误问题
- 更新备注
- 替换备案查询接口
- 优化时间统计
- 完善问文档
- v1.6.1(2024-01-28)
- 添加网站监控
- v1.5.39(2024-01-27)
- fix
- v1.5.38(2024-01-24)
- fix
- v1.5.37(2024-01-23)
- 移除icp查询的第三方接口
- fix:修复批量导入域名端口为0的情况
- 完善文档
- v1.5.36(2023-11-30)
- 新增excel格式的导入导出
- v1.5.35(2023-11-29)
- fixTypeError: 'NoneType' object is not iterable
- 完善文档
- v1.5.34(2023-11-29)
- 支持通知指定特定分组
- 新增主机列表移除功能
- docs: 增加docker-compose启动 (#73)
- v1.5.33(2023-11-28)
- bugfix: 重载docker版nginx (#72)
- 完善文档
- v1.5.32(2023-11-28)
- 优化数据库迁移升级配置
- 证书部署支持非默认的SSH连接端口
- 完善文档
- v1.5.31(2023-11-22)
- 完善注释
- fix: 新增docker和红帽系reload (#70)
- v1.5.30(2023-11-22)
- 修复命令无法取到的问题
- v1.5.29(2023-11-22)
- 证书自动部署功能新增docker 下的重启命令
- 完善文档
- v1.5.28(2023-11-21)
- prometheus metrics接口新增全部域名字段
- 完善文档
- v1.5.27(2023-11-21)
- 新增prometheus接口/metrics域名数据
- v1.5.25(2023-10-23)
- 修复 证书申请列表无法搜索的问题
- 增加开发环境debug日志
- 完善文档
- v1.5.24(2023-09-17)
- 移除flask json默认序列化
- 移除动态主机选项
- 新增SSL加密方式:STARTTLS
- 完善文档
- v1.5.23(2023-09-12)
- 新增:证书查询工具字段:证书品牌和证书类型
- 优化:自动识别动态主机
- 完善文档
- v1.5.22(2023-09-07)
- 优化:域名导入,支持标签导入
- 优化:证书部署出于安全考虑,仅能使用预设的命令
- 优化:命令白名单
- 优化 flask 响应处理
- 完善文档
- v1.5.21(2023-08-30)
- fix #54 修复 .kr域名的信息查询
- fix #52 证书列表同域名不同端口无法添加
- fix #53 register 接口从白名单移除
- 优化 证书申请域名验证通过后自动加入到证书监控列表
- 完善文档
- v1.5.20(2023-08-25)
- 优化:企业微信、飞书、钉钉通知支持模板变量
- 优化前端UI显示,将ssl证书申请移动到左侧菜单栏
- 完善文档
- v1.5.19(2023-08-24)
- 优化:增加admin使用系统给定的默认密码用户提示
- 完善文档
- v1.5.18(2023-08-23)
- 优化 增加webhook模板参数
- 完善文档
- v1.5.17(2023-08-22)
- fix: 定时任务OperationalError: (2006, MySQL server has gone away)
- 更新最新版本截图
- 完善文档
- v1.5.16(2023-08-21)
- 优化:细化通知触发类型日志
- 优化:前端时间显示
- 优化:证书申请服务器地址自动匹配问题
- 优化:邮件通知宽度调整
- 完善文档
- v1.5.15(2023-08-16)
- 修复 cron表达式星期参数的bug
- 完善文档
- v1.5.14(2023-08-15)
- 优化 证书、域名数据导出支持条件和排序
- 完善文档
- v1.5.13(2023-08-04)
- 新增 部分的英文界面支持
- 新增 主机私钥文件认证方式
- 新增 手动续期SSL证书
- 完善文档
- v1.5.12(2023-08-02)
- 优化 捕获后台任务异常
- 优化 重新申请证书执行流程
- 优化 日志执行状态显示
- 优化 通知测试使用用户真实数据
- 优化 记录通知日志
- 修复 .im域名信息查询
- 完善 文档
- v1.5.11(2023-08-01)
- 新增 证书申请后查看证书文件的功能
- 优化 完善文档
- v1.5.10(2023-07-31)
- 完善 接口文档
- 修复 证书申请后浏览器显示不受信任问题
- v1.5.9(2023-07-30)
- 增加SSL证书DNS验证,远程部署,自动续期
- 修复 查询SSL证书信息剩余时间错误的问题
- 优化 完善文档
- v1.5.8(2023-07-24)
- 优化 用户输入的cron表达式有多余的空格
- 优化 SSL证书申请添加必要的提示
- v1.5.7(2023-07-24)
- 优化 完善部分文档
- 修复 邮件收件人不显示的问题
- 修复 执行时间计算错误的bug
- v1.5.6(2023-07-23)
- 新增 基于Webroot的Lets Encrypt的SSL证书申请
- 优化 邮件通知模板
- 优化 发布前端代码文件
- v1.5.5(2023-07-22)
- 优化 接入新icp接口
- 新增 数据管理支持负责人字段修改
- 新增 添加角色字段
- 优化 完善项目文档
- v1.5.4(2023-07-22)
- 新增 域名列表自定义显示列
- 新增 更新icp备案字段功能
- 新增 工具箱icp备案查询功能
- 优化 完善项目文档
- v1.5.3(2023-07-20)
- 优化 记录异步任务日志
- 修复 Py2导入证书失败问题
- 新增 重置用户密码
- 新增 批量补全域名icp信息
- 新增 异步任务日志记录
- 修复 自动发布代码滞后的问题
- v1.5.2(2023-07-20)
- 修复 Py2下导出异常
- 新增 域名支持更新标签字段
- 新增 导出文件添加标签和备案信息字段
- 修复 Py2下代码抛出的异常返回不正确
- 新增 域名添加和更新接口支持标签和备案信息
- 新增 数据表升级v1.5.1=>v1.5.2
- 优化 增加需求记录
- v1.5.1(2023-07-16)
- 修复 域名列表点击证书数量跳转错误
- v1.5.0(2023-07-16)
- 新增 清空操作日志
- 修复 Python2证书查询失败问题
- 新增 数据管理功能
- 优化 文档新增k8s自动获取到ingress的域名到domain-admin
- 优化 添加py2兼容性备注
- v1.4.36(2023-07-14)
- 优化 自动写入CHANGELOG
- v1.4.35(2023-07-13)
- 修复 子账号无法使用子域名查询和whois的的查询功能bug
- 优化 smtp的配置密码加密处理
- 新增 工具箱 `证书信息查询`
- v1.4.34(2023-07-11)
- 修复 前端页面`极客` 模式下左侧导航栏样式异常的bug [issues#42](https://github.com/mouday/domain-admin/issues/42)
- 优化 prometheus 接口 `/metrics` 新增返回参数:root_domain、group_name
- v1.4.33(2023-07-11)
- 修复 多实例同时启动版本号写入失败bug
- 优化 移除部分无用代码
- v1.4.32(2023-07-10)
- 修复 部分代码在Python2版本下兼容性问题
- 新增 工具箱目录下新页面:域名子域名查询
- 新增 添加域名时可选项:子域证书,可以自动添加该域名下子域名到证书列表 [issues#41](https://github.com/mouday/domain-admin/issues/41)
- v1.4.31(2023-07-09)
- 修复 只读用户无法使用搜索功能bug
- 优化 手动编辑更新证书信息接口时间过长的问题
- 优化 录入域名的时候可以手动设置`自动更新`字段
- v1.4.30(2023-07-09)
- 修复 域名列表查看详情后列表显示无权限编辑的bug
- v1.4.29(2023-07-09)
- 新增 支持Python版本:Python >= 2.7 或者 Python >= 3.4
- v1.4.28(2023-07-08)
- 新增 域名后缀的信息查询: ws, cm, by [issues#39](https://github.com/mouday/domain-admin/issues/39)
- v1.4.27(2023-07-08)
- 新增 移动端H5页面
- v1.4.26(2023-07-07)
- 修复 久远的旧版本升级到新版本出错的问题
- v1.4.25(2023-07-07)
- 修复 定时任务提示missed ,没执行的问题
- 修复 前端页面`极客`模式下左侧导航栏,切换的时候会闪一次方块UI异常的问题
- 新增 前端顶部栏增加`工具箱`入口
- v1.4.24(2023-07-06)
- 优化 证书列表编辑框支持修改证书时间
- 新增 分组权限控制,每个用户可以属于多个组,组权限分为 write和read(write可以添加编辑删除,read只允许查看)
- v1.4.23(2023-07-05)
- 修复 用户通知方式只能添加一个的bug
- v1.4.22(2023-07-05)
- 修复 证书编辑接口`参数缺失'id'` 的bug
- v1.4.21(2023-07-04)
- 新增 证书表、域名表、分组表、通知表操作日志
- 新增 前端新增`操作日志` 列表
- 优化 前端`分组列表` 新增ID字段
- v1.4.20(2023-07-03)
- 修复 `.sg`后缀域名过期时间获取不到的问题
- 优化 前端界面`主办单位名称` 字段显示不全问题
- v1.4.19(2023-07-01)
- 修复 过期时间为`null` 未触发通知提醒的问题
- 优化 前端路由无权限却进入页面的问题
- 优化 拆分前端系统邮箱配置和cron定时配置
- 优化 前端提交cron表达式验证和显示执行计划
- v1.4.18(2023-06-30)
- 修复 邮件发送失败问题
- v1.4.17(2023-06-30)
- 优化 支持发送匿名邮件
- 新增 域名详情页添加ICP备案查询
- v1.4.16(2023-06-30)
- 修复 `.co` 域名服务商`registrar_url` 字段缺少`http://` 的问题
- v1.4.15(2023-06-30)
- 修复 `.co` 域名服务商`registrar_url` 字段缺少`http://` 的问题
- v1.4.14(2023-06-30)
- 新增 域名`注册商` 字段,感谢 [@mjwtc0722](https://github.com/mjwtc0722) 贡献的部分代码
- v1.4.13(2023-06-30)
- 修复 添加prometheus-client依赖
- v1.4.12(2023-06-30)
- 新增 支持`prometheus``/metrics`数据获取接口
- v1.4.11(2023-06-29)
- 新增 分组名称批量删除
- 新增 证书列表和域名列表支持`分组`字段导入
- v1.4.10(2023-06-29)
- 优化 增加`.tw`后缀域名的支持
- v1.4.9(2023-06-26)
- 优化 增加域名信息更新条件,到期时间 `<30` 才更新,减少无效更新
- 优化 移除未使用的接口和代码
- v1.4.8(2023-06-25)
- 优化 证书和域名列表的分组筛选器
- 优化 证书列表增加`域名天数`
- 修复 删除主机地址后,证书数据未更新的问题
- 修复 docker缺失mysql驱动的问题
- v1.4.7(2023-06-23)
- 优化 获取证书支持:IP证书、多域名证书、通配符证书
- 优化 移除未使用的域名信息缓存表`cache_domain_info`
- v1.4.6(2023-06-22)
- 新增 通知方式增加备注字段
- 新增 通知方式新增钉钉、飞书渠道
- v1.4.5(2023-06-22)
- 修复 证书列表和域名列表`监测` 列排序无效问题
- 优化 通知方式列表支持排序
- v1.4.4(2023-06-22)
- 新增 批量删除主机地址
- 优化 支持多个通知渠道
- v1.4.3(2023-06-20)
- 修复 邮件通知测试报错 send_domain_list_email() missing 1 required positional argument: 'rows'
- v1.4.2(2023-06-20)
- 修复 删除证书报错 type object 'DomainModel' has no attribute 'domain_id'
- v1.4.1(2023-06-20)
- 修复 前端部分显示问题
- 修复 字段domain_expire_monitor缺失问题
- v1.4.0(2023-06-20)
- 优化 移除未使用到的历史字段
- 修复 前端局部更新异常的问题
- v1.4.0-alpha(2023-06-14)
- 新增 域名监控和SSL证书监控分离
- 新增 证书监控支持动态IP
- 修复 Bug: 'AddressModel' object has no attribute 'address_id'
- v1.3.6(2023-06-13)
- 修复 ipv6查询失败问题,暂时隐藏ipv6
- 新增 支持自定义配置数据库链接方式
- v1.3.5(2023-06-13)
- 修复 添加域名时查询证书失败问题
- v1.3.4(2023-06-09)
- 新增 前端显示版本号
- v1.3.3(2023-06-07)
- 修复 添加非443端口超时问题
- 添加前端UI多风格界面
- 优化部分问题
- v1.3.2(2023-06-04)
- 修复 升级失败问题,修改字段port默认值为443
- v1.3.1(2023-06-04)
- 新增 支持一个域名解析到多主机ip地址的SSL证书查询
- 优化 优化前端界面显示
- v1.2.23(2023-06-01)
- 新增 实验室tab,增加查询whois原始信息功能
- v1.2.22(2023-05-30)
- 新增 中文域名的支持
- v1.2.21(2023-05-30)
- 修复:`ModuleNotFoundError: No module named 'pytz_deprecation_shim'`
- 移除依赖`pytz`
- 固定依赖版本号
- v1.2.18(2023-05-29)
- 新增:增加对.sg后缀的域名到期时间支持,感谢[@poctopus](https://github.com/poctopus)贡献代码
- v1.2.17(2023-05-17)
- 新增:增加对.jp后缀的域名到期时间支持
- v1.2.16(2023-04-29)
- 新增:增加筛选功能: 筛选域名状态、证书状态、网站状态
- 新增:增加批量删除域名的功能
- v1.2.15(2023-04-28)
- 优化:更新whois-servers.txt,感谢[@poctopus](https://github.com/poctopus)贡献代码
- v1.2.14(2023-04-28)
- 修复:邮件正文字段空白的问题
- v1.2.13(2023-04-26)
- 新增:支持内网用户自己设置域名过期时间、证书过期时间
- v1.2.12(2023-04-22)
- 优化:修改域名信息和证书信息更新缓存机制,避免频繁更新导致请求失败
- 修复:没有设置邮件导致webhook失效的问题
- v1.2.11(2023-04-20)
- 更新whois-servers.txt,感谢[@poctopus](https://github.com/poctopus)贡献代码
- v1.2.10 (2023-04-16)
- 新增域名后缀的支持:.hk
- v1.2.9 (2023-04-16)
- 新增域名后缀的支持:.tv .live .info .top .xyz .biz .net等等
- v1.2.8 (2023-04-08)
- 优化:移除前端打包后的静态文件
- v1.2.4(2023-04-07)
- 新增:分组批量关联域名
- 优化:域名导出新增分组字段
- 优化:域名导入支持txt(仅域名字段)、csv(域名、备注)
- 修复:域名编辑分组的时候,看不到第10个以后的分组名字。
- 优化:域名分组列表ID改为序号
- v1.2.3(2023-04-05)
- 优化:支持docker多平台
- v1.2.2(2023-04-04)
- 修复:前端域名列表只有一条数据,分页下拉和编辑“分组”下拉列表打不开
- v1.2.1(2023-04-04)
- 修复:前端域名列表删除操作bug
- v1.2.0(2023-04-04)
- 新增:支持域名分组
- 优化:异步操作的前端状态显示
- 优化:优化前端域名列表字段显示
- v1.1.10(2023-04-03)
- 修复:部分域名剩余天数检测失败的问题
- 优化:查询域名之前进行域名优化处理,减少查询请求
- 优化:增加相同域名查询缓存,减少查询请求
- v1.1.9(2023-03-31)
- 新增:webhook通知方式,支持jinja2语法
- 发布GitHub Pages
- v1.1.8(2023-03-28)
- 修改自动发布action的触发方式
- v1.1.7(2023-03-28)
- 添加自动发布action
- 1.1.6
- bugfix: 用户删除token过期天数变量后登录报错
- 1.1.5
- 移除whois依赖,使用socket方式替代
- 前端域名列表增加字段排序
- 1.1.4
- 修复Linux二级域名报错问题
- 1.1.3
- 修复二级域名查询失败的问题
- 优化域名排序显示
- 1.1.2
- 暂时使用whois模块实现域名过期时间查询
- 1.1.1
- 修复前端备注不显示的问题
- 移除whois模块,因为它依赖whois/whois.exe
- 1.1.0
- 优化日志输出文件大小,减少磁盘空间占用
- 新增域名到期时间监测,感谢群友 @Roy 提出的建议
- 移除前端不必要的信息显示
- 1.0.6
- 新增域名备注
- 1.0.5
- 新增通知方式:企业微信
- 1.0.4
- 修复 `1.0.0=> 1.0.3` 自动更新异常
- 1.0.3
- 新增单个域名的检测开关,可控制单个域名的证书监测
- 1.0.2
- 添加SMTP STARTTLS 支持 587端口,感谢[@kudosiscon](https://github.com/kudosiscon)贡献的代码
- 1.0.1
- 修改批量更新方式为异步更新,避免接口超时
- 1.0.0
- 修复前端批量导入域名按钮只能导入一次的问题
- 增加options请求缓存时间,减少请求
- 增加网站连接状态默认值为:未知(黄色)
- 修改批量导入为异步导入执行,导入测试文件大小11.8M
- 修复导入域名解析错误
- 0.0.18
- 修复部分公司邮件校验失败的问题
- 修复添加异常域名后系统奔溃的问题
- v0.017
- 通过openssl支持自签名证书
- v0.0.16
- 支持自签名证书,感谢[@star7th](https://github.com/mouday/domain-admin/issues/7#issuecomment-1304415797) 提出的建议
- v0.0.14 v0.0.15
- 修复bug: peewee.OperationalError: no such table: tb_version,感谢[@star7th](https://github.com/mouday/domain-admin/issues/7#issuecomment-1300634496) 提出的反馈
- v0.0.13
- 支持非443端口,感谢[@star7th](https://github.com/mouday/domain-admin/issues/7) 提出的建议
- 修复前端网络错误没有错误弹窗提示的问题
- v0.0.12
- 新增webhook通知方式,感谢[@star7th](https://github.com/mouday/domain-admin/issues/3) 提出的建议
- v0.0.11
- 优化前端页面显示,增加页面加载进度条
- 修复有效期天数显示不对的问题
- v0.0.10
- 更新域名证书获取方式为socket,替换curl,移除curl依赖,兼容windows,感谢[@cbr252522489](https://github.com/mouday/domain-admin/issues/1) 提出的反馈
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.7.15-alpine3.16
ADD . /app
WORKDIR /app
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apk add --no-cache --virtual .build-deps gcc musl-dev libffi-dev openssl-dev bash \
&& apk add --no-cache libffi openssl \
&& pip install --no-cache-dir --upgrade pip setuptools wheel gunicorn \
&& pip install --no-cache-dir -r requirements/production.txt \
&& apk del .build-deps
CMD gunicorn --bind '[::]:8000' --timeout 120 'domain_admin.main:app'
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018-present, Mouday
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Executable
+19
View File
@@ -0,0 +1,19 @@
graft domain_admin
include README.md
include requirements/production.txt
prune tests
prune doc
prune image
global-exclude .DS_Store
global-exclude *.pyc
global-exclude *.log
global-exclude *.db
global-exclude *.db-shm
global-exclude *.db-wal
global-exclude *.sqlite
global-exclude .env
global-exclude .gitignore
global-exclude .dockerignore
global-exclude .travis.yml
global-exclude Dockerfile
global-exclude Makefile
+69
View File
@@ -0,0 +1,69 @@
# 伪目标
.PHONY: dev pro build clean upload publish
# 运行开发环境
dev:
source venv/bin/activate && gunicorn --bind '0.0.0.0:5001' --timeout 120 --reload 'domain_admin.main:app'
# 运行生产环境
pro:
source venv/bin/activate && gunicorn --bind '0.0.0.0:5001' --timeout 120 'domain_admin.main:app'
# 发布 make release
.PHONY: release
release:
python ./version-cli/auto_release.py
# 打包
build:
python setup.py sdist bdist_wheel --python-tag py2.py3
# 制作 docker 镜像
.PHONY: docker-build
docker-build:
docker build -t mouday/domain-admin:latest -f Dockerfile .
# 构建并运行 docker 镜像
.PHONY: docker-run
docker-run:
docker run -p 8000:8000 mouday/domain-admin:latest
# 构建并运行 docker 镜像
.PHONY: docker-build-run
docker-build-run:
make docker-build
make docker-run
# 清空打包产物
clean:
rm -rf temp logs .pytest_cache
rm -rf dist build *.egg-info
# 上传打包产物到 pypi
upload:
twine check dist/*
twine upload dist/*
# 发布 make publish
publish:
make clean
make build
make upload
make clean
# 运行所有测试
.PHONY: test
test:
pytest -c pytest.ini tests/api/test_index.py
# 安装开发环境依赖
# make install-require
.PHONY: install-require
install-require:
pip install -r requirements/development.txt
# 快速提交
# make fix
.PHONY: fix
fix:
git add . && git commit -m 'fix' && git push
+55
View File
@@ -0,0 +1,55 @@
# OpenID Connect (OIDC) 单点登录配置指南
本项目已集成 OpenID Connect (OIDC) 单点登录功能,支持 pocket-id 等 OIDC 提供商。
## 功能特性
- 使用 authlib 库实现标准 OIDC 协议
- 支持自动创建用户账号
- 不修改现有数据模型
- 与现有 JWT 认证系统无缝集成
## 配置方式
### 1. 环境变量配置
`.env` 文件中添加以下配置:
```bash
# 启用 OIDC 单点登录
OIDC_ENABLED=true
# OIDC 客户端 ID(必填)
OIDC_CLIENT_ID=your_client_id
# OIDC 客户端密钥(必填)
OIDC_CLIENT_SECRET=your_client_secret
# OIDC 提供商的 Issuer URL(必填,不包含 .well-known/openid-configuration
OIDC_ISSUER_URL=https://your-oidc-provider.com
# OIDC 请求的权限范围(可选,默认为 "openid profile email"
OIDC_SCOPES=openid profile email
# 自动创建用户的默认角色(可选,默认为 1)
# 1 = 普通用户(RoleEnum.USER
# 10 = 管理员(RoleEnum.ADMIN
OIDC_AUTO_CREATE_USER_ROLE=1
# 自动创建用户的默认状态(可选,默认为 false)
# true = 启用账号
# false = 禁用账号(需要管理员手动启用)
OIDC_AUTO_CREATE_USER_STATUS=false
```
**重要说明**:
- 回调地址由系统自动生成(`/api/oidc/callback`),无需手动配置
- 在 OIDC 提供商中配置回调地址时,使用完整 URL,例如:`https://xxx.com/api/oidc/callback`
- 生产环境建议使用 HTTPS 协议
## 注意事项
1. 本实现尽可能不修改现有数据模型,OIDC 用户使用现有的 `UserModel`
2. OIDC 用户的 `password` 字段为空字符串,不影响系统功能
3. 用户可以同时使用 OIDC 登录和传统登录(如果管理员为 OIDC 用户设置了密码)
4. 系统会自动处理用户创建和更新,无需手动干预
+153
View File
@@ -0,0 +1,153 @@
# Domain Admin
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/domain-admin)](https://pypi.org/project/domain-admin)
[![PyPI](https://img.shields.io/pypi/v/domain-admin.svg)](https://pypi.org/project/domain-admin)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/domain-admin?label=pypi%20downloads)](https://pypi.org/project/domain-admin)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/mouday/domain-admin?label=docker%20version&sort=semver)](https://hub.docker.com/r/mouday/domain-admin)
[![Docker Pulls](https://img.shields.io/docker/pulls/mouday/domain-admin)](https://hub.docker.com/r/mouday/domain-admin)
[![Build Status](https://app.travis-ci.com/mouday/domain-admin.svg?branch=master)](https://app.travis-ci.com/mouday/domain-admin)
[![PyPI - License](https://img.shields.io/pypi/l/domain-admin)](https://github.com/mouday/domain-admin/blob/master/LICENSE)
[![Documentation Status](https://readthedocs.org/projects/domain-admin/badge/?version=latest)](https://domain-admin.readthedocs.io/zh_CN/latest/?badge=latest)
[![GitHub release](https://img.shields.io/github/v/release/dromara/domain-admin)](https://github.com/dromara/domain-admin/releases)
[![GitHub Stars](https://img.shields.io/github/stars/dromara/domain-admin?color=%231890FF&style=flat-square)](https://github.com/dromara/domain-admin)
[![宝塔服务器面板,一键全能部署及管理](https://img.shields.io/badge/BT_Deploy-Install-20a53a)](https://www.bt.cn/u/MaBJJC)
![](https://raw.githubusercontent.com/mouday/domain-admin/master/image/domain.svg)
> 该项目已加入 [Dromara开源社区](https://github.com/dromara/domain-admin)
基于Python + Vue3.js 技术栈实现的域名和SSL证书监测平台
用于解决,不同业务域名SSL证书,申请自不同的平台,到期后不能及时收到通知,导致线上访问异常,被老板责骂的问题
Domain Admin是一个轻量级监控方案,占用系统资源较少。同时,Domain Admin也可以作为一个Flask 和 Vue.js前后端分离的项目模板
- 项目优势
- 集中管理: 提供一个统一的平台来管理多个域名,极大地提高了管理效率。
- 自动提醒: 支持域名到期提醒,帮助用户避免因域名过期导致的服务中断。
- 开源灵活: 作为开源项目,用户可以根据自身需求进行定制和扩展。
- 社区支持: 拥有活跃的社区,可以获得持续的更新和问题支持。
- 用户友好: 界面简洁直观,容易上手。
- 功能描述
- 核心功能:`域名``SSL证书``托管证书文件` 的过期监控,到期提醒
- 支持证书:单域名证书、多域名证书、泛域名(通配符)证书、IP证书、自签名证书
- 证书部署:单一主机部署、多主机部署、动态主机部署
- 通知渠道:支持邮件、Webhook、企业微信、钉钉、飞书等通知方式
- 支持平台:macOS、Linux、Windows
- 辅助功能:`Lets Encrypt` SSL证书免费申请和SSL证书自动续期
- 多语言:支持中文、英文
- 项目地址:[后端代码(github](https://github.com/dromara/domain-admin)、[后端代码(国内镜像)](https://gitee.com/dromara/domain-admin)
- 发布渠道:[PyPI](https://pypi.org/project/domain-admin)、[Docker](https://hub.docker.com/r/mouday/domain-admin)、[Releases](https://github.com/mouday/domain-admin/releases)、[1Panel](https://apps.fit2cloud.com/1panel/domain-admin)、[宝塔](https://www.bt.cn/u/MaBJJC)
- 使用文档:[readthedocs](https://domain-admin.readthedocs.io/zh_CN/latest/?badge=latest)
- 接口文档:[github](https://mouday.github.io/domain-admin/)、[gitee](https://mouday.gitee.io/domain-admin/)
## 安装
请参考安装文档:[https://domain-admin.readthedocs.io/zh_CN/latest/manual/install.html](https://domain-admin.readthedocs.io/zh_CN/latest/manual/install.html)
如果不想安装,可以直接使用我们部署好的线上应用,需要体验的用户可以加入`QQ群``微信群`,提供邮箱即可
- 预览版(纯静态、无实际功能,账号密码随意):https://mouday.github.io/domain-admin-web/
- 体验版(由大家伙捐献的服务器,邮箱注册即可体验):https://demo.domain-admin.cn/ (服务器已到期)
建议自行部署,这样比较安全
> 服务器和域名由群友赞助提供
本项目支持的安装方式
| 安装方式 | 链接 |
|--------|----------------------------------|
| 宝塔 | [链接](https://www.bt.cn/u/MaBJJC) |
| Docker | [链接](https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html#docker) |
| 源码 | [链接](https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html#id4) |
| 1Panel | [链接](https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html#panel) |
| k8s | [链接](https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html#k8s) |
| pip | [链接](https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html#pip) |
## 项目截图
账号密码随意(例如:admin/123456),预览模式仅提供模拟数据,无法操作修改
1、网页版:
![](https://gitee.com/mouday/domain-admin/raw/master/image/dashboard.png)
![](https://gitee.com/mouday/domain-admin/raw/master/image/screencapture.png)
- 预览地址:[https://mouday.github.io/domain-admin-web/](https://mouday.github.io/domain-admin-web/)
本项目采用的是前后端分离模式,前端代码在另外一个仓库。
前端项目地址(请自行解码):aHR0cHM6Ly9naXRodWIuY29tL21vdWRheS9kb21haW4tYWRtaW4td2Vi
或者关注微信公众号:
<img src="https://gitee.com/mouday/domain-admin/raw/master/image/coding-big-tree.jpg" width="300">
回复:`domain-admin-web`,获取完整的前端代码
2、移动端版:
<img src="https://gitee.com/mouday/domain-admin/raw/master/image/screencapture-mini.png" width="220">
- 移动端预览地址(请使用移动端窗口体验):[https://mouday.github.io/domain-admin-mini/](https://mouday.github.io/domain-admin-mini/)
移动端项目地址(请自行解码):aHR0cHM6Ly9naXRodWIuY29tL21vdWRheS9kb21haW4tYWRtaW4tbWluaQ==
为了更多地人参与到项目中来,现已开放前端代码,加入QQ群即可获取前端项目地址
## 问题反馈交流
由于访问github的网络不稳定,如果需要及时获得反馈,请通过以下方式联系
QQ群号:
- (一群已满) 731742868
- (三群) 452761242
邀请码:domain-admin
<img src="https://gitee.com/mouday/domain-admin/raw/master/image/qq-group.jpeg" width="300">
微信交流群
<img src="https://gitee.com/mouday/domain-admin/raw/master/image/coding-big-tree.jpg" width="300">
回复:`domain-admin`,和众多使用者一起交流学习使用经验,反馈使用问题,获得更及时的解答和修复
提交PR的小伙伴,可以进去`核心开发者交流群`,和更多志同道合的朋友交流
## 更新日志
[CHANGELOG.md](https://domain-admin.readthedocs.io/zh_CN/latest/manual/changelog.html)
## 使用者
虚位以待,可以将使用者公司或个人的名字放到这里
赞助者可以进入`赞助商群`,优先处理你的问题
| 时间 | 赞助者 | 金额 |
|------------| - | - |
| 2023-11-21 | [@1275788667](https://github.com/1275788667) | ¥50
| - | [@hhdebb](https://github.com/hhdebb) | 若干
| 2024-04-23 | [@1275788667](https://github.com/1275788667) | ¥50
| - | `星河 ๑. ` | 若干
| 2024-07-10 | [@1275788667](https://github.com/1275788667) | ¥50
| 2024-10-31 | ldytech | ¥200
| 2024-11-01 | [@xiaobiaozhao](https://github.com/xiaobiaozhao) | ¥50
<img src="https://gitee.com/mouday/domain-admin/raw/master/image/alipay.jpg" width="300">
[![Stargazers over time](https://starchart.cc/dromara/domain-admin.svg)](https://starchart.cc/mouday/domain-admin)
<a href="https://hellogithub.com/repository/2b44fb56aca14df7a6279b0997f7325c" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=2b44fb56aca14df7a6279b0997f7325c&claim_uid=Hb7yUF2AEhnmSwJ" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`dromara/domain-admin`
- 原始仓库:https://github.com/dromara/domain-admin
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+12
View File
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""
@File : dev.py
@Date : 2023-06-14
启动方式
$ flask run
"""
from domain_admin.main import app
if __name__ == '__main__':
app.run(port=8000)
+9
View File
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
"""
@File : dev.py
@Date : 2023-06-14
"""
from domain_admin.main import app
if __name__ == '__main__':
app.run(debug=True, port=5001)
+20
View File
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+35
View File
@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
+8
View File
@@ -0,0 +1,8 @@
sphinx
sphinx_rtd_theme
sphinx-autobuild
recommonmark
sphinx-markdown-tables
myst-parser
linkify-it-py
m2r2
+62
View File
@@ -0,0 +1,62 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Domain Admin'
copyright = '2023, Peng Shiyu'
author = 'Peng Shiyu'
release = 'latest'
version = 'latest'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'sphinx.ext.napoleon',
'sphinx.ext.githubpages',
'm2r2',
# 'myst_parser',
'sphinx_markdown_tables',
]
templates_path = ['_templates']
exclude_patterns = []
language = 'zh_CN'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
source_suffix = ['.rst', '.md']
myst_enable_extensions = [
"amsmath",
"attrs_inline",
"colon_fence",
"deflist",
"dollarmath",
"fieldlist",
"html_admonition",
"html_image",
"linkify",
"replacements",
"smartquotes",
"strikethrough",
"substitution",
"tasklist",
]
+17
View File
@@ -0,0 +1,17 @@
.. mdinclude:: ../../README.md
目录
.. toctree::
:maxdepth: 2
manual/index.md
manual/install.md
manual/notify.md
manual/ssl-cert.md
manual/question.md
manual/plugins.md
manual/plan.md
manual/development.md
manual/changelog.md
manual/article.md
+46
View File
@@ -0,0 +1,46 @@
# 相关文章
## 媒体报道
- 2022-10-14 Coding Big Tree [Domain Admin:监测域名SSL证书,到期邮件提醒](https://mp.weixin.qq.com/s/batUjnNscGqYS5x5Vbn_Vw)
- 2022-10-21 阮一峰的网络日志 [科技爱好者周刊(第227期):脸书的公司入职教育](https://mp.weixin.qq.com/s/8dkgEtH5r5GgdwaT-DAysw)
- 2023-04-14 阮一峰的网络日志 [科技爱好者周刊#250:新技术的最大风险](https://mp.weixin.qq.com/s/IS4XveCr2w1poEY2NvK29w)
- 2023-04-18 HACK学习呀 [干货 | Github安全搬运工 2023年第一期](https://mp.weixin.qq.com/s/gnWTz6GhAI0A2XhFXVI_YQ)
- 2023-05-29 HelloGitHub [《HelloGitHub》第 86 期](https://mp.weixin.qq.com/s/c2iJXLyMA8YtteyNC5sbkQ)
- 2023-05-30 PHP学习网 [【开源推荐】基于 Python + Vue3.js 技术栈实现的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/slZgFXR3M4xVfpYkHnS67w)
- 2023-06-05 HelloGitHub [暑期实习开始啦「GitHub 热点速览」](https://mp.weixin.qq.com/s/Ocw6FBgSLVop2bMKU9hzmg)
- 2023-06-10 Python猫 [Python潮流周刊#6Python 3.12 有我贡献的代码!](https://mp.weixin.qq.com/s/g-vVxgY_JpMmXTC3EntNeg)
- 2023-06-19 开源前线 [YYDSGitHub 这些项目真香!](https://mp.weixin.qq.com/s/05hGJqDs8ImF6mXJ-Hff-w)
- 2023-06-19 GitHub黑板报 [重磅推荐:一款开源的域名 SSL 证书监测平台](https://mp.weixin.qq.com/s/qr5j_bJNRshN7xxwn4a7tw)
- 2023-06-27 云海天教程 [开源的域名 SSL 证书监测平台](https://mp.weixin.qq.com/s/UJkQtoXXAwPAu1lg0YO2mw)
- 2023-06-29 奇妙的Linux世界 [又一款好用的开源域名 SSL 证书监测平台](https://mp.weixin.qq.com/s/IXD6oaHTfMBkoXs7lnR1zg)
- 2023-07-06 TJ君 [重磅推荐:一款开源的域名 SSL 证书监测平台](https://mp.weixin.qq.com/s/zBQccvuwCAzapcHhVFjZXg)
- 2023-07-07 框架师 [GitHub 热榜日报:发现更多有趣的项目!](https://mp.weixin.qq.com/s/ywbubMMyrekV4sKv9NzsrQ)
- 2023-07-14 良许Linux [又一款好用的开源域名 SSL 证书监测平台](https://mp.weixin.qq.com/s/drFLJCqz3CEF-DPfa5jnSQ)
- 2023-07-21 IT仔的笔记本 [一套属于自己的域名、证书管理系统,只需要1分钟就能上手,(收藏起来,会用到的)](https://mp.weixin.qq.com/s/rYpWBgZgizqDWOaPq2g1_g)
- 2023-07-27 DevOps运维民工 [推荐一款开源的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/LlaD7KUogwdfQMs_v7QgPg)
- 2023-07-29 云原生运维技术 [运维百宝箱-https证书的过期巡检监控](https://mp.weixin.qq.com/s/iL5AHjw1gZ5_vnWsI8OfNA)
- 2023-08-20 帝国辉耀1 [domain-admin域名监控的源码搭建](https://blog.csdn.net/u013901725/article/details/132394530)
- 2023-08-21 帝国辉耀1 [domain-admin域名监控的使用(二)](https://blog.csdn.net/u013901725/article/details/132403565)
- 2023-08-21 PHP学习网 [【开源推荐】基于Python + Vue3.js 技术栈实现的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/7qvYTALe5uIlkD3jEgQ1Kw)
- 2023-08-30 运维开发故事 [基于Python + Vue3.js 技术栈实现的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/cOOJN6eUSqP0wmt8-3sOJw)
- 2023-09-11 浩道Linux [【来自读者投稿】这款开源的域名和SSL证书监测平台真香!](https://mp.weixin.qq.com/s/z1MOe8ptp76F0PEHaxVsjQ)
- 2023-09-15 Python技术 [Python:获取域名ssl证书信息和到期时间!](https://mp.weixin.qq.com/s/E9uha-1UDTahNx1lEKvfXQ)
- 2023-09-16 Python客栈 [Python:获取域名ssl证书信息和到期时间!](https://mp.weixin.qq.com/s/Mz2WSuEinOT1qQfpSiz7LA)
- 2023-11-03 测试指北 [Domain Admin—基于Python + Vue3.js 技术栈实现的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/kvUNi25ge3YLKUYcsR-CGA)
- 2023-12-28 跬步之巅 [开源初探 - domain-admin](https://mp.weixin.qq.com/s/dnFitCv_n9os20TLYLbITw)
- 2024-01-31 IT仔的笔记本 [网站管理员的福利 | 一个开源的域名和SSL证书监测平台](https://mp.weixin.qq.com/s/AzaiyhUpBQHBCMdhEkYmSQ)
- 2024-02-23 漫跑的小兔 [用Docker部署Domain Admin](https://mp.weixin.qq.com/s/sie9zDP86Ez93FqvJlnU6Q)
- 2024-03-16 ITBLOG [Domain Admin:域名和SSL证书监测系统](https://mp.weixin.qq.com/s/yZ01dMUYcvN2Ms1CsnExhg)
- 2024-03-19 ITBLOG [pip安装Domain-Admin](https://mp.weixin.qq.com/s/cxklXTktXBRzlDxnN5T5tQ)
- 2024-03-23 开源技术小栈 [开源域名SSL证书监测平台 Domain Admin](https://mp.weixin.qq.com/s/DY65JI1jLj42YM1E6-LUGw)
- 2024-04-09 辣码甄源 [Domain Admin:方便快捷的图形化域名和SSL证书监测平台](https://mp.weixin.qq.com/s/29MGqNbSX5VLBi8Im5GQDQ)
- 2024-05-10 刘哥聊技术 [开源|一款基于Python + Vue3.js 技术栈实现的域名和SSL证书监测管理平台](https://mp.weixin.qq.com/s/BOQLpvGjXvvNgIoeAGJBRg)
- 2024-05-12 云原生生态圈 [这个域名证书监控平台又吊打Uptimekuma了](https://mp.weixin.qq.com/s/6C1zQJHMo-a4sy6DLJY4Xw)
- 2024-10-28 IT运维技术圈 [推荐一个非常轻便的ssl监控工具](https://mp.weixin.qq.com/s/cq4id4OfoyTQxhXvO-bA_w)
## 参考文章
- [Python:获取域名ssl证书信息和到期时间](https://pengshiyu.blog.csdn.net/article/details/115861795)
- [一文搞定:whois数据库查询域名信息(WHOIS)](https://pengshiyu.blog.csdn.net/article/details/129691736)
+3
View File
@@ -0,0 +1,3 @@
# 升级日志
.. mdinclude:: ../../../CHANGELOG.md
+62
View File
@@ -0,0 +1,62 @@
# 二次开发
## 技术选型
前端选型(网页版)
- Node.js
- Vite.js
- Vue3.js
- Vue Router
- Pinia
- Element Plus
- Tailwind CSS
前端选型(桌面版)
- node.js v16.15.1
- vue3.js
- quasar + electron
后端选型
- Python3.7.0
- [Flask](https://flask.palletsprojects.com/en/2.2.x/)
- [jinja2](https://jinja.palletsprojects.com/en/3.1.x/)
- [peeweesqlite](http://docs.peewee-orm.com/en/latest/index.html#)
- [apscheduler](https://apscheduler.readthedocs.io/en/3.x/)
- [supervisord](http://supervisord.org/index.html) 部署推荐
- [gunicorn](https://docs.gunicorn.org/) mac/linux 推荐
- [waitress](https://github.com/Pylons/waitress) windows 推荐
## 接口文档
[https://mouday.github.io/domain-admin/](https://mouday.github.io/domain-admin/)
代码推送
```bash
# github
git push -u origin master
# gitee
git push -u gitee master
```
## 测试证书
证书测试 [https://badssl.com/](https://badssl.com/)
获取证书列表
```js
JSON.stringify([...document.querySelectorAll('a')].map(a=>a.href))
```
批量域名列表 (746314个)
- [alexa-top-1m.csv.zip](http://s3.amazonaws.com/alexa-static/top-1m.csv.zip)
- [docs/top-1m.csv](tests/top-1m.txt)
![](image/domain-admin-process.png)
+85
View File
@@ -0,0 +1,85 @@
# 用户手册
## 项目简介
功能:
- 权限
- 用户登录
- 用户退出
- 修改密码
- 域名管理
- 域名添加
- 域名删除
- 域名搜索
- 域名导入、导出功能
- 域名信息
- 证书监控
- 定时监控
- 到期邮件提醒
- 微信提醒
- 手动/自动更新证书信息
- 用户管理
- 添加用户
- 删除用户
- 禁用/启用用户
- 监控日志
- 管理界面
- api接口(用于二次开发)
- web浏览器
- 桌面
- ~~移动端(app+小程序)~~
## 使用说明
批量导入域名
### 1、快速导入
`.txt`结尾的文件,视为快速导入文件
| 支持字段 | 必填 |
| - | - |
| 域名 | 是 |
| 端口 | 否,默认:443 |
示例:domain.txt
```bash
www.baidu.com
www.zhihu.com:443
```
默认端口是443,如果是非443端口可以用`:`冒号分隔域名和端口号
### 2、完整导入
`.csv`结尾的文件,视为完整导入文件
> 注意:用普通文件编辑器编辑即可,不要用excel编辑,可能会出现乱码问题
| 支持字段 | 必填 |
| - | - |
| 域名 | 是 |
| 端口 | 否,默认:443 |
| 分组 | 否 |
| 备注 | 否 |
示例:domain.csv
```bash
域名,端口,分组,备注
www.zhihu.com,,问答,知乎
www.baidu.com,443,,
```
备注:逗号不能省略,字段顺序可以改变,多余的字段会被忽略
首次导入数据后,后台会更新证书信息,此时不要频繁操作,否则会出现锁表错误。
稍等一会,刷新页面,就能看到完整的证书信息了。
+475
View File
@@ -0,0 +1,475 @@
# 安装方式
管理员默认的账号和密码
- 账号:admin
- 密码:123456
> `强烈建议`:登录系统后修改默认密码
Domain Admin是一个Flask应用,启动部署方式和Flask应用是一样的
> `特别注意`
> - 【测试环境】可以使用默认的sqlite数据库;
> - 【生产环境】推荐使用MySQL数据库,避免锁表,配置方式:[可选配置](#id5)
## 方式一:宝塔面板安装
[![宝塔服务器面板,一键全能部署及管理](https://img.shields.io/badge/BT_Deploy-Install-20a53a)](https://www.bt.cn/u/MaBJJC)
宝塔Linux面板是提升运维效率的服务器管理软件,支持一键LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA等100多项服务器管理功能。
有30个人的专业团队研发及维护,经过200多个版本的迭代,功能全,少出错且足够安全,已获得全球百万用户认可安装。
如果还未安装宝塔,请先前往[下载](https://www.bt.cn/u/MaBJJC)注册安装
面板版本v9.2.0 宝塔面板 --> Docker --> 应用商店 --> Domain Admin --> 安装
[【教程贴】Docker应用-Domain Admin 安装帮助](https://www.bt.cn/bbs/thread-139621-1-1.html)
<a title="宝塔服务器面板,一键全能部署及管理" href="https://www.bt.cn/u/MaBJJC"><img src="https://mouday.github.io/img/2024/10/24/fxkd3qe.jpg" width="200"></a>
![](https://mouday.github.io/img/2024/10/31/63ke2ij.png)
## 方式二:docker启动
感谢[@miss85246](https://github.com/miss85246) 提供Docker支持
```bash
# 本地文件夹和容器文件夹映射
$ docker run \
-v $(pwd)/database:/app/database \
-v $(pwd)/logs:/app/logs \
-p 8000:8000 \
--name domain-admin \
mouday/domain-admin:latest
```
- databasesqlite数据库和重要数据的目录
- logs:日志目录,用于排查问题
启动命令增加参数:`-d`,可以后台运行
```bash
# 本地文件夹和容器文件夹映射
$ docker run \
-d \
-v $(pwd)/database:/app/database \
-v $(pwd)/logs:/app/logs \
-p 8000:8000 \
--name domain-admin \
mouday/domain-admin:latest
```
也可以用dokcer-compose来一键启动
可以使用`docker-compose up -d`来启动
当版本更新时,可以用`docker-compose pull`来拉取镜像,然后用`docker-compose up -d`来重启新镜像
```yaml
version: '3.3'
services:
domain-admin:
volumes:
- './database:/app/database'
- './logs:/app/logs'
ports:
- '8000:8000'
container_name: domain-admin
image: mouday/domain-admin:latest
```
可用的docker镜像站
```bash
$ sudo mkdir -p /etc/docker
$ sudo tee /etc/docker/daemon.json <<EOF
{
"registry-mirrors": [
"https://dockerpull.com",
"https://dockerproxy.cn",
"https://docker.loli.voto",
"https://docker.chenby.cn",
"https://docker-cf.registry.cyou",
"https://docker.m.daocloud.io",
"https://docker.ketches.cn",
"https://docker.1panel.dev",
"https://docker.1panel.live",
"https://docker.rainbond.cc",
"https://hub.rat.dev",
"https://docker.aityp.com/",
"https://docker.catiz.eu.org/"
]
}
EOF
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
```
## 方式三:下载源码安装
下载地址:[https://github.com/mouday/domain-admin/releases](https://github.com/mouday/domain-admin/releases)
本方式仅适用于二次开发,推荐采用 pip安装 或者 docker启动 的方式体验
例如:以`v1.5.8`为例
每次发布都会包含以下代码包
```
domain-admin-1.5.8.tar.gz # 完整源码包,包含打包后的H5端、web端代码
domain_admin-1.5.8-py2.py3-none-any.whl # pip包,可直接安装
domain-admin-mini-v1.5.8.tar.gz # H5端源码
domain-admin-web-v1.5.8.tar.gz # web端源码
Source code(zip) # Python源码 windows适用
Source code(tar.gz) # Python源码 linux/mac适用
```
此处以linux/macos为例,windows用户可直接点击下载,自行使用加压软件解压
1、下载发布包
```bash
# 下载 domain-admin-1.5.8.tar.gz
wget https://github.com/mouday/domain-admin/releases/download/v1.5.8/domain-admin-1.5.8.tar.gz
# 如果下载速度过慢,可以使用加速方案
wget https://ghproxy.com/https://github.com/mouday/domain-admin/releases/download/v1.5.8/domain-admin-1.5.8.tar.gz
```
2、解压进入
```bash
tar -zxvf domain-admin-1.5.8.tar.gz
cd domain-admin-1.5.8
```
3、创建虚拟环境
```bash
# 创建名为 venv 的虚拟环境,windows用户参考方式一
$ python3 -m venv venv && source venv/bin/activate
# 安装依赖
pip3 install .
```
4、新建启动文件 `app.py`
```python
from domain_admin.main import app
if __name__ == '__main__':
app.run(port=8000)
```
此时的目录结构如下
```bash
$ tree -L 1
.
├── LICENSE
├── MANIFEST.in
├── PKG-INFO
├── README.md
├── app.py # 新建的启动文件
├── build
├── database
├── domain_admin
├── domain_admin.egg-info
├── logs
├── requirements
├── setup.cfg
├── setup.py
├── temp
└── venv
```
5、启动运行
```bash
# 启动运行
$ python app.py
* Serving Flask app 'domain_admin.main'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:8000
Press CTRL+C to quit
```
- warning提示可以忽略,推荐参考方式一,使用`gunicorn` 或者 `waitress-serve` 启动
访问地址:[http://127.0.0.1:8000](http://127.0.0.1:8000)
## 方式四:1Panel面板安装
1Panel是一个Linux 服务器运维管理面板,Domain-Admin已发布到它的应用商店,可以直接安装
安装地址:
[https://apps.fit2cloud.com/1panel/domain-admin](https://apps.fit2cloud.com/1panel/domain-admin)
## 方式五:k8s部署
配置文件示例
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/instance: domain-admin-latest
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: domain-admin
app.kubernetes.io/version: 1.16.0
argocd.argoproj.io/instance: domain-admin-latest
helm.sh/chart: domain-admin-0.1.0
name: domain-admin-latest
namespace: domain-admin-production
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app.kubernetes.io/instance: domain-admin-latest
app.kubernetes.io/name: domain-admin
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/instance: domain-admin-latest
app.kubernetes.io/name: domain-admin
spec:
containers:
- name: domain-admin
image: mouday/domain-admin:latest
imagePullPolicy: Always
env:
- name: DB_CONNECT_URL
value: "sqlite:///database/database.db"
livenessProbe:
failureThreshold: 3
httpGet:
path: /
port: http
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
ports:
- containerPort: 8000
name: http
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /
port: http
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
```
## 方式六:pip安装
运行环境:
- Python >= 2.7 或者 Python >= 3.4
- sqlite、openssl
python的安装,可以参考文章:[Linux环境下安装Python3](https://pengshiyu.blog.csdn.net/article/details/85304557)
可以使用 `pyenv` + `venv` 管理多个Python版本和隔离虚拟环境
确保已经安装Python解释器
```bash
$ python3 --version
Python 3.7.0
```
linux / macos 安装
```bash
# 创建名为 venv 的虚拟环境并激活
$ python3 -m venv venv && source venv/bin/activate
# 安装 domain-admin
$ pip install gunicorn domain-admin
# 启动运行
$ gunicorn --bind '127.0.0.1:8000' 'domain_admin.main:app'
```
windows 安装
```bash
# 创建名为 venv 的虚拟环境
> py -3 -m venv venv
# 激活虚拟环境
> venv\Scripts\activate
# 安装 domain-admin
> pip install waitress domain-admin
# 启动运行
> waitress-serve --listen=127.0.0.1:8000 domain_admin.main:app
```
访问地址:http://127.0.0.1:8000
升级到最新版本
```bash
$ pip3 install -U domain-admin -i https://pypi.org/simple
```
Python2.7如果安装依赖报错,可以使用依赖文件 [production-2.7.txt](https://github.com/mouday/domain-admin/tree/master/requirements/production-2.7.txt)
先安装好依赖文件的依赖,再安装domain-admin即可
## 其他部署方式
可以参考[https://flask.palletsprojects.com/en/2.3.x/deploying/](https://flask.palletsprojects.com/en/2.3.x/deploying/)
## 平滑升级
Domain Admin所有版本都支持平滑升级
安装最新版,重启即可
> 注意:如果是docker安装的,注意`database` 目录是不是手动挂载的,不要删除
## 可选配置
通过配置`.env` 文件或者直接设置系统环境变量
1、修改数据库存储方式
```bash
# sqlite 默认 (少量用户推荐)
DB_CONNECT_URL=sqlite:///database/database.db
# mysql (团队使用推荐)
DB_CONNECT_URL=mysql://root:123456@127.0.0.1:3306/data_domain
# postgresql,需自己安装psycopg2依赖:pip install psycopg2
DB_CONNECT_URL=postgresql://root:123456@localhost:5432/data_domain
```
说明:以上配置仅为示例,根据自己实际情况修改
如果使用mysql数据库,建议指定默认字符编码为`utf8mb4`
```sql
create database data_domain default character set=utf8mb4;
```
## 配置参数
可以通过运行目录下添加 `.env` 文件配置项目的可选参数
```bash
# 数据库链接参数
# 支持数据库:sqlite(默认)、mysql、postgresql
DB_CONNECT_URL=mysql://root:123456@127.0.0.1:3306/data
# 运行模式
# 可选:production(默认) / development(可以打印更多的调试日志)
APP_MODE=production
# 允许远程执行的命令白名单,多个英文分号(;)分隔
ALLOW_COMMANDS=/opt/nginx/sbin/nginx -s reload;nginx -s reload
# 允许用户注册,目前仅支持邮箱验证码登录
ENABLED_REGISTER=true
```
还可以通过环境变量来暴露
```bash
export APP_MODE=production
```
`Dockerfile` 中使用 `ENV` 指令设置环境变量
```bash
# 设置单个环境变量
ENV APP_MODE production
# 设置多个环境变量
ENV APP_MODE=production ALLOW_COMMANDS='nginx -s reload'
```
使用 `docker run` 命令的 `-e``--env` 选项在运行容器时设置环境变量
```bash
# 设置单个环境变量
docker run -e APP_MODE=production my_image
# 设置多个环境变量
docker run -e APP_MODE=production -e ALLOW_COMMANDS='nginx -s reload' my_image
```
`docker-compose.yml`文件中使用`environment`关键字
```yaml
version: '3.3'
services:
domain-admin:
volumes:
- './database:/app/database'
- './logs:/app/logs'
ports:
- '8000:8000'
environment:
- APP_MODE=production
container_name: domain-admin
image: mouday/domain-admin:latest
```
`docker-compose.yml`文件从宿主机获取环境变量,可以使用`env_file`关键字指向一个文件
```yaml
version: '3.3'
services:
domain-admin:
volumes:
- './database:/app/database'
- './logs:/app/logs'
ports:
- '8000:8000'
env_file:
- .env
container_name: domain-admin
image: mouday/domain-admin:latest
```
可以通过 `printenv` 命令查看设置的环境变量
## 更新脚本
可以使用类似的shell脚本,快速更新到新的版本
```shell
# update.sh
git pull && \
supervisorctl restart domain-admin && \
echo '部署成功'
```
+507
View File
@@ -0,0 +1,507 @@
# 通知配置
> 备注:如果点击`测试` 无法接收到消息,可尝试给`剩余天数` 设置一个比较大的值大
## 1、邮件
第一步:需要在 `系统管理/系统设置/邮箱设置` 中设置好系统发件邮件
> 如果需要对域名进行到期监控和邮件提醒,必须设置发件邮件
注意:
1. 如果使用的是163邮箱,密码处填写的是`授权码`
![](https://raw.githubusercontent.com/mouday/domain-admin/master/image/system-list.png)
第二步:填写收件人列表
![](https://raw.githubusercontent.com/mouday/domain-admin/master/image/notify-email.png)
需要填写json格式的数据,例如:
```json
[
"tom@qq.com",
"jack@qq.com"
]
```
## 2WebHook
### 2.1webhook
使
[](https://pengshiyu.blog.csdn.net/article/details/124135877)
### 2.2webhook
[https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type](https://open.dingtalk.com/document/orgapp/custom-bot-send-message-type)
webhook
```
https://oapi.dingtalk.com/robot/send?access_token=<access_token>
```
设置请求头
```json
{
"Content-Type": "application/json"
}
```
示例:SSL证书到期(文本格式)
```json
{
"msgtype": "text",
"text": {
"content": "以下证书临期,请检查:\n {% for row in list %}{{row.domain}} {{row.start_date or '-' }} ~ {{row.expire_date or '-' }} (剩余{{row.expire_days}}天)\n{% endfor %}"
}
}
```
示例:SSL证书到期(markdown格式)
```json
{
"msgtype": "markdown",
"markdown": {
"title": "SSL证书到期提醒",
"text": "### SSL证书到期提醒 🔔\n\n| 域名 | 到期 |\n| ---- | ---- |\n{% for row in list %}| {{row.domain}} | {{row.expire_date or '未知'}}({{row.expire_days}}天) |\n{% endfor %}\n\n> 请及时更新即将到期的SSL证书。"
}
}
```
示例:网站监控异常(markdown格式)
```json
{
"msgtype": "markdown",
"markdown": {
"title": "网站监控异常提醒",
"text": "{{monitor_row.title}} 监测异常,请检查:\n
请求URL<font color=\"red\">{{monitor_row.http_url}}</font>
重试次数:<font color=\"red\">{{monitor_row.allow_error_count}}</font>
状态:{% if monitor_row.status==2 %}<font color=\"red\">失败</font>{% elif monitor_row.status==1 %}<font color=\"green\">成功</font>{% else %}<font color=\"comment\">未知</font>{% endif %}"
}
}
```
可以参考 [@PanZongQing](https://github.com/PanZongQing) 分享的钉钉webhook配置:
[对接钉钉群内自定义webhook机器人发送告警注意事项](https://github.com/dromara/domain-admin/issues/47)
### 2.3、webhook发送Resend邮件
Resend 是一个为开发者提供的email 接口
工作原理:通过api接口使用HTTP协议发送邮件到Resend服务器,再通过Resend服务器使用SMTP协议发送邮件到目标邮箱
步骤:
1、注册账号:[https://resend.com/](https://resend.com/)
2、获取API Key
3、配置webhook
- 请求方法: POST
- 请求地址: https://api.resend.com/emails
- 请求头
```json
{
"Authorization": "Bearer <API Key>",
"Content-Type": "application/json"
}
```
- 请求体
```json
{
"from": "onboarding@resend.dev",
"to": "123456@qq.com",
"subject": "Hello World",
"html": "<p><strong>证书到期提醒</strong></p>"
}
```
### 2.4、webhook配置showdoc
showdoc是一个从服务器推送消息到手机的工具,可以通过api接口推送到微信服务号消息
注册地址:[https://push.showdoc.com.cn/](https://push.showdoc.com.cn/)
- 请求方法: POST
- 请求地址:
```bash
https://push.showdoc.com.cn/server/api/push/<API Key>
```
- 请求头
```json
{
"Content-Type": "application/json"
}
```
- 请求体
```json
{
"title": "域名到期提醒",
"content": "{% for row in list %}{{row.domain}} {{row.start_date or '-' }} - {{row.expire_date or '-' }} ({{row.expire_days}}){% endfor %}"
}
```
### 2.5、webhook发送飞书消息
模板示例由微信群友@kaka 提供
```json
{
"msg_type": "interactive",
"card": {
"config": {
"wide_screen_mode": true,
"enable_forward": true
},
"elements": [{
"tag": "div",
"text": {
"tag": "plain_text",
"content": "",
"lines": 1
}
,
"fields": [
{
"text": {
"tag": "lark_md",
"content": "**域名证书 **: <ul> {%- for row in list %} {%- if row.expire_days > 0 %} <li> {{row.domain}} {{" : "}} {{row.start_date or '-' }} - {{": "}} {{row.expire_date or '-' }} {{": "}}( {% if row.expire_days > 0 %} {{row.expire_days}} {% endif %} ) {{"; "}} {% endif %} {% endfor %} </li> </ul> "
}
}
]
},
{
"actions": [{
"tag": "button",
"text": {
"content": "域名或证书过期提醒 :玫瑰:",
"tag": "lark_md"
},
"url": "https://dc.console.aliyun.com/",
"type": "primary",
"value": {
"chosen": "approve"
}
}],
"tag": "action"
}],
"header": {
"title": {
"content": "域名或证书过期 告警",
"tag": "plain_text"
},
"template": "red"
}
}
}
```
### 2.6、webhook发送企业微信消息
文档:[https://developer.work.weixin.qq.com/document/path/99110](https://developer.work.weixin.qq.com/document/path/99110)
webhook地址
```
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
```
消息体
```json
{
"msgtype": "text",
"text": {
"content": "广州今日天气:29度,大部分多云,降雨概率:60%",
"mentioned_list":["wangqing","@all"],
"mentioned_mobile_list":["13800001111","@all"]
}
}
```
示例:监控证书到期(文本格式)
```json
{
"msgtype": "text",
"text": {
"content": "SSL证书到期提醒:{% for row in list %}{{row.domain}} {{row.start_date or '-' }} - {{row.expire_date or '-' }} ({{row.expire_days}}){% endfor %}"
}
}
```
示例:监控证书到期(markdown格式)
```json
{
"msgtype": "markdown",
"markdown": {
"title": "SSL证书到期提醒",
"content": "### SSL证书到期提醒 🔔\n\n| 域名 | 生效日期 | 到期日期 | 剩余天数 |\n| ---- | -------- | -------- | :--------: |\n{% for row in list %}| {{row.domain}} | {{row.start_date or '未知'}} | {{row.expire_date or '未知'}} | {{row.expire_days}}天 |\n{% endfor %}\n\n> 请及时更新即将到期的SSL证书,以确保网站安全性。"
}
}
```
示例:网站监控异常
```json
{
"msgtype": "markdown",
"markdown": {
"content": "{{monitor_row.title}}监测提醒,请相关同事注意。\n
>请求URL:<font color=\"red\">{{monitor_row.http_url}}</font>
>重试次数:<font color=\"red\">{{monitor_row.allow_error_count}}</font>
>状态:{% if monitor_row.status==2 %}<font color=\"red\">失败</font>{% elif monitor_row.status==1 %}<font color=\"green\">成功</font>{% else %}<font color=\"comment\">未知</font>{% endif %}"
}
}
```
## 3、企业微信
```json
{
"touser": "UserName",
"msgtype": "text",
"agentid": 1000001,
"text": {
"content": "你的域名证书即将到期\n点击查看<a href=\"http://www.demo.com/\">Domain Admin</a>"
}
}
```
自定义通知模板示例
```json
{
"touser": "UserName",
"msgtype": "text",
"agentid": 1000001,
"text": {
"content": "SSL证书到期提醒:\n{% for row in list %}{{row.domain}} {{row.group_name or '-'}} ({{row.expire_days}}){% endfor %}"
}
}
```
参考:[https://developer.work.weixin.qq.com/document/path/90236](https://developer.work.weixin.qq.com/document/path/90236)
## 4、钉钉
```json
{
"agent_id": "<agent_id>",
"userid_list": "<userid_list>",
"msg": {
"msgtype": "text",
"text": {
"content": "域名或证书过期提醒"
}
}
}
```
参考文档:[https://open.dingtalk.com/document/orgapp/asynchronous-sending-of-enterprise-session-messages](https://open.dingtalk.com/document/orgapp/asynchronous-sending-of-enterprise-session-messages)
## 5、飞书
```json
{
"receive_id": "<receive_id>",
"msg_type": "text",
"content": "{\"text\":\"域名或证书过期提醒\"}"
}
```
参考文档:[https://open.feishu.cn/document/server-docs/im-v1/message/create](https://open.feishu.cn/document/server-docs/im-v1/message/create)
## 6、模板和参数
采用`jinja2` 模板引擎
WebHook、企业微信、飞书、钉钉均支持自定义通知模板
### 6.1、通用示例
传入模板的参数示例:
```json
{
"list":[
{
"domain": "www.demo.com",
"start_date": "2023-06-01",
"expire_date": "2023-06-21",
"expire_days": 20
}
]
}
```
参数说明
| 参数 | 类型 | 说明 |
| -| - | - |
| domain | string | 域名/证书域名
| start_date | string | 生效时间
| expire_date | string | 过期时间
| expire_days | int | 剩余天数
> 备注:list仅包含满足通知条件的数据
示例
```json
{
"title": "域名到期提醒",
"content": "{% for row in list %}{{row.domain}} {{row.start_date or '-' }} - {{row.expire_date or '-' }} ({{row.expire_days}}){% endfor %}"
}
```
渲染结果
```json
{
"title": "域名到期提醒",
"content": "www.demo.com 2023-06-01 - 2023-06-21 (20)"
}
```
不同的事件参数稍有不同,会有其独特的参数
### 6.2、域名到期
```json
{
"list": [
{
"id": 9,
"user_id": 1,
"domain": "www.baidu.com",
"group_id": 9,
"group_name": "百度系",
"comment": "备注",
"start_time": "2010-08-28 04:11:35",
"expire_time": "2023-08-28 04:11:35",
"start_date": "2010-08-28",
"expire_date": "2023-08-28",
"expire_days": 5,
"domain_registrar": "厦门易名科技股份有限公司",
"domain_registrar_url": "https://www.ename.net/",
"icp_company": "北京百度网讯科技有限公司",
"icp_licence": "京ICP证030173号-1",
"tags": [
"企业服务",
"汽车服务"
],
"tags_str": "企业服务、汽车服务",
"is_auto_update": true,
"is_expire_monitor": true,
"create_time": "2023-07-20 22:59:20",
"update_time": "2023-08-22 16:01:13",
"update_time_label": "13分钟前"
}
]
}
```
### 6.3、SSL证书到期
```json
{
"list": [
{
"id": 3,
"user_id": 1,
"domain": "www.baidu.com",
"root_domain": "baidu.com",
"port": 443,
"group_id": 4,
"group_name": "百度系",
"comment": "备注字段",
"start_time": "2023-07-06 09:51:06",
"expire_time": "2024-08-06 09:51:05",
"start_date": "2023-07-06",
"expire_date": "2024-08-06",
"expire_days": 349,
"is_auto_update": true,
"is_expire_monitor": true,
"is_dynamic_host": false,
"create_time": "2023-08-22 16:28:19",
"update_time": "2023-08-22 16:28:19",
"update_time_label": "刚刚"
}
]
}
```
### 6.4、监控异常
异常通知和异常恢复通知的字段一致
```json
{
"monitor_row": {
"title": "名称",
"http_url": "请求URL",
"allow_error_count": 3,
"status": 2
},
"error": "报错信息"
}
```
参数说明
| 参数 | 类型 | 说明 |
| -| - | - |
| error | string | 报错信息
| monitor_row.title | string | 名称
| monitor_row.http_url | string | 请求URL
| monitor_row.allow_error_count | int | 重试次数
| monitor_row.status | int | 状态:0未知,1成功,2失败
+51
View File
@@ -0,0 +1,51 @@
# 开发计划
- `已完成` 支持企业微信通知
- `已完成` 支持域名分组
- 增加理员权限,权限分级:root 管理员 普通用户
- `已完成` 解决批量导入超时问题,支持1000条数据导入
- `已完成` 支持域名备注
- `已完成` 支持域名到期数据
- `已完成` webhook支持变量
- `已完成` 异步操作的前端状态显示
- 暗黑模式
- `已完成` 支持内网用户自己设置过期时间
- `已完成` 增加筛选功能: 筛选域名状态、证书状态、网站状态
- `已完成` 增加批量删除域名的功能
- 支持Telegram 告警
- 监控网站恢复后发送恢复的报警邮件
- v1.5.x(开发中)
- 新增网站监控 [issues#17](https://github.com/mouday/domain-admin/issues/17)
- `已完成` SSL证书申请
- `已完成` SSL证书部署
- admin拥有所有权限
- `已完成` 用户密码重置
- `已完成` 域名导出功能新增字段:主办单位名称、ICP备案
- `已完成` 域名列表新增字段:标签
- `已完成` 域名列表管理端支持修改:创建人
- `已完成` 域名列表支持:标签搜索
- `已完成` 支持dns验证
- `已完成` 验证文件一键部署
- `已完成` 证书一键部署
- `已完成` 证书自动续期
- `已完成` 证书自动部署
- `已完成` SSL证书手动续期
- 首次导入SSL证书列表,显示待处理状态
- `已完成` 动态主机 开关问题
- 自定义列
- `已完成` 备案主体列表
- `已完成` 自动更新证书的地方带一个端口,有的服务器默认用的不是22
- 数据导入导出支持excel
- `已完成` 通知配置增加分组参数
- `已完成` 主机host管理列表,支持移除
- 通过webhook重启
一些又有的链接
- [小熊猫去水印](https://qushuiyin.guijianpan.com)
- [ICP备案查询:ICP_Query](https://github.com/HG-ha/ICP_Query)
- [ICP备案查询:ICP-Checker](https://github.com/wongzeon/ICP-Checker)
- [某查询网站点选逆向分析](https://mp.weixin.qq.com/s/1Dw7ylfRmBjV_khOF4nuGQ)
- [文字点选验证码-破解之路](https://mp.weixin.qq.com/s/Lo9GveZUpnn_NTF8jL2ORg)
+7
View File
@@ -0,0 +1,7 @@
# 扩展插件
## domain-admin-cronjob
import domain from kubenetes ingress
[https://github.com/mjwtc0722/domain-admin-cronjob](https://github.com/mjwtc0722/domain-admin-cronjob)
+342
View File
@@ -0,0 +1,342 @@
# 常见问题
## 1、暂不支持多进程方式启动
使用 master + 多worker 方式启动应用,会启动多个定时任务Scheduler,导致多次执行任务
如果小规模使用,启动一个进程即可
如果是需要支持并发访问,可自行改进应用
将定时器独立出来,单独一个进程控制,行成 scheduler + Flaskmaster + 多worker
## 2、为什么外网访问不到?
```bash
# 启动运行
$ gunicorn 'domain_admin.main:app'
# 支持外网可访问,云服务器(阿里云或腾讯云)需要设置安全组
# 默认内网访问 --bind 127.0.0.1:8000
$ gunicorn --bind '0.0.0.0:8000' domain_admin.main:app'
```
更多设置,可参考[gunicorn](https://docs.gunicorn.org/en/stable/index.html)
## 3、Windows平台启动报错,找不到模块 `fcntl`
gunicorn不支持Windows,可以使用[waitress](https://github.com/Pylons/waitress) 替换,感谢[@cbr252522489](https://github.com/cbr252522489)提供的解决方案
```bash
$ pip install waitress
$ waitress-serve --listen=127.0.0.1:8000 domain_admin.main:app
```
参考:[https://stackoverflow.com/questions/45228395/error-no-module-named-fcntl](https://stackoverflow.com/questions/45228395/error-no-module-named-fcntl)
## 4、添加域名数据后系统异常
可按如下步骤删除异常数据
docker 启动方式
```bash
# 查看容器的运行信息
$ docker ps
# 进入容器
$ docker exec -it <容器id> /bin/sh
# 安装依赖
$ apk add sqlite
# 进入sqlite3
$ sqlite3
sqlite> .open /app/database/database.db
sqlite> .tables
log_scheduler tb_group tb_system tb_version
tb_domain tb_notify tb_user
# 查看数据
sqlite> select * from tb_domain;
# 删除数据
sqlite> DELETE FROM tb_domain WHERE id = 1;
# 退出
sqlite> .quit
```
## 5、邮件发送失败
可尝试更换端口:25、465、587
## 7、监控域名非443的端口
域名格式
```
域名:端口
eg:
www.baidu.com:8080
```
## 8、修改数据库链接
系统默认使用sqlite数据库作为后端存储,仅支持单线程操作
如果操作频繁会出现`database locked` 提示,就是数据库锁表了,可以更换后端存储为mysql,或者其他数据库
通过配置`.env` 文件或者直接设置系统环境变量
```bash
# sqlite 默认
DB_CONNECT_URL=sqlite:///database/database.db
# mysql
# 需要安装模块 pymysql
# pip install pymysql
DB_CONNECT_URL=mysql://root:123456@127.0.0.1:3306/data_domain
# 来自群友 @〖斗魂〗繁←星 的分享
# 如果 mysql 开启了 ssl,mysql 连接字符串应该写成
DB_CONNECT_URL=mysql://root:123456@127.0.0.1:3306/data_domain?ssl_verify_cert=true
# postgresql
DB_CONNECT_URL=postgresql://root:123456@localhost:5432/data_domain
```
说明:以上配置示例,需要提前创建名为`data_domain` 的数据库,也可以自定义其他名称的数据库
更多mysql的设置可参考:[https://pymysql.readthedocs.io/en/latest/modules/connections.html](https://pymysql.readthedocs.io/en/latest/modules/connections.html)
## 10、支持`prometheus` 的`/metrics`接口
请求地址:http://127.0.0.1:8000/metrics
示例
```
POST http://127.0.0.1:8000/metrics
Content-Type: application/json
Authorization: Bearer <token>
```
1、第一步、需要在 `系统设置/API KEY` 获取授权key
2、第二步、配置 prometheus.yml
```yaml
scrape_configs:
bearer_token: 'f60c03bfff8bb42dcf6821542e5fd11e'
```
如果是夜莺-Nightingale [参考文档](https://flashcat.cloud/docs/content/flashcat-monitor/categraf/plugin/prometheus/)
```yaml
[[instances.domain]]
urls = [
"http://127.0.0.1:8000/metrics"
]
url_label_key = "instance"
url_label_value = "{{.Host}}"
headers = ["Authorization", "Bearer f60c03bfff8bb42dcf6821542e5fd11e"]
```
该配置由[@1275788667](https://github.com/1275788667) 提供
返回数据示例:
```text
# HELP domain_admin this is a domain admin data
# TYPE domain_admin gauge
domain_admin{domain="pgmanage.qnvip.com"} 0.0
domain_admin{domain="fanyi.baidu.com"} 37.0
domain_admin{domain="www.tmall.com"} 37.0
domain_admin{domain="www.baidu.com"} 37.0
domain_admin{domain="www.taobao.com"} 37.0
```
@since v1.5.27 升级为
```
# HELP domain_admin this is a domain admin data
# TYPE domain_admin gauge 证书列表数据
domain_admin{domain="www.baidu.com",group_name="百度系",root_domain="baidu.com"} 258.0
domain_admin{domain="www.163.com",group_name="",root_domain="163.com"} 153.0
# HELP domain_info this is a domain info data
# TYPE domain_info gauge 域名列表数据
domain_info{domain="163.com",group_name="百度系"} 1392.0
domain_info{domain="qq.com",group_name=""} 3535.0
```
## 11、部分域名无法查询到信息
已知不支持的域名后缀:`.lc``.ml``.ai``.my``.ch``.edu.cn``.name`
## 12、获取ingress的域名
从k8s里面自动获取ingress的域名,然后添加到domain-admin里面
安装依赖
```bash
pip install tldextract requests kubernetes
```
参考代码 由群友 `@旺仔牛奶` 贡献
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import requests
import re
from kubernetes import config, client
import tldextract
import logging
logging.basicConfig(level=logging.INFO)
headers = {
'Content-Type': 'application/json'
}
def get_token(host, username, password):
url = '{}/api/login'.format(host)
data = {
'username': username,
'password': password
}
try:
response = requests.post(url=url, headers=headers, data=json.dumps(data))
result = json.loads(response.text)
if result['code'] == 0:
token = result['data']['token']
return token
else:
logging.error(response.text)
return False
except Exception as e:
logging.error(e)
def add_domain(host, token, domain):
url = '{}/api/addDomainInfo'.format(host)
data = {
'domain': domain
}
headers['X-Token'] = token
try:
response = requests.post(url=url, headers=headers, data=json.dumps(data))
result = json.loads(response.text)
if result['code'] == 0:
return True
else:
logging.error(response.text)
return False
except Exception as e:
logging.error(e)
def main():
config.load_incluster_config()
kube_api = client.ExtensionsV1beta1Api()
ingresses = kube_api.list_ingress_for_all_namespaces()
_set = set()
for item in ingresses.items:
if not re.search(os.getenv('NAMESPACE_MATCH'), item.metadata.namespace):
continue
if re.search(os.getenv('NAMESPACE_NOT_MATCH'), item.metadata.namespace):
continue
for rule in item.spec.rules:
domain = tldextract.extract(rule.host).registered_domain
_set.add(domain)
token = get_token(host=os.getenv('DOMAIN_ADMIN_HOST'),
username=os.getenv('DOMAIN_ADMIN_USERNAME'),
password=os.getenv('DOMAIN_ADMIN_PASSWORD'))
for item in _set:
add_domain(host=os.getenv('DOMAIN_ADMIN_HOST'), token=token, domain=item)
if __name__ == '__main__':
main()
```
## 13、证书监控原理
第一步`证书数据更新`通过实时访问网站取回网站部署的证书解析证书签发时间和过期时间更新到数据库
第二步`到期通知`通过配置好的通知触发条件查询数据库如果查询到记录则发送通知没有查询到数据就忽略
> 如果网站不可访问可以将 `自动更新` 关闭不影响到期通知
## 14、域名监控原理
第一步`域名数据更新`通过访问域名服务器取回域名信息解析出域名注册时间和过期时间更新到数据库
第二步`到期通知`通过配置好的通知触发条件查询数据库如果查询到记录则发送通知没有查询到数据就忽略
> 如果域名查询不到信息可以将 `自动更新` 关闭不影响到期通知
> 考虑到域名注册后到期时间不会改变所以增加了到期前`30` 天才更新域名信息的条件减少程序运行时间
> 如果实时查询域名信息请求过快会被域名信息服务器会拦截从而查询不到域名信息触发误报的问题
>
## 15、提示缺少依赖
可参考网友写的总结文章
- [domain-admin域名监控的源码搭建](https://blog.csdn.net/u013901725/article/details/132394530)
## 16、主机地址显示错误
由于系统不会主动清空已存在的主机地址对于CND等证书可以打开动态主机每次更新数据都会先清空主机IP列表重新获取数据避免误报的问题
## 17、忘记admin密码怎么办
把数据库表`tb_user`密码字段`password`替换为这个
```bash
$2b$12$c/tJvOYaWxzis4CXSyGN9ua4B7wzor8j9WrGsgV/2pdJnsrAMJxiK
```
可以重置为:123456
示例
```sql
# sqlite3
sqlite> ATTACH DATABASE '/opt/domain-admin/database/database.db' AS mydatabase;
sqlite> UPDATE tb_user SET password = "$2b$12$c/tJvOYaWxzis4CXSyGN9ua4B7wzor8j9WrGsgV/2pdJnsrAMJxiK" ;
```
## 18、database is locked
数据库锁表了,由于默认使用sqlite数据库,不支持多线程操作
如果遇到此提示,可以等待一会再进行操作
还可以直接换成mysql数据库,参考[可选配置](https://domain-admin.readthedocs.io/zh_CN/latest/manual/install.html#id5)
## 19、Python2.7 依赖问题
可以手动安装适合Python2.7 的依赖版本,也可以使用整理好的依赖文件
[production-2.7.txt](https://github.com/mouday/domain-admin/blob/master/requirements/production-2.7.txt)
## 20、grafana 的展示模版
可参考群友 友志 提供的模板
https://grafana.com/grafana/dashboards/14605-domain-exporter-for-prometheus/
https://grafana.com/grafana/dashboards/13924-9116-domain/
+210
View File
@@ -0,0 +1,210 @@
# SSL证书申请
申请步骤与阿里云申请证书一致
考虑到大部分用户采用的是nginx,其他用户可自行按照如下思路研究
首次使用申请ssl证书,推荐使用`手动验证`,流程跑通熟练之后,再使用`一键部署`功能
申请步骤:
## 1、填写域名
可以是一个域名,也可以是多个域名,每行一个
例如:
```bash
baidu.com
www.baidu.com
```
## 2、验证域名
2.1、手动文件验证
- 下载`验证文件`,到服务器
- 浏览器打开`验证URL`,如果没有报错,有任何响应结果,就是部署正确
- 点击`验证` 按钮,进行域名验证;如果验证失败,请自行检查部署是否正确
> 说明:如果是多域名,需要将所有的待验证文件都放到服务器
2.2、自动部署验证文件
- 填写`服务器账号和密码`
- 填写`服务器目录`,如果目录不存在,需要自行创建
- 点击`一键部署`,验证文件就会自动拷贝到指定的服务器目录
- 点击`验证` 按钮,进行域名验证;如果验证失败,请自行检查部署是否正确
## 3、下载证书
3.1、手动部署
- 点击zip 下载SSL证书,自行解压
- 上传到服务器对应目录下
3.2、自动部署证书文件
- 同样配置好这几个参数:`服务器地址``私钥部署路径``公钥部署路径``重启命令`
- 点击`一键部署`,证书文件就会自动拷贝到指定的服务器目录,并重启nginx服务
## 4、远程部署
通过调用远程api接口,将证书数据发送到远程接口,可自定义实现的部署逻辑
申请证书api推送格式
```json
{
"domains": [
"www.baidu.com",
"zhidao.baidu.com"
],
"ssl_certificate":"-----BEGIN CERTIFICATE-----\nMIIGdTCCBN2gAwI\n-----END CERTIFICATE-----",
"ssl_certificate_key":"-----BEGIN PRIVATE KEY-----\nMIIEvH+bpTwI=\n-----END PRIVATE KEY-----",
"start_time": "2023-01-04 14:33:39",
"expire_time": "2023-04-04 14:33:39"
}
```
托管证书api推送格式
```json
{
"domain": "www.baidu.com",
"ssl_certificate":"-----BEGIN CERTIFICATE-----\nMIIGdTCCBN2gAwI\n-----END CERTIFICATE-----",
"ssl_certificate_key":"-----BEGIN PRIVATE KEY-----\nMIIEvH+bpTwI=\n-----END PRIVATE KEY-----",
"start_time": "2023-01-04 14:33:39",
"expire_time": "2023-04-04 14:33:39"
}
```
参数说明
| 参数 | 类型 | 说明 |
| -|--------------------------| - |
| domains | `array<string>` / string | 域名列表
| ssl_certificate | string | 证书公钥
| ssl_certificate_key | string | 证书私钥
| start_time | string | 证书生效时间
| expire_time | string | 证书过期时间
远程服务器实现示例
```python
# -*- coding: utf-8 -*-
"""
@File : fake_server.py
@Date : 2024-03-22
"""
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route(rule='/issueCertificate', methods=['POST'])
def issue_certificate():
print(request.json)
print(request.headers)
# 此处可自行实现部署逻辑
return jsonify({'result': 'ok'})
if __name__ == '__main__':
app.run(port=8082)
```
完整示例,由群友 `@星河 ๑.`提供
```python
import os
import shutil
import subprocess
import threading
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
def delayed_restart_nginx():
"""在延迟5秒后重启nginx"""
time.sleep(5)
try:
subprocess.run(['docker', 'restart', 'nginx'])
print('重启nginx完成。')
except subprocess.CalledProcessError as e:
print('重启nginx失败: ', e)
@app.route('/deploy-cert', methods=['POST'])
def deploy_cert():
token = request.headers.get('token')
if token != 'token':
return jsonify({'error': 'Invalid token'}), 401
# 获取请求参数
data = request.get_json()
# 获取域名列表
domains = data.get('domains', [])
# 获取证书文件
ssl_certificate = data.get('ssl_certificate', '')
# 获取证书密钥
ssl_certificate_key = data.get('ssl_certificate_key', '')
# 检测是否有值,避免无效请求
if not domains or not ssl_certificate or not ssl_certificate_key:
return jsonify({'error': 'Missing required fields'}), 400
# 定义基础路径,因为本人的 nginx 是 docker 运行的,做了磁盘映射
# nginx 的配置文件都在此目录,加上域名作为不同域名的证书目录
# 如:/server/dockerApps/nginx/nginx/certs/simple.com/fullchain.pem
# 如:/server/dockerApps/nginx/nginx/certs/simple.com/cert.key
base_path = '/server/dockerApps/nginx/nginx/certs'
# 遍历域名列表
for domain in domains:
# 将泛域名中的 *. 去掉
if domain.startswith('*.'):
domain = domain.replace('*.', '', 1)
# 创建目录
domain_dir = os.path.join(base_path, domain)
if os.path.exists(domain_dir):
shutil.rmtree(domain_dir)
os.makedirs(domain_dir)
# 生成证书文件
cert_path = os.path.join(domain_dir, 'fullchain.pem')
key_path = os.path.join(domain_dir, 'cert.key')
with open(cert_path, 'w') as cert_file:
cert_file.write(ssl_certificate)
with open(key_path, 'w') as key_file:
key_file.write(ssl_certificate_key)
print(domain, " ==> 证书创建成功")
# 执行重启nginx的线程
threading.Thread(target=delayed_restart_nginx).start()
# 返回前端结果
return jsonify({'result': 'ok'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8001)
```
基于golang实现的部署方案,由网友提供
- [https://github.com/Gourds/domain-admin-ssl-deploy](https://github.com/Gourds/domain-admin-ssl-deploy) [issues#121](https://github.com/mouday/domain-admin/issues/121)
## 说明
- 如果是全程采用`一键部署`方式操作,域名到期前30天会自动续期
- 申请到的证书,不会到期提醒,如需到期提醒,请将部署该证书的域名添加到`证书监控`列表
- 如果总是验证不通过,可能是验证服务器有问题,可以使用其他证书申请手段,临时处理证书申请问题
- 阿里云:每一个自然年内,提供20个90天个人测试证书(免费版) [点击跳转](https://help.aliyun.com/zh/ssl-certificate/user-guide/purchase-an-individual-test-certificate?from=domain-admin.cn)
- 腾讯云:申请20张免费证书 [点击跳转](https://cloud.tencent.com/document/product/400/6814?from=domain-admin.cn)
- acme.sh 自动申请证书也可以很好的配合domain-admin的到期提醒
View File
+156
View File
@@ -0,0 +1,156 @@
import {defineConfig} from "vitepress";
import AutoSidebar from "vite-plugin-vitepress-auto-sidebar";
// https://vitepress.dev/zh/reference/site-config
// ref: https://gitee.com/zhontai/zhontai-admin-doc/blob/master/docs/.vitepress/config.ts
export default defineConfig({
lang: "zh-CN",
title: "Domain Admin-SSL证书监测平台、到期提醒",
base: "/domain-admin/",
ignoreDeadLinks: true,
lastUpdated: true,
markdown: {
image: {
// 默认禁用图片懒加载
lazyLoading: true,
},
},
head: [
["link", {rel: "icon", href: "https://demo.domain-admin.cn/favicon.ico"}],
[
'meta',
{
name: 'keywords',
content: 'SSL,SSL证书监测,Lets Encrypt,免费申请SSL证书,自动SSL证书续签,SSL证书到期提醒',
},
],
[
'meta',
{
name: 'description',
content: 'Domain Admin是域名和SSL证书监测平台,基于Let’s Encrypt实现免费申请SSL证书,自动SSL证书续签,SSL证书到期提醒,主要的竞品有:certd、okhttp、acme.sh、cetrbot',
},
],
// 百度统计
[
"script",
{},
`
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?1323b8c197b13150a6592146d5dbc928";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`,
],
],
sitemap: {
hostname: "https://mouday.github.io/domain-admin",
},
locales: {
"/": {
lang: "zh-CN", // 设置为中文
},
},
themeConfig: {
search: {
provider: "local",
options: {
translations: {
button: {
buttonText: "搜索文档",
buttonAriaLabel: "搜索文档",
},
modal: {
noResultsText: "无法找到相关结果",
resetButtonTitle: "清除查询条件",
displayDetails: "显示详细列表",
footer: {
navigateText: "切换",
selectText: "选择",
closeText: "关闭",
},
},
},
},
},
// https://vitepress.dev/reference/default-theme-config
nav: [
{text: "接口文档", link: "/api"},
{text: "使用文档", link: "https://domain-admin.readthedocs.io/"},
],
socialLinks: [
{icon: "github", link: "https://github.com/dromara/domain-admin"},
{icon: {
svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" t=\"1695538305440\" class=\"icon\" viewBox=\"0 0 1024 1024\" version=\"1.1\" p-id=\"1444\" style=\"width:1.25rem;height:1.25rem;vertical-align:middle;\"><path d=\"M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512z m259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.026 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.293-25.267z\" fill=\"#C71D23\" p-id=\"1445\"></path></svg>",
},
link: "https://gitee.com/dromara/domain-admin"},
],
outlineTitle: "导航目录",
darkModeSwitchLabel: "外观",
sidebarMenuLabel: "菜单",
returnToTopLabel: "返回顶部",
lastUpdatedText: "上次更新",
outline: {
/**
* outline 中要显示的标题级别。
* 单个数字表示只显示该级别的标题。
* 如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
* `'deep'` 与 `[2, 6]` 相同,将显示从 `<h2>` 到 `<h6>` 的所有标题。
*
* @default 2
*/
level: "deep",
/**
* 显示在 outline 上的标题。
*
* @default 'On this page'
*/
label: "大纲",
},
sidebar: [],
footer: {
message: '<a href="https://beian.miit.gov.cn/" target="_blank">京ICP备2024081455号-2</a> | <a href="https://beian.mps.gov.cn/#/query/webSearch?code=11011302007323" rel="noreferrer" target="_blank">京公网安备11011302007323</a>',
copyright: 'Copyright © 2024'
}
},
vite: {
plugins: [
// https://github.com/QC2168/vite-plugin-vitepress-auto-sidebar
AutoSidebar({
ignoreList: ["/public/", "assets", "img", "demo"],
titleFromFile: true,
// 侧边栏排序
beforeCreateSideBarItems: (data: string[]): string[] => {
// console.log(data);
// 通过正则提取文件名中的数字
function getOrder(item: string): number {
if (item == "index.md") {
return 0;
}
let res = item.match(/(?<order>\d+)/);
if (res && res.groups) {
return parseInt(res.groups.order);
} else {
return 999;
}
}
data.sort((a, b) => {
return getOrder(a) - getOrder(b);
});
return data.filter((n) => n != ".DS_Store");
},
}),
],
},
docFooter: {
prev: "上一页",
next: "下一页",
},
});
+3
View File
@@ -0,0 +1,3 @@
- [使用说明](/)
- [接口文档](/api.md)
+99
View File
@@ -0,0 +1,99 @@
# 二次开发接口
## 使用说明
请求方式统一为`POST json`
```
POST /api/<url>
Content-Type: application/json
X-TOKEN: <token>
{}
```
## 注册登录
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/register | 用户注册 | [register.md](../auth/register.md) |
| /api/login | 用户登录 | [login.md](../auth/login.md) |
## 域名管理
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/addDomain | 添加域名 | [addDomain.md](../domain/addDomain.md) |
| /api/getDomainList | 获取域名列表 | [getDomainList.md](../domain/getDomainList.md) |
| /api/getDomainById | 获取域名 | [getDomainById.md](../domain/getDomainById.md) |
| /api/updateDomainById | 更新数据 | [updateDomainById.md](../domain/updateDomainById.md) |
| /api/deleteDomainById | 删除域名 | [deleteDomainById.md](../domain/deleteDomainById.md) |
| /api/updateDomainCertInfoById | 更新域名证书信息 | [updateDomainCertInfoById.md](../domain/updateDomainCertInfoById.md) |
| /api/updateAllDomainCertInfo | 更新所有域名证书信息 | [updateAllDomainCertInfo.md](../domain/updateAllDomainCertInfo.md) |
| /api/updateAllDomainCertInfoOfUser | 更新当前用户的所有域名信息 | [updateAllDomainCertInfoOfUser.md](../domain/updateAllDomainCertInfoOfUser.md) |
| /api/sendDomainInfoListEmail | 发送域名证书信息到邮箱 | [sendDomainInfoListEmail.md](../domain/sendDomainInfoListEmail.md) |
| /api/checkDomainCert | 检查域名证书到期信息 | [checkDomainCert.md](../domain/checkDomainCert.md) |
| /api/importDomainFromFile | 从文件导入域名 | [importDomainFromFile.md](../domain/importDomainFromFile.md) |
| /api/getAllDomainListOfUser | 获取用户的所有域名数据 | [getAllDomainListOfUser.md](../domain/getAllDomainListOfUser.md) |
## 分组管理
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/addGroup | 添加分组 | [addGroup.md](../group/addGroup.md) |
| /api/getGroupList | 获取分组列表 | [getGroupList.md](../group/getGroupList.md) |
| /api/getGroupById | 获取分组数据 | [getGroupById.md](../group/getGroupById.md) |
| /api/updateGroupById | 更新数据 | [updateGroupById.md](../group/updateGroupById.md) |
| /api/deleteGroupById | 删除分组 | [deleteGroupById.md](../group/deleteGroupById.md) |
## 用户信息管理
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getUserInfo | 获取当前用户信息 | [getUserInfo.md](../user/getUserInfo.md) |
| /api/updateUserInfo | 更新当前用户信息 | [updateUserInfo.md](../user/updateUserInfo.md) |
| /api/updateUserPassword | 更新用户密码 | [updateUserPassword.md](../user/updateUserPassword.md) |
## 调度日志
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getLogSchedulerList | 获取调度日志列表 | [getLogSchedulerList.md](../log_scheduler/getLogSchedulerList.md) |
## 系统管理
需要管理员权限
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getAllSystemConfig | 获取所有配置项 | [getAllSystemConfig.md](../system_config/getAllSystemConfig.md) |
| /api/updateSystemConfig | 更新单个配置 | [updateSystemConfig.md](../system_config/updateSystemConfig.md) |
| /api/getSystemVersion | 获取当前应用版本号 | [getSystemVersion.md](../system_config/getSystemVersion.md) |
## 用户管理
管理员权限
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getUserList | 获取用户列表 | [getUserList.md](../user/getUserList.md) |
| /api/addUser | 添加用户 | [addUser.md](../user/addUser.md) |
| /api/updateUserStatus | 更新账号可用状态 | [updateUserStatus.md](../user/updateUserStatus.md) |
| /api/deleteUser | 删除用户账号 | [deleteUser.md](../user/deleteUser.md) |
## 域名信息
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getCertInformation | 获取域名证书信息 | [getCertInformation.md](../cert/getCertInformation.md) |
## 通知配置管理
`v0.0.12`
| 接口 | 说明 | 文档 |
| - | - | -|
| /api/getNotifyOfUser | 获取用户通知配置 | [getNotifyOfUser.md](../notify/getNotifyOfUser.md) |
| /api/updateNotifyOfUser | 更新用户通知配置 | [updateNotifyOfUser.md](../notify/updateNotifyOfUser.md) |
| /api/testWebhookNotifyOfUser | 测试webhook调用 | [testWebhookNotifyOfUser.md](../notify/testWebhookNotifyOfUser.md) |
+45
View File
@@ -0,0 +1,45 @@
# 用户登录
1、请求地址:/api/login
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| username | string | 是 | 用户名(账号)
| password | string | 是 | 密码
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| token | string | 登录凭据
5、请求示例
```
POST {{baseUrl}}/api/login
Content-Type: application/json
{
"username": "tom",
"password": "123456"
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJleHAiOjE2NjUwNDQ5ODB9.4YnSKathN753sUp4a1njGsFUTanU-xEDoZflHprLYB0"
},
"msg": "success"
}
```
+42
View File
@@ -0,0 +1,42 @@
# 用户注册
1、请求地址:/api/register
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| username | string | 是 | 用户名(账号)
| password | string | 是 | 密码
| password_repeat | string | 是 | 重复密码
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/register
Content-Type: application/json
{
"username": "tom",
"password": "123456",
"password_repeat": "123456"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+76
View File
@@ -0,0 +1,76 @@
# 获取域名证书信息
1、请求地址:/api/getCertInformation
2、请求方式:GET / POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|domain | string | 是 | 查询的域名,eg: www.baidu.com
4、返回参数
| 参数 | 类型 | 说明 |
|-|-|-|
| domain | string | 域名 |
| start_date | string | 颁发时间(东八区时间)|
| expire_date | string | 截止时间(东八区时间) |
| issuer | object | 颁发者 |
| issuer.CN |string | 公用名 (CN) |
| issuer.O |string | 组织 (O) |
| issuer.OU |string | 组织单位 (OU) |
| subject | object | 颁发对象 |
| subject.CN | string | 公用名 (CN) |
| subject.O | string | 组织 (O) |
| subject.OU | string | 组织单位 (OU) |
5、请求示例
GET
```
GET {{baseUrl}}/api/getCertInformation?domain=www.baidu.com
X-Token: <token>
```
POST
```
POST {{baseUrl}}/api/getCertInformation
Content-Type: application/json
X-Token: <token>
{
"domain": "www.baidu.com"
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"domain": "www.baidu.com",
"expire_date": "2023-08-06 13:16:01",
"issuer": {
"C": "BE",
"CN": "GlobalSign RSA OV SSL CA 2018",
"O": "GlobalSign nv-sa"
},
"start_date": "2022-07-05 13:16:02",
"subject": {
"C": "CN",
"CN": "baidu.com",
"L": "beijing",
"O": "Beijing Baidu Netcom Science Technology Co., Ltd",
"OU": "service operation department",
"ST": "beijing"
}
},
"msg": "success"
}
```
+42
View File
@@ -0,0 +1,42 @@
# 添加域名
1、请求地址:/api/addDomain
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| domain | string | 是 | 查询的域名,eg: www.baidu.com
| alias | string | 否 | 域名别名
| group_id | int | 否 | 域名分组id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/addDomain
Content-Type: application/json
X-Token: <token>
{
"domain": "www.baidu.com",
"alias": "百度",
"group_id": 1,
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+41
View File
@@ -0,0 +1,41 @@
# 检查域名证书到期信息
1、请求地址:/api/checkDomainCert
2、请求方式:POST
3、请求参数
4、返回参数
5、请求示例
GET
```
GET {{baseUrl}}/api/checkDomainCert
X-Token: <token>
```
POST
```
POST {{baseUrl}}/api/checkDomainCert
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+38
View File
@@ -0,0 +1,38 @@
# 删除域名
1、请求地址:/api/deleteDomainById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|id | int | 是 | 域名id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/deleteDomainById
Content-Type: application/json
X-Token: <token>
{
"id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+44
View File
@@ -0,0 +1,44 @@
# 获取用户的所有域名数据
1、请求地址:/api/getAllDomainListOfUser
2、请求方式:POST
3、请求参数
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| domain | string | 查询的域名
5、请求示例
```
POST {{baseUrl}}/api/getAllDomainListOfUser
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"domain": "www.qq.com"
}
],
"total": 1
},
"msg": "success"
}
```
+99
View File
@@ -0,0 +1,99 @@
# 获取域名
1、请求地址:/api/getDomainById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|id | int | 是 | 域名id
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 域名id
| domain | string | 查询的域名
| ip | string | 域名ip地址
| alias | string | 域名别名
| group_id | int | 域名分组id
| group | object/null | 域名分组对象,同 [getGroupById.md](/group/getGroupById.md)
| start_time | datetime | 证书颁发时间
| expire_time | datetime | 证书过期时间
| check_time | datetime | 证书检查时间
| connect_status | bool | 域名连接状态
| notify_status | bool | 域名到期后是否通知
| total_days | int | 域名有效期总天数
| expire_days | int | 域名过期剩余天数
| real_time_expire_days | int | 域名过期剩余天数(实时计算)
| detail | object | 域名信息,同 [getCertInformation.md](/cert/getCertInformation.md)
| create_time | datetime | 域名添加时间
| update_time | datetime | 域名更新时间
5、请求示例
```
POST {{baseUrl}}/api/getDomainById
Content-Type: application/json
X-Token: <token>
{
"id": 1
}
```
6、返回示例
未获取证书信息
```json
{
"code": 0,
"data": {
"alias": "百度",
"check_time": "2022-09-24 21:19:34",
"connect_status": true,
"create_time": "2022-09-24 21:13:19",
"detail": {
"domain": "www.baidu.com",
"expire_date": "2023-08-06 13:16:01",
"issuer": {
"C": "BE",
"CN": "GlobalSign RSA OV SSL CA 2018",
"O": "GlobalSign nv-sa"
},
"start_date": "2022-07-05 13:16:02",
"subject": {
"C": "CN",
"CN": "baidu.com",
"L": "beijing",
"O": "Beijing Baidu Netcom Science Technology Co., Ltd",
"OU": "service operation department",
"ST": "beijing"
}
},
"domain": "www.baidu.com",
"expire_days": 315,
"real_time_expire_days": 314,
"expire_time": "2023-08-06 13:16:01",
"group": {
"create_time": "2022-09-24 21:52:42",
"id": 1,
"name": "项目1",
"update_time": "2022-09-24 21:52:42"
},
"group_id": 1,
"id": 1,
"start_time": "2022-07-05 13:16:02",
"total_days": 396,
"update_time": "2022-09-24 21:13:19",
"ip": "110.242.68.3",
"notify_status": true
},
"msg": "success"
}
```
+62
View File
@@ -0,0 +1,62 @@
# 获取域名列表
1、请求地址:/api/getDomainList
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|page | int | 否 | 页码,默认1
|size | int | 否 | 每页数量,默认10
|group_id | int | 否 | 分组id
4、返回参数
参见:[getDomainById.md](/domain/getDomainById.md)
5、请求示例
```
POST {{baseUrl}}/api/getDomainList
Content-Type: application/json
X-Token: <token>
{
"page": 1,
"size": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"alias": "百度",
"check_time": "2022-09-24 22:31:55",
"connect_status": true,
"create_time": "2022-09-24 22:29:02",
"domain": "www.baidu.com",
"expire_days": 315,
"real_time_expire_days": 314,
"expire_time": "2023-08-06 13:16:01",
"group_id": 2,
"id": 3,
"start_time": "2022-07-05 13:16:02",
"total_days": 396,
"update_time": "2022-09-24 22:29:02",
"ip": "110.242.68.3",
"notify_status": true
}
],
"total": 3
},
"msg": "success"
}
```
+52
View File
@@ -0,0 +1,52 @@
# 从文件导入域名
1、请求地址:/api/importDomainFromFile
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| file | file | 是 | file对象
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| count | int | 导入成功的数量
5、请求示例
```
POST {{baseUrl}}/api/updateDomainCertInfoById
Content-Type: multipart/form-data
X-Token: <token>
file: (二进制)
```
6、返回示例
```json
{
"code": 0,
"data": {
"count": 10
},
"msg": "success"
}
```
> 说明:导入的文件域名按照每行一个排列
例如
domain.txt
```
www.baidu.com
www.taobao.com
```
+39
View File
@@ -0,0 +1,39 @@
# 发送域名证书信息到邮箱
1、请求地址:/api/sendDomainInfoListEmail
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| to_addresses | `array[string]` | 是 | 邮箱列表
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/sendDomainInfoListEmail
Content-Type: application/json
X-Token: <token>
{
"to_addresses": ["xxx@qq.com"]
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+41
View File
@@ -0,0 +1,41 @@
# 更新所有域名证书信息
1、请求地址:/api/updateAllDomainCertInfo
2、请求方式:GET/POST
3、请求参数
4、返回参数
5、请求示例
GET
```
GET {{baseUrl}}/api/updateAllDomainCertInfo
X-Token: <token>
```
POST
```
POST {{baseUrl}}/api/updateAllDomainCertInfo
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
@@ -0,0 +1,41 @@
# 更新当前用户的所有域名信息
1、请求地址:/api/updateAllDomainCertInfoOfUser
2、请求方式:GET/POST
3、请求参数
4、返回参数
5、请求示例
GET
```
GET {{baseUrl}}/api/updateAllDomainCertInfoOfUser
X-Token: <token>
```
POST
```
POST {{baseUrl}}/api/updateAllDomainCertInfoOfUser
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+44
View File
@@ -0,0 +1,44 @@
# 更新数据
1、请求地址:/api/updateDomainById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| id | int | 是 | 域名id
| domain | string | 是 | 查询的域名,eg: www.baidu.com
| alias | string | 是 | 域名别名
| group_id | int | 是 | 域名分组id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateDomainById
Content-Type: application/json
X-Token: <token>
{
"id": 1,
"domain": "www.baidu.com",
"alias": "百度",
"group_id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+39
View File
@@ -0,0 +1,39 @@
# 更新域名证书信息
1、请求地址:/api/updateDomainCertInfoById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| id | int | 是 | 域名id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateDomainCertInfoById
Content-Type: application/json
X-Token: <token>
{
"id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+172
View File
@@ -0,0 +1,172 @@
# 域名检查接口文档
## 功能描述
提供域名检查功能,可以检测指定域名是否被封禁。该接口通过调用腾讯安全接口来实现域名状态的检查。
## API 接口
### 请求信息
- 接口路径:`/api/v1/domain/check`
- 请求方法:`GET`
- 请求参数:
| 参数名 | 类型 | 是否必须 | 描述 |
|--------|--------|----------|----------|
| url | string | 是 | 待检查的URL |
### 响应信息
响应格式:
```json
{
"code": 200,
"msg": "结果信息"
}
```
响应状态码说明:
| 状态码 | 说明 |
|--------|------|
| 200 | 域名正常 |
| 202 | 域名被封或参数错误 |
| 500 | 系统错误 |
### 示例
1. 请求示例:
```
GET /api/v1/domain/check?url=http://example.com
```
2. 响应示例:
域名正常:
```json
{
"code": 200,
"msg": "normal"
}
```
域名被封:
```json
{
"code": 202,
"msg": "域名被封"
}
```
参数错误:
```json
{
"code": 202,
"msg": "请传入URL参数"
}
```
系统错误:
```json
{
"code": 500,
"msg": "检测失败: Connection error"
}
```
## 实现细节
### 代码位置
- API实现:`domain_admin/api/domain_check.py`
- 测试用例:`tests/test_domain_check.py`
### 主要依赖
- FlaskWeb框架
- requestsHTTP客户端
- domain_admin.log:日志模块
- domain_admin.utils.response:统一响应处理
### 错误处理
1. 参数验证
- 检查URL参数是否存在
- 返回202状态码和提示信息
2. 网络请求异常
- 捕获requests.exceptions.RequestException
- 记录错误日志
- 返回500状态码和错误信息
3. 响应解析异常
- 捕获json.JSONDecodeError
- 记录错误日志
- 返回500状态码和解析失败提示
## 测试用例
测试用例覆盖以下场景:
1. 测试未提供URL参数的情况
```python
def test_check_domain_no_url(self):
response = self.client.get('/api/v1/domain/check')
# 验证返回202状态码和相应提示
```
2. 测试域名被封的情况
```python
@patch('requests.get')
def test_check_domain_blocked(self, mock_get):
# 模拟域名被封的响应
# 验证返回202状态码和域名被封提示
```
3. 测试域名正常的情况
```python
@patch('requests.get')
def test_check_domain_normal(self, mock_get):
# 模拟域名正常的响应
# 验证返回200状态码和正常状态
```
4. 测试请求异常的情况
```python
@patch('requests.get')
def test_check_domain_request_error(self, mock_get):
# 模拟网络请求异常
# 验证返回500状态码和错误信息
```
5. 测试JSON解析错误的情况
```python
@patch('requests.get')
def test_check_domain_json_error(self, mock_get):
# 模拟JSON解析错误
# 验证返回500状态码和解析失败提示
```
### 运行测试
执行以下命令运行测试:
```bash
python -m pytest tests/test_domain_check.py -v
```
## 日志记录
接口在关键操作点都添加了日志记录:
1. 开始检查域名时记录信息日志
2. 域名被封时记录警告日志
3. 域名正常时记录信息日志
4. 发生异常时记录错误日志
## 注意事项
1. 接口依赖腾讯安全检查服务,需确保网络可访问
2. 建议在调用接口时设置合适的超时时间
3. 对于高频调用,建议考虑添加缓存机制
4. 需要注意处理特殊字符的URL编码
+9
View File
@@ -0,0 +1,9 @@
# NotifyTypeEnum
通知方式枚举值
| 枚举值 | 变量 | 含义 |
| - | - | - |
| 0 | Unknown | 未知 |
| 1 | Email | 邮件 |
| 2 | WebHook | webHook |
+38
View File
@@ -0,0 +1,38 @@
# 添加分组
1、请求地址:/api/addGroup
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| name | string | 是 | 分组名称
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/addGroup
Content-Type: application/json
X-Token: <token>
{
"name": "项目1"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+38
View File
@@ -0,0 +1,38 @@
# 删除分组
1、请求地址:/api/deleteGroupById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|id | int | 是 | 数据id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/deleteGroupById
Content-Type: application/json
X-Token: <token>
{
"id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+49
View File
@@ -0,0 +1,49 @@
# 获取分组数据
1、请求地址:/api/getGroupById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|id | int | 是 | 数据id
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | id
| name | string | 名称
| create_time | datetime | 域名添加时间
| update_time | datetime | 域名更新时间
5、请求示例
```
POST {{baseUrl}}/api/getGroupById
Content-Type: application/json
X-Token: <token>
{
"id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"create_time": "2022-09-24 16:21:59",
"id": 1,
"name": "项目1",
"update_time": "2022-09-24 16:21:59"
},
"msg": "success"
}
```
+47
View File
@@ -0,0 +1,47 @@
# 获取分组列表
1、请求地址:/api/getGroupList
2、请求方式:POST
3、请求参数
会返回全部分组
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/getGroupList
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"create_time": "2022-09-24 16:21:59",
"id": 1,
"name": "项目1",
"update_time": "2022-09-24 16:21:59"
}
],
"total": 1
},
"msg": "success"
}
```
+40
View File
@@ -0,0 +1,40 @@
# 更新数据
1、请求地址:/api/updateGroupById
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| id | int | 是 | 域名id
| name | string | 是 | 分组名称
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateGroupById
Content-Type: application/json
X-Token: <token>
{
"id": 1,
"name": "项目2"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+51
View File
@@ -0,0 +1,51 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "Domain Admin"
text: "SSL证书监测平台"
tagline: 申请证书,自动续签,到期提醒,支持独立部署
image:
src: https://mouday.github.io/img/2024/06/20/xvg0dfg.png
alt: Domain Admin
actions:
- theme: brand
text: 在线体验
link: https://demo.domain-admin.cn/
- theme: alt
text: 快速开始
link: https://domain-admin.readthedocs.io/zh-cn/latest/manual/install.html
- theme: alt
text: Github
link: https://github.com/dromara/domain-admin
features:
- title: 申请证书
details: 基于Lets Encrypt实现免费SSL证书申请
- title: 自动续签
details: 自动续签SSL证书,证书永不过期
- title: 到期提醒
details: 到期前7天提醒,保障网站安全
---
微信交流群
<img src="https://mouday.github.io/img/2024/06/20/h1buset.png" width="300">
回复:`domain-admin`,和众多使用者一起交流学习使用经验,反馈使用问题,获得更及时的解答和修复
## 友情链接
<a href="http://captcha.tianai.cloud/?from=domain-admin.cn" target="_blank">tianai-captcha</a>
<a href="https://sa-token.cc/?from=domain-admin.cn" target="_blank">Sa-Token</a>
<a href="https://x-file-storage.xuyanwu.cn/?from=domain-admin.cn" target="_blank">X Spring File Storage</a>
<a href="https://springdoc.cn/?from=domain-admin.cn" target="_blank">spring 中文网</a>
+61
View File
@@ -0,0 +1,61 @@
# 获取调度日志列表
1、请求地址:/api/getLogSchedulerList
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|page | int | 否 | 页码,默认1
|size | int | 否 | 每页数量,默认10
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 数据id
| status | bool | 状态
| error_message | string | 错误信息
| total_time | int | 执行时长
| total_time_label | string | 执行时长显示值
| create_time | datetime | 添加时间
| update_time | datetime | 更新时间
5、请求示例
```
POST {{baseUrl}}/api/getLogSchedulerList
Content-Type: application/json
X-Token: <token>
{
"page": 1,
"size": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"create_time": "2022-10-09 18:10:00",
"error_message": "",
"id": 423,
"status": true,
"total_time": 11,
"total_time_label": "11s",
"update_time": "2022-10-09 18:10:11"
}
],
"total": 423
},
"msg": "success"
}
```
+81
View File
@@ -0,0 +1,81 @@
# 获取用户通知配置
1、请求地址:/api/getNotifyOfUser
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|type_id | int | 是 | 通知类型 [NotifyTypeEnum](/enums/NotifyTypeEnum.md)
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 主键id
| user_id | int | 用户id
| type_id | int | 通知类型id [NotifyTypeEnum](/enums/NotifyTypeEnum.md)
| value | object | 通知配置参数,见返回示例
| create_time | datetime | 域名添加时间
| update_time | datetime | 域名更新时间
5、请求示例
```
POST {{baseUrl}}/api/getNotifyOfUser
Content-Type: application/json
X-Token: <token>
{
"type_id": 1
}
```
6、返回示例
邮件配置
```json
{
"code": 0,
"data": {
"create_time": "2022-10-14 18:15:21",
"id": 1,
"type_id": 1,
"update_time": "2022-10-14 18:15:21",
"user_id": 1,
"value": {
"email_list": [
"123456@qq.com"
]
}
},
"msg": "success"
}
```
Webhook配置
```json
{
"code": 0,
"data": {
"create_time": "2022-10-30 21:38:01",
"id": 2,
"type_id": 2,
"update_time": "2022-10-30 21:38:01",
"user_id": 1,
"value": {
"body": "{\n \"title\": \"SSL证书到期\",\n \"content\": \"查看:http://127.0.0.1:5173/\"\n}",
"headers": {
"Content-Type": "application/json"
},
"method": "POST",
"url": "https://push.showdoc.com.cn/server/api/push/a8f4fe6f8a5ac089193ac05472bf3c972003778561"
}
},
"msg": "success"
}
```
+35
View File
@@ -0,0 +1,35 @@
# 测试webhook调用
1、请求地址:/api/testWebhookNotifyOfUser
2、请求方式:POST
3、请求参数
4、返回参数
5、请求示例
POST
```
POST {{baseUrl}}/api/testWebhookNotifyOfUser
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+75
View File
@@ -0,0 +1,75 @@
# 更新用户通知配置
1、请求地址:/api/updateNotifyOfUser
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| type_id | int | 是 | 通知类型id [NotifyTypeEnum](/enums/NotifyTypeEnum.md)
| value | object | 是 | 通知配置参数,见示例
4、返回参数
5、请求示例
邮件配置
```
POST {{baseUrl}}/api/updateNotifyOfUser
Content-Type: application/json
X-Token: <token>
{
"type_id": 1,
"value": {
"email_list": ["123@qq.com"]
}
}
```
Webhook配置
```json
POST {{baseUrl}}/api/updateNotifyOfUser
Content-Type: application/json
X-Token: <token>
{
"type_id":2,
"value": {
"method":"POST",
"url":"https://server.com/api/push/",
"headers":{
"Content-Type":"application/json"
},
"body":"{\n \"title\": \"SSL证书到期\",\n \"content\": \"查看:http://127.0.0.1:5173/\"\n}"
}
}
```
webhook方式提交的body参数支持[jinja2模板语法](http://doc.yonyoucloud.com/doc/jinja2-docs-cn/index.html)
例如
```json
{
"title": "推送的消息标题",
"content": "{% for item in domain_list %} {{item.domain}} - {{item.expire_days}} \n {% endfor %}"
}
```
模板变量同 [获取域名列表](/domain/getDomainList.md)
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+57
View File
@@ -0,0 +1,57 @@
# 获取所有配置项
1、请求地址:/api/getAllSystemConfig
2、请求方式:POST
3、请求参数
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 数据id
| key | string | 键
| value | string | 值
| label | string | 显示
| placeholder | string | 输入提示
| create_time | datetime | 添加时间
| update_time | datetime | 更新时间
5、请求示例
```
POST {{baseUrl}}/api/getAllSystemConfig
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"is_show_value": true,
"key": "mail_host",
"value": "smtp.163.com",
"label": "发件邮箱服务器地址",
"placeholder": "发件邮箱服务器地址",
"create_time": "2022-10-03 23:07:21",
"update_time": "2022-10-03 23:07:21"
}
],
"total": 9
},
"msg": "success"
}
```
+38
View File
@@ -0,0 +1,38 @@
# 获取当前应用版本号
1、请求地址:/api/getSystemVersion
2、请求方式:POST
3、请求参数
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| version | string | 版本号
5、请求示例
```
POST {{baseUrl}}/api/getSystemVersion
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": {
"version": "0.0.8"
},
"msg": "success"
}
```
+39
View File
@@ -0,0 +1,39 @@
# 更新单个配置
1、请求地址:/api/updateSystemConfig
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| key | string | 键
| value | string | 值
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateSystemConfig
Content-Type: application/json
X-Token: <token>
{
"key": "mail_username",
"value": "123@qq.com"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+41
View File
@@ -0,0 +1,41 @@
# 添加用户
1、请求地址:/api/addUser
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| username | string | 用户名
| password | string | 登录密码
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/addUser
Content-Type: application/json
X-Token: <token>
{
"username": "mouday",
"password": "ooxx"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+38
View File
@@ -0,0 +1,38 @@
# 删除用户账号
1、请求地址:/api/deleteUser
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| user_id | int | 是 | 用户id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/deleteUser
Content-Type: application/json
X-Token: <token>
{
"user_id": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+53
View File
@@ -0,0 +1,53 @@
# 获取当前用户信息
1、请求地址:/api/getUserInfo
2、请求方式:POST
3、请求参数
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 用户id
| username | string | 用户名
| avatar_url | string | 头像url
| before_expire_days | int | 过期前多少天提醒
| email_list | string | 邮件列表
| create_time | datetime | 添加时间
| update_time | datetime | 更新时间
5、请求示例
```
POST {{baseUrl}}/api/getUserInfo
Content-Type: application/json
X-Token: <token>
{}
```
6、返回示例
```json
{
"code": 0,
"data": {
"id": 1,
"username": "admin",
"avatar_url": "",
"before_expire_days": 3,
"email_list": [
"1940607002@qq.com"
],
"status": true,
"create_time": "2022-10-03 23:07:21",
"update_time": "2022-10-08 16:54:08"
},
"msg": "success"
}
```
+66
View File
@@ -0,0 +1,66 @@
# 获取用户列表
1、请求地址:/api/getUserList
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
|page | int | 否 | 页码,默认1
|size | int | 否 | 每页数量,默认10
|keyword | string | 否 | 搜索关键词
4、返回参数
| 参数 | 类型 | 说明 |
| -| - | - |
| id | int | 用户id
| username | string | 用户名
| avatar_url | string | 头像url
| before_expire_days | int | 过期前多少天提醒
| email_list | string | 邮件列表
| create_time | datetime | 添加时间
| update_time | datetime | 更新时间
5、请求示例
```
POST {{baseUrl}}/api/getUserList
Content-Type: application/json
X-Token: <token>
{
"page": 1,
"size": 10,
"keyword": ""
}
```
6、返回示例
```json
{
"code": 0,
"data": {
"list": [
{
"id": 1,
"username": "admin",
"status": true,
"avatar_url": "https://www.image.com/image.png",
"before_expire_days": 1,
"email_list": [
"123@qq.com"
],
"create_time": "2022-10-03 23:07:21",
"update_time": "2022-10-11 17:52:56"
}
],
"total": 1
},
"msg": "success"
}
```
+44
View File
@@ -0,0 +1,44 @@
# 更新当前用户信息
1、请求地址:/api/updateUserInfo
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| avatar_url | string | 是 | 头像url
| before_expire_days | int | 是 | 过期前多少天提醒
| email_list | string | 是 | 邮件列表
> 备注:该接口仅更新当前登录用户的信息,token中解析用户id
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateUserInfo
Content-Type: application/json
X-Token: <token>
{
"avatar_url": "https://www.image.com/image.png",
"email_list": ["123@qq.com"],
"before_expire_days": 1
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+40
View File
@@ -0,0 +1,40 @@
# 更新用户密码
1、请求地址:/api/updateUserPassword
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| password | string | 是 | 旧密码
| new_password | string | 是 | 新密码
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateUserPassword
Content-Type: application/json
X-Token: <token>
{
"password": "123456",
"new_password": "234567"
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+40
View File
@@ -0,0 +1,40 @@
# 更新账号可用状态
1、请求地址:/api/updateUserStatus
2、请求方式:POST
3、请求参数
| 参数 | 类型 | 必须 | 说明 |
| -| - | - | - |
| user_id | int | 是 | 用户id
| status | bool | 是 | 状态
4、返回参数
5、请求示例
```
POST {{baseUrl}}/api/updateUserStatus
Content-Type: application/json
X-Token: <token>
{
"user_id": 1,
"status": false
}
```
6、返回示例
```json
{
"code": 0,
"data": null,
"msg": "success"
}
```
+1
View File
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
+1
View File
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
+267
View File
@@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
"""
address_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.address_model import AddressModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.service import domain_service, auth_service
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException, ForbiddenAppException
@auth_service.permission(role=RoleEnum.USER)
def get_address_list_by_domain_id():
"""
通过域名获取关联的主机地址列表
:return:
@since v1.3.1
"""
current_user_id = g.user_id
domain_id = request.json['domain_id']
page = request.json.get('page', 1)
size = request.json.get('size', 10)
# check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id,
DomainModel.user_id == current_user_id
).first()
if not domain_row:
raise ForbiddenAppException()
query = AddressModel.select().where(
AddressModel.domain_id == domain_id,
)
total = query.count()
lst = []
if total > 0:
rows = query.order_by(
AddressModel.ssl_expire_days,
AddressModel.id,
).paginate(page, size)
lst = list(map(lambda m: model_to_dict(
model=m,
extra_attrs=[
'ssl_start_date',
'ssl_expire_date',
'real_time_ssl_expire_days',
'update_time_label',
]
), rows))
return {
"list": lst,
"total": total
}
@auth_service.permission(role=RoleEnum.USER)
def add_address():
"""
添加主机地址
:return:
@since v1.3.1
"""
current_user_id = g.user_id
domain_id = request.json['domain_id']
host = request.json['host']
ssl_start_time = request.json.get('ssl_start_time')
ssl_expire_time = request.json.get('ssl_expire_time')
comment = request.json.get('comment') or ''
# ssl_auto_update = request.json.get('ssl_auto_update', True)
# ssl_expire_monitor = request.json.get('ssl_expire_monitor', True)
# check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id,
DomainModel.user_id == current_user_id
).first()
if not domain_row:
raise ForbiddenAppException()
data = {
'domain_id': domain_id,
'host': host,
'source': 1,
'comment': comment,
}
if not domain_row.auto_update:
data['ssl_start_time'] = ssl_start_time
data['ssl_expire_time'] = ssl_expire_time
address_row = AddressModel.create(**data)
if domain_row.auto_update:
domain_service.update_address_row_info_with_sync_domain_row(address_row.id)
else:
domain_service.sync_address_info_to_domain_info(domain_row)
@auth_service.permission(role=RoleEnum.USER)
def delete_address_by_id():
"""
删除主机地址
:return:
@since v1.3.1
"""
current_user_id = g.user_id
address_id = request.json['address_id']
# update to cert
address_row = AddressModel.get_by_id(address_id)
# check
domain_row = DomainModel.select().where(
DomainModel.id == address_row.domain_id,
DomainModel.user_id == current_user_id
).first()
if not domain_row:
raise ForbiddenAppException()
AddressModel.delete().where(
AddressModel.id == address_id
).execute()
domain_service.sync_address_info_to_domain_info(domain_row)
@auth_service.permission(role=RoleEnum.USER)
def delete_address_by_ids():
"""
批量删除主机地址
:return:
@since v1.4.4
"""
current_user_id = g.user_id
address_ids = request.json['address_ids']
# update to cert
address_rows = AddressModel.select(
AddressModel.domain_id
).where(
AddressModel.id.in_(address_ids)
)
AddressModel.delete().where(
AddressModel.id.in_(address_ids)
).execute()
domain_ids = [row.domain_id for row in address_rows]
domain_rows = DomainModel.select().where(
DomainModel.id.in_(domain_ids),
DomainModel.user_id == current_user_id
)
for domain_row in domain_rows:
domain_service.sync_address_info_to_domain_info(domain_row)
@auth_service.permission(role=RoleEnum.USER)
def get_address_by_id():
"""
获取主机地址
:return:
@since v1.3.1
"""
current_user_id = g.user_id
address_id = request.json['address_id']
return AddressModel.get_by_id(address_id)
@auth_service.permission(role=RoleEnum.USER)
def update_address_by_id():
"""
更新主机地址
:return:
@since v1.3.1
"""
current_user_id = g.user_id
address_id = request.json['address_id']
host = request.json['host']
ssl_start_time = request.json.get('ssl_start_time')
ssl_expire_time = request.json.get('ssl_expire_time')
comment = request.json.get('comment') or ''
# ssl_auto_update = request.json.get('ssl_auto_update', True)
# ssl_expire_monitor = request.json.get('ssl_expire_monitor', True)
address_row = AddressModel.get_by_id(address_id)
domain_row = DomainModel.get_by_id(address_row.domain_id)
data = {
'host': host,
'comment': comment,
}
if not domain_row.auto_update:
data['ssl_start_time'] = ssl_start_time
data['ssl_expire_time'] = ssl_expire_time
AddressModel.update(data).where(
AddressModel.id == address_id
).execute()
if domain_row.auto_update:
domain_service.update_address_row_info_with_sync_domain_row(address_id)
else:
domain_service.sync_address_info_to_domain_info(domain_row)
@auth_service.permission(role=RoleEnum.USER)
def update_address_list_info_by_domain_id():
"""
更新主机地址信息
:return:
@since v1.3.1
"""
current_user_id = g.user_id
domain_id = request.json['domain_id']
domain_row = DomainModel.get_by_id(domain_id)
err = domain_service.update_domain_row(domain_row)
if err:
raise AppException(err)
@auth_service.permission(role=RoleEnum.USER)
def update_address_row_info_by_id():
"""
更新主机地址信息
:return:
@since v1.3.1
"""
current_user_id = g.user_id
address_id = request.json['address_id']
domain_service.update_address_row_info_with_sync_domain_row(address_id)
+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request
from domain_admin import config
from domain_admin.service import auth_service
from domain_admin.utils.flask_ext.app_exception import AppException
def login():
"""
用户登录
"""
username = request.json['username']
password = request.json['password']
token = auth_service.login(username, password)
return {'token': token}
def login_by_email():
"""
邮箱登录
"""
email = request.json['email']
code = request.json['code']
token = auth_service.login_by_email(email, code)
return {'token': token}
def send_code():
"""
发送验证码
"""
email = request.json['email']
if not config.ENABLED_REGISTER:
raise AppException("请联系管理员开放注册")
auth_service.send_verify_code_async(email)
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, Response
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.service import auth_service, cert_service
from domain_admin.utils import domain_util
from domain_admin.utils.cert_util import cert_openssl_v2, cert_common
@auth_service.permission(role=RoleEnum.USER)
def get_cert_information():
"""
获取域名证书信息
:return:
"""
if request.method == 'GET':
domain = request.args['domain']
else:
domain = request.json['domain']
# 解析域名
return cert_service.get_cert_information(domain=domain)
@auth_service.permission(role=RoleEnum.USER)
def parse_public_cert():
certificate = request.json['certificate']
parsed_cert = cert_common.parse_public_cert(certificate)
return parsed_cert.to_dict() if parsed_cert else parsed_cert
+316
View File
@@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
"""
@File : certificate_api.py
@Date : 2024-02-25
"""
import json
from flask import g, request
from peewee import SQL
from domain_admin.enums.deploy_status_enum import DeployStatusEnum
from domain_admin.enums.object_enum import ObjectEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.certificate_model import CertificateModel
from domain_admin.model.deploy_webhook_model import DeployWebhookModel
from domain_admin.service import certificate_service, auth_service
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException, ForbiddenAppException
@auth_service.permission(role=RoleEnum.USER)
def get_certificate_list():
"""
获取列表
:return:
@since v1.6.12
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
order_prop = request.json.get('order_prop') or 'expire_time'
order_type = request.json.get('order_type') or 'asc'
if order_prop not in ['create_time','expire_time']:
raise AppException('params error: order_prop')
if order_type not in ['desc', 'asc']:
raise AppException('params error: order_type')
query = CertificateModel.select().where(
CertificateModel.user_id == current_user_id,
)
if keyword:
query = query.where(CertificateModel.domain.contains(keyword))
total = query.count()
lst = []
if total > 0:
ordering = [
SQL(f"`{order_prop}` {order_type}"),
CertificateModel.id.desc()
]
rows = query.order_by(*ordering).paginate(page, size)
lst = [row.to_dict() for row in rows]
# 查询部署数量
certificate_service.load_cert_deploy_count(lst)
# 查询api部署状态
certificate_service.load_api_deploy_status(lst)
return {
"list": lst,
"total": total
}
@auth_service.permission(role=RoleEnum.USER)
def add_certificate():
"""
添加
:return:
@since v1.6.12
"""
current_user_id = g.user_id
domain = request.json['domain']
ssl_certificate = request.json['ssl_certificate']
ssl_certificate_key = request.json['ssl_certificate_key']
start_time = request.json.get('start_time')
expire_time = request.json.get('expire_time')
comment = request.json.get('comment') or ''
data = {
'user_id': current_user_id,
'domain': domain,
'start_time': start_time,
'expire_time': expire_time,
'ssl_certificate': ssl_certificate,
'ssl_certificate_key': ssl_certificate_key,
'comment': comment,
}
CertificateModel.create(**data)
@auth_service.permission(role=RoleEnum.USER)
def update_certificate_by_id():
"""
更新主机地址
:return:
@since v1.6.12
"""
current_user_id = g.user_id
certificate_id = request.json['certificate_id']
domain = request.json['domain']
ssl_certificate = request.json['ssl_certificate']
ssl_certificate_key = request.json['ssl_certificate_key']
start_time = request.json.get('start_time')
expire_time = request.json.get('expire_time')
comment = request.json.get('comment') or ''
# check data
certificate_row = CertificateModel.select().where(
CertificateModel.id == certificate_id,
CertificateModel.user_id == current_user_id
).first()
if not certificate_row:
raise DataNotFoundAppException()
data = {
'domain': domain,
'start_time': start_time,
'expire_time': expire_time,
'ssl_certificate': ssl_certificate,
'ssl_certificate_key': ssl_certificate_key,
'comment': comment,
}
CertificateModel.update(data).where(
CertificateModel.id == certificate_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def delete_certificate_by_id():
"""
删除
:return:
@since v1.6.12
"""
current_user_id = g.user_id
certificate_id = request.json['certificate_id']
# check data
certificate_row = CertificateModel.select().where(
CertificateModel.id == certificate_id,
CertificateModel.user_id == current_user_id
).first()
if not certificate_row:
raise DataNotFoundAppException()
# delete
CertificateModel.delete().where(
CertificateModel.id == certificate_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def delete_certificate_by_ids():
"""
批量删除
:return:
@since v1.6.12
"""
current_user_id = g.user_id
certificate_ids = request.json['certificate_ids']
CertificateModel.delete().where(
CertificateModel.id.in_(certificate_ids),
CertificateModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_certificate_by_id():
"""
获取
:return:
@since v1.6.12
"""
current_user_id = g.user_id
certificate_id = request.json['certificate_id']
# check data
certificate_row = CertificateModel.select().where(
CertificateModel.id == certificate_id,
CertificateModel.user_id == current_user_id
).first()
if not certificate_row:
raise DataNotFoundAppException()
# 查询部署数量
certificate_dict = certificate_row.to_dict()
certificate_service.load_cert_deploy_count([certificate_dict])
# 查询api部署状态
certificate_service.load_api_deploy_status([certificate_dict])
return certificate_dict
@auth_service.permission(role=RoleEnum.USER)
def deploy_certificate_by_webhook():
"""
通过webhook部署托管证书
@since v1.6.52
"""
current_user_id = g.user_id
certificate_id = request.json['certificate_id']
url = request.json['url']
headers = request.json.get('headers') or {}
# check data
certificate_row = certificate_service.get_certificate_row(
certificate_id=certificate_id,
user_id=current_user_id
)
# update config
deploy_webhook_row = DeployWebhookModel.select().where(
DeployWebhookModel.user_id == current_user_id,
DeployWebhookModel.object_id == certificate_row.id,
DeployWebhookModel.object_type == ObjectEnum.Certificate
).first()
if not deploy_webhook_row:
deploy_webhook_row = DeployWebhookModel.create(
user_id=current_user_id,
object_id=certificate_row.id,
object_type=ObjectEnum.Certificate,
url=url,
header_raw=json.dumps(headers),
)
else:
DeployWebhookModel.update(
url=url,
header_raw=json.dumps(headers),
).where(
DeployWebhookModel.id == deploy_webhook_row.id
).execute()
# refresh
deploy_webhook_row = DeployWebhookModel.get_by_id(deploy_webhook_row.id)
# deploy
res = certificate_service.deploy_certificate_by_webhook(
certificate_row=certificate_row,
deploy_webhook_row=deploy_webhook_row,
)
# report result
if res.ok:
DeployWebhookModel.update(
status=DeployStatusEnum.SUCCESS
).where(
DeployWebhookModel.id == deploy_webhook_row.id
).execute()
return {
'result': res.text
}
else:
DeployWebhookModel.update(
status=DeployStatusEnum.ERROR
).where(
DeployWebhookModel.id == deploy_webhook_row.id
).execute()
raise res.raise_for_status()
@auth_service.permission(role=RoleEnum.USER)
def get_deploy_webhook():
"""
获取配置部署托管证书
@since v1.6.52
"""
current_user_id = g.user_id
certificate_id = request.json['certificate_id']
# check data
certificate_row = certificate_service.get_certificate_row(
certificate_id=certificate_id,
user_id=current_user_id
)
deploy_webhook_row = DeployWebhookModel.select().where(
DeployWebhookModel.user_id == current_user_id,
DeployWebhookModel.object_id == certificate_row.id,
DeployWebhookModel.object_type == ObjectEnum.Certificate
).first()
if deploy_webhook_row:
return deploy_webhook_row.to_dict()
+15
View File
@@ -0,0 +1,15 @@
"""
@File : check_wechat_api.py
@Date : 2025-06-04
"""
from flask import request
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.service import auth_service
from domain_admin.utils import wechat_util
@auth_service.permission(role=RoleEnum.USER)
def validate_url_for_wechat():
url = request.json['url']
return wechat_util.validate_url_for_wechat(url)
+221
View File
@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
"""
@File : deploy_cert_api.py
@Date : 2024-03-31
"""
from flask import g, request
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.deploy_cert_model import DeployCertModel
from domain_admin.model.host_model import HostModel
from domain_admin.service import deploy_cert_service, auth_service
from domain_admin.utils.flask_ext.app_exception import DataNotFoundAppException
from domain_admin.utils.open_api import aliyun_oss_api
@auth_service.permission(role=RoleEnum.USER)
def get_deploy_list_by_cert_id():
"""
获取列表
:return:
@since v1.6.20
"""
current_user_id = g.user_id
cert_id = request.json['cert_id']
page = request.json.get('page', 1)
size = request.json.get('size', 10)
query = DeployCertModel.select().where(
DeployCertModel.cert_id == cert_id,
DeployCertModel.user_id == current_user_id,
)
total = query.count()
lst = []
if total > 0:
rows = query.order_by(
DeployCertModel.id,
).paginate(page, size)
lst = [row.to_dict() for row in rows]
# deploy_host
deploy_cert_service.load_cert_deploy_host(lst)
return {
"list": lst,
"total": total
}
@auth_service.permission(role=RoleEnum.USER)
def add_deploy_cert():
"""
添加详情
:return:
@since v1.6.20
"""
current_user_id = g.user_id
cert_id = request.json['cert_id']
deploy_host_id = request.json['deploy_host_id']
deploy_key_file = request.json['deploy_key_file']
deploy_fullchain_file = request.json['deploy_fullchain_file']
deploy_reloadcmd = request.json['deploy_reloadcmd']
data = {
'user_id': current_user_id,
'cert_id': cert_id,
'deploy_host_id': deploy_host_id,
'deploy_key_file': deploy_key_file,
'deploy_fullchain_file': deploy_fullchain_file,
'deploy_reloadcmd': deploy_reloadcmd,
}
DeployCertModel.create(**data)
@auth_service.permission(role=RoleEnum.USER)
def update_deploy_cert_by_id():
"""
更新详情
:return:
@since v1.6.20
"""
current_user_id = g.user_id
deploy_cert_id = request.json['deploy_cert_id']
deploy_host_id = request.json['deploy_host_id']
deploy_key_file = request.json['deploy_key_file']
deploy_fullchain_file = request.json['deploy_fullchain_file']
deploy_reloadcmd = request.json['deploy_reloadcmd']
# check data
deploy_cert_row = DeployCertModel.select().where(
DeployCertModel.id == deploy_cert_id,
DeployCertModel.user_id == current_user_id
).first()
if not deploy_cert_row:
raise DataNotFoundAppException()
data = {
'deploy_host_id': deploy_host_id,
'deploy_key_file': deploy_key_file,
'deploy_fullchain_file': deploy_fullchain_file,
'deploy_reloadcmd': deploy_reloadcmd,
}
DeployCertModel.update(data).where(
DeployCertModel.id == deploy_cert_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def delete_by_deploy_cert_id():
"""
删除详情
:return:
@since v1.6.20
"""
current_user_id = g.user_id
deploy_cert_id = request.json['deploy_cert_id']
# check data
deploy_cert_row = DeployCertModel.select().where(
DeployCertModel.id == deploy_cert_id,
DeployCertModel.user_id == current_user_id
).first()
if not deploy_cert_row:
raise DataNotFoundAppException()
DeployCertModel.delete_by_id(deploy_cert_id)
@auth_service.permission(role=RoleEnum.USER)
def delete_by_deploy_cert_ids():
"""
批量删除详情
:return:
@since v1.6.20
"""
current_user_id = g.user_id
deploy_cert_ids = request.json['deploy_cert_ids']
DeployCertModel.delete().where(
DeployCertModel.id.in_(deploy_cert_ids),
DeployCertModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_deploy_cert_by_id():
"""
获取详情
:return:
@since v1.6.20
"""
current_user_id = g.user_id
deploy_cert_id = request.json['deploy_cert_id']
# check data
deploy_cert_row = DeployCertModel.select().where(
DeployCertModel.id == deploy_cert_id,
DeployCertModel.user_id == current_user_id
).first()
if not deploy_cert_row:
raise DataNotFoundAppException()
deploy_cert_dict = deploy_cert_row.to_dict()
deploy_cert_dict['deploy_host'] = HostModel.get_by_id(deploy_cert_dict['deploy_host_id'])
return deploy_cert_dict
@auth_service.permission(role=RoleEnum.USER)
def handle_deploy_cert():
"""
部署证书
@since v1.6.20
:return:
"""
current_user_id = g.user_id
deploy_cert_id = request.json['deploy_cert_id']
# check data
deploy_cert_row = DeployCertModel.select().where(
DeployCertModel.id == deploy_cert_id,
DeployCertModel.user_id == current_user_id
).first()
if not deploy_cert_row:
raise DataNotFoundAppException()
err = deploy_cert_service.handle_deploy_cert(deploy_cert_row)
if err:
raise err
@auth_service.permission(role=RoleEnum.USER)
def get_aliyun_endpoint_options():
"""
阿里云endpoint
:return:
"""
return aliyun_oss_api.ENDPOINT_OPTIONS
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""
@File : dns_api.py
@Date : 2023-07-29
"""
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.dns_type_enum import DnsTypeEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.dns_model import DnsModel
from domain_admin.service import auth_service
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
def add_dns():
"""
添加Dns
:return:
"""
current_user_id = g.user_id
dns_type_id = request.json.get('dns_type_id') or DnsTypeEnum.ALIYUN
name = request.json['name']
access_key = request.json['access_key']
secret_key = request.json['secret_key']
row = DnsModel.create(
user_id=current_user_id,
dns_type_id=dns_type_id,
name=name,
access_key=access_key,
secret_key=secret_key,
)
return row
@auth_service.permission(role=RoleEnum.USER)
def update_dns_by_id():
"""
更新Dns
:return:
"""
current_user_id = g.user_id
dns_type_id = request.json.get('dns_type_id') or DnsTypeEnum.ALIYUN
dns_id = request.json['dns_id']
name = request.json['name']
access_key = request.json['access_key']
secret_key = request.json['secret_key']
# data check
dns_row = DnsModel.select().where(
DnsModel.id == dns_id,
DnsModel.user_id == current_user_id
).first()
if not dns_row:
raise DataNotFoundAppException()
DnsModel.update(
dns_type_id=dns_type_id,
name=name,
access_key=access_key,
secret_key=secret_key,
).where(
DnsModel.id == dns_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_dns_by_id():
"""
获取Dns
:return:
"""
current_user_id = g.user_id
dns_id = request.json['dns_id']
dns_row = DnsModel.select().where(
DnsModel.id == dns_id,
DnsModel.user_id == current_user_id
).first()
if not dns_row:
raise DataNotFoundAppException()
dns_row.secret_key = ''
return dns_row
@auth_service.permission(role=RoleEnum.USER)
def delete_dns_by_id():
"""
移除Dns
:return:
"""
current_user_id = g.user_id
dns_id = request.json['dns_id']
dns_row = DnsModel.select().where(
DnsModel.id == dns_id,
DnsModel.user_id == current_user_id
).first()
if not dns_row:
raise DataNotFoundAppException()
return DnsModel.delete_by_id(dns_row.id)
@auth_service.permission(role=RoleEnum.USER)
def get_dns_list():
"""
Dns列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
dns_type_id = request.json.get('dns_type_id')
query = DnsModel.select().where(
DnsModel.user_id == current_user_id
)
if keyword:
query = query.where(DnsModel.name.contains(keyword))
if dns_type_id:
query = query.where(DnsModel.dns_type_id == dns_type_id)
total = query.count()
rows = query.order_by(
DnsModel.create_time.desc(),
DnsModel.id.desc()
)
lst = list(map(lambda m: model_to_dict(model=m), rows))
for row in lst:
row['secret_key'] = ''
return {
'list': lst,
'total': total,
}
+738
View File
@@ -0,0 +1,738 @@
# -*- coding: utf-8 -*-
"""
由于历史原因,domain指代 SSL证书的域名
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from operator import itemgetter
from flask import request, g
from playhouse.shortcuts import model_to_dict, fn
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.enums.ssl_type_enum import SSLTypeEnum
from domain_admin.model.address_model import AddressModel
from domain_admin.model.domain_info_model import DomainInfoModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.group_model import GroupModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.service import async_task_service, domain_info_service, group_service, operation_service, auth_service
from domain_admin.service import domain_service
from domain_admin.service import file_service
from domain_admin.utils import datetime_util, domain_util
from domain_admin.utils.cert_util import cert_consts
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException, ForbiddenAppException
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainModel,
operation_type_id=OperationEnum.CREATE
)
def add_domain():
"""
添加域名
:return:
"""
current_user_id = g.user_id
domain = request.json['domain']
alias = request.json.get('alias') or ''
group_id = request.json.get('group_id') or 0
# is_dynamic_host = request.json.get('is_dynamic_host', False)
start_time = request.json.get('start_time')
expire_time = request.json.get('expire_time')
auto_update = request.json.get('auto_update', True)
port = request.json.get('port') or cert_consts.SSL_DEFAULT_PORT
ssl_type = request.json.get('ssl_type', SSLTypeEnum.SSL_TLS)
data = {
# 基本信息
'user_id': current_user_id,
'domain': domain.strip(),
'port': int(port), # fix: TypeError: an integer is required (got type str)
'root_domain': domain_util.get_root_domain(domain),
'alias': alias,
'group_id': group_id,
# 'is_dynamic_host': is_dynamic_host,
'start_time': start_time,
'expire_time': expire_time,
'auto_update': auto_update,
'ssl_type': ssl_type,
}
row = DomainModel.create(**data)
if auto_update:
domain_service.update_domain_row(row)
# 顺带添加到域名监测列表
if not domain_util.is_ipv4(domain):
first_domain_info_row = DomainInfoModel.select(
DomainInfoModel.id
).where(
DomainInfoModel.domain == data['root_domain'],
DomainInfoModel.user_id == current_user_id
).first()
if not first_domain_info_row:
domain_info_service.add_domain_info(
domain=domain_util.get_root_domain(domain),
comment=alias,
group_id=group_id,
user_id=current_user_id,
)
return {'id': row.id}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainModel,
operation_type_id=OperationEnum.UPDATE
)
def update_domain_by_id():
"""
更新数据
id domain alias group_id notify_status
:return:
"""
current_user_id = g.user_id
data = request.json
domain_id = request.json['id']
# domain_service.check_permission_and_get_row(domain_id, current_user_id)
data['update_time'] = datetime_util.get_datetime()
data['group_id'] = data.get('group_id') or 0
# data check
before_domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not before_domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_edit_permission(
domain_row=before_domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
DomainModel.update(data).where(
DomainModel.id == domain_id
).execute()
after_domain_row = DomainModel.get_by_id(domain_id)
# 域名、端口、SSL加密方式没改变,就不更新
if before_domain_row.domain == after_domain_row.domain \
and before_domain_row.port == after_domain_row.port \
and before_domain_row.ssl_type == after_domain_row.ssl_type:
pass
else:
if after_domain_row.auto_update:
domain_service.update_domain_row(after_domain_row)
@auth_service.permission(role=RoleEnum.USER)
def update_domain_expire_monitor_by_id():
"""
更新监控状态
:return:
"""
current_user_id = g.user_id
domain_id = request.json.get('domain_id')
# data check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_edit_permission(
domain_row=domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
data = {
"is_monitor": request.json.get('is_monitor', True)
}
DomainModel.update(
data
).where(
DomainModel.id == domain_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='domain_id'
)
def update_domain_field_by_id():
"""
更新单个数据
:return:
"""
current_user_id = g.user_id
domain_id = request.json['domain_id']
field = request.json.get('field')
value = request.json.get('value')
if field not in ['auto_update']:
raise AppException("not allow field")
data = {
field: value,
}
# data check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_edit_permission(
domain_row=domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
DomainModel.update(data).where(
DomainModel.id == domain_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def update_domain_field_by_ids():
"""
批量更新单个字段值
:return:
"""
current_user_id = g.user_id
domain_ids = request.json['domain_ids']
field = request.json.get('field')
value = request.json.get('value')
if field not in ['auto_update']:
raise AppException("not allow field")
data = {
field: value,
}
DomainModel.update(data).where(
DomainModel.id.in_(domain_ids),
DomainModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainModel,
operation_type_id=OperationEnum.DELETE,
primary_key='id'
)
def delete_domain_by_id():
"""
删除
:return:
"""
current_user_id = g.user_id
domain_id = request.json['id']
# domain_service.check_permission_and_get_row(domain_id, current_user_id)
# data check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_edit_permission(
domain_row=domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
DomainModel.delete_by_id(domain_row.id)
# 同时移除主机信息
AddressModel.delete().where(
AddressModel.domain_id == domain_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainModel,
operation_type_id=OperationEnum.BATCH_DELETE,
primary_key='ids'
)
def delete_domain_by_ids():
"""
批量删除
@since v1.2.16
:return:
"""
current_user_id = g.user_id
domain_ids = request.json['ids']
DomainModel.delete().where(
DomainModel.id.in_(domain_ids),
DomainModel.user_id == current_user_id
).execute()
# 同时移除主机信息
AddressModel.delete().where(
AddressModel.domain_id.in_(domain_ids)
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_domain_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
domain_id = request.json.get('domain_id') or request.json['id']
# row = domain_service.check_permission_and_get_row(domain_id, current_user_id)
# row = DomainModel.get_by_id(domain_id)
# data check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_edit_permission(
domain_row=domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
row = model_to_dict(
model=domain_row,
extra_attrs=[
'real_time_expire_days',
'domain_url',
'update_time_label',
'expire_status',
]
)
# 主机数量
address_count = AddressModel.select().where(
AddressModel.domain_id == domain_id
).count()
row['address_count'] = address_count
row['group_name'] = group_service.get_group_name_by_id(row['group_id'])
if row['user_id'] == current_user_id:
has_edit_permission = True
else:
first_row = GroupUserModel.select().where(
GroupUserModel.group_id == row['group_id'],
GroupUserModel.user_id == current_user_id,
GroupUserModel.has_edit_permission == True
).first()
if first_row:
has_edit_permission = True
else:
has_edit_permission = False
row['has_edit_permission'] = has_edit_permission
return row
@auth_service.permission(role=RoleEnum.USER)
def update_all_domain_cert_info():
"""
更新所有域名证书信息
:return:
"""
domain_service.update_all_domain_cert_info()
@auth_service.permission(role=RoleEnum.USER)
def update_all_domain_cert_info_of_user():
"""
更新当前用户的所有域名信息
:return:
"""
current_user_id = g.user_id
domain_service.update_all_domain_cert_info_of_user(user_id=current_user_id)
# async_task_service.submit_task(fn=domain_service.update_all_domain_cert_info_of_user, user_id=current_user_id)
@auth_service.permission(role=RoleEnum.USER)
def update_domain_row_info_by_id():
"""
更新域名关联的证书信息
:return:
@since v1.3.1
"""
current_user_id = g.user_id
# @since v1.2.24 支持参数 domain_id
domain_id = request.json.get('domain_id') or request.json['id']
# row = domain_service.check_permission_and_get_row(domain_id, current_user_id)
# row = DomainModel.get_by_id(domain_id)
# data check
domain_row = DomainModel.select().where(
DomainModel.id == domain_id
).first()
if not domain_row:
raise DataNotFoundAppException()
# edit permission check
has_permission = domain_service.has_read_permission(
domain_row=domain_row,
user_id=current_user_id
)
if not has_permission:
raise ForbiddenAppException()
domain_service.update_domain_row(domain_row=domain_row)
@auth_service.permission(role=RoleEnum.USER)
def get_all_domain_list_of_user():
"""
获取用户的所有域名数据
:return:
"""
current_user_id = g.user_id
# temp_filename = domain_service.export_domain_to_file(current_user_id)
rows = DomainModel.select().where(
DomainModel.user_id == current_user_id
)
lst = [{'domain': row.domain} for row in rows]
return {
'list': lst,
'total': len(lst)
}
@auth_service.permission(role=RoleEnum.USER)
def import_domain_from_file():
"""
从文件导入域名
支持 txt 和 csv格式
:return:
"""
current_user_id = g.user_id
update_file = request.files.get('file')
filename = file_service.save_temp_file(update_file)
# 导入数据
domain_service.add_domain_from_file(filename, current_user_id)
# 异步导入
# async_task_service.submit_task(fn=domain_service.add_domain_from_file, filename=filename, user_id=current_user_id)
# 异步查询
domain_service.update_all_domain_cert_info_of_user(user_id=current_user_id)
# async_task_service.submit_task(fn=domain_service.update_all_domain_cert_info_of_user, user_id=current_user_id)
@auth_service.permission(role=RoleEnum.USER)
def export_domain_file():
"""
导出域名文件
csv格式
:return:
"""
current_user_id = g.user_id
keyword = request.json.get('keyword')
group_id = request.json.get('group_id')
group_ids = request.json.get('group_ids')
expire_days = request.json.get('expire_days')
role = request.json.get('role')
ext = request.json.get('ext', 'csv')
order_prop = request.json.get('order_prop') or 'expire_days'
order_type = request.json.get('order_type') or 'ascending'
params = {
'keyword': keyword,
'group_id': group_id,
'group_ids': group_ids,
'expire_days': expire_days,
'role': role,
'user_id': current_user_id,
}
query = domain_service.get_domain_list_query(**params)
ordering = domain_service.get_domain_ordering(order_prop=order_prop, order_type=order_type)
rows = query.order_by(*ordering)
# 分组名
lst = list(map(lambda m: model_to_dict(
model=m,
extra_attrs=[
'start_date',
'expire_date',
'expire_days',
'create_time_label',
'real_time_expire_days',
'real_time_ssl_total_days',
'real_time_ssl_expire_days',
'domain_url',
'update_time_label',
'expire_status',
]
), rows))
group_service.load_group_name(lst)
filename = domain_service.export_domain_to_file(rows=lst, ext=ext)
return {
'name': filename,
'url': file_service.resolve_temp_url(filename)
}
@auth_service.permission(role=RoleEnum.USER)
def domain_relation_group():
"""
分组关联域名
:return:
"""
current_user_id = g.user_id
# temp_filename = domain_service.export_domain_to_file(current_user_id)
domain_ids = request.json['domain_ids']
group_id = request.json['group_id']
DomainModel.update(
group_id=group_id
).where(
DomainModel.id.in_(domain_ids),
DomainModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_domain_list():
"""
获取域名列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
group_id = request.json.get('group_id')
group_ids = request.json.get('group_ids')
expire_days = request.json.get('expire_days')
role = request.json.get('role')
order_prop = request.json.get('order_prop') or 'expire_days'
order_type = request.json.get('order_type') or 'ascending'
# 组权限
group_user_permission_map = {}
if role == RoleEnum.ADMIN:
pass
else:
# 所在分组
group_user_rows = GroupUserModel.select().where(
GroupUserModel.user_id == current_user_id
)
group_user_list = list(group_user_rows)
# 组员权限
group_user_permission_map = {row.group_id: row.has_edit_permission for row in group_user_list}
params = {
'keyword': keyword,
'group_id': group_id,
'group_ids': group_ids,
'expire_days': expire_days,
'role': role,
'user_id': current_user_id,
}
query = domain_service.get_domain_list_query(**params)
ordering = domain_service.get_domain_ordering(order_prop=order_prop, order_type=order_type)
total = query.count()
lst = []
if total > 0:
lst = query.order_by(*ordering).paginate(page, size)
lst = list(map(lambda m: model_to_dict(
model=m,
extra_attrs=[
'expire_days',
'create_time_label',
'real_time_expire_days',
'real_time_ssl_total_days',
'real_time_ssl_expire_days',
'domain_url',
'update_time_label',
'expire_status',
]
), lst))
# 加载主机数量
domain_service.load_address_count(lst)
# 加载域名过期时间
domain_service.load_domain_expire_days(lst)
# 分组名
group_service.load_group_name(lst)
for row in lst:
if role == RoleEnum.ADMIN:
has_edit_permission = True
elif row['user_id'] == current_user_id:
has_edit_permission = True
else:
has_edit_permission = group_user_permission_map.get(row['group_id'], False)
row['has_edit_permission'] = has_edit_permission
# lst = model_util.list_with_relation_one(lst, 'group', GroupModel)
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.USER)
def get_domain_group_filter():
"""
获取证书分组筛选器
:return:
"""
current_user_id = g.user_id
# 分组列表数据
query = GroupModel.select().where(
GroupModel.user_id == current_user_id
)
# 所在分组
group_user_rows = GroupUserModel.select().where(
GroupUserModel.user_id == current_user_id
)
group_user_list = list(group_user_rows)
user_group_ids = [row.group_id for row in group_user_list]
if user_group_ids:
query = query.orwhere(GroupModel.id.in_(user_group_ids))
total = query.count()
lst = []
if total > 0:
lst = [model_to_dict(row) for row in query]
group_ids = [row['id'] for row in lst]
# 证书分组统计
cert_groups = DomainModel.select(
DomainModel.group_id,
fn.COUNT(DomainModel.id).alias('count')
).where(
DomainModel.group_id.in_(group_ids)
).group_by(DomainModel.group_id)
cert_groups_map = {
str(row.group_id): row.count
for row in cert_groups
}
for row in lst:
row['cert_count'] = cert_groups_map.get(str(row['id']), 0)
# leader
if row['user_id'] == current_user_id:
row['is_leader'] = True
else:
row['is_leader'] = False
# if cert_groups_map.get('0'):
# lst.append({
# 'cert_count': cert_groups_map.get('0'),
# 'id': 0,
# 'name': '未分组',
# })
lst.sort(key=itemgetter('cert_count'), reverse=True)
return {
'list': lst,
'total': len(lst),
}
+678
View File
@@ -0,0 +1,678 @@
# -*- coding: utf-8 -*-
"""
domain_info_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
import json
from datetime import datetime
from operator import itemgetter
from flask import request, g
from peewee import fn
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.domain_info_model import DomainInfoModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.group_model import GroupModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.service import domain_info_service, async_task_service, file_service, group_service, \
operation_service, group_user_service, domain_service, common_service, domain_icp_service, tag_service, auth_service
from domain_admin.utils import domain_util, time_util, icp_util
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException
from domain_admin.utils.open_api import crtsh_api
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainInfoModel,
operation_type_id=OperationEnum.CREATE,
primary_key='domain_info_id'
)
def add_domain_info():
"""
添加域名
:return:
"""
current_user_id = g.user_id
domain = request.json['domain']
root_domain = domain_util.get_root_domain(domain)
if not root_domain:
raise AppException("不能解析根域名:{}".format(domain))
else:
domain = root_domain
domain_start_time = request.json.get('domain_start_time')
domain_expire_time = request.json.get('domain_expire_time')
is_auto_update = request.json.get('is_auto_update', True)
is_auto_subdomain = request.json.get('is_auto_subdomain', False)
comment = request.json.get('comment', '')
tags = request.json.get('tags')
icp_company = request.json.get('icp_company', '')
icp_licence = request.json.get('icp_licence', '')
group_id = request.json.get('group_id') or 0
user_id = request.json.get('user_id')
row = domain_info_service.add_domain_info(
domain=domain,
domain_start_time=domain_start_time,
domain_expire_time=domain_expire_time,
comment=comment,
tags=tags,
group_id=group_id,
user_id=user_id or current_user_id,
icp_company=icp_company,
icp_licence=icp_licence,
is_auto_update=is_auto_update
)
# 异步提交
if is_auto_subdomain:
domain_service.auto_import_from_domain_async(
root_domain=domain,
group_id=group_id,
user_id=current_user_id
)
# async_task_service.submit_task(
# fn=domain_service.auto_import_from_domain,
# root_domain=domain,
# group_id=group_id,
# user_id=current_user_id
# )
tag_service.add_tags(tags)
return {'domain_info_id': row.id}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainInfoModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='domain_info_id'
)
def update_domain_info_by_id():
"""
更新数据
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
domain = domain_util.get_root_domain(request.json['domain'])
domain_start_time = request.json.get('domain_start_time')
domain_expire_time = request.json.get('domain_expire_time')
is_auto_update = request.json.get('is_auto_update', True)
is_auto_subdomain = request.json.get('is_auto_subdomain', False)
comment = request.json.get('comment', '')
group_id = request.json.get('group_id') or 0
tags = request.json.get('tags')
icp_company = request.json.get('icp_company', '')
icp_licence = request.json.get('icp_licence', '')
user_id = request.json.get('user_id')
# check data
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
# is_auto_update = request.json.get('is_auto_update', True)
# is_expire_monitor = request.json.get('is_expire_monitor', True)
data = {
'domain': domain,
'comment': comment,
'group_id': group_id,
'tags_raw': json.dumps(tags, ensure_ascii=False),
'icp_company': icp_company,
'icp_licence': icp_licence,
'is_auto_update': is_auto_update
}
# 不自动更新,才可以提交开始时间和结束时间
if is_auto_update is False:
data['domain_start_time'] = domain_start_time
data['domain_expire_time'] = domain_expire_time
if domain_expire_time:
data['domain_expire_days'] = time_util.get_diff_days(datetime.now(),
time_util.parse_time(domain_expire_time))
else:
data['domain_expire_days'] = 0
if user_id:
data['user_id'] = user_id
DomainInfoModel.update(data).where(
DomainInfoModel.id == domain_info_id
).execute()
if domain_info_row.domain != domain and is_auto_update:
# 需要自动更新
domain_info_service.update_domain_info_row(domain_info_row)
if is_auto_subdomain:
domain_service.auto_import_from_domain_async(
root_domain=domain,
group_id=group_id,
user_id=current_user_id
)
#
# async_task_service.submit_task(
# fn=domain_service.auto_import_from_domain,
# root_domain=domain,
# group_id=group_id,
# user_id=current_user_id
# )
tag_service.add_tags(tags)
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainInfoModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='domain_info_id'
)
def update_domain_info_field_by_id():
"""
更新单个数据
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
field = request.json.get('field')
value = request.json.get('value')
if field not in ['is_auto_update', 'is_expire_monitor']:
raise AppException("not allow field")
data = {
field: value,
}
# check data
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
DomainInfoModel.update(data).where(
DomainInfoModel.id == domain_info_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainInfoModel,
operation_type_id=OperationEnum.DELETE,
primary_key='domain_info_id'
)
def delete_domain_info_by_id():
"""
删除
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
# check data
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
DomainInfoModel.delete_by_id(domain_info_row.id)
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=DomainInfoModel,
operation_type_id=OperationEnum.BATCH_DELETE,
primary_key='domain_info_ids'
)
def delete_domain_info_by_ids():
"""
批量删除
"""
current_user_id = g.user_id
domain_info_ids = request.json['domain_info_ids']
DomainInfoModel.delete().where(
DomainInfoModel.id.in_(domain_info_ids),
DomainInfoModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_domain_info_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
# check data
if g.current_user_row.role == RoleEnum.ADMIN:
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id
).first()
else:
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
domain_row = model_to_dict(
model=domain_info_row,
extra_attrs=[
'real_domain_expire_days',
'update_time_label',
'tags',
],
exclude=['tags_raw'],
)
# 主机数量
ssl_count = DomainModel.select().where(
DomainModel.root_domain == domain_row['domain'],
DomainModel.user_id == current_user_id
).count()
domain_row['ssl_count'] = ssl_count
domain_row['group_name'] = group_service.get_group_name_by_id(domain_row['group_id'])
# 编辑权限
group_user_permission_map = group_user_service.get_group_user_permission_map(current_user_id)
if domain_row['user_id'] == current_user_id:
has_edit_permission = True
else:
has_edit_permission = group_user_permission_map.get(domain_row['group_id'], False)
domain_row['has_edit_permission'] = has_edit_permission
common_service.load_user_name([domain_row])
return domain_row
@auth_service.permission(role=RoleEnum.USER)
def update_domain_info_row_by_id():
"""
更新当前行的域名信息
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
# check data
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
domain_info_service.update_domain_info_row(row=domain_info_row)
@auth_service.permission(role=RoleEnum.USER)
def update_all_domain_info_of_user():
"""
更新当前用户的域名信息
:return:
"""
current_user_id = g.user_id
domain_info_service.update_all_domain_info_of_user(user_id=current_user_id)
# async_task_service.submit_task(fn=domain_info_service.update_all_domain_info_of_user, user_id=current_user_id)
@auth_service.permission(role=RoleEnum.USER)
def update_all_domain_icp_of_user():
"""
更新当前用户的域名icp信息
:return:
"""
current_user_id = g.user_id
domain_info_service.update_all_domain_icp_of_user(current_user_id)
# async_task_service.submit_task(fn=domain_info_service.update_all_domain_icp_of_user, user_id=current_user_id)
@auth_service.permission(role=RoleEnum.USER)
def update_domain_row_icp():
"""
更新当前域名icp信息
:return:
"""
current_user_id = g.user_id
domain_info_id = request.json['domain_info_id']
# check data
domain_info_row = DomainInfoModel.select().where(
DomainInfoModel.id == domain_info_id,
DomainInfoModel.user_id == current_user_id
).first()
if not domain_info_row:
raise DataNotFoundAppException()
domain_info_service.update_domain_row_icp(row=domain_info_row)
@auth_service.permission(role=RoleEnum.USER)
def import_domain_info_from_file():
"""
从文件导入域名
支持格式: txt、xlsx、csv
:return:
"""
current_user_id = g.user_id
update_file = request.files.get('file')
filename = file_service.save_temp_file(update_file)
# 导入数据
domain_info_service.add_domain_from_file(filename, current_user_id)
# 异步查询
domain_info_service.handle_auto_import_domain_info(current_user_id)
# async_task_service.submit_task(fn=domain_info_service.update_all_domain_info_of_user, user_id=current_user_id)
@auth_service.permission(role=RoleEnum.USER)
def export_domain_info_file():
"""
导出域名文件
csv格式
:return:
"""
current_user_id = g.user_id
keyword = request.json.get('keyword')
group_ids = request.json.get('group_ids')
domain_expire_days = request.json.get('domain_expire_days')
role = request.json.get('role')
ext = request.json.get('ext', 'csv')
order_prop = request.json.get('order_prop') or 'domain_expire_days'
order_type = request.json.get('order_type') or 'ascending'
params = {
'keyword': keyword,
'group_ids': group_ids,
'domain_expire_days': domain_expire_days,
'role': role,
'user_id': current_user_id
}
# 列表数据
query = domain_info_service.get_domain_info_query(**params)
ordering = domain_info_service.get_ordering(order_prop=order_prop, order_type=order_type)
rows = query.order_by(*ordering)
lst = [model_to_dict(
model=row,
extra_attrs=[
'domain_start_date',
'domain_expire_date',
'real_domain_expire_days',
'update_time_label',
'tags',
'tags_str',
]
) for row in rows]
# 分组名
group_service.load_group_name(lst)
filename = domain_info_service.export_domain_to_file(ext=ext, rows=lst)
return {
'name': filename,
'url': file_service.resolve_temp_url(filename)
}
@auth_service.permission(role=RoleEnum.USER)
def get_domain_info_list():
"""
获取域名列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
group_ids = request.json.get('group_ids')
domain_expire_days = request.json.get('domain_expire_days')
role = request.json.get('role')
order_prop = request.json.get('order_prop') or 'domain_expire_days'
order_type = request.json.get('order_type') or 'ascending'
group_user_permission_map = {}
if role == RoleEnum.ADMIN:
pass
else:
# 所在分组
group_user_rows = GroupUserModel.select().where(
GroupUserModel.user_id == current_user_id
)
group_user_list = list(group_user_rows)
# 组员权限
group_user_permission_map = {row.group_id: row.has_edit_permission for row in group_user_list}
params = {
'keyword': keyword,
'group_ids': group_ids,
'domain_expire_days': domain_expire_days,
'role': role,
'user_id': current_user_id
}
# 列表数据
query = domain_info_service.get_domain_info_query(**params)
total = query.count()
lst = []
if total > 0:
ordering = domain_info_service.get_ordering(order_prop=order_prop, order_type=order_type)
rows = query.order_by(*ordering).paginate(page, size)
lst = [model_to_dict(
model=row,
extra_attrs=[
'real_domain_expire_days',
'update_time_label',
'tags',
]
) for row in rows]
domain_list = [row['domain'] for row in lst]
# 域名证书
root_domain_groups = DomainModel.select(
DomainModel.root_domain,
fn.COUNT(DomainModel.id).alias('count')
).where(
DomainModel.root_domain.in_(domain_list),
DomainModel.user_id == current_user_id
).group_by(DomainModel.root_domain)
root_domain_groups_map = {
row.root_domain: row.count
for row in root_domain_groups
}
for row in lst:
row['ssl_count'] = root_domain_groups_map.get(row['domain'], 0)
if role == RoleEnum.ADMIN:
has_edit_permission = True
elif row['user_id'] == current_user_id:
has_edit_permission = True
else:
has_edit_permission = group_user_permission_map.get(row['group_id'], False)
row['has_edit_permission'] = has_edit_permission
# 分组名
group_service.load_group_name(lst)
# 用户名
common_service.load_user_name(lst)
return {
'list': lst,
'total': total,
}
@auth_service.permission(role=RoleEnum.USER)
def get_domain_info_group_filter():
"""
获取域名分组筛选器
:return:
"""
current_user_id = g.user_id
# 分组列表数据
query = GroupModel.select().where(
GroupModel.user_id == current_user_id
)
total = query.count()
lst = []
if total > 0:
# 证书分组统计
cert_groups = DomainInfoModel.select(
DomainInfoModel.group_id,
fn.COUNT(DomainInfoModel.id).alias('count')
).group_by(DomainInfoModel.group_id)
groups_map = {
str(row.group_id): row.count
for row in cert_groups
}
lst = []
for row in query:
row_dict = model_to_dict(row)
row_dict['domain_count'] = groups_map.get(str(row.id), 0)
lst.append(row_dict)
if groups_map.get('0'):
lst.append({
'domain_count': groups_map.get('0'),
'id': 0,
'name': '未分组',
})
lst.sort(key=itemgetter('domain_count'), reverse=True)
return {
'list': lst,
'total': len(lst),
}
@auth_service.permission(role=RoleEnum.USER)
def get_icp():
"""
查询icp信息
"""
domain = request.json['domain']
# 解析域名
resolve_domain = domain_util.parse_domain(domain)
item = domain_icp_service.get_domain_icp(resolve_domain)
if not item:
raise AppException('没有查到icp信息')
res = item.to_dict()
res['resolve_domain'] = resolve_domain
return res
@auth_service.permission(role=RoleEnum.USER)
def get_sub_domain_cert():
"""
获取子域证书列表
:return:
"""
keyword = request.json.get('keyword', 1)
lst = crtsh_api.search(keyword)
return {
'list': lst,
'total': len(lst)
}
@auth_service.permission(role=RoleEnum.USER)
def auto_import_subdomain_by_ids():
"""
批量导入子域证书
@since v1.6.30
:return:
"""
current_user_id = g.user_id
domain_ids = request.json['domain_ids']
rows = DomainInfoModel.select().where(
DomainInfoModel.id.in_(domain_ids),
DomainInfoModel.user_id == current_user_id
)
# 异步提交
domain_service.auto_import_from_domain_batch_async(rows=rows, user_id=current_user_id)
+295
View File
@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals, absolute_import, division
from operator import itemgetter
from flask import request, g
from peewee import fn
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.domain_info_model import DomainInfoModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.group_model import GroupModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.service import group_service, operation_service, group_user_service, auth_service
from domain_admin.utils.flask_ext.app_exception import ForbiddenAppException, DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupModel,
operation_type_id=OperationEnum.CREATE,
primary_key='id'
)
def add_group():
"""
添加
:return:
"""
current_user_id = g.user_id
name = request.json['name']
row = GroupModel.create(
name=name,
user_id=current_user_id
)
return {'id': row.id}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='id'
)
def update_group_by_id():
"""
更新数据
:return:
"""
current_user_id = g.user_id
group_id = request.json['id']
name = request.json.get('name')
# group_service.check_group_permission(group_id, current_user_id)
# check data
group_row = GroupModel.select().where(
GroupModel.id == group_id,
GroupModel.user_id == current_user_id
).first()
if not group_row:
raise DataNotFoundAppException()
GroupModel.update(
name=name,
).where(
GroupModel.id == group_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupModel,
operation_type_id=OperationEnum.DELETE,
primary_key='id'
)
def delete_group_by_id():
"""
删除
:return:
"""
current_user_id = g.user_id
group_id = request.json['id']
# check data
group_row = GroupModel.select().where(
GroupModel.id == group_id,
GroupModel.user_id == current_user_id
).first()
if not group_row:
raise DataNotFoundAppException()
GroupModel.delete_by_id(group_row.id)
# 重置已分类的证书 和 域名
DomainModel.update(
group_id=0
).where(
DomainModel.group_id == group_id
).execute()
DomainInfoModel.update(
group_id=0
).where(
DomainInfoModel.group_id == group_id
).execute()
GroupUserModel.delete().where(
GroupUserModel.group_id == group_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupModel,
operation_type_id=OperationEnum.BATCH_DELETE,
primary_key='group_ids'
)
def delete_group_by_ids():
"""
批量删除
:return:
"""
current_user_id = g.user_id
group_ids = request.json['group_ids']
GroupModel.delete().where(
GroupModel.id.in_(group_ids),
GroupModel.user_id == current_user_id
).execute()
# 重置已分类的证书 和 域名
DomainModel.update(
group_id=0
).where(
DomainModel.group_id.in_(group_ids)
).execute()
DomainInfoModel.update(
group_id=0
).where(
DomainInfoModel.group_id.in_(group_ids)
).execute()
GroupUserModel.delete().where(
GroupUserModel.group_id.in_(group_ids)
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_group_list():
"""
获取域名列表
:return:
"""
# page = request.json.get('page', 1)
# size = request.json.get('size', 10)
keyword = request.json.get('keyword')
role = request.json.get('role')
current_user_id = g.user_id
# 分组列表数据
query = GroupModel.select()
# 所在分组
group_user_list = list(GroupUserModel.select().where(
GroupUserModel.user_id == current_user_id
))
user_group_ids = [row.group_id for row in group_user_list]
group_user_map = {row.group_id: row.has_edit_permission for row in group_user_list}
if role == RoleEnum.ADMIN:
pass
elif user_group_ids:
query = query.where(
(GroupModel.user_id == current_user_id)
| (GroupModel.id.in_(user_group_ids))
)
else:
query = query.where(
GroupModel.user_id == current_user_id
)
if keyword:
query = query.where(GroupModel.name.contains(keyword))
total = query.count()
# bugfix: 分组不需要分页
rows = query.order_by(
GroupModel.create_time.asc(),
GroupModel.id.asc()
)
rows = list(rows)
# 证书分组统计
cert_groups = DomainModel.select(
DomainModel.group_id,
fn.COUNT(DomainModel.id).alias('count')
).group_by(DomainModel.group_id)
cert_groups_map = {
row.group_id: row.count
for row in cert_groups
}
# 域名分组统计
domain_groups = DomainInfoModel.select(
DomainInfoModel.group_id,
fn.COUNT(DomainInfoModel.id).alias('count')
).group_by(DomainInfoModel.group_id)
domain_groups_map = {
row.group_id: row.count
for row in domain_groups
}
# 成员分组统计
group_ids = [row.id for row in rows]
domain_groups = GroupUserModel.select(
GroupUserModel.group_id,
fn.COUNT(GroupUserModel.id).alias('count')
).where(
GroupUserModel.group_id.in_(group_ids)
).group_by(GroupUserModel.group_id)
group_user_groups_map = {
row.group_id: row.count
for row in domain_groups
}
lst = []
for row in rows:
row_dict = model_to_dict(row)
row_dict['cert_count'] = cert_groups_map.get(row.id, 0)
row_dict['domain_count'] = domain_groups_map.get(row.id, 0)
row_dict['group_user_count'] = group_user_groups_map.get(row.id, 0) + 1
# 组权限
if role == RoleEnum.ADMIN:
has_edit_permission = True
elif row.user_id == current_user_id:
has_edit_permission = True
else:
has_edit_permission = group_user_map.get(row.id, False)
row_dict['has_edit_permission'] = has_edit_permission
row_dict['is_leader'] = row.user_id == current_user_id
lst.append(row_dict)
lst.sort(key=itemgetter('is_leader', 'has_edit_permission'), reverse=True)
return {
'list': lst,
'total': total,
}
@auth_service.permission(role=RoleEnum.USER)
def get_group_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
group_id = request.json['id']
# group_service.check_group_permission(group_id, current_user_id)
# check data
group_row = GroupModel.select().where(
GroupModel.id == group_id,
GroupModel.user_id == current_user_id
).first()
if not group_row:
raise DataNotFoundAppException()
return group_row
+186
View File
@@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
"""
group_user_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.group_model import GroupModel
from domain_admin.model.group_user_model import GroupUserModel
from domain_admin.service import group_service, operation_service, common_service, auth_service
from domain_admin.utils.flask_ext.app_exception import DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupUserModel,
operation_type_id=OperationEnum.CREATE,
primary_key='id'
)
def add_group_user():
"""
添加
:return:
"""
current_user_id = g.user_id
group_id = request.json['group_id']
user_id = request.json['user_id']
has_edit_permission = request.json.get('has_edit_permission', False)
# 权限校验
group_service.check_group_permission(group_id, current_user_id)
row = GroupUserModel.create(
group_id=group_id,
user_id=user_id,
has_edit_permission=has_edit_permission
)
return {'id': row.id}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupUserModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='group_user_id'
)
def update_group_user_by_id():
"""
更新数据
:return:
"""
current_user_id = g.user_id
group_user_id = request.json['group_user_id']
has_edit_permission = request.json.get('has_edit_permission', False)
group_user_row = GroupUserModel.get_by_id(group_user_id)
group_service.check_group_permission(group_user_row.group_id, current_user_id)
GroupUserModel.update(
has_edit_permission=has_edit_permission,
).where(
GroupUserModel.id == group_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupUserModel,
operation_type_id=OperationEnum.DELETE,
primary_key='group_user_id'
)
def delete_group_user_by_id():
"""
删除
:return:
"""
current_user_id = g.user_id
group_user_id = request.json['group_user_id']
group_user_row = GroupUserModel.get_by_id(group_user_id)
group_service.check_group_permission(group_user_row.group_id, current_user_id)
GroupUserModel.delete().where(
GroupUserModel.id == group_user_id,
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=GroupUserModel,
operation_type_id=OperationEnum.BATCH_DELETE,
primary_key='group_user_ids'
)
def delete_group_user_by_ids():
"""
批量删除
:return:
"""
current_user_id = g.user_id
group_user_ids = request.json['group_user_ids']
group_user_rows = GroupUserModel.select().where(
GroupUserModel.id.in_(group_user_ids)
)
for group_user_row in group_user_rows:
group_service.check_group_permission(group_user_row.group_id, current_user_id)
GroupUserModel.delete().where(
GroupUserModel.id.in_(group_user_ids),
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_group_user_list():
"""
获取列表
:return:
"""
current_user_id = g.user_id
group_id = request.json.get('group_id')
# 分组列表数据
query = GroupUserModel.select().where(
GroupUserModel.group_id == group_id
)
total = query.count()
rows = query.order_by(
GroupUserModel.create_time.asc(),
GroupUserModel.id.asc()
)
lst = [model_to_dict(row) for row in rows]
# 添加leader
group_row = GroupModel.get_by_id(group_id)
leader = GroupUserModel()
leader.group_id = group_id
leader.user_id = group_row.user_id
leader.has_edit_permission = True
lst.insert(0, model_to_dict(leader))
common_service.load_user_name(lst)
for row in lst:
# add leader
if row['user_id'] == group_row.user_id:
row['is_leader'] = True
else:
row['is_leader'] = False
return {
'list': lst,
'total': len(lst),
}
@auth_service.permission(role=RoleEnum.USER)
def get_group_user_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
group_id = request.json['group_id']
group_service.check_group_permission(group_id, current_user_id)
return GroupUserModel.get_by_id(group_id)
+173
View File
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
"""
@File : host_api.py
@Date : 2023-07-29
"""
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.config import DEFAULT_SSH_PORT
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.log import logger
from domain_admin.model.host_model import HostModel
from domain_admin.service import auth_service
from domain_admin.utils.flask_ext.app_exception import DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
def add_host():
"""
添加主机
:return:
"""
current_user_id = g.user_id
host = request.json['host']
port = request.json.get('port') or DEFAULT_SSH_PORT
user = request.json['user']
auth_type = request.json['auth_type']
private_key = request.json['private_key']
password = request.json['password']
dns_id = request.json.get('dns_id', 0)
row = HostModel.create(
user_id=current_user_id,
host=host,
port=int(port),
user=user,
auth_type=auth_type,
private_key=private_key,
password=password,
dns_id=dns_id
)
return row
@auth_service.permission(role=RoleEnum.USER)
def update_host_by_id():
"""
更新主机
:return:
"""
current_user_id = g.user_id
host_id = request.json['host_id']
host = request.json['host']
port = request.json.get('port') or DEFAULT_SSH_PORT
user = request.json['user']
password = request.json['password']
auth_type = request.json['auth_type']
private_key = request.json['private_key']
dns_id = request.json.get('dns_id', 0)
# check data
host_row = HostModel.select().where(
HostModel.id == host_id,
HostModel.user_id == current_user_id
).first()
if not host_row:
raise DataNotFoundAppException()
HostModel.update(
host=host,
port=int(port),
user=user,
password=password,
auth_type=auth_type,
private_key=private_key,
dns_id=dns_id
).where(
HostModel.id == host_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_host_by_id():
"""
获取主机
:return:
"""
current_user_id = g.user_id
host_id = request.json['host_id']
# check data
host_row = HostModel.select().where(
HostModel.id == host_id,
HostModel.user_id == current_user_id
).first()
if not host_row:
raise DataNotFoundAppException()
host_row.password = ''
host_row.private_key = ''
return host_row
@auth_service.permission(role=RoleEnum.USER)
def delete_host_by_id():
"""
移除主机
:return:
"""
current_user_id = g.user_id
host_id = request.json['host_id']
# check data
host_row = HostModel.select().where(
HostModel.id == host_id,
HostModel.user_id == current_user_id
).first()
if not host_row:
raise DataNotFoundAppException()
return HostModel.delete_by_id(host_row.id)
@auth_service.permission(role=RoleEnum.USER)
def get_host_list():
"""
主机列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
host = request.json.get('host')
query = HostModel.select().where(
HostModel.user_id == current_user_id
)
if keyword:
query = query.where(HostModel.host.contains(keyword))
if host:
query = query.where(HostModel.host == host)
total = query.count()
rows = query.order_by(
HostModel.create_time.desc(),
HostModel.id.desc()
).paginate(page, size)
lst = list(map(lambda m: model_to_dict(model=m), rows))
for row in lst:
row['password'] = ''
row['private_key'] = ''
return {
'list': lst,
'total': total,
}
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
@File : ip_api.py
@Date : 2022-10-14
@Author : Peng Shiyu
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.service import auth_service
from domain_admin.utils import ip_util, dns_util
@auth_service.permission(role=RoleEnum.USER)
def get_ip_info():
"""
获取ip地址的信息
:return:
"""
ip = request.json['ip']
return ip_util.get_ip_info(ip)
@auth_service.permission(role=RoleEnum.USER)
def query_domain_cname():
"""
查询域名的CNAME记录
:return:
"""
domain = request.json['domain']
return dns_util.query_domain_cname(domain)
+740
View File
@@ -0,0 +1,740 @@
# -*- coding: utf-8 -*-
"""
@File : issue_certificate_api.py
@Date : 2023-07-23
"""
import json
import requests
from flask import g, request
from playhouse.shortcuts import model_to_dict, chunked
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.dns_model import DnsModel
from domain_admin.model.domain_model import DomainModel
from domain_admin.model.host_model import HostModel
from domain_admin.model.issue_certificate_model import IssueCertificateModel, ChallengeDeployTypeEnum, \
SSLDeployTypeEnum, DeployStatusEnum
from domain_admin.service import issue_certificate_service, auth_service
from domain_admin.utils import ip_util, domain_util, fabric_util, datetime_util, validate_util
from domain_admin.utils.acme_util.challenge_type import ChallengeType
from domain_admin.utils.acme_util.key_type_enum import KEY_TYPE_OPTIONS, KeyTypeEnum
from domain_admin.utils.acme_util.directory_type_enum import DIRECTORY_URL_OPTIONS, DirectoryTypeEnum
from domain_admin.utils.flask_ext.app_exception import AppException, DataNotFoundAppException
from domain_admin.utils.open_api import aliyun_domain_api
from domain_admin.utils.open_api.aliyun_domain_api import RecordTypeEnum
@auth_service.permission(role=RoleEnum.USER)
def issue_certificate():
"""
发起申请
:return:
"""
current_user_id = g.user_id
domains = request.json['domains']
directory_type = request.json.get('directory_type') or DirectoryTypeEnum.LETS_ENCRYPT
key_type = request.json.get('key_type') or KeyTypeEnum.RSA
issue_certificate_id = issue_certificate_service.issue_certificate(
domains=domains,
user_id=current_user_id,
directory_type=directory_type,
key_type=key_type
)
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
return issue_certificate_row.to_dict()
@auth_service.permission(role=RoleEnum.USER)
def verify_certificate():
"""
通知验证
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
challenge_type = request.json['challenge_type']
# 验证文件部署信息 可选参数
verify_deploy_path = request.json.get('verify_deploy_path')
host_id = request.json.get('host_id')
dns_id = request.json.get('dns_id')
# 验证成功后,自动添加到证书监控列表
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
issue_certificate_service.verify_certificate(
issue_certificate_id=issue_certificate_id,
challenge_type=challenge_type,
verify_deploy_path=verify_deploy_path,
host_id=host_id,
dns_id=dns_id
)
if host_id and verify_deploy_path:
IssueCertificateModel.update(
challenge_deploy_type_id=ChallengeDeployTypeEnum.SSH,
challenge_deploy_id=host_id,
deploy_verify_path=verify_deploy_path,
challenge_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
if dns_id:
IssueCertificateModel.update(
challenge_deploy_type_id=ChallengeDeployTypeEnum.DNS,
challenge_deploy_id=dns_id,
challenge_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
issue_certificate_service.renew_certificate(issue_certificate_id)
# fix: 过滤通配符的域名
lst = [
{
'domain': domain,
'root_domain': domain_util.get_root_domain(domain),
'port': 443,
'alias': '',
'user_id': current_user_id,
'group_id': 0,
}
for domain in issue_certificate_row.domains
if validate_util.is_domain(domain)
]
for batch in chunked(lst, 10):
DomainModel.insert_many(batch).on_conflict_ignore().execute()
@auth_service.permission(role=RoleEnum.USER)
def get_certificate_challenges():
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
lst = issue_certificate_service.get_certificate_challenges(issue_certificate_id)
return {
'total': len(lst),
'list': lst
}
@auth_service.permission(role=RoleEnum.USER)
def get_domain_host():
domain = request.json['domain']
host = ip_util.get_domain_ip(domain)
return {
'domain': domain,
'host': host
}
@auth_service.permission(role=RoleEnum.USER)
def deploy_verify_file():
"""
部署验证文件
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
verify_deploy_path = request.json['verify_deploy_path']
challenges = request.json['challenges']
host_id = request.json['host_id']
if not verify_deploy_path.endswith("/"):
raise AppException("verify_deploy_path must endswith '/'")
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
# deploy
issue_certificate_service.deploy_verify_file(
host_id=host_id,
verify_deploy_path=verify_deploy_path,
challenges=challenges
)
IssueCertificateModel.update(
challenge_deploy_type_id=ChallengeDeployTypeEnum.SSH,
challenge_deploy_id=host_id,
deploy_verify_path=verify_deploy_path,
challenge_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def deploy_certificate_file():
"""
ssh方式部署证书文件
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
host_id = request.json['host_id']
key_deploy_path = request.json['key_deploy_path']
pem_deploy_path = request.json['pem_deploy_path']
reload_cmd = request.json['reloadcmd']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
# host_row = HostModel.get_by_id(host_id)
# data check
host_row = HostModel.select().where(
HostModel.id == host_id,
HostModel.user_id == current_user_id,
).first()
if not host_row:
raise DataNotFoundAppException()
host = host_row.host
user = host_row.user
password = host_row.password
# issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row.ssl_certificate:
issue_certificate_service.renew_certificate(issue_certificate_id)
# issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
# deploy key
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
issue_certificate_service.deploy_certificate_file(
host_id=host_id,
key_content=issue_certificate_row.ssl_certificate_key,
pem_content=issue_certificate_row.ssl_certificate,
key_deploy_path=key_deploy_path,
pem_deploy_path=pem_deploy_path,
reload_cmd=reload_cmd
)
# update only support file verify
# if issue_certificate_row.challenge_type == ChallengeType.HTTP01:
# is_auto_renew = True
# else:
# is_auto_renew = False
IssueCertificateModel.update(
deploy_type_id=SSLDeployTypeEnum.SSH,
deploy_host_id=host_id,
deploy_key_file=key_deploy_path,
deploy_fullchain_file=pem_deploy_path,
deploy_reloadcmd=reload_cmd,
ssl_deploy_status=DeployStatusEnum.SUCCESS,
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
# 验证成功后, check_auto_renew
issue_certificate_service.check_auto_renew(
issue_certificate_id=issue_certificate_id
)
@auth_service.permission(role=RoleEnum.USER)
def renew_certificate():
"""
发起申请
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
issue_certificate_service.renew_certificate(issue_certificate_id)
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row:
raise AppException('数据不存在')
return issue_certificate_row.to_dict()
@auth_service.permission(role=RoleEnum.USER)
def get_issue_certificate_list():
"""
发起申请
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
keyword = request.json.get('keyword')
query = IssueCertificateModel.select().where(
IssueCertificateModel.user_id == current_user_id
)
if keyword:
query = query.where(IssueCertificateModel.domain_raw.contains(keyword))
total = query.count()
rows = query.order_by(
IssueCertificateModel.update_time.desc(),
IssueCertificateModel.id.desc()
).paginate(page, size)
lst = []
for row in rows:
item = model_to_dict(
row,
extra_attrs=[
'domains',
'create_time_label',
'update_time_label',
'start_date',
'expire_date',
'has_ssl_certificate',
'can_auto_renew',
],
exclude=[
IssueCertificateModel.ssl_certificate,
IssueCertificateModel.ssl_certificate_key
])
# 关联信息
item['challenge_deploy_name'] = '-'
if row.challenge_deploy_type_id == ChallengeDeployTypeEnum.SSH:
host = HostModel.get_or_none(HostModel.id == row.challenge_deploy_id)
if host:
item['challenge_deploy_name'] = host.host
elif row.challenge_deploy_type_id == ChallengeDeployTypeEnum.DNS:
dns = DnsModel.get_or_none(DnsModel.id == row.challenge_deploy_id)
if dns:
item['challenge_deploy_name'] = dns.name
item['deploy_host_name'] = '-'
if row.deploy_type_id == SSLDeployTypeEnum.SSH:
host = HostModel.get_or_none(HostModel.id == row.deploy_host_id)
if host:
item['deploy_host_name'] = host.host
elif row.deploy_type_id in [SSLDeployTypeEnum.OSS, SSLDeployTypeEnum.CDN, SSLDeployTypeEnum.DCDN]:
dns = DnsModel.get_or_none(DnsModel.id == row.deploy_host_id)
if dns:
item['deploy_host_name'] = dns.name
lst.append(item)
return {
'list': lst,
'total': total,
}
@auth_service.permission(role=RoleEnum.USER)
def get_issue_certificate_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
# check data
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
data = issue_certificate_row.to_dict()
data['deploy_dns'] = None
data['deploy_host'] = None
data['cert_deploy_host'] = None
data['cert_deploy_dns'] = None
if issue_certificate_row.challenge_deploy_type_id == ChallengeDeployTypeEnum.SSH:
host_row = HostModel.get_or_none(HostModel.id == issue_certificate_row.challenge_deploy_id)
data['deploy_host'] = host_row
if host_row and host_row.dns_id:
data['deploy_dns'] = DnsModel.get_or_none(DnsModel.id == host_row.dns_id)
elif issue_certificate_row.challenge_deploy_type_id == ChallengeDeployTypeEnum.DNS:
data['deploy_dns'] = DnsModel.get_or_none(DnsModel.id == issue_certificate_row.challenge_deploy_id)
if issue_certificate_row.deploy_type_id == SSLDeployTypeEnum.SSH:
data['cert_deploy_host'] = HostModel.get_or_none(HostModel.id == issue_certificate_row.deploy_host_id)
elif issue_certificate_row.deploy_type_id in [SSLDeployTypeEnum.OSS, SSLDeployTypeEnum.CDN, SSLDeployTypeEnum.DCDN]:
data['cert_deploy_dns'] = DnsModel.get_or_none(DnsModel.id == issue_certificate_row.deploy_host_id)
return data
@auth_service.permission(role=RoleEnum.USER)
def delete_issue_certificate_by_id():
"""
获取
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
# check data
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
IssueCertificateModel.delete_by_id(issue_certificate_row.id)
@auth_service.permission(role=RoleEnum.USER)
def delete_certificate_by_batch():
"""
批量删除
@since v1.2.16
:return:
"""
current_user_id = g.user_id
ids = request.json['ids']
IssueCertificateModel.delete().where(
IssueCertificateModel.id.in_(ids),
IssueCertificateModel.user_id == current_user_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def renew_issue_certificate_by_id():
"""
手动续期SSL证书
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
# check data
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
issue_certificate_service.renew_certificate_row(issue_certificate_row)
@auth_service.permission(role=RoleEnum.USER)
def get_allow_commands():
"""
命令白名单
:return:
"""
return fabric_util.allow_commands
@auth_service.permission(role=RoleEnum.USER)
def notify_web_hook():
"""
用户调用webhook
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
url = request.json['url']
headers = request.json.get('headers')
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
ret = issue_certificate_service.deploy_ssl_by_web_hook(
issue_certificate_id=issue_certificate_id,
url=url,
headers=headers,
)
# 更新验证信息
IssueCertificateModel.update(
deploy_type_id=SSLDeployTypeEnum.WEB_HOOK,
deploy_url=url,
deploy_header_raw=json.dumps(headers or {}),
ssl_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
# 验证成功后, check_auto_renew
issue_certificate_service.check_auto_renew(
issue_certificate_id=issue_certificate_id
)
return ret
@auth_service.permission(role=RoleEnum.USER)
def deploy_cert_to_oss():
"""
部署证书到阿里云oss
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
dns_id = request.json['dns_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
ret = issue_certificate_service.deploy_cert_to_oss(
issue_certificate_id=issue_certificate_id,
dns_id=dns_id,
)
# 更新验证信息
IssueCertificateModel.update(
deploy_type_id=SSLDeployTypeEnum.OSS,
deploy_host_id=dns_id,
ssl_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
# 验证成功后, check_auto_renew
issue_certificate_service.check_auto_renew(
issue_certificate_id=issue_certificate_id
)
return ret
@auth_service.permission(role=RoleEnum.USER)
def deploy_cert_to_cdn():
"""
部署证书到阿里云cdn
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
dns_id = request.json['dns_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
ret = issue_certificate_service.deploy_cert_to_cdn(
issue_certificate_id=issue_certificate_id,
dns_id=dns_id,
)
# 更新验证信息
IssueCertificateModel.update(
deploy_type_id=SSLDeployTypeEnum.CDN,
deploy_host_id=dns_id,
ssl_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
# 验证成功后, check_auto_renew
issue_certificate_service.check_auto_renew(
issue_certificate_id=issue_certificate_id
)
return ret
@auth_service.permission(role=RoleEnum.USER)
def deploy_cert_to_dcdn():
"""
部署证书到阿里云dcdn
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
dns_id = request.json['dns_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
ret = issue_certificate_service.deploy_cert_to_dcdn(
issue_certificate_id=issue_certificate_id,
dns_id=dns_id,
)
# 更新验证信息
IssueCertificateModel.update(
deploy_type_id=SSLDeployTypeEnum.DCDN,
deploy_host_id=dns_id,
ssl_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
# 验证成功后, check_auto_renew
issue_certificate_service.check_auto_renew(
issue_certificate_id=issue_certificate_id
)
return ret
@auth_service.permission(role=RoleEnum.USER)
def add_dns_domain_record():
"""
添加dns记录
:return:
"""
current_user_id = g.user_id
dns_id = request.json['dns_id']
issue_certificate_id = request.json['issue_certificate_id']
# data check
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id,
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
challenges = issue_certificate_service.get_certificate_challenges(issue_certificate_id)
# 添加txt记录
issue_certificate_service.add_dns_domain_record(
dns_id=dns_id,
challenges=challenges
)
# 更新验证信息
IssueCertificateModel.update(
challenge_deploy_type_id=ChallengeDeployTypeEnum.DNS,
challenge_deploy_id=dns_id,
challenge_deploy_status=DeployStatusEnum.SUCCESS
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def update_row_auto_renew():
"""
修改自动更新字段
:return:
"""
current_user_id = g.user_id
issue_certificate_id = request.json['issue_certificate_id']
is_auto_renew = request.json['is_auto_renew']
# check data
issue_certificate_row = IssueCertificateModel.select().where(
IssueCertificateModel.id == issue_certificate_id,
IssueCertificateModel.user_id == current_user_id
).first()
if not issue_certificate_row:
raise DataNotFoundAppException()
if issue_certificate_row and issue_certificate_row.can_auto_renew:
# 更新验证信息
IssueCertificateModel.update(
is_auto_renew=is_auto_renew
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
else:
raise AppException("不支持自动续期")
@auth_service.permission(role=RoleEnum.USER)
def get_issue_certificate_options():
"""
获取常量
:return:
"""
return {
'KEY_TYPE_OPTIONS': KEY_TYPE_OPTIONS,
'DIRECTORY_URL_OPTIONS': DIRECTORY_URL_OPTIONS
}
+58
View File
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""
log_async_task_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.log_async_task_model import AsyncTaskModel
from domain_admin.model.log_operation_model import LogOperationModel
from domain_admin.service import common_service, auth_service
@auth_service.permission(role=RoleEnum.ADMIN)
def get_async_task_log_list():
"""
获取操作日志列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
query = AsyncTaskModel.select()
total = query.count()
lst = []
if total > 0:
rows = query.order_by(
AsyncTaskModel.create_time.desc(),
AsyncTaskModel.id.desc(),
).paginate(page, size)
lst = [model_to_dict(model=row, extra_attrs=[
'total_time',
'total_time_label',
'create_time_label',
]) for row in rows]
common_service.load_user_name(lst)
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.ADMIN)
def clear_async_task_log_list():
"""
清空日志
:return:
"""
AsyncTaskModel.truncate_table()
+72
View File
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""
@File : log_monitor_api.py
@Date : 2024-01-28
@Author : Peng Shiyu
"""
from flask import g, request
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.log_monitor_model import LogMonitorModel
from domain_admin.service import auth_service
@auth_service.permission(role=RoleEnum.USER)
def get_log_monitor_list():
"""
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
monitor_id = request.json.get('monitor_id')
query = LogMonitorModel.select()
if monitor_id:
query = query.where(LogMonitorModel.monitor_id == monitor_id)
total = query.count()
lst = []
if total > 0:
rows = query.order_by(LogMonitorModel.id.desc()).paginate(page, size)
lst = [
model_to_dict(
model=row,
extra_attrs=[
'create_time_label',
'update_time_label',
'total_time_label',
'total_microsecond_time',
]
) for row in rows]
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.USER)
def clear_log_monitor():
"""
:return:
"""
monitor_id = request.json.get('monitor_id')
LogMonitorModel.delete().where(
LogMonitorModel.monitor_id == monitor_id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def clear_all_log_monitor():
"""
:return:
"""
LogMonitorModel.truncate_table()
+55
View File
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
"""
log_operation_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.log_operation_model import LogOperationModel
from domain_admin.service import common_service, auth_service
@auth_service.permission(role=RoleEnum.ADMIN)
def get_operation_log_list():
"""
获取操作日志列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
query = LogOperationModel.select()
total = query.count()
lst = []
if total > 0:
rows = query.order_by(
LogOperationModel.create_time.desc(),
LogOperationModel.id.desc(),
).paginate(page, size)
lst = [model_to_dict(model=row, extra_attrs=[
'type_label',
'create_time_label',
]) for row in rows]
common_service.load_user_name(lst)
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.ADMIN)
def clear_log_operation_list():
"""
清空日志
:return:
"""
LogOperationModel.truncate_table()
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""
log_scheduler_api.py
"""
from __future__ import print_function, unicode_literals, absolute_import, division
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.model.log_scheduler_model import LogSchedulerModel
from domain_admin.service import auth_service
@auth_service.permission(role=RoleEnum.ADMIN)
def get_log_scheduler_list():
"""
获取调度日志列表
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
query = LogSchedulerModel.select()
total = query.count()
lst = []
if total > 0:
rows = query.order_by(
LogSchedulerModel.create_time.desc(),
LogSchedulerModel.id.desc(),
).paginate(page, size)
lst = list(map(lambda m: model_to_dict(
model=m,
extra_attrs=[
'total_time',
'total_time_label',
]
), rows))
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.ADMIN)
def clear_log_scheduler_list():
"""
清空日志
:return:
"""
LogSchedulerModel.truncate_table()
+305
View File
@@ -0,0 +1,305 @@
# -*- coding: utf-8 -*-
"""
@File : monitor_api.py
@Date : 2024-01-28
@Author : Peng Shiyu
"""
import json
from datetime import datetime, timedelta
from flask import request, g
from peewee import SQL, fn
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.monitor_type_enum import MonitorTypeEnum
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.enums.time_unit_enum import TimeUnitEnum
from domain_admin.model.log_monitor_model import LogMonitorModel
from domain_admin.model.monitor_model import MonitorModel
from domain_admin.service import monitor_service, file_service, async_task_service, operation_service, auth_service
from domain_admin.service.scheduler_service import scheduler_main
from domain_admin.utils.flask_ext.app_exception import DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
def add_monitor():
"""
:return:
"""
current_user_id = g.user_id
title = request.json['title']
monitor_type = request.json['monitor_type']
allow_error_count = request.json.get('allow_error_count') or 0
content = request.json['content']
interval = request.json['interval']
interval_unit = request.json.get('interval_unit', TimeUnitEnum.Minute)
monitor_row = MonitorModel.create(
user_id=current_user_id,
title=title,
allow_error_count=allow_error_count,
monitor_type=monitor_type,
content=json.dumps(content),
interval=interval,
interval_unit=interval_unit
)
scheduler_main.run_one_monitor_task(MonitorModel.get_by_id(monitor_row.id))
@auth_service.permission(role=RoleEnum.USER)
def update_monitor_by_id():
"""
:return:
"""
current_user_id = g.user_id
monitor_id = request.json['monitor_id']
title = request.json['title']
content = request.json['content']
interval = request.json['interval']
allow_error_count = request.json.get('allow_error_count') or 0
interval_unit = request.json.get('interval_unit', TimeUnitEnum.Minute)
monitor_row = MonitorModel.select().where(
MonitorModel.id == monitor_id,
MonitorModel.user_id == current_user_id
).first()
if not monitor_row:
raise DataNotFoundAppException()
MonitorModel.update(
title=title,
content=json.dumps(content),
interval=interval,
interval_unit=interval_unit,
allow_error_count=allow_error_count,
).where(
MonitorModel.id == monitor_id
).execute()
scheduler_main.run_one_monitor_task(MonitorModel.get_by_id(monitor_id))
@auth_service.permission(role=RoleEnum.USER)
def update_monitor_active():
"""
:return:
"""
current_user_id = g.user_id
monitor_id = request.json['monitor_id']
is_active = request.json['is_active']
if is_active:
next_run_time = datetime.now()
else:
next_run_time = None
# data check
monitor_row = MonitorModel.select().where(
MonitorModel.id == monitor_id,
MonitorModel.user_id == current_user_id
).first()
if not monitor_row:
raise DataNotFoundAppException()
MonitorModel.update(
is_active=is_active,
next_run_time=next_run_time
).where(
MonitorModel.id == monitor_row.id
).execute()
if is_active:
scheduler_main.run_one_monitor_task(MonitorModel.get_by_id(monitor_id))
@auth_service.permission(role=RoleEnum.USER)
def remove_monitor_by_id():
"""
:return:
"""
current_user_id = g.user_id
monitor_id = request.json['monitor_id']
# data check
monitor_row = MonitorModel.select().where(
MonitorModel.id == monitor_id,
MonitorModel.user_id == current_user_id
).first()
if not monitor_row:
raise DataNotFoundAppException()
MonitorModel.delete_by_id(monitor_row.id)
# remote log
LogMonitorModel.delete().where(
LogMonitorModel.monitor_id == monitor_row.id,
LogMonitorModel.monitor_type == MonitorTypeEnum.HTTP
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=MonitorModel,
operation_type_id=OperationEnum.BATCH_DELETE,
primary_key='monitor_ids'
)
def delete_monitor_by_ids():
"""
批量删除
@since v1.6.12
:return:
"""
current_user_id = g.user_id
monitor_ids = request.json['monitor_ids']
MonitorModel.delete().where(
MonitorModel.id.in_(monitor_ids),
MonitorModel.user_id == current_user_id
).execute()
# remote log
LogMonitorModel.delete().where(
LogMonitorModel.monitor_id.in_(monitor_ids),
LogMonitorModel.monitor_type == MonitorTypeEnum.HTTP
).execute()
@auth_service.permission(role=RoleEnum.USER)
def get_monitor_by_id():
"""
:return:
"""
current_user_id = g.user_id
monitor_id = request.json['monitor_id']
# data check
monitor_row = MonitorModel.select().where(
MonitorModel.id == monitor_id,
MonitorModel.user_id == current_user_id
).first()
if not monitor_row:
raise DataNotFoundAppException()
return monitor_row.to_dict()
@auth_service.permission(role=RoleEnum.USER)
def get_monitor_list():
"""
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
order_prop = request.json.get('order_prop') or 'create_time'
order_type = request.json.get('order_type') or 'desc'
keyword = request.json.get('keyword')
status = request.json.get('status')
query = MonitorModel.select().where(
MonitorModel.user_id == current_user_id
)
if keyword:
query = query.where(MonitorModel.title.contains(keyword))
if isinstance(status, int):
query = query.where(MonitorModel.status == status)
total = query.count()
lst = []
if total > 0:
ordering = [
SQL(f"`{order_prop}` {order_type}"),
MonitorModel.id.desc()
]
rows = query.order_by(*ordering).paginate(page, size)
lst = [row.to_dict() for row in rows]
monitor_service.load_monitor_log_count(lst)
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.USER)
def export_monitor_file():
"""
导出监控文件
csv格式
:return:
"""
current_user_id = g.user_id
keyword = request.json.get('keyword')
status = request.json.get('status')
ext = request.json.get('ext', 'csv')
order_prop = request.json.get('order_prop') or 'create_time'
order_type = request.json.get('order_type') or 'desc'
params = {
'keyword': keyword,
'status': status,
'user_id': current_user_id,
}
query = monitor_service.get_monitor_list_query(**params)
ordering = [
SQL(f"`{order_prop}` {order_type}"),
MonitorModel.id.desc()
]
rows = query.order_by(*ordering)
lst = [row.to_dict() for row in rows]
filename = monitor_service.export_monitor_to_file(rows=lst, ext=ext)
return {
'name': filename,
'url': file_service.resolve_temp_url(filename)
}
@auth_service.permission(role=RoleEnum.USER)
def import_monitor_from_file():
"""
从文件导入域名
支持 xlsx 和 csv格式
:return:
"""
current_user_id = g.user_id
update_file = request.files.get('file')
filename = file_service.save_temp_file(update_file)
# 导入数据
monitor_service.import_monitor_from_file(filename, current_user_id)
# 异步查询
monitor_service.run_init_monitor_task_async(user_id=current_user_id)
+356
View File
@@ -0,0 +1,356 @@
# -*- coding: utf-8 -*-
"""
@File : notify_api.py
@Date : 2022-10-14
@Author : Peng Shiyu
"""
from __future__ import print_function, unicode_literals, absolute_import, division
import json
import random
import traceback
from datetime import datetime, timedelta
from flask import request, g
from playhouse.shortcuts import model_to_dict
from domain_admin.enums.operation_enum import OperationEnum
from domain_admin.enums.role_enum import RoleEnum
from domain_admin.enums.status_enum import StatusEnum
from domain_admin.log import logger
from domain_admin.model.group_model import GroupModel
from domain_admin.model.notify_model import NotifyModel
from domain_admin.service import notify_service, operation_service, auth_service
from domain_admin.utils import datetime_util
from domain_admin.utils.flask_ext.app_exception import DataNotFoundAppException
@auth_service.permission(role=RoleEnum.USER)
def get_notify_list_of_user():
"""
获取用户通知配置
:return:
"""
current_user_id = g.user_id
page = request.json.get('page', 1)
size = request.json.get('size', 10)
order_prop = request.json.get('order_prop') or 'create_time'
order_type = request.json.get('order_type') or 'descending'
query = NotifyModel.select().where(
NotifyModel.user_id == current_user_id
)
total = query.count()
lst = []
if total > 0:
ordering = []
# order by event_id
if order_prop == 'event_id':
if order_type == 'descending':
ordering.append(NotifyModel.event_id.desc())
else:
ordering.append(NotifyModel.event_id.asc())
# order by type_id
if order_prop == 'type_id':
if order_type == 'descending':
ordering.append(NotifyModel.type_id.desc())
else:
ordering.append(NotifyModel.type_id.asc())
# order by expire_days
if order_prop == 'expire_days':
if order_type == 'descending':
ordering.append(NotifyModel.expire_days.desc())
else:
ordering.append(NotifyModel.expire_days.asc())
# order by status
if order_prop == 'status':
if order_type == 'descending':
ordering.append(NotifyModel.status.desc())
else:
ordering.append(NotifyModel.status.asc())
ordering.append(NotifyModel.id.desc())
rows = query.order_by(*ordering).paginate(page, size)
lst = list(map(lambda m: model_to_dict(
model=m,
exclude=[NotifyModel.value_raw],
extra_attrs=[
'value',
'groups',
]
), rows))
group_ids = []
for row in lst:
group_ids.extend(row['groups'])
group_rows = GroupModel.select().where(
GroupModel.id.in_(list(set(group_ids)))
)
group_dict = {
row.id: row
for row in group_rows
}
for row in lst:
group_list = []
for group in row['groups']:
# bugfix: 如果用户删除分组后,分组数据不存在会出现null问题
if group in group_dict:
group_list.append(group_dict.get(group))
row['group_list'] = group_list
return {
'list': lst,
'total': total
}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=NotifyModel,
operation_type_id=OperationEnum.CREATE,
primary_key='id'
)
def add_notify():
"""
添加用户通知配置
:return:
"""
current_user_id = g.user_id
type_id = request.json['type_id']
event_id = request.json['event_id']
value = request.json['value']
expire_days = request.json['expire_days']
groups = request.json.get('groups') or []
comment = request.json.get('comment') or ''
value_raw = json.dumps(value, ensure_ascii=False)
groups_raw = json.dumps(groups, ensure_ascii=False)
row = NotifyModel.create(
user_id=current_user_id,
event_id=event_id,
type_id=type_id,
value_raw=value_raw,
groups_raw=groups_raw,
expire_days=expire_days,
comment=comment,
status=StatusEnum.Enabled
)
return {'id': row.id}
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=NotifyModel,
operation_type_id=OperationEnum.DELETE,
primary_key='notify_id'
)
def delete_notify_by_id():
"""
删除用户通知配置
:return:
"""
current_user_id = g.user_id
notify_id = request.json['notify_id']
# data check
notify_row = NotifyModel.select().where(
NotifyModel.id == notify_id,
NotifyModel.user_id == current_user_id,
).first()
if not notify_row:
raise DataNotFoundAppException()
NotifyModel.delete_by_id(notify_row.id)
@auth_service.permission(role=RoleEnum.USER)
def get_notify_by_id():
"""
获取用户通知配置
:return:
"""
current_user_id = g.user_id
notify_id = request.json['notify_id']
# data check
notify_row = NotifyModel.select().where(
NotifyModel.id == notify_id,
NotifyModel.user_id == current_user_id,
).first()
if not notify_row:
raise DataNotFoundAppException()
data = model_to_dict(
model=notify_row,
exclude=[NotifyModel.value_raw],
extra_attrs=[
'value',
'groups',
])
return data
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=NotifyModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='notify_id'
)
def update_notify_by_id():
"""
更新用户通知配置
:return:
"""
current_user_id = g.user_id
notify_id = request.json['notify_id']
event_id = request.json['event_id']
value = request.json['value']
expire_days = request.json['expire_days']
comment = request.json.get('comment') or ''
groups = request.json.get('groups') or []
value_raw = json.dumps(value, ensure_ascii=False)
groups_raw = json.dumps(groups, ensure_ascii=False)
# data check
notify_row = NotifyModel.select().where(
NotifyModel.id == notify_id,
NotifyModel.user_id == current_user_id,
).first()
if not notify_row:
raise DataNotFoundAppException()
NotifyModel.update(
event_id=event_id,
value_raw=value_raw,
groups_raw=groups_raw,
expire_days=expire_days,
comment=comment,
).where(
NotifyModel.id == notify_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
@operation_service.operation_log_decorator(
model=NotifyModel,
operation_type_id=OperationEnum.UPDATE,
primary_key='notify_id'
)
def update_notify_status_by_id():
"""
更新用户通知配置状态
:return:
"""
current_user_id = g.user_id
notify_id = request.json['notify_id']
status = request.json['status']
# data check
notify_row = NotifyModel.select().where(
NotifyModel.id == notify_id,
NotifyModel.user_id == current_user_id,
).first()
if not notify_row:
raise DataNotFoundAppException()
NotifyModel.update(
status=status,
).where(
NotifyModel.id == notify_row.id
).execute()
@auth_service.permission(role=RoleEnum.USER)
def handle_test_notify_by_id():
"""
测试通知配置
:return:
"""
current_user_id = g.user_id
notify_id = request.json['notify_id']
# data check
notify_row = NotifyModel.select().where(
NotifyModel.id == notify_id,
NotifyModel.user_id == current_user_id,
).first()
if not notify_row:
raise DataNotFoundAppException()
# days = random.randint(1, 365)
# start_date = datetime.now()
# expire_date = start_date + timedelta(days=days)
# lst = [
# {
# 'domain': 'www.demo.com',
# 'start_date': datetime_util.format_date(start_date),
# 'expire_date': datetime_util.format_date(expire_date),
# 'expire_days': days
# }
# ]
# return notify_service.notify_user(notify_row, lst)
return notify_service.notify_user_about_some_event(notify_row)
@auth_service.permission(role=RoleEnum.USER)
def handle_notify_by_event_id():
"""
触发用户的某一类通知操作
:return:
"""
current_user_id = g.user_id
event_id = request.json['event_id']
rows = NotifyModel.select().where(
NotifyModel.event_id == event_id,
NotifyModel.user_id == current_user_id
)
total = 0
success = 0
for row in rows:
try:
notify_service.notify_user_about_some_event(row)
success = success + 1
except:
logger.error(traceback.format_exc())
total = total + 1
return {
'total': total,
'success': success
}

Some files were not shown because too many files have changed in this diff Show More