Refactor the backend architecture (#299)

* define the basic architecture

* Update script and deployment file locations

* Update the route registration

* Fix CI download dependencies

* Updated ruff to 0.3.3

* Update app subdirectory naming

* Update the model import

* fix pre-commit pdm lock

* Update the service directory naming

* Add CRUD method documents

* Fix the issue of circular import

* Update the README document

* Update the SQL statement for create tables

* Update docker scripts and documentation

* Fix docker scripts

* Update the backend README.md

* Add the security folder and move the redis client

* Update the configuration item

* Fix environment configuration reads

* Update the default configuration

* Updated README description

* Updated the user registration API

* Fix test cases

* Update the celery configuration

* Update and fix celery configuration

* Updated the celery structure

* Update celery tasks and api

* Add celery flower

* Update the import style

* Update contributors
This commit is contained in:
Wu Clan
2024-03-22 18:16:15 +08:00
committed by GitHub
parent a98621b40d
commit 5e438c685d
190 changed files with 2408 additions and 1494 deletions

View File

@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r ./backend/requirements.txt
- name: pre-commit
uses: pre-commit/action@v3.0.0

12
.gitignore vendored
View File

@ -1,12 +1,2 @@
__pycache__/
.idea/
.env
venv/
.venv/
.mypy_cache/
backend/app/log/
backend/app/alembic/versions/
backend/app/static/media/
.ruff_cache/
.pytest_cache/
.pdm-python
.vscode/

View File

@ -13,18 +13,24 @@ repos:
- id: ruff
args:
- '--config'
- '.ruff.toml'
- 'backend/.ruff.toml'
- '--fix'
- '--unsafe-fixes'
- id: ruff-format
- repo: https://github.com/pdm-project/pdm
rev: 2.12.2
rev: 2.12.4
hooks:
- id: pdm-export
args:
- -o
- 'requirements.txt'
- '-p'
- 'backend'
- '-o'
- 'backend/requirements.txt'
- '--without-hashes'
files: ^pdm.lock$
files: backend/pdm\.lock$
- id: pdm-lock-check
args:
- '-p'
- 'backend'
files: backend/pdm\.lock$

130
README.md
View File

@ -3,12 +3,15 @@
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Static Badge](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev)
> [!Tip]
> **2024-3-22 (公告)**
> [!CAUTION]
> **For 2023-12-22 (announcement)**
>
> You're looking at the legacy-single-app-pydantic-v2 branch, which has been locked and no longer provides any updates
> and fixes
> The master branch has completed the app architecture refactoring, please pay extra attention to sync fork operations
> to avoid irreparable damage!
>
> We have kept and locked the original branch (legacy-single-app-pydantic-v2), which you can get in the branch selector
English | [简体中文](./README.zh-CN.md)
@ -83,117 +86,117 @@ Luckily, we now have a demo site: [FBA UI](https://fba.xwboy.top/)
* Redis: The latest stable version is recommended
* Nodejs: 14.0+
### BackEnd
### Backend
1. Install dependencies
1. Enter the `backend` directory
```shell
cd backend
```
2. Install the dependencies
```shell
pip install -r requirements.txt
```
2. Create a database `fba`, choose utf8mb4 encoding
3. Install and start Redis
4. Create a `.env` file in the `backend/app/` directory
3. Create a database `fba` with utf8mb4 encoding.
4. Install and start Redis
5. Create a `.env` file in the `backend` directory.
```shell
cd backend/app/
touch .env
```
5. Copy `.env.example` to `.env`
```shell
cp .env.example .env
```
6. Modify the configuration file as needed
7. Database migration [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
6. Modify the configuration files `core/conf.py` and `.env` as needed.
7. database migration [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
```shell
cd backend/app/
# Generate migration file
# Generate the migration file
alembic revision --autogenerate
# Execute the migration
alembic upgrade head
```
8. Start celery worker and beat
8. Start celery worker, beat and flower
```shell
celery -A tasks worker --loglevel=INFO
# Optional, if you don't need to use the scheduled task
celery -A tasks beat --loglevel=INFO
celery -A app.task.celery worker -l info
# Scheduled tasks (optional)
celery -A app.task.celery beat -l info
# Web monitor (optional)
celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456
```
9. Execute the `backend/app/main.py` file to start the service
10. Browser access: http://127.0.0.1:8000/api/v1/docs
9. [Initialize test data](#test-data) (Optional)
10. Execute the `main.py` file to start the service
11. Open a browser and visit: http://127.0.0.1:8000/api/v1/docs
### Front end
Jump to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) View details
---
### Front
Go to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) for details
### Docker deploy
### Docker Deployment
> [!WARNING]
> Default port conflict8000330663795672
>
> As a best practice, shut down on-premises services before deploymentmysqlredisrabbitmq...
> Default port conflicts: 8000, 3306, 6379, 5672.
>
> It is recommended to shut down local services: mysql, redis, rabbitmq... before deployment.
1. Go to the directory where the ``docker-compose.yml`` file is located and create the environment variable
file ``.env``
1. Go to the `deploy/backend/docker-compose` directory, and create the environment variable file `.env`.
```shell
cd deploy/docker-compose/
cd deploy/backend/docker-compose
cp .env.server ../../backend/app/.env
touch .env.server ../../../backend/.env
# This command is optional
cp .env.docker .env
cp .env.server ../../../backend/.env
```
2. Modify the configuration file as needed
3. Execute the one-click boot command
2. Modify the configuration files `backend/core/conf.py` and `.env` as needed.
3. Execute the one-click startup command
```shell
docker-compose up -d --build
```
4. Wait for the command to complete automatically
5. Visit the browser: http://127.0.0.1:8000/api/v1/docs
4. Wait for the command to complete.
5. Open a browser and visit: http://127.0.0.1:8000/api/v1/docs
## Test data
Initialize the test data using the `backend/sql/init_test_data.sql` file
Initialize the test data using the `backend/sql/init_test_data.sql` file.
## Development process
## Development Process
(For reference only)
1. Define the database model (model) and remember to perform database migration for each change
2. Define the data validation model (schema)
3. Define routes (router) and views (api)
4. Define the business logic (service)
5. Write database operations (crud)
1. define the database model (model)
2. define the data validation model (schema)
3. define the view (api) and routing (router)
4. write business (service)
5. write database operations (crud)
## Test
## Testing
Execute unittests via pytest
Execute unit tests through `pytest`.
1. Create the test database `fba_test`, select utf8mb4 encoding
2. Using `backend/sql/create_tables.sql` file to create database tables
3. Initialize the test data using the `backend/sql/init_pytest_data.sql` file
4. Enter the app directory
1. create a test database `fba_test` with utf8mb4 encoding
2. create database tables using the `backend/sql/create_tables.sql` file
3. initialize the test data using the `backend/sql/init_pytest_data.sql` file
4. Go to the `backend` directory and execute the test commands.
```shell
cd backend/app/
```
cd backend/
5. Execute the test command
```shell
pytest -vs --disable-warnings
```
@ -203,8 +206,9 @@ Execute unittests via pytest
## Contributors
<span style="margin: 0 5px;" ><a href="https://github.com/wu-clan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52145145?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/downdawn" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/41266749?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<a href="https://github.com/fastapi-practices/fastapi_best_architecture/graphs/contributors">
<img src="https://contrib.rocks/image?repo=fastapi-practices/fastapi_best_architecture"/>
</a>
## Special thanks
@ -226,7 +230,7 @@ beans: [:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/)
## License
This project is licensed under the terms of
This project is licensed by the terms of
the [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) license
[![Stargazers over time](https://starchart.cc/fastapi-practices/fastapi_best_architecture.svg?variant=adaptive)](https://starchart.cc/fastapi-practices/fastapi_best_architecture)

View File

@ -3,11 +3,14 @@
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Static Badge](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev)
> [!Tip]
> [!CAUTION]
> **2024-3-22 (公告)**
>
> 你正在查看 legacy-single-app-pydantic-v2 分支,此分支已被锁定,不再提供任何更新和修复
> 主分支已完成 app 架构重构,请格外注意 sync fork 操作,以免造成不可挽回的损失!
>
> 我们保留并锁定了原始分支legacy-single-app-pydantic-v2您可以在分支选择器中找到它
简体中文 | [English](./README.md)
@ -78,33 +81,32 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
### 后端
1. 安装依赖项
1. 进入 `backend` 目录
```shell
cd backend
```
2. 安装依赖包
```shell
pip install -r requirements.txt
```
2. 创建一个数据库 `fba`,选择 utf8mb4 编码
3. 安装并启动 Redis
4. 在 `backend/app/` 目录下创建一个 `.env` 文件
3. 创建一个数据库 `fba`,选择 utf8mb4 编码
4. 安装并启动 Redis
5. 在 `backend` 目录下创建 `.env` 文件
```shell
cd backend/app/
touch .env
```
5. 复制 `.env.example` 到 `.env`
```shell
cp .env.example .env
```
6. 按需修改配置文件
6. 按需修改配置文件 `core/conf.py` 和 `.env`
7. 数据库迁移 [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
```shell
cd backend/app/
# 生成迁移文件
alembic revision --autogenerate
@ -112,50 +114,55 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
alembic upgrade head
```
8. 启动 celery worker beat
8. 启动 celery worker, beat 和 flower
```shell
celery -A tasks worker --loglevel=INFO
# 可选,如果您不需要使用计划任务
celery -A tasks beat --loglevel=INFO
celery -A app.task.celery worker -l info
# 定时任务(可选)
celery -A app.task.celery beat -l info
# web 监控(可选)
celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456
```
9. 执行 `backend/app/main.py` 文件启动服务
10. 浏览器访问http://127.0.0.1:8000/api/v1/docs
---
9. [初始化测试数据](#测试数据)(可选)
10. 执行 `main.py` 文件启动服务
11. 打开浏览器访问http://127.0.0.1:8000/api/v1/docs
### 前端
跳转 [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) 查看详情
---
### Docker 部署
> [!WARNING]
>
> 默认端口冲突8000330663795672
>
> 最佳做法是在部署前关闭本地服务mysqlredisrabbitmq...
> 建议在部署前关闭本地服务mysqlredisrabbitmq...
1. 进入 `docker-compose.yml` 文件所在目录,创建环境变量文件`.env`
1. 进入 `deploy/backend/docker-compose` 目录,创建环境变量文件`.env`
```shell
cd deploy/docker-compose/
cd deploy/backend/docker-compose
cp .env.server ../../backend/app/.env
touch .env.server ../../../backend/.env
# 此命令为可选
cp .env.docker .env
cp .env.server ../../../backend/.env
```
2. 按需修改配置文件
2. 按需修改配置文件 `backend/core/conf.py` 和 `.env`
3. 执行一键启动命令
```shell
docker-compose up -d --build
```
4. 等待命令自动完成
5. 浏览器访问http://127.0.0.1:8000/api/v1/docs
4. 等待命令执行完成
5. 打开浏览器访问http://127.0.0.1:8000/api/v1/docs
## 测试数据
@ -165,28 +172,24 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
(仅供参考)
1. 定义数据库模型model,每次变化记得执行数据库迁移
1. 定义数据库模型model
2. 定义数据验证模型schema
3. 定义路由router和视图api
4. 定义业务逻辑service
3. 定义视图api路由router
4. 编写业务service
5. 编写数据库操作crud
## 测试
通过 pytest 执行单元测试
通过 `pytest` 执行单元测试
1. 创建测试数据库 `fba_test`,选择 utf8mb4 编码
2. 使用 `backend/sql/create_tables.sql` 文件创建数据库表
3. 使用 `backend/sql/init_pytest_data.sql` 文件初始化测试数据
4. 进入app目录
4. 进入 `backend` 目录,执行测试命令
```shell
cd backend/app/
```
cd backend/
5. 执行测试命令
```shell
pytest -vs --disable-warnings
```
@ -196,8 +199,9 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
## 贡献者
<span style="margin: 0 5px;" ><a href="https://github.com/wu-clan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52145145?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<span style="margin: 0 5px;" ><a href="https://github.com/downdawn" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/41266749?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
<a href="https://github.com/fastapi-practices/fastapi_best_architecture/graphs/contributors">
<img src="https://contrib.rocks/image?repo=fastapi-practices/fastapi_best_architecture"/>
</a>
## 特别鸣谢
@ -218,6 +222,6 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
## 许可证
本项目根据 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可
本项目 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可
[![Stargazers over time](https://starchart.cc/fastapi-practices/fastapi_best_architecture.svg?variant=adaptive)](https://starchart.cc/fastapi-practices/fastapi_best_architecture)

4
backend/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
venv/
.venv/
.pdm-python

View File

@ -1,19 +1,25 @@
# Env: dev、pro
ENVIRONMENT='dev'
# MySQL
DB_HOST='127.0.0.1'
DB_PORT=3306
DB_USER='root'
DB_PASSWORD='123456'
MYSQL_HOST='127.0.0.1'
MYSQL_PORT=3306
MYSQL_USER='root'
MYSQL_PASSWORD='123456'
# Redis
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=''
REDIS_DATABASE=0
# Token
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
# Opera Log
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
# Admin
# OAuth2
OAUTH2_GITHUB_CLIENT_ID='test'
OAUTH2_GITHUB_CLIENT_SECRET='test'
# Task
# Celery
CELERY_REDIS_HOST='127.0.0.1'
CELERY_REDIS_PORT=6379
CELERY_REDIS_PASSWORD=''
CELERY_BROKER_REDIS_DATABASE=1
CELERY_BACKEND_REDIS_DATABASE=2
# Rabbitmq
@ -21,10 +27,3 @@ RABBITMQ_HOST='127.0.0.1'
RABBITMQ_PORT=5672
RABBITMQ_USERNAME='guest'
RABBITMQ_PASSWORD='guest'
# Token
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
# Opera Log
OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b'
# OAuth2
OAUTH2_GITHUB_CLIENT_ID='test'
OAUTH2_GITHUB_CLIENT_SECRET='test'

12
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
.env
venv/
.venv/
.mypy_cache/
log/
alembic/versions/
static/media/
.ruff_cache/
.pytest_cache/
.pdm-python
celerybeat-schedule.*

View File

@ -1,29 +1,33 @@
line-length = 120
unsafe-fixes = true
cache-dir = ".ruff_cache"
target-version = "py310"
cache-dir = "./.ruff_cache"
[per-file-ignores]
"backend/app/api/v1/*.py" = ["TCH"]
"backend/app/models/*.py" = ["TCH003"]
"backend/app/**/__init__.py" = ["F401"]
"backend/app/tests/*.py" = ["E402"]
[lint]
select = [
"E",
"F",
"I",
"TCH",
# W
"W505",
# PT
"PT018",
# SIM
"SIM101",
"SIM114",
# PGH
"PGH004",
# PL
"PLE1142",
# RUF
"RUF100",
"F404",
"TCH",
# UP
"UP007"
]
preview = true
ignore-init-module-imports = true
ignore = ["FURB101"]
[lint.flake8-pytest-style]
mark-parentheses = false
@ -38,5 +42,13 @@ ignore-variadic-names = true
lines-between-types = 1
order-by-type = true
[lint.per-file-ignores]
"**/api/v1/*.py" = ["TCH"]
"**/model/*.py" = ["TCH003"]
"**/model/__init__.py" = ["F401"]
"**/tests/*.py" = ["E402"]
[format]
preview = true
quote-style = "single"
docstring-code-format = true

60
backend/README.md Normal file
View File

@ -0,0 +1,60 @@
# FBA Project - Backend
## Docker
> [!IMPORTANT]
> Due to Docker context limitations, you cannot successfully build a image using a Dockerfile in the current directory
1. Make sure you're at the root of the project
2. Run the following Docker command to build a image:
```shell
docker build -f backend/backend.dockerfile -t fba_backend_independent .
```
3. Start decker image
```shell
docker run -d fba_backend_independent -p 8000:8000 --name fba_app
```
## Contributing
1. Prerequisites
You'll need the following prerequisites:
- Any Python version between Python >= 3.10
- virtualenv or other virtual environment tool
- git
- [PDM](https://pdm-project.org/latest/)
2. Installation and setup
```shell
# Clone your fork and cd into the repo directory
git clone https://github.com/<your username>/fastapi_best_architecture.git
cd fastapi_best_architecture/backend
# Install requirements.txt
pdm install
```
3. Check out a new branch and make your changes
```shell
# Checkout a new branch and make your changes
git checkout -b your-new-feature-branch
# Make your changes...
```
4. Run linting
```shell
# Run automated code formatting and linting
pdm lint
```
5. Commit and push your changes
Commit your changes, push your branch to GitHub, and create a pull request.

View File

@ -9,12 +9,12 @@ from alembic import context
from sqlalchemy import engine_from_config, pool
from sqlalchemy.ext.asyncio import AsyncEngine
sys.path.append('../../')
sys.path.append('../')
from backend.app.core import path_conf # noqa: E402
from backend.core import path_conf
if not os.path.exists(path_conf.Versions):
os.makedirs(path_conf.Versions)
if not os.path.exists(path_conf.ALEMBIC_Versions_DIR):
os.makedirs(path_conf.ALEMBIC_Versions_DIR)
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@ -26,12 +26,12 @@ fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from backend.app.models import MappedBase # noqa: E402
from backend.common.msd.model import MappedBase # noqa: E402
target_metadata = MappedBase.metadata
# other values from the config, defined by the needs of env.py,
from backend.app.database.db_mysql import SQLALCHEMY_DATABASE_URL # noqa: E402
from backend.database.db_mysql import SQLALCHEMY_DATABASE_URL # noqa: E402
config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL)

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.auth import router as auth_router
from backend.app.admin.api.v1.log import router as log_router
from backend.app.admin.api.v1.monitor import router as monitor_router
from backend.app.admin.api.v1.sys import router as sys_router
v1 = APIRouter()
v1.include_router(auth_router)
v1.include_router(sys_router)
v1.include_router(log_router)
v1.include_router(monitor_router)

View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.auth.auth import router as auth_router
from backend.app.admin.api.v1.auth.captcha import router as captcha_router
from backend.app.admin.api.v1.auth.github import router as github_router
router = APIRouter(prefix='/auth', tags=['授权管理'])
router.include_router(auth_router)
router.include_router(captcha_router, prefix='/captcha')
router.include_router(github_router, prefix='/github')

View File

@ -7,17 +7,17 @@ from fastapi.security import HTTPBasicCredentials
from fastapi_limiter.depends import RateLimiter
from starlette.background import BackgroundTasks
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.schemas.token import GetSwaggerToken
from backend.app.schemas.user import AuthLoginParam
from backend.app.services.auth_service import auth_service
from backend.app.admin.schema.token import GetSwaggerToken
from backend.app.admin.schema.user import AuthLoginParam
from backend.app.admin.service.auth_service import auth_service
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
router = APIRouter()
@router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证')
async def swagger_user_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken:
token, user = await auth_service.swagger_login(obj=obj)
return GetSwaggerToken(access_token=token, user=user) # type: ignore
@ -33,7 +33,7 @@ async def user_login(request: Request, obj: AuthLoginParam, background_tasks: Ba
return await response_base.success(data=data)
@router.post('/new_token', summary='创建新 token', dependencies=[DependsJwtAuth])
@router.post('/token/new', summary='创建新 token', dependencies=[DependsJwtAuth])
async def create_new_token(request: Request, refresh_token: Annotated[str, Query(...)]) -> ResponseModel:
data = await auth_service.new_token(request=request, refresh_token=refresh_token)
return await response_base.success(data=data)

View File

@ -5,15 +5,15 @@ from fastapi import APIRouter, Depends, Request
from fastapi_limiter.depends import RateLimiter
from starlette.concurrency import run_in_threadpool
from backend.app.common.redis import redis_client
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.core.conf import settings
from backend.app.admin.conf import admin_settings
from backend.common.response.response_schema import ResponseModel, response_base
from backend.database.db_redis import redis_client
router = APIRouter()
@router.get(
'/captcha',
'',
summary='获取登录验证码',
dependencies=[Depends(RateLimiter(times=5, seconds=10))],
)
@ -25,6 +25,6 @@ async def get_captcha(request: Request) -> ResponseModel:
img, code = await run_in_threadpool(img_captcha, img_byte=img_type)
ip = request.state.ip
await redis_client.set(
f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS
f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=admin_settings.CAPTCHA_LOGIN_EXPIRE_SECONDS
)
return await response_base.success(data={'image_type': img_type, 'image': img})

View File

@ -3,33 +3,33 @@
from fastapi import APIRouter, BackgroundTasks, Depends, Request
from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.core.conf import settings
from backend.app.services.github_service import github_service
from backend.app.admin.conf import admin_settings
from backend.app.admin.service.github_service import github_service
from backend.common.response.response_schema import ResponseModel, response_base
router = APIRouter()
github_client = GitHubOAuth20(settings.OAUTH2_GITHUB_CLIENT_ID, settings.OAUTH2_GITHUB_CLIENT_SECRET)
github_oauth2 = FastAPIOAuth20(github_client, settings.OAUTH2_GITHUB_REDIRECT_URI)
github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET)
github_oauth2 = FastAPIOAuth20(github_client, admin_settings.OAUTH2_GITHUB_REDIRECT_URI)
@router.get('/github', summary='获取 Github 授权链接')
async def auth_github() -> ResponseModel:
auth_url = await github_client.get_authorization_url(redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI)
@router.get('', summary='获取 Github 授权链接')
async def github_auth2() -> ResponseModel:
auth_url = await github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI)
return await response_base.success(data=auth_url)
@router.get(
'/github/callback',
'/callback',
summary='Github 授权重定向',
description='Github 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
response_model=None,
)
async def login_github(
async def github_login(
request: Request, background_tasks: BackgroundTasks, oauth: FastAPIOAuth20 = Depends(github_oauth2)
) -> ResponseModel:
token, state = oauth
token, _state = oauth
access_token = token['access_token']
user = await github_client.get_userinfo(access_token)
data = await github_service.add_with_login(request, background_tasks, user)
data = await github_service.create_with_login(request, background_tasks, user)
return await response_base.success(data=data)

View File

@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.api.v1.log.login_log import router as login_log
from backend.app.api.v1.log.opera_log import router as opera_log
from backend.app.admin.api.v1.log.login_log import router as login_log
from backend.app.admin.api.v1.log.opera_log import router as opera_log
router = APIRouter(prefix='/logs')

View File

@ -4,14 +4,14 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.login_log import GetLoginLogListDetails
from backend.app.services.login_log_service import login_log_service
from backend.app.admin.schema.login_log import GetLoginLogListDetails
from backend.app.admin.service.login_log_service import login_log_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
router = APIRouter()

View File

@ -4,14 +4,14 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.opera_log import GetOperaLogListDetails
from backend.app.services.opera_log_service import opera_log_service
from backend.app.admin.schema.opera_log import GetOperaLogListDetails
from backend.app.admin.service.opera_log_service import opera_log_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
router = APIRouter()

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.admin.api.v1.monitor.redis import router as redis_router
from backend.app.admin.api.v1.monitor.server import router as server_router
router = APIRouter(prefix='/monitors', tags=['监控管理'])
router.include_router(redis_router, prefix='/redis')
router.include_router(server_router, prefix='/server')

View File

@ -2,16 +2,16 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.utils.redis_info import redis_info
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.utils.redis_info import redis_info
router = APIRouter()
@router.get(
'/redis',
'',
summary='redis 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:redis')),

View File

@ -3,16 +3,16 @@
from fastapi import APIRouter, Depends
from starlette.concurrency import run_in_threadpool
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.utils.server_info import server_info
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.utils.server_info import server_info
router = APIRouter()
@router.get(
'/server',
'',
summary='server 监控',
dependencies=[
Depends(RequestPermission('sys:monitor:server')),

View File

@ -2,14 +2,14 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.api.v1.sys.api import router as api_router
from backend.app.api.v1.sys.casbin import router as casbin_router
from backend.app.api.v1.sys.dept import router as dept_router
from backend.app.api.v1.sys.dict_data import router as dict_data_router
from backend.app.api.v1.sys.dict_type import router as dict_type_router
from backend.app.api.v1.sys.menu import router as menu_router
from backend.app.api.v1.sys.role import router as role_router
from backend.app.api.v1.sys.user import router as user_router
from backend.app.admin.api.v1.sys.api import router as api_router
from backend.app.admin.api.v1.sys.casbin import router as casbin_router
from backend.app.admin.api.v1.sys.dept import router as dept_router
from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router
from backend.app.admin.api.v1.sys.menu import router as menu_router
from backend.app.admin.api.v1.sys.role import router as role_router
from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys')

View File

@ -4,21 +4,21 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.api import CreateApiParam, GetApiListDetails, UpdateApiParam
from backend.app.services.api_service import api_service
from backend.app.admin.schema.api import CreateApiParam, GetApiListDetails, UpdateApiParam
from backend.app.admin.service.api_service import api_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
router = APIRouter()
@router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth])
async def get_all_apis() -> ResponseModel:
data = await api_service.get_api_list()
data = await api_service.get_all()
return await response_base.success(data=data)

View File

@ -5,13 +5,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.casbin_rule import (
from backend.app.admin.schema.casbin_rule import (
CreatePolicyParam,
CreateUserRoleParam,
DeleteAllPoliciesParam,
@ -20,14 +14,20 @@ from backend.app.schemas.casbin_rule import (
GetPolicyListDetails,
UpdatePolicyParam,
)
from backend.app.services.casbin_service import casbin_service
from backend.app.admin.service.casbin_service import casbin_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取所有权限规则',
summary='(模糊条件)分页获取所有权限策略',
dependencies=[
DependsJwtAuth,
DependsPagination,
@ -35,7 +35,7 @@ router = APIRouter()
)
async def get_pagination_casbin(
db: CurrentSession,
ptype: Annotated[str | None, Query(description='规则类型, p / g')] = None,
ptype: Annotated[str | None, Query(description='策略类型, p / g')] = None,
sub: Annotated[str | None, Query(description='用户 uuid / 角色')] = None,
) -> ResponseModel:
casbin_select = await casbin_service.get_casbin_list(ptype=ptype, sub=sub)
@ -43,7 +43,7 @@ async def get_pagination_casbin(
return await response_base.success(data=page_data)
@router.get('/policies', summary='获取所有P权限规则', dependencies=[DependsJwtAuth])
@router.get('/policies', summary='获取所有P权限策略', dependencies=[DependsJwtAuth])
async def get_all_policies(role: Annotated[int | None, Query(description='角色ID')] = None) -> ResponseModel:
policies = await casbin_service.get_policy_list(role=role)
return await response_base.success(data=policies)
@ -51,7 +51,7 @@ async def get_all_policies(role: Annotated[int | None, Query(description='角色
@router.post(
'/policy',
summary='添加P权限规则',
summary='添加P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:add')),
DependsRBAC,
@ -59,12 +59,12 @@ async def get_all_policies(role: Annotated[int | None, Query(description='角色
)
async def create_policy(p: CreatePolicyParam) -> ResponseModel:
"""
p 规则:
p 策略:
- 推荐添加基于角色的访问权限, 需配合添加 g 规则才能真正拥有访问权限适合配置全局接口访问策略<br>
- 推荐添加基于角色的访问权限, 需配合添加 g 策略才能真正拥有访问权限适合配置全局接口访问策略<br>
**格式**: 角色 role + 访问路径 path + 访问方法 method
- 如果添加基于用户的访问权限, 不需配合添加 g 规则就能真正拥有权限适合配置指定用户接口访问策略<br>
- 如果添加基于用户的访问权限, 不需配合添加 g 策略就能真正拥有权限适合配置指定用户接口访问策略<br>
**格式**: 用户 uuid + 访问路径 path + 访问方法 method
"""
data = await casbin_service.create_policy(p=p)
@ -73,7 +73,7 @@ async def create_policy(p: CreatePolicyParam) -> ResponseModel:
@router.post(
'/policies',
summary='添加多组P权限规则',
summary='添加多组P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:add')),
DependsRBAC,
@ -86,7 +86,7 @@ async def create_policies(ps: list[CreatePolicyParam]) -> ResponseModel:
@router.put(
'/policy',
summary='更新P权限规则',
summary='更新P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:edit')),
DependsRBAC,
@ -99,7 +99,7 @@ async def update_policy(old: UpdatePolicyParam, new: UpdatePolicyParam) -> Respo
@router.put(
'/policies',
summary='更新多组P权限规则',
summary='更新多组P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:edit')),
DependsRBAC,
@ -112,7 +112,7 @@ async def update_policies(old: list[UpdatePolicyParam], new: list[UpdatePolicyPa
@router.delete(
'/policy',
summary='删除P权限规则',
summary='删除P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:del')),
DependsRBAC,
@ -125,7 +125,7 @@ async def delete_policy(p: DeletePolicyParam) -> ResponseModel:
@router.delete(
'/policies',
summary='删除多组P权限规则',
summary='删除多组P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:del')),
DependsRBAC,
@ -138,7 +138,7 @@ async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseModel:
@router.delete(
'/policies/all',
summary='删除所有P权限规则',
summary='删除所有P权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:empty')),
DependsRBAC,
@ -151,7 +151,7 @@ async def delete_all_policies(sub: DeleteAllPoliciesParam) -> ResponseModel:
return await response_base.fail()
@router.get('/groups', summary='获取所有G权限规则', dependencies=[DependsJwtAuth])
@router.get('/groups', summary='获取所有G权限策略', dependencies=[DependsJwtAuth])
async def get_all_groups() -> ResponseModel:
data = await casbin_service.get_group_list()
return await response_base.success(data=data)
@ -159,7 +159,7 @@ async def get_all_groups() -> ResponseModel:
@router.post(
'/group',
summary='添加G权限规则',
summary='添加G权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:add')),
DependsRBAC,
@ -167,13 +167,13 @@ async def get_all_groups() -> ResponseModel:
)
async def create_group(g: CreateUserRoleParam) -> ResponseModel:
"""
g 规则 (**依赖 p 规则**):
g 策略 (**依赖 p 策略**):
- 如果在 p 规则中添加了基于角色的访问权限, 则还需要在 g 规则中添加基于用户组的访问权限, 才能真正拥有访问权限<br>
- 如果在 p 策略中添加了基于角色的访问权限, 则还需要在 g 策略中添加基于用户组的访问权限, 才能真正拥有访问权限<br>
**格式**: 用户 uuid + 角色 role
- 如果在 p 策略中添加了基于用户的访问权限, 则不添加相应的 g 规则能直接拥有访问权限<br>
但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 规则所添加的访问权限
- 如果在 p 策略中添加了基于用户的访问权限, 则不添加相应的 g 策略能直接拥有访问权限<br>
但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 策略所添加的访问权限
"""
data = await casbin_service.create_group(g=g)
return await response_base.success(data=data)
@ -181,7 +181,7 @@ async def create_group(g: CreateUserRoleParam) -> ResponseModel:
@router.post(
'/groups',
summary='添加多组G权限规则',
summary='添加多组G权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:group:add')),
DependsRBAC,
@ -194,7 +194,7 @@ async def create_groups(gs: list[CreateUserRoleParam]) -> ResponseModel:
@router.delete(
'/group',
summary='删除G权限规则',
summary='删除G权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:del')),
DependsRBAC,
@ -207,7 +207,7 @@ async def delete_group(g: DeleteUserRoleParam) -> ResponseModel:
@router.delete(
'/groups',
summary='删除多组G权限规则',
summary='删除多组G权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:group:del')),
DependsRBAC,
@ -220,7 +220,7 @@ async def delete_groups(gs: list[DeleteUserRoleParam]) -> ResponseModel:
@router.delete(
'/groups/all',
summary='删除所有G权限规则',
summary='删除所有G权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:empty')),
DependsRBAC,

View File

@ -4,13 +4,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.schemas.dept import CreateDeptParam, GetDeptListDetails, UpdateDeptParam
from backend.app.services.dept_service import dept_service
from backend.app.utils.serializers import select_as_dict
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptListDetails, UpdateDeptParam
from backend.app.admin.service.dept_service import dept_service
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()

View File

@ -4,15 +4,15 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.dict_data import CreateDictDataParam, GetDictDataListDetails, UpdateDictDataParam
from backend.app.services.dict_data_service import dict_data_service
from backend.app.utils.serializers import select_as_dict
from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataListDetails, UpdateDictDataParam
from backend.app.admin.service.dict_data_service import dict_data_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()

View File

@ -4,14 +4,14 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.dict_type import CreateDictTypeParam, GetDictTypeListDetails, UpdateDictTypeParam
from backend.app.services.dict_type_service import dict_type_service
from backend.app.admin.schema.dict_type import CreateDictTypeParam, GetDictTypeListDetails, UpdateDictTypeParam
from backend.app.admin.service.dict_type_service import dict_type_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
router = APIRouter()

View File

@ -4,19 +4,19 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.schemas.menu import CreateMenuParam, GetMenuListDetails, UpdateMenuParam
from backend.app.services.menu_service import menu_service
from backend.app.utils.serializers import select_as_dict
from backend.app.admin.schema.menu import CreateMenuParam, GetMenuListDetails, UpdateMenuParam
from backend.app.admin.service.menu_service import menu_service
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth])
async def get_user_menus(request: Request) -> ResponseModel:
async def get_user_sidebar_tree(request: Request) -> ResponseModel:
menu = await menu_service.get_user_menu_tree(request=request)
return await response_base.success(data=menu)

View File

@ -4,16 +4,16 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam
from backend.app.services.menu_service import menu_service
from backend.app.services.role_service import role_service
from backend.app.utils.serializers import select_as_dict, select_list_serialize
from backend.app.admin.schema.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam
from backend.app.admin.service.menu_service import menu_service
from backend.app.admin.service.role_service import role_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()

View File

@ -4,13 +4,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.common.jwt import DependsJwtAuth
from backend.app.common.pagination import DependsPagination, paging_data
from backend.app.common.permission import RequestPermission
from backend.app.common.rbac import DependsRBAC
from backend.app.common.response.response_schema import ResponseModel, response_base
from backend.app.database.db_mysql import CurrentSession
from backend.app.schemas.user import (
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
GetCurrentUserInfoDetail,
@ -20,14 +14,20 @@ from backend.app.schemas.user import (
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.app.services.user_service import user_service
from backend.app.utils.serializers import select_as_dict
from backend.app.admin.service.user_service import user_service
from backend.common.pagination import DependsPagination, paging_data
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db_mysql import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.post('/register', summary='用户注册')
async def user_register(obj: RegisterUserParam) -> ResponseModel:
@router.post('/register', summary='注册用户')
async def register_user(obj: RegisterUserParam) -> ResponseModel:
await user_service.register(obj=obj)
return await response_base.success()
@ -49,7 +49,7 @@ async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseM
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'})
async def get_current_userinfo(request: Request) -> ResponseModel:
async def get_current_user(request: Request) -> ResponseModel:
data = GetCurrentUserInfoDetail(**await select_as_dict(request.user))
return await response_base.success(data=data)
@ -62,7 +62,7 @@ async def get_user(username: Annotated[str, Path(...)]) -> ResponseModel:
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
async def update_userinfo(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
count = await user_service.update(request=request, username=username, obj=obj)
if count > 0:
return await response_base.success()

33
backend/app/admin/conf.py Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
from backend.core.path_conf import BasePath
class AdminSettings(BaseSettings):
"""Admin Settings"""
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
# OAuth2https://github.com/fastapi-practices/fastapi_oauth20
OAUTH2_GITHUB_CLIENT_ID: str
OAUTH2_GITHUB_CLIENT_SECRET: str
# OAuth2
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth/github/callback'
# Captcha
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
@lru_cache
def get_admin_settings() -> AdminSettings:
"""获取 admin 配置"""
return AdminSettings()
admin_settings = get_admin_settings()

View File

@ -5,16 +5,31 @@ from typing import Sequence
from sqlalchemy import Select, and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models import Api
from backend.app.schemas.api import CreateApiParam, UpdateApiParam
from backend.app.admin.model import Api
from backend.app.admin.schema.api import CreateApiParam, UpdateApiParam
from backend.common.msd.crud import CRUDBase
class CRUDApi(CRUDBase[Api, CreateApiParam, UpdateApiParam]):
async def get(self, db: AsyncSession, pk: int) -> Api | None:
"""
获取 API
:param db:
:param pk:
:return:
"""
return await self.get_(db, pk=pk)
async def get_list(self, name: str = None, method: str = None, path: str = None) -> Select:
"""
获取 API 列表
:param name:
:param method:
:param path:
:return:
"""
se = select(self.model).order_by(desc(self.model.created_time))
where_list = []
if name:
@ -28,20 +43,55 @@ class CRUDApi(CRUDBase[Api, CreateApiParam, UpdateApiParam]):
return se
async def get_all(self, db: AsyncSession) -> Sequence[Api]:
"""
获取所有 API
:param db:
:return:
"""
apis = await db.execute(select(self.model))
return apis.scalars().all()
async def get_by_name(self, db: AsyncSession, name: str) -> Api | None:
"""
通过 name 获取 API
:param db:
:param name:
:return:
"""
api = await db.execute(select(self.model).where(self.model.name == name))
return api.scalars().first()
async def create(self, db: AsyncSession, obj_in: CreateApiParam) -> None:
"""
创建 API
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateApiParam) -> int:
"""
更新 API
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除 API
:param db:
:param pk:
:return:
"""
apis = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
return apis.rowcount

View File

@ -5,13 +5,20 @@ from uuid import UUID
from sqlalchemy import Select, and_, delete, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models import CasbinRule
from backend.app.schemas.casbin_rule import CreatePolicyParam, DeleteAllPoliciesParam, UpdatePolicyParam
from backend.app.admin.model import CasbinRule
from backend.app.admin.schema.casbin_rule import CreatePolicyParam, DeleteAllPoliciesParam, UpdatePolicyParam
from backend.common.msd.crud import CRUDBase
class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]):
async def get_all_policy(self, ptype: str, sub: str) -> Select:
async def get_list(self, ptype: str, sub: str) -> Select:
"""
获取策略列表
:param ptype:
:param sub:
:return:
"""
se = select(self.model).order_by(self.model.id)
where_list = []
if ptype:
@ -23,6 +30,13 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]):
return se
async def delete_policies_by_sub(self, db: AsyncSession, sub: DeleteAllPoliciesParam) -> int:
"""
删除角色所有P策略
:param db:
:param sub:
:return:
"""
where_list = []
if sub.uuid:
where_list.append(self.model.v0 == sub.uuid)
@ -31,6 +45,13 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]):
return result.rowcount
async def delete_groups_by_uuid(self, db: AsyncSession, uuid: UUID) -> int:
"""
删除用户所有G策略
:param db:
:param uuid:
:return:
"""
result = await db.execute(delete(self.model).where(self.model.v0 == str(uuid)))
return result.rowcount

View File

@ -6,21 +6,45 @@ from sqlalchemy import and_, asc, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from backend.app.crud.base import CRUDBase
from backend.app.models import Dept, User
from backend.app.schemas.dept import CreateDeptParam, UpdateDeptParam
from backend.app.admin.model import Dept, User
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.msd.crud import CRUDBase
class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]):
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
"""
获取部门
:param db:
:param dept_id:
:return:
"""
return await self.get_(db, pk=dept_id, del_flag=0)
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
"""
通过 name 获取 API
:param db:
:param name:
:return:
"""
return await self.get_(db, name=name, del_flag=0)
async def get_all(
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: int = None
) -> Sequence[Dept]:
"""
获取所有部门
:param db:
:param name:
:param leader:
:param phone:
:param status:
:return:
"""
se = select(self.model).order_by(asc(self.model.sort))
where_list = [self.model.del_flag == 0]
conditions = []
@ -45,15 +69,44 @@ class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]):
return dept.scalars().all()
async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None:
"""
创建部门
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db: AsyncSession, dept_id: int, obj_in: UpdateDeptParam) -> int:
"""
更新部门
:param db:
:param dept_id:
:param obj_in:
:return:
"""
return await self.update_(db, dept_id, obj_in)
async def delete(self, db: AsyncSession, dept_id: int) -> int:
"""
删除部门
:param db:
:param dept_id:
:return:
"""
return await self.delete_(db, dept_id, del_flag=1)
async def get_user_relation(self, db: AsyncSession, dept_id: int) -> list[User]:
async def get_relation(self, db: AsyncSession, dept_id: int) -> list[User]:
"""
获取关联
:param db:
:param dept_id:
:return:
"""
result = await db.execute(
select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
)
@ -61,6 +114,13 @@ class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]):
return user_relation.users
async def get_children(self, db: AsyncSession, dept_id: int) -> list[Dept]:
"""
获取子部门
:param db:
:param dept_id:
:return:
"""
result = await db.execute(
select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id)
)

View File

@ -4,16 +4,31 @@ from sqlalchemy import Select, and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from backend.app.crud.base import CRUDBase
from backend.app.models.sys_dict_data import DictData
from backend.app.schemas.dict_data import CreateDictDataParam, UpdateDictDataParam
from backend.app.admin.model import DictData
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
from backend.common.msd.crud import CRUDBase
class CRUDDictData(CRUDBase[DictData, CreateDictDataParam, UpdateDictDataParam]):
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据
:param db:
:param pk:
:return:
"""
return await self.get_(db, pk=pk)
async def get_all(self, label: str = None, value: str = None, status: int = None) -> Select:
async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select:
"""
获取所有字典数据
:param label:
:param value:
:param status:
:return:
"""
se = select(self.model).options(selectinload(self.model.type)).order_by(desc(self.model.sort))
where_list = []
if label:
@ -27,20 +42,56 @@ class CRUDDictData(CRUDBase[DictData, CreateDictDataParam, UpdateDictDataParam])
return se
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
"""
通过 label 获取字典数据
:param db:
:param label:
:return:
"""
api = await db.execute(select(self.model).where(self.model.label == label))
return api.scalars().first()
async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None:
"""
创建数据字典
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int:
"""
更新数据字典
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典数据
:param db:
:param pk:
:return:
"""
apis = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
return apis.rowcount
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据和类型
:param db:
:param pk:
:return:
"""
where = [self.model.id == pk]
dict_data = await db.execute(select(self.model).options(selectinload(self.model.type)).where(*where))
return dict_data.scalars().first()

View File

@ -3,16 +3,31 @@
from sqlalchemy import Select, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models.sys_dict_type import DictType
from backend.app.schemas.dict_type import CreateDictTypeParam, UpdateDictTypeParam
from backend.app.admin.model import DictType
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
from backend.common.msd.crud import CRUDBase
class CRUDDictType(CRUDBase[DictType, CreateDictTypeParam, UpdateDictTypeParam]):
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
"""
获取字典类型
:param db:
:param pk:
:return:
"""
return await self.get_(db, pk=pk)
async def get_all(self, *, name: str = None, code: str = None, status: int = None) -> Select:
async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select:
"""
获取所有字典类型
:param name:
:param code:
:param status:
:return:
"""
se = select(self.model).order_by(desc(self.model.created_time))
where_list = []
if name:
@ -26,16 +41,45 @@ class CRUDDictType(CRUDBase[DictType, CreateDictTypeParam, UpdateDictTypeParam])
return se
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
"""
通过 code 获取字典类型
:param db:
:param code:
:return:
"""
dept = await db.execute(select(self.model).where(self.model.code == code))
return dept.scalars().first()
async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None:
"""
创建字典类型
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int:
"""
更新字典类型
:param db:
:param pk:
:param obj_in:
:return:
"""
return await self.update_(db, pk, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典类型
:param db:
:param pk:
:return:
"""
apis = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
return apis.rowcount

View File

@ -3,13 +3,21 @@
from sqlalchemy import Select, and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models import LoginLog
from backend.app.schemas.login_log import CreateLoginLogParam, UpdateLoginLogParam
from backend.app.admin.model import LoginLog
from backend.app.admin.schema.login_log import CreateLoginLogParam, UpdateLoginLogParam
from backend.common.msd.crud import CRUDBase
class CRUDLoginLog(CRUDBase[LoginLog, CreateLoginLogParam, UpdateLoginLogParam]):
async def get_all(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取登录日志列表
:param username:
:param status:
:param ip:
:return:
"""
se = select(self.model).order_by(desc(self.model.created_time))
where_list = []
if username:
@ -22,15 +30,35 @@ class CRUDLoginLog(CRUDBase[LoginLog, CreateLoginLogParam, UpdateLoginLogParam])
se = se.where(and_(*where_list))
return se
async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam):
async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam) -> None:
"""
创建登录日志
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
await db.commit()
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除登录日志
:param db:
:param pk:
:return:
"""
logs = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
return logs.rowcount
async def delete_all(self, db: AsyncSession) -> int:
"""
删除所有登录日志
:param db:
:return:
"""
logs = await db.execute(delete(self.model))
return logs.rowcount

View File

@ -5,20 +5,42 @@ from typing import Sequence
from sqlalchemy import and_, asc, select
from sqlalchemy.orm import selectinload
from backend.app.crud.base import CRUDBase
from backend.app.models import Menu
from backend.app.schemas.menu import CreateMenuParam, UpdateMenuParam
from backend.app.admin.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.msd.crud import CRUDBase
class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]):
async def get(self, db, menu_id: int) -> Menu | None:
"""
获取菜单
:param db:
:param menu_id:
:return:
"""
return await self.get_(db, pk=menu_id)
async def get_by_title(self, db, title: str) -> Menu | None:
"""
通过 title 获取菜单
:param db:
:param title:
:return:
"""
result = await db.execute(select(self.model).where(and_(self.model.title == title, self.model.menu_type != 2)))
return result.scalars().first()
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
"""
获取所有菜单
:param db:
:param title:
:param status:
:return:
"""
se = select(self.model).order_by(asc(self.model.sort))
where_list = []
if title:
@ -31,6 +53,14 @@ class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]):
return menu.scalars().all()
async def get_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
"""
获取角色菜单
:param db:
:param superuser:
:param menu_ids:
:return:
"""
se = select(self.model).order_by(asc(self.model.sort))
where_list = [self.model.menu_type.in_([0, 1])]
if not superuser:
@ -40,16 +70,45 @@ class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]):
return menu.scalars().all()
async def create(self, db, obj_in: CreateMenuParam) -> None:
"""
创建菜单
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db, menu_id: int, obj_in: UpdateMenuParam) -> int:
"""
更新菜单
:param db:
:param menu_id:
:param obj_in:
:return:
"""
count = await self.update_(db, menu_id, obj_in)
return count
async def delete(self, db, menu_id: int) -> int:
"""
删除菜单
:param db:
:param menu_id:
:return:
"""
return await self.delete_(db, menu_id)
async def get_children(self, db, menu_id: int) -> list[Menu]:
"""
获取子菜单
:param db:
:param menu_id:
:return:
"""
result = await db.execute(
select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
)

View File

@ -3,13 +3,21 @@
from sqlalchemy import Select, and_, delete, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.crud.base import CRUDBase
from backend.app.models import OperaLog
from backend.app.schemas.opera_log import CreateOperaLogParam, UpdateOperaLogParam
from backend.app.admin.model import OperaLog
from backend.app.admin.schema.opera_log import CreateOperaLogParam, UpdateOperaLogParam
from backend.common.msd.crud import CRUDBase
class CRUDOperaLogDao(CRUDBase[OperaLog, CreateOperaLogParam, UpdateOperaLogParam]):
async def get_all(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取操作日志列表
:param username:
:param status:
:param ip:
:return:
"""
se = select(self.model).order_by(desc(self.model.created_time))
where_list = []
if username:
@ -23,13 +31,33 @@ class CRUDOperaLogDao(CRUDBase[OperaLog, CreateOperaLogParam, UpdateOperaLogPara
return se
async def create(self, db: AsyncSession, obj_in: CreateOperaLogParam) -> None:
"""
创建操作日志
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除操作日志
:param db:
:param pk:
:return:
"""
logs = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
return logs.rowcount
async def delete_all(self, db: AsyncSession) -> int:
"""
删除所有操作日志
:param db:
:return:
"""
logs = await db.execute(delete(self.model))
return logs.rowcount

View File

@ -5,30 +5,65 @@ from typing import Sequence
from sqlalchemy import Select, delete, desc, select
from sqlalchemy.orm import selectinload
from backend.app.crud.base import CRUDBase
from backend.app.models import Menu, Role, User
from backend.app.schemas.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam
from backend.app.admin.model import Menu, Role, User
from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam
from backend.common.msd.crud import CRUDBase
class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]):
async def get(self, db, role_id: int) -> Role | None:
"""
获取角色
:param db:
:param role_id:
:return:
"""
return await self.get_(db, pk=role_id)
async def get_with_relation(self, db, role_id: int) -> Role | None:
"""
获取角色和菜单
:param db:
:param role_id:
:return:
"""
role = await db.execute(
select(self.model).options(selectinload(self.model.menus)).where(self.model.id == role_id)
)
return role.scalars().first()
async def get_all(self, db) -> Sequence[Role]:
"""
获取所有角色
:param db:
:return:
"""
roles = await db.execute(select(self.model))
return roles.scalars().all()
async def get_user_all(self, db, user_id: int) -> Sequence[Role]:
async def get_user_roles(self, db, user_id: int) -> Sequence[Role]:
"""
获取用户所有角色
:param db:
:param user_id:
:return:
"""
roles = await db.execute(select(self.model).join(self.model.users).where(User.id == user_id))
return roles.scalars().all()
async def get_list(self, name: str = None, data_scope: int = None, status: int = None) -> Select:
"""
获取角色列表
:param name:
:param data_scope:
:param status:
:return:
"""
se = select(self.model).options(selectinload(self.model.menus)).order_by(desc(self.model.created_time))
where_list = []
if name:
@ -42,17 +77,47 @@ class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]):
return se
async def get_by_name(self, db, name: str) -> Role | None:
"""
通过 name 获取角色
:param db:
:param name:
:return:
"""
role = await db.execute(select(self.model).where(self.model.name == name))
return role.scalars().first()
async def create(self, db, obj_in: CreateRoleParam) -> None:
"""
创建角色
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def update(self, db, role_id: int, obj_in: UpdateRoleParam) -> int:
"""
更新角色
:param db:
:param role_id:
:param obj_in:
:return:
"""
rowcount = await self.update_(db, pk=role_id, obj_in=obj_in)
return rowcount
async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
"""
更新角色菜单
:param db:
:param role_id:
:param menu_ids:
:return:
"""
current_role = await self.get_with_relation(db, role_id)
# 更新菜单
menus = await db.execute(select(Menu).where(Menu.id.in_(menu_ids.menus)))
@ -60,6 +125,13 @@ class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]):
return len(current_role.menus)
async def delete(self, db, role_id: list[int]) -> int:
"""
删除角色
:param db:
:param role_id:
:return:
"""
roles = await db.execute(delete(self.model).where(self.model.id.in_(role_id)))
return roles.rowcount

View File

@ -6,35 +6,77 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy.sql import Select
from backend.app.common import jwt
from backend.app.crud.base import CRUDBase
from backend.app.models import Role, User
from backend.app.schemas.user import AddUserParam, AvatarParam, RegisterUserParam, UpdateUserParam, UpdateUserRoleParam
from backend.app.utils.timezone import timezone
from backend.app.admin.model import Role, User
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
RegisterUserParam,
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.common.msd.crud import CRUDBase
from backend.common.security.jwt import get_hash_password
from backend.utils.timezone import timezone
class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
async def get(self, db: AsyncSession, user_id: int) -> User | None:
"""
获取用户
:param db:
:param user_id:
:return:
"""
return await self.get_(db, pk=user_id)
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
"""
通过 username 获取用户
:param db:
:param username:
:return:
"""
user = await db.execute(select(self.model).where(self.model.username == username))
return user.scalars().first()
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
"""
通过 nickname 获取用户
:param db:
:param nickname:
:return:
"""
user = await db.execute(select(self.model).where(self.model.nickname == nickname))
return user.scalars().first()
async def update_login_time(self, db: AsyncSession, username: str) -> int:
"""
更新用户登录时间
:param db:
:param username:
:return:
"""
user = await db.execute(
update(self.model).where(self.model.username == username).values(last_login_time=timezone.now())
)
return user.rowcount
async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None:
"""
创建用户
:param db:
:param obj:
:param social:
:return:
"""
if not social:
salt = text_captcha(5)
obj.password = await jwt.get_hash_password(f'{obj.password}{salt}')
obj.password = await get_hash_password(f'{obj.password}{salt}')
dict_obj = obj.model_dump()
dict_obj.update({'salt': salt})
else:
@ -44,8 +86,15 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
db.add(new_user)
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
"""
后台添加用户
:param db:
:param obj:
:return:
"""
salt = text_captcha(5)
obj.password = await jwt.get_hash_password(f'{obj.password}{salt}')
obj.password = await get_hash_password(f'{obj.password}{salt}')
dict_obj = obj.model_dump(exclude={'roles'})
dict_obj.update({'salt': salt})
new_user = self.model(**dict_obj)
@ -56,11 +105,27 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
db.add(new_user)
async def update_userinfo(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param db:
:param input_user:
:param obj:
:return:
"""
user = await db.execute(update(self.model).where(self.model.id == input_user.id).values(**obj.model_dump()))
return user.rowcount
@staticmethod
async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None:
"""
更新用户角色
:param db:
:param input_user:
:param obj:
:return:
"""
# 删除用户所有角色
for i in list(input_user.roles):
input_user.roles.remove(i)
@ -71,23 +136,63 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
input_user.roles.extend(role_list)
async def update_avatar(self, db: AsyncSession, current_user: User, avatar: AvatarParam) -> int:
"""
更新用户头像
:param db:
:param current_user:
:param avatar:
:return:
"""
user = await db.execute(update(self.model).where(self.model.id == current_user.id).values(avatar=avatar.url))
return user.rowcount
async def delete(self, db: AsyncSession, user_id: int) -> int:
"""
删除用户
:param db:
:param user_id:
:return:
"""
return await self.delete_(db, user_id)
async def check_email(self, db: AsyncSession, email: str) -> User | None:
"""
检查邮箱是否存在
:param db:
:param email:
:return:
"""
mail = await db.execute(select(self.model).where(self.model.email == email))
return mail.scalars().first()
async def reset_password(self, db: AsyncSession, pk: int, password: str, salt: str) -> int:
"""
重置用户密码
:param db:
:param pk:
:param password:
:param salt:
:return:
"""
user = await db.execute(
update(self.model).where(self.model.id == pk).values(password=await jwt.get_hash_password(password + salt))
update(self.model).where(self.model.id == pk).values(password=await get_hash_password(password + salt))
)
return user.rowcount
async def get_all(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
"""
获取用户列表
:param dept:
:param username:
:param phone:
:param status:
:return:
"""
se = (
select(self.model)
.options(selectinload(self.model.dept))
@ -108,22 +213,57 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
return se
async def get_super(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户超级管理员状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.is_superuser
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户后台登录状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.is_staff
async def get_status(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.status
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户多点登录状态
:param db:
:param user_id:
:return:
"""
user = await self.get(db, user_id)
return user.is_multi_login
async def set_super(self, db: AsyncSession, user_id: int) -> int:
"""
设置用户超级管理员
:param db:
:param user_id:
:return:
"""
super_status = await self.get_super(db, user_id)
user = await db.execute(
update(self.model).where(self.model.id == user_id).values(is_superuser=False if super_status else True)
@ -131,6 +271,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
return user.rowcount
async def set_staff(self, db: AsyncSession, user_id: int) -> int:
"""
设置用户后台登录
:param db:
:param user_id:
:return:
"""
staff_status = await self.get_staff(db, user_id)
user = await db.execute(
update(self.model).where(self.model.id == user_id).values(is_staff=False if staff_status else True)
@ -138,6 +285,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
return user.rowcount
async def set_status(self, db: AsyncSession, user_id: int) -> int:
"""
设置用户状态
:param db:
:param user_id:
:return:
"""
status = await self.get_status(db, user_id)
user = await db.execute(
update(self.model).where(self.model.id == user_id).values(status=False if status else True)
@ -145,6 +299,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
return user.rowcount
async def set_multi_login(self, db: AsyncSession, user_id: int) -> int:
"""
设置用户多点登录
:param db:
:param user_id:
:return:
"""
multi_login = await self.get_multi_login(db, user_id)
user = await db.execute(
update(self.model).where(self.model.id == user_id).values(is_multi_login=False if multi_login else True)
@ -152,6 +313,14 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]):
return user.rowcount
async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, username: str = None) -> User | None:
"""
获取用户和部门角色菜单
:param db:
:param user_id:
:param username:
:return:
"""
where = []
if user_id:
where.append(self.model.id == user_id)

View File

@ -3,22 +3,44 @@
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.common.enums import UserSocialType
from backend.app.crud.base import CRUDBase
from backend.app.models import UserSocial
from backend.app.schemas.user_social import CreateUserSocialParam, UpdateUserSocialParam
from backend.app.admin.model import UserSocial
from backend.app.admin.schema.user_social import CreateUserSocialParam, UpdateUserSocialParam
from backend.common.enums import UserSocialType
from backend.common.msd.crud import CRUDBase
class CRUDOUserSocial(CRUDBase[UserSocial, CreateUserSocialParam, UpdateUserSocialParam]):
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
"""
获取用户社交账号绑定
:param db:
:param pk:
:param source:
:return:
"""
se = select(self.model).where(and_(self.model.id == pk, self.model.source == source))
user_social = await db.execute(se)
return user_social.scalars().first()
async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None:
"""
创建用户社交账号绑定
:param db:
:param obj_in:
:return:
"""
await self.create_(db, obj_in)
async def delete(self, db: AsyncSession, social_id: int) -> int:
"""
删除用户社交账号绑定
:param db:
:param social_id:
:return:
"""
return await self.delete_(db, social_id)

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.msd.model import MappedBase # noqa: I001
from backend.app.admin.model.sys_api import Api
from backend.app.admin.model.sys_casbin_rule import CasbinRule
from backend.app.admin.model.sys_dept import Dept
from backend.app.admin.model.sys_dict_data import DictData
from backend.app.admin.model.sys_dict_type import DictType
from backend.app.admin.model.sys_login_log import LoginLog
from backend.app.admin.model.sys_menu import Menu
from backend.app.admin.model.sys_opera_log import OperaLog
from backend.app.admin.model.sys_role import Role
from backend.app.admin.model.sys_user import User
from backend.app.admin.model.sys_user_social import UserSocial

View File

@ -1,11 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.models.base import Base, id_key
from backend.common.msd.model import Base, id_key
class Api(Base):

View File

@ -4,7 +4,7 @@ from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.models.base import MappedBase, id_key
from backend.common.msd.model import MappedBase, id_key
class CasbinRule(MappedBase):

View File

@ -5,7 +5,7 @@ from typing import Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.common.msd.model import Base, id_key
class Dept(Base):

View File

@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.common.msd.model import Base, id_key
class DictData(Base):

View File

@ -4,7 +4,7 @@ from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.common.msd.model import Base, id_key
class DictType(Base):

View File

@ -6,8 +6,8 @@ from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.models.base import DataClassBase, id_key
from backend.app.utils.timezone import timezone
from backend.common.msd.model import DataClassBase, id_key
from backend.utils.timezone import timezone
class LoginLog(DataClassBase):

View File

@ -6,8 +6,8 @@ from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.app.models.sys_role_menu import sys_role_menu
from backend.app.admin.model.sys_role_menu import sys_role_menu
from backend.common.msd.model import Base, id_key
class Menu(Base):

View File

@ -6,8 +6,8 @@ from sqlalchemy import String
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.app.models.base import DataClassBase, id_key
from backend.app.utils.timezone import timezone
from backend.common.msd.model import DataClassBase, id_key
from backend.utils.timezone import timezone
class OperaLog(DataClassBase):

View File

@ -4,9 +4,9 @@ from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.app.models.sys_role_menu import sys_role_menu
from backend.app.models.sys_user_role import sys_user_role
from backend.app.admin.model.sys_role_menu import sys_role_menu
from backend.app.admin.model.sys_user_role import sys_user_role
from backend.common.msd.model import Base, id_key
class Role(Base):

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
from backend.app.models.base import MappedBase
from backend.common.msd.model import MappedBase
sys_role_menu = Table(
'sys_role_menu',

View File

@ -6,10 +6,10 @@ from typing import Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.database.db_mysql import uuid4_str
from backend.app.models.base import Base, id_key
from backend.app.models.sys_user_role import sys_user_role
from backend.app.utils.timezone import timezone
from backend.app.admin.model.sys_user_role import sys_user_role
from backend.common.msd.model import Base, id_key
from backend.database.db_mysql import uuid4_str
from backend.utils.timezone import timezone
class User(Base):

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
from backend.app.models.base import MappedBase
from backend.common.msd.model import MappedBase
sys_user_role = Table(
'sys_user_role',

View File

@ -5,7 +5,7 @@ from typing import Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.models.base import Base, id_key
from backend.common.msd.model import Base, id_key
class UserSocial(Base):

View File

@ -4,8 +4,8 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import MethodType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import MethodType
from backend.common.msd.schema import SchemaBase
class ApiSchemaBase(SchemaBase):

View File

@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-
from pydantic import ConfigDict, Field
from backend.app.common.enums import MethodType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import MethodType
from backend.common.msd.schema import SchemaBase
class CreatePolicyParam(SchemaBase):

View File

@ -4,8 +4,8 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import StatusType
from backend.app.schemas.base import CustomEmailStr, CustomPhoneNumber, SchemaBase
from backend.common.enums import StatusType
from backend.common.msd.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
class DeptSchemaBase(SchemaBase):

View File

@ -4,9 +4,9 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import StatusType
from backend.app.schemas.base import SchemaBase
from backend.app.schemas.dict_type import GetDictTypeListDetails
from backend.app.admin.schema.dict_type import GetDictTypeListDetails
from backend.common.enums import StatusType
from backend.common.msd.schema import SchemaBase
class DictDataSchemaBase(SchemaBase):

View File

@ -4,8 +4,8 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import StatusType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import StatusType
from backend.common.msd.schema import SchemaBase
class DictTypeSchemaBase(SchemaBase):

View File

@ -4,7 +4,7 @@ from datetime import datetime
from pydantic import ConfigDict
from backend.app.schemas.base import SchemaBase
from backend.common.msd.schema import SchemaBase
class LoginLogSchemaBase(SchemaBase):

View File

@ -4,8 +4,8 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import MenuType, StatusType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import MenuType, StatusType
from backend.common.msd.schema import SchemaBase
class MenuSchemaBase(SchemaBase):

View File

@ -4,8 +4,8 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import StatusType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import StatusType
from backend.common.msd.schema import SchemaBase
class OperaLogSchemaBase(SchemaBase):

View File

@ -4,9 +4,9 @@ from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.common.enums import RoleDataScopeType, StatusType
from backend.app.schemas.base import SchemaBase
from backend.app.schemas.menu import GetMenuListDetails
from backend.app.admin.schema.menu import GetMenuListDetails
from backend.common.enums import RoleDataScopeType, StatusType
from backend.common.msd.schema import SchemaBase
class RoleSchemaBase(SchemaBase):

View File

@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from backend.app.schemas.base import SchemaBase
from backend.app.schemas.user import GetUserInfoNoRelationDetail
from backend.app.admin.schema.user import GetUserInfoNoRelationDetail
from backend.common.msd.schema import SchemaBase
class GetSwaggerToken(SchemaBase):

View File

@ -4,10 +4,10 @@ from datetime import datetime
from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
from backend.app.common.enums import StatusType
from backend.app.schemas.base import CustomPhoneNumber, SchemaBase
from backend.app.schemas.dept import GetDeptListDetails
from backend.app.schemas.role import GetRoleListDetails
from backend.app.admin.schema.dept import GetDeptListDetails
from backend.app.admin.schema.role import GetRoleListDetails
from backend.common.enums import StatusType
from backend.common.msd.schema import CustomPhoneNumber, SchemaBase
class AuthSchemaBase(SchemaBase):

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.common.enums import UserSocialType
from backend.app.schemas.base import SchemaBase
from backend.common.enums import UserSocialType
from backend.common.msd.schema import SchemaBase
class UserSocialSchemaBase(SchemaBase):

View File

@ -4,11 +4,11 @@ from typing import Sequence
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.crud.crud_api import api_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import Api
from backend.app.schemas.api import CreateApiParam, UpdateApiParam
from backend.app.admin.crud.crud_api import api_dao
from backend.app.admin.model import Api
from backend.app.admin.schema.api import CreateApiParam, UpdateApiParam
from backend.common.exception import errors
from backend.database.db_mysql import async_db_session
class ApiService:
@ -25,7 +25,7 @@ class ApiService:
return await api_dao.get_list(name=name, method=method, path=path)
@staticmethod
async def get_api_list() -> Sequence[Api]:
async def get_all() -> Sequence[Api]:
async with async_db_session() as db:
apis = await api_dao.get_all(db)
return apis
@ -51,4 +51,4 @@ class ApiService:
return count
api_service: ApiService = ApiService()
api_service = ApiService()

View File

@ -4,19 +4,27 @@ from fastapi import Request
from fastapi.security import HTTPBasicCredentials
from starlette.background import BackgroundTask, BackgroundTasks
from backend.app.common import jwt
from backend.app.common.enums import LoginLogStatusType
from backend.app.common.exception import errors
from backend.app.common.redis import redis_client
from backend.app.common.response.response_code import CustomErrorCode
from backend.app.core.conf import settings
from backend.app.crud.crud_user import user_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import User
from backend.app.schemas.token import GetLoginToken, GetNewToken
from backend.app.schemas.user import AuthLoginParam
from backend.app.services.login_log_service import LoginLogService
from backend.app.utils.timezone import timezone
from backend.app.admin.conf import admin_settings
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.model import User
from backend.app.admin.schema.token import GetLoginToken, GetNewToken
from backend.app.admin.schema.user import AuthLoginParam
from backend.app.admin.service.login_log_service import LoginLogService
from backend.common.enums import LoginLogStatusType
from backend.common.exception import errors
from backend.common.response.response_code import CustomErrorCode
from backend.common.security.jwt import (
create_access_token,
create_new_token,
create_refresh_token,
get_token,
jwt_decode,
password_verify,
)
from backend.core.conf import settings
from backend.database.db_mysql import async_db_session
from backend.database.db_redis import redis_client
from backend.utils.timezone import timezone
class AuthService:
@ -26,13 +34,11 @@ class AuthService:
current_user = await user_dao.get_by_username(db, obj.username)
if not current_user:
raise errors.NotFoundError(msg='用户不存在')
elif not await jwt.password_verify(f'{obj.password}{current_user.salt}', current_user.password):
elif not await password_verify(f'{obj.password}{current_user.salt}', current_user.password):
raise errors.AuthorizationError(msg='密码错误')
elif not current_user.status:
raise errors.AuthorizationError(msg='用户已锁定, 登陆失败')
access_token, _ = await jwt.create_access_token(
str(current_user.id), multi_login=current_user.is_multi_login
)
access_token, _ = await create_access_token(str(current_user.id), multi_login=current_user.is_multi_login)
await user_dao.update_login_time(db, obj.username)
return access_token, current_user
@ -43,19 +49,19 @@ class AuthService:
current_user = await user_dao.get_by_username(db, obj.username)
if not current_user:
raise errors.NotFoundError(msg='用户不存在')
elif not await jwt.password_verify(obj.password + current_user.salt, current_user.password):
elif not await password_verify(obj.password + current_user.salt, current_user.password):
raise errors.AuthorizationError(msg='密码错误')
elif not current_user.status:
raise errors.AuthorizationError(msg='用户已锁定, 登陆失败')
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
captcha_code = await redis_client.get(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
if not captcha_code:
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
if captcha_code.lower() != obj.captcha.lower():
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
access_token, access_token_expire_time = await jwt.create_access_token(
access_token, access_token_expire_time = await create_access_token(
str(current_user.id), multi_login=current_user.is_multi_login
)
refresh_token, refresh_token_expire_time = await jwt.create_refresh_token(
refresh_token, refresh_token_expire_time = await create_refresh_token(
str(current_user.id), access_token_expire_time, multi_login=current_user.is_multi_login
)
await user_dao.update_login_time(db, obj.username)
@ -85,7 +91,7 @@ class AuthService:
msg='登录成功',
)
background_tasks.add_task(LoginLogService.create, **login_log)
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
data = GetLoginToken(
access_token=access_token,
refresh_token=refresh_token,
@ -97,7 +103,7 @@ class AuthService:
@staticmethod
async def new_token(*, request: Request, refresh_token: str) -> GetNewToken:
user_id = await jwt.jwt_decode(refresh_token)
user_id = await jwt_decode(refresh_token)
if request.user.id != user_id:
raise errors.TokenError(msg='刷新 token 无效')
async with async_db_session() as db:
@ -106,13 +112,13 @@ class AuthService:
raise errors.NotFoundError(msg='用户不存在')
elif not current_user.status:
raise errors.AuthorizationError(msg='用户已锁定,操作失败')
current_token = await jwt.get_token(request)
current_token = await get_token(request)
(
new_access_token,
new_refresh_token,
new_access_token_expire_time,
new_refresh_token_expire_time,
) = await jwt.create_new_token(
) = await create_new_token(
str(current_user.id), current_token, refresh_token, multi_login=current_user.is_multi_login
)
data = GetNewToken(
@ -125,7 +131,7 @@ class AuthService:
@staticmethod
async def logout(*, request: Request) -> None:
token = await jwt.get_token(request)
token = await get_token(request)
if request.user.is_multi_login:
key = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token}'
await redis_client.delete(key)
@ -134,4 +140,4 @@ class AuthService:
await redis_client.delete_prefix(prefix)
auth_service: AuthService = AuthService()
auth_service = AuthService()

View File

@ -4,11 +4,8 @@ from uuid import UUID
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.common.rbac import rbac
from backend.app.crud.crud_casbin import casbin_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.casbin_rule import (
from backend.app.admin.crud.crud_casbin import casbin_dao
from backend.app.admin.schema.casbin_rule import (
CreatePolicyParam,
CreateUserRoleParam,
DeleteAllPoliciesParam,
@ -16,12 +13,15 @@ from backend.app.schemas.casbin_rule import (
DeleteUserRoleParam,
UpdatePolicyParam,
)
from backend.common.exception import errors
from backend.common.security.rbac import rbac
from backend.database.db_mysql import async_db_session
class CasbinService:
@staticmethod
async def get_casbin_list(*, ptype: str, sub: str) -> Select:
return await casbin_dao.get_all_policy(ptype, sub)
return await casbin_dao.get_list(ptype, sub)
@staticmethod
async def get_policy_list(*, role: int | None = None) -> list:
@ -32,12 +32,6 @@ class CasbinService:
data = enforcer.get_policy()
return data
@staticmethod
async def get_policy_list_by_role(*, role: str) -> list:
enforcer = await rbac.enforcer()
data = enforcer.get_filtered_named_policy('p', 0, role)
return data
@staticmethod
async def create_policy(*, p: CreatePolicyParam) -> bool:
enforcer = await rbac.enforcer()
@ -140,4 +134,4 @@ class CasbinService:
return count
casbin_service: CasbinService = CasbinService()
casbin_service = CasbinService()

View File

@ -2,12 +2,12 @@
# -*- coding: utf-8 -*-
from typing import Any
from backend.app.common.exception import errors
from backend.app.crud.crud_dept import dept_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import Dept
from backend.app.schemas.dept import CreateDeptParam, UpdateDeptParam
from backend.app.utils.build_tree import get_tree_data
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.exception import errors
from backend.database.db_mysql import async_db_session
from backend.utils.build_tree import get_tree_data
class DeptService:
@ -61,7 +61,7 @@ class DeptService:
@staticmethod
async def delete(*, pk: int) -> int:
async with async_db_session.begin() as db:
dept_user = await dept_dao.get_user_relation(db, pk)
dept_user = await dept_dao.get_relation(db, pk)
if dept_user:
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
children = await dept_dao.get_children(db, pk)
@ -71,4 +71,4 @@ class DeptService:
return count
dept_service: DeptService = DeptService()
dept_service = DeptService()

View File

@ -2,12 +2,12 @@
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.crud.crud_dict_data import dict_data_dao
from backend.app.crud.crud_dict_type import dict_type_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models.sys_dict_data import DictData
from backend.app.schemas.dict_data import CreateDictDataParam, UpdateDictDataParam
from backend.app.admin.crud.crud_dict_data import dict_data_dao
from backend.app.admin.crud.crud_dict_type import dict_type_dao
from backend.app.admin.model import DictData
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
from backend.common.exception import errors
from backend.database.db_mysql import async_db_session
class DictDataService:
@ -21,7 +21,7 @@ class DictDataService:
@staticmethod
async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select:
return await dict_data_dao.get_all(label=label, value=value, status=status)
return await dict_data_dao.get_list(label=label, value=value, status=status)
@staticmethod
async def create(*, obj: CreateDictDataParam) -> None:
@ -56,4 +56,4 @@ class DictDataService:
return count
dict_data_service: DictDataService = DictDataService()
dict_data_service = DictDataService()

View File

@ -2,16 +2,16 @@
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.crud.crud_dict_type import dict_type_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.dict_type import CreateDictTypeParam, UpdateDictTypeParam
from backend.app.admin.crud.crud_dict_type import dict_type_dao
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
from backend.common.exception import errors
from backend.database.db_mysql import async_db_session
class DictTypeService:
@staticmethod
async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select:
return await dict_type_dao.get_all(name=name, code=code, status=status)
return await dict_type_dao.get_list(name=name, code=code, status=status)
@staticmethod
async def create(*, obj: CreateDictTypeParam) -> None:
@ -40,4 +40,4 @@ class DictTypeService:
return count
dict_type_service: DictTypeService = DictTypeService()
dict_type_service = DictTypeService()

View File

@ -3,24 +3,26 @@
from fast_captcha import text_captcha
from fastapi import BackgroundTasks, Request
from backend.app.common import jwt
from backend.app.common.enums import LoginLogStatusType, UserSocialType
from backend.app.common.exception.errors import AuthorizationError
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_user import user_dao
from backend.app.crud.crud_user_social import user_social_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.token import GetLoginToken
from backend.app.schemas.user import RegisterUserParam
from backend.app.schemas.user_social import CreateUserSocialParam
from backend.app.services.login_log_service import LoginLogService
from backend.app.utils.timezone import timezone
from backend.app.admin.conf import admin_settings
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.crud.crud_user_social import user_social_dao
from backend.app.admin.schema.token import GetLoginToken
from backend.app.admin.schema.user import RegisterUserParam
from backend.app.admin.schema.user_social import CreateUserSocialParam
from backend.app.admin.service.login_log_service import LoginLogService
from backend.common.enums import LoginLogStatusType, UserSocialType
from backend.common.exception.errors import AuthorizationError
from backend.common.security import jwt
from backend.database.db_mysql import async_db_session
from backend.database.db_redis import redis_client
from backend.utils.timezone import timezone
class GithubService:
@staticmethod
async def add_with_login(request: Request, background_tasks: BackgroundTasks, user: dict) -> GetLoginToken | None:
async def create_with_login(
request: Request, background_tasks: BackgroundTasks, user: dict
) -> GetLoginToken | None:
async with async_db_session.begin() as db:
github_email = user['email']
if not github_email:
@ -68,7 +70,7 @@ class GithubService:
msg='登录成功OAuth2',
)
background_tasks.add_task(LoginLogService.create, **login_log)
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
data = GetLoginToken(
access_token=access_token,
refresh_token=refresh_token,
@ -79,4 +81,4 @@ class GithubService:
return data
github_service: GithubService = GithubService()
github_service = GithubService()

View File

@ -6,17 +6,17 @@ from fastapi import Request
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.common.log import log
from backend.app.crud.crud_login_log import login_log_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import User
from backend.app.schemas.login_log import CreateLoginLogParam
from backend.app.admin.crud.crud_login_log import login_log_dao
from backend.app.admin.model import User
from backend.app.admin.schema.login_log import CreateLoginLogParam
from backend.common.log import log
from backend.database.db_mysql import async_db_session
class LoginLogService:
@staticmethod
async def get_select(*, username: str, status: int, ip: str) -> Select:
return await login_log_dao.get_all(username=username, status=status, ip=ip)
return await login_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
async def create(
@ -56,4 +56,4 @@ class LoginLogService:
return count
login_log_service: LoginLogService = LoginLogService()
login_log_service = LoginLogService()

View File

@ -4,15 +4,15 @@ from typing import Any
from fastapi import Request
from backend.app.common.exception import errors
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_menu import menu_dao
from backend.app.crud.crud_role import role_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import Menu
from backend.app.schemas.menu import CreateMenuParam, UpdateMenuParam
from backend.app.utils.build_tree import get_tree_data
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db_mysql import async_db_session
from backend.database.db_redis import redis_client
from backend.utils.build_tree import get_tree_data
class MenuService:
@ -96,4 +96,4 @@ class MenuService:
return count
menu_service: MenuService = MenuService()
menu_service = MenuService()

View File

@ -2,15 +2,15 @@
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.crud.crud_opera_log import opera_log_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.schemas.opera_log import CreateOperaLogParam
from backend.app.admin.crud.crud_opera_log import opera_log_dao
from backend.app.admin.schema.opera_log import CreateOperaLogParam
from backend.database.db_mysql import async_db_session
class OperaLogService:
@staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
return await opera_log_dao.get_all(username=username, status=status, ip=ip)
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
async def create(*, obj_in: CreateOperaLogParam):
@ -30,4 +30,4 @@ class OperaLogService:
return count
opera_log_service: OperaLogService = OperaLogService()
opera_log_service = OperaLogService()

View File

@ -5,14 +5,14 @@ from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_menu import menu_dao
from backend.app.crud.crud_role import role_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import Role
from backend.app.schemas.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam
from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Role
from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db_mysql import async_db_session
from backend.database.db_redis import redis_client
class RoleService:
@ -33,7 +33,7 @@ class RoleService:
@staticmethod
async def get_user_roles(*, pk: int) -> Sequence[Role]:
async with async_db_session() as db:
roles = await role_dao.get_user_all(db, user_id=pk)
roles = await role_dao.get_user_roles(db, user_id=pk)
return roles
@staticmethod
@ -82,4 +82,4 @@ class RoleService:
return count
role_service: RoleService = RoleService()
role_service = RoleService()

View File

@ -5,16 +5,11 @@ import random
from fastapi import Request
from sqlalchemy import Select
from backend.app.common.exception import errors
from backend.app.common.jwt import get_token, password_verify, superuser_verify
from backend.app.common.redis import redis_client
from backend.app.core.conf import settings
from backend.app.crud.crud_dept import dept_dao
from backend.app.crud.crud_role import role_dao
from backend.app.crud.crud_user import user_dao
from backend.app.database.db_mysql import async_db_session
from backend.app.models import User
from backend.app.schemas.user import (
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.crud.crud_user import user_dao
from backend.app.admin.model import User
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
RegisterUserParam,
@ -22,6 +17,11 @@ from backend.app.schemas.user import (
UpdateUserParam,
UpdateUserRoleParam,
)
from backend.common.exception import errors
from backend.common.security.jwt import get_token, password_verify, superuser_verify
from backend.core.conf import settings
from backend.database.db_mysql import async_db_session
from backend.database.db_redis import redis_client
class UserService:
@ -147,7 +147,7 @@ class UserService:
@staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select:
return await user_dao.get_all(dept=dept, username=username, phone=phone, status=status)
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
@staticmethod
async def update_permission(*, request: Request, pk: int) -> int:
@ -225,4 +225,4 @@ class UserService:
return count
user_service: UserService = UserService()
user_service = UserService()

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from starlette.testclient import TestClient
from backend.core.conf import settings
def test_logout(client: TestClient, token_headers: dict[str, str]) -> None:
response = client.post(f'{settings.API_V1_STR}/auth/logout', headers=token_headers)
assert response.status_code == 200
assert response.json()['code'] == 200

View File

@ -1,19 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('../../')
from typing import Dict, Generator
import pytest
from starlette.testclient import TestClient
from backend.app.database.db_mysql import get_db
from backend.app.main import app
from backend.app.tests.utils.db_mysql import override_get_db
from backend.app.tests.utils.get_headers import get_token_headers
from backend.app.admin.tests.utils.db_mysql import override_get_db
from backend.app.admin.tests.utils.get_headers import get_token_headers
from backend.database.db_mysql import get_db
from backend.main import app
app.dependency_overrides[get_db] = override_get_db

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy.ext.asyncio import AsyncSession
from backend.core.conf import settings
from backend.database.db_mysql import create_engine_and_session
TEST_SQLALCHEMY_DATABASE_URL = (
f'mysql+asyncmy://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:'
f'{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}_test?charset={settings.MYSQL_CHARSET}'
)
_, test_async_db_session = create_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
async def override_get_db() -> AsyncSession:
"""session 生成器"""
session = test_async_db_session()
try:
yield session
except Exception as se:
await session.rollback()
raise se
finally:
await session.close()

View File

@ -1,18 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Dict
#!/usr/bin/env from typing import Dict
from starlette.testclient import TestClient
from backend.app.core.conf import settings
from backend.core.conf import settings
def get_token_headers(client: TestClient, username: str, password: str) -> Dict[str, str]:
def get_token_headers(client: TestClient, username: str, password: str) -> dict[str, str]:
data = {
'username': username,
'password': password,
}
response = client.post(f'{settings.API_V1_STR}/auth/swagger_login', data=data)
response = client.post(f'{settings.API_V1_STR}/auth/login/swagger', params=data)
response.raise_for_status()
token_type = response.json()['token_type']
access_token = response.json()['access_token']
headers = {'Authorization': f'{token_type} {access_token}'}

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.api.v1.auth import router as auth_router
from backend.app.api.v1.log import router as log_router
from backend.app.api.v1.mixed import router as mixed_router
from backend.app.api.v1.monitor import router as monitor_router
from backend.app.api.v1.sys import router as sys_router
from backend.app.api.v1.task import router as task_router
from backend.app.core.conf import settings
v1 = APIRouter(prefix=settings.API_V1_STR)
# 集合
v1.include_router(auth_router)
v1.include_router(sys_router)
v1.include_router(log_router)
v1.include_router(monitor_router)
v1.include_router(mixed_router)
# 独立
v1.include_router(task_router, prefix='/tasks', tags=['任务管理'])

View File

@ -1,13 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.api.v1.auth.auth import router as auth_router
from backend.app.api.v1.auth.captcha import router as captcha_router
from backend.app.api.v1.auth.github import router as github_router
router = APIRouter(prefix='/auth', tags=['授权管理'])
router.include_router(auth_router)
router.include_router(captcha_router)
router.include_router(github_router)

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