中文汉化
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
Deploy to Staging / deploy (push) Has been cancelled
Conflict detector / main (push) Has been cancelled
Lint Backend / lint-backend (push) Has been cancelled
Playwright Tests / changes (push) Has been cancelled
Test Backend / test-backend (push) Has been cancelled
Test Docker Compose / test-docker-compose (push) Has been cancelled
Playwright Tests / test-playwright (1, 4) (push) Has been cancelled
Playwright Tests / test-playwright (2, 4) (push) Has been cancelled
Playwright Tests / test-playwright (3, 4) (push) Has been cancelled
Playwright Tests / test-playwright (4, 4) (push) Has been cancelled
Playwright Tests / merge-playwright-reports (push) Has been cancelled
Playwright Tests / alls-green-playwright (push) Has been cancelled
Issue Manager / issue-manager (push) Has been cancelled
This commit is contained in:
@@ -1,233 +1,233 @@
|
||||
# Full Stack FastAPI Template
|
||||
# 全栈 FastAPI 模板(Full Stack FastAPI Template)
|
||||
|
||||
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Docker+Compose%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Docker%20Compose/badge.svg" alt="Test Docker Compose"></a>
|
||||
<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3A%22Test+Backend%22" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test%20Backend/badge.svg" alt="Test Backend"></a>
|
||||
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/full-stack-fastapi-template" target="_blank"><img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/full-stack-fastapi-template.svg" alt="Coverage"></a>
|
||||
|
||||
## Technology Stack and Features
|
||||
## 技术栈与特性
|
||||
|
||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com) for the Python backend API.
|
||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com) for the Python SQL database interactions (ORM).
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev), used by FastAPI, for the data validation and settings management.
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org) as the SQL database.
|
||||
- 🚀 [React](https://react.dev) for the frontend.
|
||||
- 💃 Using TypeScript, hooks, [Vite](https://vitejs.dev), and other parts of a modern frontend stack.
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) and [shadcn/ui](https://ui.shadcn.com) for the frontend components.
|
||||
- 🤖 An automatically generated frontend client.
|
||||
- 🧪 [Playwright](https://playwright.dev) for End-to-End testing.
|
||||
- 🦇 Dark mode support.
|
||||
- 🐋 [Docker Compose](https://www.docker.com) for development and production.
|
||||
- 🔒 Secure password hashing by default.
|
||||
- 🔑 JWT (JSON Web Token) authentication.
|
||||
- 📫 Email based password recovery.
|
||||
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
|
||||
- ✅ Tests with [Pytest](https://pytest.org).
|
||||
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
||||
- 🚢 Deployment instructions using Docker Compose, including how to set up a frontend Traefik proxy to handle automatic HTTPS certificates.
|
||||
- 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions.
|
||||
- ⚡ [**FastAPI**](https://fastapi.tiangolo.com):Python 后端 API 框架。
|
||||
- 🧰 [SQLModel](https://sqlmodel.tiangolo.com):用于 Python 与 SQL 数据库交互的 ORM。
|
||||
- 🔍 [Pydantic](https://docs.pydantic.dev):被 FastAPI 使用,用于数据校验与配置管理。
|
||||
- 💾 [PostgreSQL](https://www.postgresql.org):关系型数据库。
|
||||
- 🚀 [React](https://react.dev):前端框架。
|
||||
- 💃 使用 TypeScript、Hooks、[Vite](https://vitejs.dev) 等现代前端技术栈。
|
||||
- 🎨 [Tailwind CSS](https://tailwindcss.com) 与 [shadcn/ui](https://ui.shadcn.com) 作为前端组件系统。
|
||||
- 🤖 自动生成的前端 API 客户端。
|
||||
- 🧪 使用 [Playwright](https://playwright.dev) 做端到端测试。
|
||||
- 🦇 支持暗黑模式。
|
||||
- 🐋 使用 [Docker Compose](https://www.docker.com) 进行开发与生产部署。
|
||||
- 🔒 默认安全的密码哈希。
|
||||
- 🔑 JWT(JSON Web Token)认证。
|
||||
- 📫 基于邮箱的密码找回功能。
|
||||
- 📬 使用 [Mailcatcher](https://mailcatcher.me) 做本地邮件测试。
|
||||
- ✅ 使用 [Pytest](https://pytest.org) 编写与运行测试。
|
||||
- 📞 使用 [Traefik](https://traefik.io) 作为反向代理 / 负载均衡器。
|
||||
- 🚢 提供基于 Docker Compose 的部署说明,包括如何配置 Traefik 代理与自动 HTTPS 证书。
|
||||
- 🏭 提供基于 GitHub Actions 的 CI/CD(持续集成与持续部署)配置。
|
||||
|
||||
### Dashboard Login
|
||||
### 仪表盘登录页
|
||||
|
||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||
|
||||
### Dashboard - Admin
|
||||
### 仪表盘 - 管理后台
|
||||
|
||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||
|
||||
### Dashboard - Items
|
||||
### 仪表盘 - Items
|
||||
|
||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||
|
||||
### Dashboard - Dark Mode
|
||||
### 仪表盘 - 暗黑模式
|
||||
|
||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||
|
||||
### Interactive API Documentation
|
||||
### 交互式 API 文档
|
||||
|
||||
[](https://github.com/fastapi/full-stack-fastapi-template)
|
||||
|
||||
## How To Use It
|
||||
## 如何使用本模板
|
||||
|
||||
You can **just fork or clone** this repository and use it as is.
|
||||
你可以**直接 fork 或 clone** 本仓库,然后在此基础上开发。
|
||||
|
||||
✨ It just works. ✨
|
||||
✨ 开箱即用。✨
|
||||
|
||||
### How to Use a Private Repository
|
||||
### 在私有仓库中使用
|
||||
|
||||
If you want to have a private repository, GitHub won't allow you to simply fork it as it doesn't allow changing the visibility of forks.
|
||||
如果你希望代码在私有仓库中,GitHub 不允许直接将一个公开仓库 fork 成私有仓库(fork 无法更改可见性)。
|
||||
|
||||
But you can do the following:
|
||||
你可以按以下步骤操作:
|
||||
|
||||
- Create a new GitHub repo, for example `my-full-stack`.
|
||||
- Clone this repository manually, set the name with the name of the project you want to use, for example `my-full-stack`:
|
||||
- 在 GitHub 上创建一个新的仓库,例如 `my-full-stack`。
|
||||
- 在本地手动克隆本模板仓库,并将目录名设置为你想要的项目名,例如 `my-full-stack`:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:fastapi/full-stack-fastapi-template.git my-full-stack
|
||||
```
|
||||
|
||||
- Enter into the new directory:
|
||||
- 进入新目录:
|
||||
|
||||
```bash
|
||||
cd my-full-stack
|
||||
```
|
||||
|
||||
- Set the new origin to your new repository, copy it from the GitHub interface, for example:
|
||||
- 将远程仓库地址改为你自己的仓库(在 GitHub 页面中复制地址),例如:
|
||||
|
||||
```bash
|
||||
git remote set-url origin git@github.com:octocat/my-full-stack.git
|
||||
```
|
||||
|
||||
- Add this repo as another "remote" to allow you to get updates later:
|
||||
- 将本模板仓库作为另一个远程仓库添加进来,方便后续同步更新:
|
||||
|
||||
```bash
|
||||
git remote add upstream git@github.com:fastapi/full-stack-fastapi-template.git
|
||||
```
|
||||
|
||||
- Push the code to your new repository:
|
||||
- 将代码推送到你自己的仓库:
|
||||
|
||||
```bash
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
### Update From the Original Template
|
||||
### 从原始模板同步更新
|
||||
|
||||
After cloning the repository, and after doing changes, you might want to get the latest changes from this original template.
|
||||
在你克隆并修改项目之后,可能希望定期从原始模板仓库获取最新改动。
|
||||
|
||||
- Make sure you added the original repository as a remote, you can check it with:
|
||||
- 确认已经添加了原始模板仓库作为远程仓库,可以通过下面的命令检查:
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
|
||||
origin git@github.com:octocat/my-full-stack.git (fetch)
|
||||
origin git@github.com:octocat/my-full-stack.git (push)
|
||||
upstream git@github.com:fastapi/full-stack-fastapi-template.git (fetch)
|
||||
upstream git@github.com:fastapi/full-stack-fastapi-template.git (push)
|
||||
upstream git@github.com:fastapi/full-stack-fastapi-template.git (fetch)
|
||||
upstream git@github.com:fastapi/full-stack-fastapi-template.git (push)
|
||||
```
|
||||
|
||||
- Pull the latest changes without merging:
|
||||
- 在本地拉取上游模板的最新改动(不自动提交):
|
||||
|
||||
```bash
|
||||
git pull --no-commit upstream master
|
||||
```
|
||||
|
||||
This will download the latest changes from this template without committing them, that way you can check everything is right before committing.
|
||||
这会拉取模板最新改动但不会立刻生成提交,方便你在提交前检查是否一切正常。
|
||||
|
||||
- If there are conflicts, solve them in your editor.
|
||||
- 如果有冲突,请在编辑器中解决。
|
||||
|
||||
- Once you are done, commit the changes:
|
||||
- 解决完冲突后,继续合并并提交:
|
||||
|
||||
```bash
|
||||
git merge --continue
|
||||
```
|
||||
|
||||
### Configure
|
||||
### 配置
|
||||
|
||||
You can then update configs in the `.env` files to customize your configurations.
|
||||
你可以在 `.env` 文件中修改配置来定制你的项目。
|
||||
|
||||
Before deploying it, make sure you change at least the values for:
|
||||
在部署之前,至少要修改以下值:
|
||||
|
||||
- `SECRET_KEY`
|
||||
- `FIRST_SUPERUSER_PASSWORD`
|
||||
- `POSTGRES_PASSWORD`
|
||||
|
||||
You can (and should) pass these as environment variables from secrets.
|
||||
推荐将这些值作为环境变量从机密管理系统中注入。
|
||||
|
||||
Read the [deployment.md](./deployment.md) docs for more details.
|
||||
更多细节请阅读 [deployment.md](./deployment.md)。
|
||||
|
||||
### Generate Secret Keys
|
||||
### 生成密钥
|
||||
|
||||
Some environment variables in the `.env` file have a default value of `changethis`.
|
||||
`.env` 中有一些环境变量的默认值为 `changethis`,需要替换为真正的随机密钥。
|
||||
|
||||
You have to change them with a secret key, to generate secret keys you can run the following command:
|
||||
可以使用下面的命令生成一个安全随机密钥:
|
||||
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
```
|
||||
|
||||
Copy the content and use that as password / secret key. And run that again to generate another secure key.
|
||||
复制输出内容,作为密码 / 密钥使用。需要多个密钥时,多运行几次即可。
|
||||
|
||||
## How To Use It - Alternative With Copier
|
||||
## 使用 Copier 生成项目(可选方式)
|
||||
|
||||
This repository also supports generating a new project using [Copier](https://copier.readthedocs.io).
|
||||
本仓库也支持使用 [Copier](https://copier.readthedocs.io) 来生成新项目。
|
||||
|
||||
It will copy all the files, ask you configuration questions, and update the `.env` files with your answers.
|
||||
Copier 会复制所有文件、询问你一系列配置问题,并根据你的回答更新 `.env` 文件。
|
||||
|
||||
### Install Copier
|
||||
### 安装 Copier
|
||||
|
||||
You can install Copier with:
|
||||
你可以通过以下命令安装 Copier:
|
||||
|
||||
```bash
|
||||
pip install copier
|
||||
```
|
||||
|
||||
Or better, if you have [`pipx`](https://pipx.pypa.io/), you can run it with:
|
||||
或(推荐)如果你已经安装了 [`pipx`](https://pipx.pypa.io/),可以使用:
|
||||
|
||||
```bash
|
||||
pipx install copier
|
||||
```
|
||||
|
||||
**Note**: If you have `pipx`, installing copier is optional, you could run it directly.
|
||||
**注意**:如果有 `pipx`,也可以不先安装 Copier,直接运行它。
|
||||
|
||||
### Generate a Project With Copier
|
||||
### 使用 Copier 生成项目
|
||||
|
||||
Decide a name for your new project's directory, you will use it below. For example, `my-awesome-project`.
|
||||
先决定一个新项目目录名,例如 `my-awesome-project`。
|
||||
|
||||
Go to the directory that will be the parent of your project, and run the command with your project's name:
|
||||
进入想要存放项目的父级目录,然后执行:
|
||||
|
||||
```bash
|
||||
copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust
|
||||
```
|
||||
|
||||
If you have `pipx` and you didn't install `copier`, you can run it directly:
|
||||
如果你安装了 `pipx` 但没有单独安装 `copier`,也可以直接运行:
|
||||
|
||||
```bash
|
||||
pipx run copier copy https://github.com/fastapi/full-stack-fastapi-template my-awesome-project --trust
|
||||
```
|
||||
|
||||
**Note** the `--trust` option is necessary to be able to execute a [post-creation script](https://github.com/fastapi/full-stack-fastapi-template/blob/master/.copier/update_dotenv.py) that updates your `.env` files.
|
||||
**注意**:`--trust` 选项用于允许执行一个 [创建后的脚本](https://github.com/fastapi/full-stack-fastapi-template/blob/master/.copier/update_dotenv.py),该脚本会自动更新你的 `.env` 文件。
|
||||
|
||||
### Input Variables
|
||||
### 输入变量
|
||||
|
||||
Copier will ask you for some data, you might want to have at hand before generating the project.
|
||||
Copier 会在生成项目时询问一些配置项,你可以提前准备好这些信息。
|
||||
|
||||
But don't worry, you can just update any of that in the `.env` files afterwards.
|
||||
不过不用担心,这些值之后都可以在 `.env` 文件中再次修改。
|
||||
|
||||
The input variables, with their default values (some auto generated) are:
|
||||
输入变量及其默认值(有些会自动生成)如下:
|
||||
|
||||
- `project_name`: (default: `"FastAPI Project"`) The name of the project, shown to API users (in .env).
|
||||
- `stack_name`: (default: `"fastapi-project"`) The name of the stack used for Docker Compose labels and project name (no spaces, no periods) (in .env).
|
||||
- `secret_key`: (default: `"changethis"`) The secret key for the project, used for security, stored in .env, you can generate one with the method above.
|
||||
- `first_superuser`: (default: `"admin@example.com"`) The email of the first superuser (in .env).
|
||||
- `first_superuser_password`: (default: `"changethis"`) The password of the first superuser (in .env).
|
||||
- `smtp_host`: (default: "") The SMTP server host to send emails, you can set it later in .env.
|
||||
- `smtp_user`: (default: "") The SMTP server user to send emails, you can set it later in .env.
|
||||
- `smtp_password`: (default: "") The SMTP server password to send emails, you can set it later in .env.
|
||||
- `emails_from_email`: (default: `"info@example.com"`) The email account to send emails from, you can set it later in .env.
|
||||
- `postgres_password`: (default: `"changethis"`) The password for the PostgreSQL database, stored in .env, you can generate one with the method above.
|
||||
- `sentry_dsn`: (default: "") The DSN for Sentry, if you are using it, you can set it later in .env.
|
||||
- `project_name`:默认 `"FastAPI Project"`,展示给 API 使用者的项目名称(写入 .env)。
|
||||
- `stack_name`:默认 `"fastapi-project"`,用于 Docker Compose 标签与项目名(不允许空格和句号)(写入 .env)。
|
||||
- `secret_key`:默认 `"changethis"`,项目安全相关的密钥,存储在 .env 中,你应使用上文提到的方式生成真正的随机值。
|
||||
- `first_superuser`:默认 `"admin@example.com"`,首个超级用户邮箱(写入 .env)。
|
||||
- `first_superuser_password`:默认 `"changethis"`,首个超级用户密码(写入 .env)。
|
||||
- `smtp_host`:默认 `""`,用于发送邮件的 SMTP 服务器地址,可稍后在 .env 中配置。
|
||||
- `smtp_user`:默认 `""`,SMTP 用户名。
|
||||
- `smtp_password`:默认 `""`,SMTP 密码。
|
||||
- `emails_from_email`:默认 `"info@example.com"`,发件邮箱,可稍后在 .env 中配置。
|
||||
- `postgres_password`:默认 `"changethis"`,PostgreSQL 数据库密码,存储在 .env 中,也建议用上文方法生成。
|
||||
- `sentry_dsn`:默认 `""`,如果你使用 Sentry,这里填写 DSN,稍后也可以在 .env 中更新。
|
||||
|
||||
## Backend Development
|
||||
## 后端开发
|
||||
|
||||
Backend docs: [backend/README.md](./backend/README.md).
|
||||
后端开发文档参见:[`backend/README.md`](./backend/README.md)。
|
||||
|
||||
## Frontend Development
|
||||
## 前端开发
|
||||
|
||||
Frontend docs: [frontend/README.md](./frontend/README.md).
|
||||
前端开发文档参见:[`frontend/README.md`](./frontend/README.md)。
|
||||
|
||||
## Deployment
|
||||
## 部署
|
||||
|
||||
Deployment docs: [deployment.md](./deployment.md).
|
||||
部署相关文档参见:[`deployment.md`](./deployment.md)。
|
||||
|
||||
## Development
|
||||
## 开发说明
|
||||
|
||||
General development docs: [development.md](./development.md).
|
||||
通用开发说明参见:[`development.md`](./development.md)。
|
||||
|
||||
This includes using Docker Compose, custom local domains, `.env` configurations, etc.
|
||||
其中包括 Docker Compose 使用方式、自定义本地域名、`.env` 配置等内容。
|
||||
|
||||
## Release Notes
|
||||
## 发布日志
|
||||
|
||||
Check the file [release-notes.md](./release-notes.md).
|
||||
发布记录请查看 [`release-notes.md`](./release-notes.md)。
|
||||
|
||||
## License
|
||||
## 许可证
|
||||
|
||||
The Full Stack FastAPI Template is licensed under the terms of the MIT license.
|
||||
Full Stack FastAPI Template 使用 MIT 许可证发布。
|
||||
|
||||
+14
-14
@@ -1,29 +1,29 @@
|
||||
# Security Policy
|
||||
# 安全政策(Security Policy)
|
||||
|
||||
Security is very important for this project and its community. 🔒
|
||||
安全对于本项目及其社区非常重要。🔒
|
||||
|
||||
Learn more about it below. 👇
|
||||
请阅读以下内容了解更多信息。👇
|
||||
|
||||
## Versions
|
||||
## 支持的版本
|
||||
|
||||
The latest version or release is supported.
|
||||
我们仅支持最新版本或最新发布版。
|
||||
|
||||
You are encouraged to write tests for your application and update your versions frequently after ensuring that your tests are passing. This way you will benefit from the latest features, bug fixes, and **security fixes**.
|
||||
建议您为应用程序编写测试,并在确认测试通过后及时更新版本。这样可以享受最新的功能、Bug 修复和**安全修复**。
|
||||
|
||||
## Reporting a Vulnerability
|
||||
## 报告漏洞
|
||||
|
||||
If you think you found a vulnerability, and even if you are not sure about it, please report it right away by sending an email to: security@tiangolo.com. Please try to be as explicit as possible, describing all the steps and example code to reproduce the security issue.
|
||||
如果您发现了漏洞(或者不确定是否为漏洞),请立即发送邮件至 security@tiangolo.com 进行报告。请尽可能详细地描述所有步骤和示例代码,以便重现该安全问题。
|
||||
|
||||
I (the author, [@tiangolo](https://twitter.com/tiangolo)) will review it thoroughly and get back to you.
|
||||
我(作者,[@tiangolo](https://twitter.com/tiangolo))会仔细审核并尽快回复您。
|
||||
|
||||
## Public Discussions
|
||||
## 公开讨论
|
||||
|
||||
Please restrain from publicly discussing a potential security vulnerability. 🙊
|
||||
请避免公开讨论潜在的安全漏洞。🙊
|
||||
|
||||
It's better to discuss privately and try to find a solution first, to limit the potential impact as much as possible.
|
||||
最好先私下讨论并尝试找到解决方案,以尽可能减少潜在的影响。
|
||||
|
||||
---
|
||||
|
||||
Thanks for your help!
|
||||
感谢您的帮助!
|
||||
|
||||
The community and I thank you for that. 🙇
|
||||
社区和我对此表示衷心感谢。🙇
|
||||
|
||||
+12
-12
@@ -4,24 +4,24 @@ ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
# Install uv
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
# 安装 uv
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||
# 将可执行文件放在环境路径的最前面
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Compile bytecode
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
# 编译字节码
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# uv Cache
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||
# uv 缓存
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install dependencies
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
# 安装依赖
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
@@ -36,8 +36,8 @@ COPY ./pyproject.toml ./uv.lock ./alembic.ini /app/
|
||||
COPY ./app /app/app
|
||||
COPY ./tests /app/tests
|
||||
|
||||
# Sync the project
|
||||
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
# 同步项目
|
||||
# 参考: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync
|
||||
|
||||
|
||||
+64
-54
@@ -1,172 +1,182 @@
|
||||
# FastAPI Project - Backend
|
||||
# FastAPI 项目 - 后端(Backend)
|
||||
|
||||
## Requirements
|
||||
## 前置要求
|
||||
|
||||
* [Docker](https://www.docker.com/).
|
||||
* [uv](https://docs.astral.sh/uv/) for Python package and environment management.
|
||||
* 已安装 [Docker](https://www.docker.com/)。
|
||||
* 已安装 [uv](https://docs.astral.sh/uv/),用于管理 Python 包与虚拟环境。
|
||||
|
||||
## Docker Compose
|
||||
## 使用 Docker Compose
|
||||
|
||||
Start the local development environment with Docker Compose following the guide in [../development.md](../development.md).
|
||||
按照 [../development.md](../development.md) 中的说明,使用 Docker Compose 启动本地开发环境。
|
||||
|
||||
## General Workflow
|
||||
## 一般工作流
|
||||
|
||||
By default, the dependencies are managed with [uv](https://docs.astral.sh/uv/), go there and install it.
|
||||
本项目默认使用 [uv](https://docs.astral.sh/uv/) 管理依赖,请先安装它。
|
||||
|
||||
From `./backend/` you can install all the dependencies with:
|
||||
在 `./backend/` 目录下安装所有依赖:
|
||||
|
||||
```console
|
||||
$ uv sync
|
||||
```
|
||||
|
||||
Then you can activate the virtual environment with:
|
||||
然后激活虚拟环境:
|
||||
|
||||
```console
|
||||
$ source .venv/bin/activate
|
||||
```
|
||||
|
||||
Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`.
|
||||
请确保你的编辑器使用的是该虚拟环境中的 Python 解释器:`backend/.venv/bin/python`。
|
||||
|
||||
Modify or add SQLModel models for data and SQL tables in `./backend/app/models.py`, API endpoints in `./backend/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/crud.py`.
|
||||
你可以在 `./backend/app/models.py` 中修改或新增 SQLModel 模型(数据表结构),在 `./backend/app/api/` 中添加 API 路由,在 `./backend/app/crud.py` 中添加 / 修改 CRUD(创建、读取、更新、删除)工具函数。
|
||||
|
||||
## VS Code
|
||||
|
||||
There are already configurations in place to run the backend through the VS Code debugger, so that you can use breakpoints, pause and explore variables, etc.
|
||||
项目中已经包含了 VS Code 的调试配置,你可以直接使用断点、单步调试、查看变量等功能来运行后端。
|
||||
|
||||
The setup is also already configured so you can run the tests through the VS Code Python tests tab.
|
||||
测试相关配置也已就绪,可以通过 VS Code 的 Python 测试面板直接运行测试。
|
||||
|
||||
## Docker Compose Override
|
||||
## Docker Compose 覆盖配置
|
||||
|
||||
During development, you can change Docker Compose settings that will only affect the local development environment in the file `docker-compose.override.yml`.
|
||||
开发阶段,你可以在 `docker-compose.override.yml` 中修改仅作用于本地开发环境的 Docker Compose 配置。
|
||||
|
||||
The changes to that file only affect the local development environment, not the production environment. So, you can add "temporary" changes that help the development workflow.
|
||||
这些改动只影响本地开发环境,不会影响生产环境。因此你可以在这里加入一些“临时”设置来提升本地开发效率。
|
||||
|
||||
For example, the directory with the backend code is synchronized in the Docker container, copying the code you change live to the directory inside the container. That allows you to test your changes right away, without having to build the Docker image again. It should only be done during development, for production, you should build the Docker image with a recent version of the backend code. But during development, it allows you to iterate very fast.
|
||||
例如:后端代码目录会以 volume 形式挂载进容器,你在本机修改代码后会实时同步到容器中,无需重新构建镜像即可看到效果。生产环境则应该基于最新代码重新构建镜像,而不是使用挂载的源码。
|
||||
|
||||
There is also a command override that runs `fastapi run --reload` instead of the default `fastapi run`. It starts a single server process (instead of multiple, as would be for production) and reloads the process whenever the code changes. Have in mind that if you have a syntax error and save the Python file, it will break and exit, and the container will stop. After that, you can restart the container by fixing the error and running again:
|
||||
配置中还包含一个覆盖命令,将默认的 `fastapi run` 改为 `fastapi run --reload`。它只启动一个进程(适合开发环境),并在检测到代码变更时自动重载。注意,如果保存了含语法错误的 Python 文件,进程会直接退出、容器也会停止。修复错误后,可以再次运行:
|
||||
|
||||
```console
|
||||
$ docker compose watch
|
||||
```
|
||||
|
||||
There is also a commented out `command` override, you can uncomment it and comment the default one. It makes the backend container run a process that does "nothing", but keeps the container alive. That allows you to get inside your running container and execute commands inside, for example a Python interpreter to test installed dependencies, or start the development server that reloads when it detects changes.
|
||||
文件中还提供了一个被注释掉的 `command` 覆盖项,你可以取消注释它并注释掉默认命令。该命令会让后端容器运行一个“空转”的进程,只是保持容器存活,方便你进入容器内部执行命令,例如打开 Python 交互解释器测试依赖,或手动启动支持热重载的开发服务器。
|
||||
|
||||
To get inside the container with a `bash` session you can start the stack with:
|
||||
如果你想通过 `bash` 进入容器,可以先启动整个 stack:
|
||||
|
||||
```console
|
||||
$ docker compose watch
|
||||
```
|
||||
|
||||
and then in another terminal, `exec` inside the running container:
|
||||
然后在另一个终端中执行:
|
||||
|
||||
```console
|
||||
$ docker compose exec backend bash
|
||||
```
|
||||
|
||||
You should see an output like:
|
||||
你会看到类似输出:
|
||||
|
||||
```console
|
||||
root@7f2607af31c3:/app#
|
||||
```
|
||||
|
||||
that means that you are in a `bash` session inside your container, as a `root` user, under the `/app` directory, this directory has another directory called "app" inside, that's where your code lives inside the container: `/app/app`.
|
||||
这说明你已经以 `root` 用户进入了容器中的 `/app` 目录,该目录下还有一个 `app` 子目录,即你的后端代码在容器中的位置:`/app/app`。
|
||||
|
||||
There you can use the `fastapi run --reload` command to run the debug live reloading server.
|
||||
在这里可以运行支持热重载的开发服务器:
|
||||
|
||||
```console
|
||||
$ fastapi run --reload app/main.py
|
||||
```
|
||||
|
||||
...it will look like:
|
||||
终端看起来会类似:
|
||||
|
||||
```console
|
||||
root@7f2607af31c3:/app# fastapi run --reload app/main.py
|
||||
```
|
||||
|
||||
and then hit enter. That runs the live reloading server that auto reloads when it detects code changes.
|
||||
然后按回车即可启动,它会在检测到代码变化时自动重载。
|
||||
|
||||
Nevertheless, if it doesn't detect a change but a syntax error, it will just stop with an error. But as the container is still alive and you are in a Bash session, you can quickly restart it after fixing the error, running the same command ("up arrow" and "Enter").
|
||||
如果碰到的不是代码变更而是语法错误,进程会直接退出。不过因为容器还在且你仍在 Bash 会话中,修复错误后按方向键上翻历史命令并回车即可快速重启。
|
||||
|
||||
...this previous detail is what makes it useful to have the container alive doing nothing and then, in a Bash session, make it run the live reload server.
|
||||
正是因为这一点,让“容器空转 + Bash 中手动启动热重载服务”的方式在开发中非常实用。
|
||||
|
||||
## Backend tests
|
||||
## 后端测试
|
||||
|
||||
To test the backend run:
|
||||
运行后端测试:
|
||||
|
||||
```console
|
||||
$ bash ./scripts/test.sh
|
||||
```
|
||||
|
||||
The tests run with Pytest, modify and add tests to `./backend/tests/`.
|
||||
测试使用 Pytest,测试文件位于 `./backend/tests/`,你可以在其中新增或修改测试。
|
||||
|
||||
If you use GitHub Actions the tests will run automatically.
|
||||
如果你使用 GitHub Actions,推送代码后测试会自动运行。
|
||||
|
||||
### Test running stack
|
||||
### 在已运行的 stack 中执行测试
|
||||
|
||||
If your stack is already up and you just want to run the tests, you can use:
|
||||
如果 Docker Compose 已经启动,只想在当前 stack 中运行测试,可以使用:
|
||||
|
||||
```bash
|
||||
docker compose exec backend bash scripts/tests-start.sh
|
||||
```
|
||||
|
||||
That `/app/scripts/tests-start.sh` script just calls `pytest` after making sure that the rest of the stack is running. If you need to pass extra arguments to `pytest`, you can pass them to that command and they will be forwarded.
|
||||
`/app/scripts/tests-start.sh` 脚本会在确认依赖服务已就绪后调用 `pytest`。如果需要给 `pytest` 传额外参数,可以直接附加在命令后面,它们会被转发给 `pytest`。
|
||||
|
||||
For example, to stop on first error:
|
||||
例如,遇到第一个错误就停止:
|
||||
|
||||
```bash
|
||||
docker compose exec backend bash scripts/tests-start.sh -x
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
### 测试覆盖率
|
||||
|
||||
When the tests are run, a file `htmlcov/index.html` is generated, you can open it in your browser to see the coverage of the tests.
|
||||
测试运行后会生成 `htmlcov/index.html` 文件,可在浏览器中打开查看覆盖率报告。
|
||||
|
||||
## Migrations
|
||||
## 数据库迁移(Migrations)
|
||||
|
||||
As during local development your app directory is mounted as a volume inside the container, you can also run the migrations with `alembic` commands inside the container and the migration code will be in your app directory (instead of being only inside the container). So you can add it to your git repository.
|
||||
本地开发时,应用代码目录以 volume 形式挂载到容器中,因此可以在容器内使用 `alembic` 命令生成迁移文件,这些文件会直接写入你的项目目录,方便你提交到 git 仓库。
|
||||
|
||||
Make sure you create a "revision" of your models and that you "upgrade" your database with that revision every time you change them. As this is what will update the tables in your database. Otherwise, your application will have errors.
|
||||
每次修改模型后,请确保:
|
||||
|
||||
* Start an interactive session in the backend container:
|
||||
1. 创建新的迁移“revision”,描述本次模型变更;
|
||||
2. 使用该 revision 升级数据库。
|
||||
|
||||
否则数据库表结构不会更新,应用就会报错。
|
||||
|
||||
基本流程如下:
|
||||
|
||||
* 在后端容器中启动交互会话:
|
||||
|
||||
```console
|
||||
$ docker compose exec backend bash
|
||||
```
|
||||
|
||||
* Alembic is already configured to import your SQLModel models from `./backend/app/models.py`.
|
||||
* `alembic` 已配置好从 `./backend/app/models.py` 导入 SQLModel 模型。
|
||||
|
||||
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
|
||||
* 修改模型(例如新增字段)后,在容器内创建迁移 revision,例如:
|
||||
|
||||
```console
|
||||
$ alembic revision --autogenerate -m "Add column last_name to User model"
|
||||
```
|
||||
|
||||
* Commit to the git repository the files generated in the alembic directory.
|
||||
* 将 Alembic 目录中生成的 Python 迁移文件提交到 git。
|
||||
|
||||
* After creating the revision, run the migration in the database (this is what will actually change the database):
|
||||
* 创建完 revision 后,在数据库中执行迁移(真正修改数据库结构):
|
||||
|
||||
```console
|
||||
$ alembic upgrade head
|
||||
```
|
||||
|
||||
If you don't want to use migrations at all, uncomment the lines in the file at `./backend/app/core/db.py` that end in:
|
||||
如果你完全不想使用迁移,可以在 `./backend/app/core/db.py` 中取消注释以 `SQLModel.metadata.create_all(engine)` 结尾的代码:
|
||||
|
||||
```python
|
||||
SQLModel.metadata.create_all(engine)
|
||||
```
|
||||
|
||||
and comment the line in the file `scripts/prestart.sh` that contains:
|
||||
同时在 `scripts/prestart.sh` 中注释掉包含以下内容的那一行:
|
||||
|
||||
```console
|
||||
$ alembic upgrade head
|
||||
```
|
||||
|
||||
If you don't want to start with the default models and want to remove them / modify them, from the beginning, without having any previous revision, you can remove the revision files (`.py` Python files) under `./backend/app/alembic/versions/`. And then create a first migration as described above.
|
||||
如果你不想从默认模型开始,而是从零开始定义自己的模型,并且不希望有任何已有迁移,可以删除 `./backend/app/alembic/versions/` 下的所有迁移文件(`.py`),然后按上述步骤创建第一条迁移。
|
||||
|
||||
## Email Templates
|
||||
## 邮件模板
|
||||
|
||||
The email templates are in `./backend/app/email-templates/`. Here, there are two directories: `build` and `src`. The `src` directory contains the source files that are used to build the final email templates. The `build` directory contains the final email templates that are used by the application.
|
||||
邮件模板位于 `./backend/app/email-templates/` 目录,其中:
|
||||
|
||||
Before continuing, ensure you have the [MJML extension](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml) installed in your VS Code.
|
||||
- `src`:存放源模板(`.mjml` 文件等),用于生成最终 HTML 模板;
|
||||
- `build`:存放构建后的 HTML 邮件模板,实际由应用使用。
|
||||
|
||||
Once you have the MJML extension installed, you can create a new email template in the `src` directory. After creating the new email template and with the `.mjml` file open in your editor, open the command palette with `Ctrl+Shift+P` and search for `MJML: Export to HTML`. This will convert the `.mjml` file to a `.html` file and now you can save it in the build directory.
|
||||
继续之前,请先在 VS Code 中安装 [MJML 扩展](https://marketplace.visualstudio.com/items?itemName=attilabuti.vscode-mjml)。
|
||||
|
||||
安装完成后,你可以在 `src` 目录中创建新的邮件模板。创建好 `.mjml` 文件并在编辑器中打开后,按 `Ctrl+Shift+P` 打开命令面板,搜索 `MJML: Export to HTML`。该命令会将 `.mjml` 文件转换为 `.html` 文件,然后你可以将其保存到 `build` 目录中供应用使用。
|
||||
|
||||
+15
-20
@@ -1,41 +1,36 @@
|
||||
# A generic, single database configuration.
|
||||
# 通用单数据库配置
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
# 迁移脚本路径
|
||||
script_location = app/alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# 用于生成迁移文件的模板
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# timezone to use when rendering the date
|
||||
# within the migration file as well as the filename.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# 在迁移文件和文件名中渲染日期时使用的时区
|
||||
# 字符串值传递给 dateutil.tz.gettz()
|
||||
# 留空则使用本地时间
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# 应用于 "slug" 字段的最大字符长度
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# 设置为 'true' 以在 'revision' 命令期间运行环境,
|
||||
# 无论是否自动生成
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# 设置为 'true' 以允许在没有源 .py 文件的情况下
|
||||
# 将 .pyc 和 .pyo 文件检测为 versions/ 目录中的修订版本
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# 版本位置规范;默认为 alembic/versions
|
||||
# 使用多个版本目录时,必须使用 --version-path 指定初始修订版本
|
||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# 从 script.py.mako 写入修订文件时使用的输出编码
|
||||
# output_encoding = utf-8
|
||||
|
||||
# Logging configuration
|
||||
# 日志配置
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
|
||||
+15
-19
@@ -4,16 +4,15 @@ from logging.config import fileConfig
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
# 这是 Alembic 配置对象,提供对正在使用的 .ini 文件中值的访问。
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
# 解析配置文件以进行 Python 日志记录。
|
||||
# 这行代码用于设置日志记录器。
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# 在此处添加模型的 MetaData 对象
|
||||
# 以支持 'autogenerate'
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
# target_metadata = None
|
||||
@@ -23,10 +22,9 @@ from app.core.config import settings # noqa
|
||||
|
||||
target_metadata = SQLModel.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# 根据 env.py 的需要,可以从配置中获取其他值:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
# ... 等等。
|
||||
|
||||
|
||||
def get_url():
|
||||
@@ -34,15 +32,13 @@ def get_url():
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
"""在"离线"模式下运行迁移。
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
这使用 URL 而不是 Engine 来配置上下文,
|
||||
尽管 Engine 在这里也是可以接受的。
|
||||
通过跳过 Engine 的创建,我们甚至不需要 DBAPI 可用。
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
在这里调用 context.execute() 会将给定的字符串输出到脚本输出。
|
||||
|
||||
"""
|
||||
url = get_url()
|
||||
@@ -55,10 +51,10 @@ def run_migrations_offline():
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
"""在"在线"模式下运行迁移。
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
在这种情况下,我们需要创建一个 Engine
|
||||
并将连接与上下文关联。
|
||||
|
||||
"""
|
||||
configuration = config.get_section(config.config_ini_section)
|
||||
|
||||
@@ -36,13 +36,13 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
except (InvalidTokenError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Could not validate credentials",
|
||||
detail="无法验证凭据",
|
||||
)
|
||||
user = session.get(User, token_data.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(status_code=404, detail="用户未找到")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
return user
|
||||
|
||||
|
||||
@@ -52,6 +52,6 @@ CurrentUser = Annotated[User, Depends(get_current_user)]
|
||||
def get_current_active_superuser(current_user: CurrentUser) -> User:
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="The user doesn't have enough privileges"
|
||||
status_code=403, detail="用户权限不足"
|
||||
)
|
||||
return current_user
|
||||
|
||||
@@ -15,7 +15,7 @@ def read_items(
|
||||
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve items.
|
||||
获取 Item 列表。
|
||||
"""
|
||||
|
||||
if current_user.is_superuser:
|
||||
@@ -44,13 +44,13 @@ def read_items(
|
||||
@router.get("/{id}", response_model=ItemPublic)
|
||||
def read_item(session: SessionDep, current_user: CurrentUser, id: uuid.UUID) -> Any:
|
||||
"""
|
||||
Get item by ID.
|
||||
通过 ID 获取 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
return item
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ def create_item(
|
||||
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
|
||||
) -> Any:
|
||||
"""
|
||||
Create new item.
|
||||
创建新的 Item。
|
||||
"""
|
||||
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
|
||||
session.add(item)
|
||||
@@ -77,13 +77,13 @@ def update_item(
|
||||
item_in: ItemUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
Update an item.
|
||||
更新 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
update_dict = item_in.model_dump(exclude_unset=True)
|
||||
item.sqlmodel_update(update_dict)
|
||||
session.add(item)
|
||||
@@ -97,13 +97,13 @@ def delete_item(
|
||||
session: SessionDep, current_user: CurrentUser, id: uuid.UUID
|
||||
) -> Message:
|
||||
"""
|
||||
Delete an item.
|
||||
删除 Item。
|
||||
"""
|
||||
item = session.get(Item, id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="项目未找到")
|
||||
if not current_user.is_superuser and (item.owner_id != current_user.id):
|
||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||
raise HTTPException(status_code=400, detail="权限不足")
|
||||
session.delete(item)
|
||||
session.commit()
|
||||
return Message(message="Item deleted successfully")
|
||||
return Message(message="项目删除成功")
|
||||
|
||||
@@ -26,15 +26,15 @@ def login_access_token(
|
||||
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||
) -> Token:
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests
|
||||
OAuth2 兼容的令牌登录,获取用于后续请求的访问令牌
|
||||
"""
|
||||
user = crud.authenticate(
|
||||
session=session, email=form_data.username, password=form_data.password
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||
raise HTTPException(status_code=400, detail="邮箱或密码错误")
|
||||
elif not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return Token(
|
||||
access_token=security.create_access_token(
|
||||
@@ -46,7 +46,7 @@ def login_access_token(
|
||||
@router.post("/login/test-token", response_model=UserPublic)
|
||||
def test_token(current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Test access token
|
||||
测试访问令牌
|
||||
"""
|
||||
return current_user
|
||||
|
||||
@@ -54,14 +54,14 @@ def test_token(current_user: CurrentUser) -> Any:
|
||||
@router.post("/password-recovery/{email}")
|
||||
def recover_password(email: str, session: SessionDep) -> Message:
|
||||
"""
|
||||
Password Recovery
|
||||
密码找回
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this email does not exist in the system.",
|
||||
detail="系统中不存在该邮箱的用户。",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(email=email)
|
||||
email_data = generate_reset_password_email(
|
||||
@@ -72,30 +72,30 @@ def recover_password(email: str, session: SessionDep) -> Message:
|
||||
subject=email_data.subject,
|
||||
html_content=email_data.html_content,
|
||||
)
|
||||
return Message(message="Password recovery email sent")
|
||||
return Message(message="密码找回邮件已发送")
|
||||
|
||||
|
||||
@router.post("/reset-password/")
|
||||
def reset_password(session: SessionDep, body: NewPassword) -> Message:
|
||||
"""
|
||||
Reset password
|
||||
重置密码
|
||||
"""
|
||||
email = verify_password_reset_token(token=body.token)
|
||||
if not email:
|
||||
raise HTTPException(status_code=400, detail="Invalid token")
|
||||
raise HTTPException(status_code=400, detail="无效的令牌")
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this email does not exist in the system.",
|
||||
detail="系统中不存在该邮箱的用户。",
|
||||
)
|
||||
elif not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
raise HTTPException(status_code=400, detail="用户未激活")
|
||||
hashed_password = get_password_hash(password=body.new_password)
|
||||
user.hashed_password = hashed_password
|
||||
session.add(user)
|
||||
session.commit()
|
||||
return Message(message="Password updated successfully")
|
||||
return Message(message="密码更新成功")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -105,14 +105,14 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
|
||||
)
|
||||
def recover_password_html_content(email: str, session: SessionDep) -> Any:
|
||||
"""
|
||||
HTML Content for Password Recovery
|
||||
获取密码找回邮件的 HTML 内容
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=email)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this username does not exist in the system.",
|
||||
detail="系统中不存在该用户名的用户。",
|
||||
)
|
||||
password_reset_token = generate_password_reset_token(email=email)
|
||||
email_data = generate_reset_password_email(
|
||||
|
||||
@@ -23,7 +23,7 @@ class PrivateUserCreate(BaseModel):
|
||||
@router.post("/users/", response_model=UserPublic)
|
||||
def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any:
|
||||
"""
|
||||
Create a new user.
|
||||
创建新用户。
|
||||
"""
|
||||
|
||||
user = User(
|
||||
|
||||
@@ -36,7 +36,7 @@ router = APIRouter(prefix="/users", tags=["users"])
|
||||
)
|
||||
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
||||
"""
|
||||
Retrieve users.
|
||||
获取用户列表。
|
||||
"""
|
||||
|
||||
count_statement = select(func.count()).select_from(User)
|
||||
@@ -53,13 +53,13 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
|
||||
)
|
||||
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
|
||||
"""
|
||||
Create new user.
|
||||
创建新用户。
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this email already exists in the system.",
|
||||
detail="系统中已存在该邮箱的用户。",
|
||||
)
|
||||
|
||||
user = crud.create_user(session=session, user_create=user_in)
|
||||
@@ -80,14 +80,14 @@ def update_user_me(
|
||||
*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Update own user.
|
||||
更新当前用户信息。
|
||||
"""
|
||||
|
||||
if user_in.email:
|
||||
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if existing_user and existing_user.id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="User with this email already exists"
|
||||
status_code=409, detail="该邮箱已被其他用户使用"
|
||||
)
|
||||
user_data = user_in.model_dump(exclude_unset=True)
|
||||
current_user.sqlmodel_update(user_data)
|
||||
@@ -102,25 +102,25 @@ def update_password_me(
|
||||
*, session: SessionDep, body: UpdatePassword, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Update own password.
|
||||
更新当前用户密码。
|
||||
"""
|
||||
if not verify_password(body.current_password, current_user.hashed_password):
|
||||
raise HTTPException(status_code=400, detail="Incorrect password")
|
||||
raise HTTPException(status_code=400, detail="密码错误")
|
||||
if body.current_password == body.new_password:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="New password cannot be the same as the current one"
|
||||
status_code=400, detail="新密码不能与当前密码相同"
|
||||
)
|
||||
hashed_password = get_password_hash(body.new_password)
|
||||
current_user.hashed_password = hashed_password
|
||||
session.add(current_user)
|
||||
session.commit()
|
||||
return Message(message="Password updated successfully")
|
||||
return Message(message="密码更新成功")
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserPublic)
|
||||
def read_user_me(current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Get current user.
|
||||
获取当前用户信息。
|
||||
"""
|
||||
return current_user
|
||||
|
||||
@@ -128,27 +128,27 @@ def read_user_me(current_user: CurrentUser) -> Any:
|
||||
@router.delete("/me", response_model=Message)
|
||||
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
|
||||
"""
|
||||
Delete own user.
|
||||
删除当前用户。
|
||||
"""
|
||||
if current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Super users are not allowed to delete themselves"
|
||||
status_code=403, detail="超级用户不允许删除自己"
|
||||
)
|
||||
session.delete(current_user)
|
||||
session.commit()
|
||||
return Message(message="User deleted successfully")
|
||||
return Message(message="用户删除成功")
|
||||
|
||||
|
||||
@router.post("/signup", response_model=UserPublic)
|
||||
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
|
||||
"""
|
||||
Create new user without the need to be logged in.
|
||||
无需登录即可创建新用户(用户注册)。
|
||||
"""
|
||||
user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this email already exists in the system",
|
||||
detail="系统中已存在该邮箱的用户",
|
||||
)
|
||||
user_create = UserCreate.model_validate(user_in)
|
||||
user = crud.create_user(session=session, user_create=user_create)
|
||||
@@ -160,7 +160,7 @@ def read_user_by_id(
|
||||
user_id: uuid.UUID, session: SessionDep, current_user: CurrentUser
|
||||
) -> Any:
|
||||
"""
|
||||
Get a specific user by id.
|
||||
通过 ID 获取指定用户。
|
||||
"""
|
||||
user = session.get(User, user_id)
|
||||
if user == current_user:
|
||||
@@ -168,7 +168,7 @@ def read_user_by_id(
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="The user doesn't have enough privileges",
|
||||
detail="用户权限不足",
|
||||
)
|
||||
return user
|
||||
|
||||
@@ -185,20 +185,20 @@ def update_user(
|
||||
user_in: UserUpdate,
|
||||
) -> Any:
|
||||
"""
|
||||
Update a user.
|
||||
更新用户信息。
|
||||
"""
|
||||
|
||||
db_user = session.get(User, user_id)
|
||||
if not db_user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="The user with this id does not exist in the system",
|
||||
detail="系统中不存在该ID的用户",
|
||||
)
|
||||
if user_in.email:
|
||||
existing_user = crud.get_user_by_email(session=session, email=user_in.email)
|
||||
if existing_user and existing_user.id != user_id:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="User with this email already exists"
|
||||
status_code=409, detail="该邮箱已被其他用户使用"
|
||||
)
|
||||
|
||||
db_user = crud.update_user(session=session, db_user=db_user, user_in=user_in)
|
||||
@@ -210,17 +210,17 @@ def delete_user(
|
||||
session: SessionDep, current_user: CurrentUser, user_id: uuid.UUID
|
||||
) -> Message:
|
||||
"""
|
||||
Delete a user.
|
||||
删除用户。
|
||||
"""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
raise HTTPException(status_code=404, detail="用户未找到")
|
||||
if user == current_user:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Super users are not allowed to delete themselves"
|
||||
status_code=403, detail="超级用户不允许删除自己"
|
||||
)
|
||||
statement = delete(Item).where(col(Item.owner_id) == user_id)
|
||||
session.exec(statement) # type: ignore
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
return Message(message="User deleted successfully")
|
||||
return Message(message="用户删除成功")
|
||||
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/utils", tags=["utils"])
|
||||
)
|
||||
def test_email(email_to: EmailStr) -> Message:
|
||||
"""
|
||||
Test emails.
|
||||
测试邮件发送。
|
||||
"""
|
||||
email_data = generate_test_email(email_to=email_to)
|
||||
send_email(
|
||||
@@ -23,7 +23,7 @@ def test_email(email_to: EmailStr) -> Message:
|
||||
subject=email_data.subject,
|
||||
html_content=email_data.html_content,
|
||||
)
|
||||
return Message(message="Test email sent")
|
||||
return Message(message="测试邮件已发送")
|
||||
|
||||
|
||||
@router.get("/health-check/")
|
||||
|
||||
@@ -22,7 +22,7 @@ wait_seconds = 1
|
||||
def init(db_engine: Engine) -> None:
|
||||
try:
|
||||
with Session(db_engine) as session:
|
||||
# Try to create session to check if DB is awake
|
||||
# 尝试创建会话以检查数据库是否已就绪
|
||||
session.exec(select(1))
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Initializing service")
|
||||
logger.info("正在初始化服务")
|
||||
init(engine)
|
||||
logger.info("Service finished initializing")
|
||||
logger.info("服务初始化完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -25,14 +25,14 @@ def parse_cors(v: Any) -> list[str] | str:
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
# Use top level .env file (one level above ./backend/)
|
||||
# 使用顶层的 .env 文件(在 ./backend/ 上一级目录)
|
||||
env_file="../.env",
|
||||
env_ignore_empty=True,
|
||||
extra="ignore",
|
||||
)
|
||||
API_V1_STR: str = "/api/v1"
|
||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
# 60 minutes * 24 hours * 8 days = 8 days
|
||||
# 60 分钟 * 24 小时 * 8 天 = 8 天
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||
FRONTEND_HOST: str = "http://localhost:5173"
|
||||
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||
@@ -97,8 +97,8 @@ class Settings(BaseSettings):
|
||||
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||
if value == "changethis":
|
||||
message = (
|
||||
f'The value of {var_name} is "changethis", '
|
||||
"for security, please change it, at least for deployments."
|
||||
f'{var_name} 的值为 "changethis",'
|
||||
"出于安全考虑,请修改它,至少在部署时需要修改。"
|
||||
)
|
||||
if self.ENVIRONMENT == "local":
|
||||
warnings.warn(message, stacklevel=1)
|
||||
|
||||
@@ -7,18 +7,17 @@ from app.models import User, UserCreate
|
||||
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
|
||||
|
||||
# make sure all SQLModel models are imported (app.models) before initializing DB
|
||||
# otherwise, SQLModel might fail to initialize relationships properly
|
||||
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
|
||||
# 确保在初始化数据库之前已导入所有 SQLModel 模型(app.models)
|
||||
# 否则,SQLModel 可能无法正确初始化关系
|
||||
# 更多详情请参考:https://github.com/fastapi/full-stack-fastapi-template/issues/28
|
||||
|
||||
|
||||
def init_db(session: Session) -> None:
|
||||
# Tables should be created with Alembic migrations
|
||||
# But if you don't want to use migrations, create
|
||||
# the tables un-commenting the next lines
|
||||
# 数据库表应该通过 Alembic 迁移创建
|
||||
# 但如果你不想使用迁移,可以取消注释下面的代码来创建表
|
||||
# from sqlmodel import SQLModel
|
||||
|
||||
# This works because the models are already imported and registered from app.models
|
||||
# 这样可以正常工作,因为模型已经从 app.models 导入并注册
|
||||
# SQLModel.metadata.create_all(engine)
|
||||
|
||||
user = session.exec(
|
||||
|
||||
@@ -14,9 +14,9 @@ def init() -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Creating initial data")
|
||||
logger.info("正在创建初始数据")
|
||||
init()
|
||||
logger.info("Initial data created")
|
||||
logger.info("初始数据创建完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ app = FastAPI(
|
||||
generate_unique_id_function=custom_generate_unique_id,
|
||||
)
|
||||
|
||||
# Set all CORS enabled origins
|
||||
# 设置所有允许 CORS 的源
|
||||
if settings.all_cors_origins:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
+13
-13
@@ -4,7 +4,7 @@ from pydantic import EmailStr
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
||||
# Shared properties
|
||||
# 共享属性
|
||||
class UserBase(SQLModel):
|
||||
email: EmailStr = Field(unique=True, index=True, max_length=255)
|
||||
is_active: bool = True
|
||||
@@ -12,7 +12,7 @@ class UserBase(SQLModel):
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
# 创建用户时通过 API 接收的属性
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
@@ -23,7 +23,7 @@ class UserRegister(SQLModel):
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on update, all are optional
|
||||
# 更新用户时通过 API 接收的属性,所有字段均为可选
|
||||
class UserUpdate(UserBase):
|
||||
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
|
||||
password: str | None = Field(default=None, min_length=8, max_length=128)
|
||||
@@ -39,14 +39,14 @@ class UpdatePassword(SQLModel):
|
||||
new_password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
# 数据库模型,数据库表名从类名推断
|
||||
class User(UserBase, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
hashed_password: str
|
||||
items: list["Item"] = Relationship(back_populates="owner", cascade_delete=True)
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
# 通过 API 返回的属性,id 始终必填
|
||||
class UserPublic(UserBase):
|
||||
id: uuid.UUID
|
||||
|
||||
@@ -56,23 +56,23 @@ class UsersPublic(SQLModel):
|
||||
count: int
|
||||
|
||||
|
||||
# Shared properties
|
||||
# 共享属性
|
||||
class ItemBase(SQLModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
description: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive on item creation
|
||||
# 创建 Item 时接收的属性
|
||||
class ItemCreate(ItemBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive on item update
|
||||
# 更新 Item 时接收的属性
|
||||
class ItemUpdate(ItemBase):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
# 数据库模型,数据库表名从类名推断
|
||||
class Item(ItemBase, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
owner_id: uuid.UUID = Field(
|
||||
@@ -81,7 +81,7 @@ class Item(ItemBase, table=True):
|
||||
owner: User | None = Relationship(back_populates="items")
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
# 通过 API 返回的属性,id 始终必填
|
||||
class ItemPublic(ItemBase):
|
||||
id: uuid.UUID
|
||||
owner_id: uuid.UUID
|
||||
@@ -92,18 +92,18 @@ class ItemsPublic(SQLModel):
|
||||
count: int
|
||||
|
||||
|
||||
# Generic message
|
||||
# 通用消息
|
||||
class Message(SQLModel):
|
||||
message: str
|
||||
|
||||
|
||||
# JSON payload containing access token
|
||||
# 包含访问令牌的 JSON 载荷
|
||||
class Token(SQLModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
# Contents of JWT token
|
||||
# JWT 令牌内容
|
||||
class TokenPayload(SQLModel):
|
||||
sub: str | None = None
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ wait_seconds = 1
|
||||
)
|
||||
def init(db_engine: Engine) -> None:
|
||||
try:
|
||||
# Try to create session to check if DB is awake
|
||||
# 尝试创建会话以检查数据库是否已就绪
|
||||
with Session(db_engine) as session:
|
||||
session.exec(select(1))
|
||||
except Exception as e:
|
||||
@@ -30,9 +30,9 @@ def init(db_engine: Engine) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logger.info("Initializing service")
|
||||
logger.info("正在初始化服务")
|
||||
init(engine)
|
||||
logger.info("Service finished initializing")
|
||||
logger.info("服务初始化完成")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -36,7 +36,7 @@ def send_email(
|
||||
subject: str = "",
|
||||
html_content: str = "",
|
||||
) -> None:
|
||||
assert settings.emails_enabled, "no provided configuration for email variables"
|
||||
assert settings.emails_enabled, "未提供邮件相关配置变量"
|
||||
message = emails.Message(
|
||||
subject=subject,
|
||||
html=html_content,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Let the DB start
|
||||
# 等待数据库启动
|
||||
python app/backend_pre_start.py
|
||||
|
||||
# Run migrations
|
||||
# 运行数据库迁移
|
||||
alembic upgrade head
|
||||
|
||||
# Create initial data in DB
|
||||
# 在数据库中创建初始数据
|
||||
python app/initial_data.py
|
||||
|
||||
+17
-17
@@ -1,66 +1,66 @@
|
||||
project_name:
|
||||
type: str
|
||||
help: The name of the project, shown to API users (in .env)
|
||||
help: 项目名称,展示给 API 用户(保存在 .env)
|
||||
default: FastAPI Project
|
||||
|
||||
stack_name:
|
||||
type: str
|
||||
help: The name of the stack used for Docker Compose labels (no spaces) (in .env)
|
||||
help: 用于 Docker Compose 标签的堆栈名称(不能有空格)(保存在 .env)
|
||||
default: fastapi-project
|
||||
|
||||
secret_key:
|
||||
type: str
|
||||
help: |
|
||||
'The secret key for the project, used for security,
|
||||
stored in .env, you can generate one with:
|
||||
'项目的密钥,用于安全相关功能,
|
||||
保存在 .env,你可以用以下命令生成:
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"'
|
||||
default: changethis
|
||||
|
||||
first_superuser:
|
||||
type: str
|
||||
help: The email of the first superuser (in .env)
|
||||
help: 第一个超级用户的邮箱(保存在 .env)
|
||||
default: admin@example.com
|
||||
|
||||
first_superuser_password:
|
||||
type: str
|
||||
help: The password of the first superuser (in .env)
|
||||
help: 第一个超级用户的密码(保存在 .env)
|
||||
default: changethis
|
||||
|
||||
smtp_host:
|
||||
type: str
|
||||
help: The SMTP server host to send emails, you can set it later in .env
|
||||
help: 用于发送邮件的 SMTP 服务器地址,你可以稍后在 .env 中设置
|
||||
default: ""
|
||||
|
||||
smtp_user:
|
||||
type: str
|
||||
help: The SMTP server user to send emails, you can set it later in .env
|
||||
help: 用于发送邮件的 SMTP 服务器用户名,你可以稍后在 .env 中设置
|
||||
default: ""
|
||||
|
||||
smtp_password:
|
||||
type: str
|
||||
help: The SMTP server password to send emails, you can set it later in .env
|
||||
help: 用于发送邮件的 SMTP 服务器密码,你可以稍后在 .env 中设置
|
||||
default: ""
|
||||
|
||||
emails_from_email:
|
||||
type: str
|
||||
help: The email account to send emails from, you can set it later in .env
|
||||
help: 发送邮件的发件邮箱地址,你可以稍后在 .env 中设置
|
||||
default: info@example.com
|
||||
|
||||
postgres_password:
|
||||
type: str
|
||||
help: |
|
||||
'The password for the PostgreSQL database, stored in .env,
|
||||
you can generate one with:
|
||||
'PostgreSQL 数据库的密码,保存在 .env,
|
||||
你可以用以下命令生成:
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"'
|
||||
default: changethis
|
||||
|
||||
sentry_dsn:
|
||||
type: str
|
||||
help: The DSN for Sentry, if you are using it, you can set it later in .env
|
||||
help: Sentry 的 DSN,如果你正在使用 Sentry,可以稍后在 .env 中设置
|
||||
default: ""
|
||||
|
||||
_exclude:
|
||||
# Global
|
||||
# 全局
|
||||
- .vscode
|
||||
- .mypy_cache
|
||||
# Python
|
||||
@@ -72,8 +72,8 @@ _exclude:
|
||||
- htmlcov
|
||||
- .cache
|
||||
- .venv
|
||||
# Frontend
|
||||
# Logs
|
||||
# 前端
|
||||
# 日志
|
||||
- logs
|
||||
- "*.log"
|
||||
- npm-debug.log*
|
||||
@@ -85,7 +85,7 @@ _exclude:
|
||||
- dist
|
||||
- dist-ssr
|
||||
- "*.local"
|
||||
# Editor directories and files
|
||||
# 编辑器目录和文件
|
||||
- .idea
|
||||
- .DS_Store
|
||||
- "*.suo"
|
||||
|
||||
+115
-108
@@ -1,263 +1,270 @@
|
||||
# FastAPI Project - Deployment
|
||||
# FastAPI 项目 - 部署(Deployment)
|
||||
|
||||
You can deploy the project using Docker Compose to a remote server.
|
||||
你可以使用 Docker Compose 将本项目部署到远程服务器。
|
||||
|
||||
This project expects you to have a Traefik proxy handling communication to the outside world and HTTPS certificates.
|
||||
本项目假定你已经有一个 Traefik 代理,用于处理对外的 HTTP/HTTPS 流量以及证书管理。
|
||||
|
||||
You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions.
|
||||
你可以使用 CI/CD(持续集成与持续部署)系统自动部署,本项目已经提供了 GitHub Actions 的示例配置。
|
||||
|
||||
But you have to configure a couple things first. 🤓
|
||||
在此之前,你需要完成一些基础配置。🤓
|
||||
|
||||
## Preparation
|
||||
## 部署前准备
|
||||
|
||||
* Have a remote server ready and available.
|
||||
* Configure the DNS records of your domain to point to the IP of the server you just created.
|
||||
* Configure a wildcard subdomain for your domain, so that you can have multiple subdomains for different services, e.g. `*.fastapi-project.example.com`. This will be useful for accessing different components, like `dashboard.fastapi-project.example.com`, `api.fastapi-project.example.com`, `traefik.fastapi-project.example.com`, `adminer.fastapi-project.example.com`, etc. And also for `staging`, like `dashboard.staging.fastapi-project.example.com`, `adminer.staging.fastapi-project.example.com`, etc.
|
||||
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
||||
* 准备一台可访问的远程服务器。
|
||||
* 将你的域名 DNS 记录解析到该服务器的 IP。
|
||||
* 为该域名配置通配符子域名(Wildcard),例如 `*.fastapi-project.example.com`,以便为不同服务使用不同子域名,如 `dashboard.fastapi-project.example.com`、`api.fastapi-project.example.com`、`traefik.fastapi-project.example.com`、`adminer.fastapi-project.example.com` 等;同时也便于配置 `staging` 环境,比如 `dashboard.staging.fastapi-project.example.com`、`adminer.staging.fastapi-project.example.com` 等。
|
||||
* 在远程服务器上安装并配置 [Docker](https://docs.docker.com/engine/install/)(Docker Engine,而非 Docker Desktop)。
|
||||
|
||||
## Public Traefik
|
||||
## 公共 Traefik
|
||||
|
||||
We need a Traefik proxy to handle incoming connections and HTTPS certificates.
|
||||
我们需要一个 Traefik 代理来处理外部请求以及 HTTPS 证书。
|
||||
|
||||
You need to do these next steps only once.
|
||||
下面的步骤只需执行一次。
|
||||
|
||||
### Traefik Docker Compose
|
||||
### Traefik 的 Docker Compose
|
||||
|
||||
* Create a remote directory to store your Traefik Docker Compose file:
|
||||
* 在远程服务器上创建一个目录,用来存放 Traefik 的 Docker Compose 文件:
|
||||
|
||||
```bash
|
||||
mkdir -p /root/code/traefik-public/
|
||||
```
|
||||
|
||||
Copy the Traefik Docker Compose file to your server. You could do it by running the command `rsync` in your local terminal:
|
||||
在本地终端中使用 `rsync` 将 Traefik 的 Docker Compose 文件拷贝到服务器:
|
||||
|
||||
```bash
|
||||
rsync -a docker-compose.traefik.yml root@your-server.example.com:/root/code/traefik-public/
|
||||
```
|
||||
|
||||
### Traefik Public Network
|
||||
### Traefik 公共网络
|
||||
|
||||
This Traefik will expect a Docker "public network" named `traefik-public` to communicate with your stack(s).
|
||||
Traefik 会期望有一个名为 `traefik-public` 的 Docker “公共网络”,用来与各个应用 stack 通信。
|
||||
|
||||
This way, there will be a single public Traefik proxy that handles the communication (HTTP and HTTPS) with the outside world, and then behind that, you could have one or more stacks with different domains, even if they are on the same single server.
|
||||
通过这种方式,可以用一个公共 Traefik 代理对外处理 HTTP/HTTPS 流量,其后可以挂载一个或多个不同域名的应用 stack,即便它们都在同一台物理服务器上。
|
||||
|
||||
To create a Docker "public network" named `traefik-public` run the following command in your remote server:
|
||||
在远程服务器中创建该网络:
|
||||
|
||||
```bash
|
||||
docker network create traefik-public
|
||||
```
|
||||
|
||||
### Traefik Environment Variables
|
||||
### Traefik 环境变量
|
||||
|
||||
The Traefik Docker Compose file expects some environment variables to be set in your terminal before starting it. You can do it by running the following commands in your remote server.
|
||||
Traefik 的 Docker Compose 文件在启动前需要若干环境变量,你可以在远程服务器终端中通过以下命令设置:
|
||||
|
||||
* Create the username for HTTP Basic Auth, e.g.:
|
||||
* 为 HTTP Basic Auth 创建用户名,例如:
|
||||
|
||||
```bash
|
||||
export USERNAME=admin
|
||||
```
|
||||
|
||||
* Create an environment variable with the password for HTTP Basic Auth, e.g.:
|
||||
* 创建 Basic Auth 密码对应的环境变量,例如:
|
||||
|
||||
```bash
|
||||
export PASSWORD=changethis
|
||||
```
|
||||
|
||||
* Use openssl to generate the "hashed" version of the password for HTTP Basic Auth and store it in an environment variable:
|
||||
* 使用 openssl 生成该密码的哈希值并保存到环境变量中:
|
||||
|
||||
```bash
|
||||
export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD)
|
||||
```
|
||||
|
||||
To verify that the hashed password is correct, you can print it:
|
||||
可以通过打印检查哈希是否已生成:
|
||||
|
||||
```bash
|
||||
echo $HASHED_PASSWORD
|
||||
```
|
||||
|
||||
* Create an environment variable with the domain name for your server, e.g.:
|
||||
* 创建保存服务器主域名的环境变量,例如:
|
||||
|
||||
```bash
|
||||
export DOMAIN=fastapi-project.example.com
|
||||
```
|
||||
|
||||
* Create an environment variable with the email for Let's Encrypt, e.g.:
|
||||
* 创建保存 Let's Encrypt 邮箱的环境变量,例如:
|
||||
|
||||
```bash
|
||||
export EMAIL=admin@example.com
|
||||
```
|
||||
|
||||
**Note**: you need to set a different email, an email `@example.com` won't work.
|
||||
**注意**:邮箱不能是 `@example.com` 这样的占位域名,需要使用真实邮箱。
|
||||
|
||||
### Start the Traefik Docker Compose
|
||||
### 启动 Traefik Docker Compose
|
||||
|
||||
Go to the directory where you copied the Traefik Docker Compose file in your remote server:
|
||||
在远程服务器上进入存放 Traefik Docker Compose 文件的目录:
|
||||
|
||||
```bash
|
||||
cd /root/code/traefik-public/
|
||||
```
|
||||
|
||||
Now with the environment variables set and the `docker-compose.traefik.yml` in place, you can start the Traefik Docker Compose running the following command:
|
||||
在环境变量已经设置、`docker-compose.traefik.yml` 已就位的情况下,启动 Traefik:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.traefik.yml up -d
|
||||
```
|
||||
|
||||
## Deploy the FastAPI Project
|
||||
## 部署 FastAPI 项目
|
||||
|
||||
Now that you have Traefik in place you can deploy your FastAPI project with Docker Compose.
|
||||
在 Traefik 准备好之后,就可以使用 Docker Compose 部署 FastAPI 项目。
|
||||
|
||||
**Note**: You might want to jump ahead to the section about Continuous Deployment with GitHub Actions.
|
||||
**提示**:你也可以直接跳到后面的「使用 GitHub Actions 持续部署」部分。
|
||||
|
||||
## Environment Variables
|
||||
## 环境变量
|
||||
|
||||
You need to set some environment variables first.
|
||||
在部署前,需要设置若干环境变量。
|
||||
|
||||
Set the `ENVIRONMENT`, by default `local` (for development), but when deploying to a server you would put something like `staging` or `production`:
|
||||
设置 `ENVIRONMENT`,默认是 `local`(开发环境),部署到服务器时可以设置为 `staging` 或 `production`,例如:
|
||||
|
||||
```bash
|
||||
export ENVIRONMENT=production
|
||||
```
|
||||
|
||||
Set the `DOMAIN`, by default `localhost` (for development), but when deploying you would use your own domain, for example:
|
||||
设置 `DOMAIN`,本地默认是 `localhost`,在部署时应改为你的实际域名,例如:
|
||||
|
||||
```bash
|
||||
export DOMAIN=fastapi-project.example.com
|
||||
```
|
||||
|
||||
You can set several variables, like:
|
||||
你还可以设置以下变量:
|
||||
|
||||
* `PROJECT_NAME`: The name of the project, used in the API for the docs and emails.
|
||||
* `STACK_NAME`: The name of the stack used for Docker Compose labels and project name, this should be different for `staging`, `production`, etc. You could use the same domain replacing dots with dashes, e.g. `fastapi-project-example-com` and `staging-fastapi-project-example-com`.
|
||||
* `BACKEND_CORS_ORIGINS`: A list of allowed CORS origins separated by commas.
|
||||
* `SECRET_KEY`: The secret key for the FastAPI project, used to sign tokens.
|
||||
* `FIRST_SUPERUSER`: The email of the first superuser, this superuser will be the one that can create new users.
|
||||
* `FIRST_SUPERUSER_PASSWORD`: The password of the first superuser.
|
||||
* `SMTP_HOST`: The SMTP server host to send emails, this would come from your email provider (E.g. Mailgun, Sparkpost, Sendgrid, etc).
|
||||
* `SMTP_USER`: The SMTP server user to send emails.
|
||||
* `SMTP_PASSWORD`: The SMTP server password to send emails.
|
||||
* `EMAILS_FROM_EMAIL`: The email account to send emails from.
|
||||
* `POSTGRES_SERVER`: The hostname of the PostgreSQL server. You can leave the default of `db`, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider.
|
||||
* `POSTGRES_PORT`: The port of the PostgreSQL server. You can leave the default. You normally wouldn't need to change this unless you are using a third-party provider.
|
||||
* `POSTGRES_PASSWORD`: The Postgres password.
|
||||
* `POSTGRES_USER`: The Postgres user, you can leave the default.
|
||||
* `POSTGRES_DB`: The database name to use for this application. You can leave the default of `app`.
|
||||
* `SENTRY_DSN`: The DSN for Sentry, if you are using it.
|
||||
* `PROJECT_NAME`:项目名称,用于 API 文档及邮件内容中展示。
|
||||
* `STACK_NAME`:在 Docker Compose 中使用的 stack 名称,也用于标签与项目名。该值在 `staging`、`production` 等环境中应各不相同。一个常见做法是将域名中的点替换为短横线,例如 `fastapi-project-example-com` 与 `staging-fastapi-project-example-com`。
|
||||
* `BACKEND_CORS_ORIGINS`:允许的 CORS 源列表,使用逗号分隔。
|
||||
* `SECRET_KEY`:FastAPI 项目的密钥,用于签发 token 等安全相关操作。
|
||||
* `FIRST_SUPERUSER`:首个超级用户的邮箱地址,该用户可以创建其他用户。
|
||||
* `FIRST_SUPERUSER_PASSWORD`:首个超级用户的密码。
|
||||
* `SMTP_HOST`:SMTP 服务器地址,用于发邮件(如 Mailgun、Sparkpost、Sendgrid 等提供的地址)。
|
||||
* `SMTP_USER`:SMTP 用户名。
|
||||
* `SMTP_PASSWORD`:SMTP 密码。
|
||||
* `EMAILS_FROM_EMAIL`:发件邮箱地址。
|
||||
* `POSTGRES_SERVER`:PostgreSQL 服务器主机名。使用 Docker Compose 时可以保持默认值 `db`,除非你改为使用第三方数据库服务。
|
||||
* `POSTGRES_PORT`:PostgreSQL 端口,通常保持默认即可。
|
||||
* `POSTGRES_PASSWORD`:PostgreSQL 密码。
|
||||
* `POSTGRES_USER`:PostgreSQL 用户名,一般使用默认值即可。
|
||||
* `POSTGRES_DB`:本应用使用的数据库名称,默认 `app`。
|
||||
* `SENTRY_DSN`:如果接入了 Sentry,这里填入 DSN。
|
||||
|
||||
## GitHub Actions Environment Variables
|
||||
## GitHub Actions 环境变量
|
||||
|
||||
There are some environment variables only used by GitHub Actions that you can configure:
|
||||
有一些环境变量只在 GitHub Actions 中使用,可以按需配置:
|
||||
|
||||
* `LATEST_CHANGES`: Used by the GitHub Action [latest-changes](https://github.com/tiangolo/latest-changes) to automatically add release notes based on the PRs merged. It's a personal access token, read the docs for details.
|
||||
* `SMOKESHOW_AUTH_KEY`: Used to handle and publish the code coverage using [Smokeshow](https://github.com/samuelcolvin/smokeshow), follow their instructions to create a (free) Smokeshow key.
|
||||
* `LATEST_CHANGES`:用于 [latest-changes](https://github.com/tiangolo/latest-changes) GitHub Action,根据合并的 PR 自动生成发布日志。它需要一个个人访问令牌(PAT),具体可见其文档。
|
||||
* `SMOKESHOW_AUTH_KEY`:用于 [Smokeshow](https://github.com/samuelcolvin/smokeshow) 发布测试覆盖率报告,同样需要按其说明生成一个(免费的)密钥。
|
||||
|
||||
### Generate secret keys
|
||||
### 生成密钥
|
||||
|
||||
Some environment variables in the `.env` file have a default value of `changethis`.
|
||||
`.env` 文件中的一些环境变量默认为 `changethis`,需要改为真正的随机密钥。
|
||||
|
||||
You have to change them with a secret key, to generate secret keys you can run the following command:
|
||||
可以使用下面的命令生成密钥:
|
||||
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
```
|
||||
|
||||
Copy the content and use that as password / secret key. And run that again to generate another secure key.
|
||||
复制输出内容并用作密码 / 密钥;需要多个密钥时多执行几次即可。
|
||||
|
||||
### Deploy with Docker Compose
|
||||
### 使用 Docker Compose 部署
|
||||
|
||||
With the environment variables in place, you can deploy with Docker Compose:
|
||||
环境变量准备就绪后,可以使用 Docker Compose 部署:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml up -d
|
||||
```
|
||||
|
||||
For production you wouldn't want to have the overrides in `docker-compose.override.yml`, that's why we explicitly specify `docker-compose.yml` as the file to use.
|
||||
在生产环境中,一般不希望加载 `docker-compose.override.yml` 中的开发用覆盖配置,因此这里显式指定只使用 `docker-compose.yml`。
|
||||
|
||||
## Continuous Deployment (CD)
|
||||
## 持续部署(CD)
|
||||
|
||||
You can use GitHub Actions to deploy your project automatically. 😎
|
||||
你可以使用 GitHub Actions 自动部署你的项目。😎
|
||||
|
||||
You can have multiple environment deployments.
|
||||
可以为多个环境配置不同的部署流程。
|
||||
|
||||
There are already two environments configured, `staging` and `production`. 🚀
|
||||
本项目中已经预置了 `staging` 与 `production` 两个环境。🚀
|
||||
|
||||
### Install GitHub Actions Runner
|
||||
### 安装 GitHub Actions Runner
|
||||
|
||||
* On your remote server, create a user for your GitHub Actions:
|
||||
* 在远程服务器上为 GitHub Actions 创建一个用户,例如:
|
||||
|
||||
```bash
|
||||
sudo adduser github
|
||||
```
|
||||
|
||||
* Add Docker permissions to the `github` user:
|
||||
* 为 `github` 用户添加 Docker 权限:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker github
|
||||
```
|
||||
|
||||
* Temporarily switch to the `github` user:
|
||||
* 临时切换为 `github` 用户:
|
||||
|
||||
```bash
|
||||
sudo su - github
|
||||
```
|
||||
|
||||
* Go to the `github` user's home directory:
|
||||
* 进入 `github` 用户的 home 目录:
|
||||
|
||||
```bash
|
||||
cd
|
||||
```
|
||||
|
||||
* [Install a GitHub Action self-hosted runner following the official guide](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository).
|
||||
* 按照官方文档安装 GitHub Action 自托管 Runner:
|
||||
<https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository>
|
||||
|
||||
* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later.
|
||||
* 在安装过程中,当被询问标签(labels)时,为 Runner 添加对应环境的标签,例如 `production`;也可以在后续再补充标签。
|
||||
|
||||
After installing, the guide would tell you to run a command to start the runner. Nevertheless, it would stop once you terminate that process or if your local connection to your server is lost.
|
||||
安装完成后,官方文档会让你手动运行一个命令以启动 Runner。但一旦你断开终端连接或进程退出,Runner 就会停止。
|
||||
|
||||
To make sure it runs on startup and continues running, you can install it as a service. To do that, exit the `github` user and go back to the `root` user:
|
||||
为了确保服务器重启后 Runner 仍能自动运行,建议将其安装为服务。具体步骤如下:
|
||||
|
||||
* 从 `github` 用户退出,回到之前的用户:
|
||||
|
||||
```bash
|
||||
exit
|
||||
```
|
||||
|
||||
After you do it, you will be on the previous user again. And you will be on the previous directory, belonging to that user.
|
||||
此时你会回到之前的用户及其当前目录。
|
||||
|
||||
Before being able to go the `github` user directory, you need to become the `root` user (you might already be):
|
||||
* 在能访问 `github` 用户 home 目录之前,你需要切换成 `root` 用户(如果已经是则可略过):
|
||||
|
||||
```bash
|
||||
sudo su
|
||||
```
|
||||
|
||||
* As the `root` user, go to the `actions-runner` directory inside of the `github` user's home directory:
|
||||
* 在 `root` 用户下,进入 `github` 用户 home 下的 `actions-runner` 目录:
|
||||
|
||||
```bash
|
||||
cd /home/github/actions-runner
|
||||
```
|
||||
|
||||
* Install the self-hosted runner as a service with the user `github`:
|
||||
* 安装 Runner 为服务,并指定服务用户为 `github`:
|
||||
|
||||
```bash
|
||||
./svc.sh install github
|
||||
```
|
||||
|
||||
* Start the service:
|
||||
* 启动该服务:
|
||||
|
||||
```bash
|
||||
./svc.sh start
|
||||
```
|
||||
|
||||
* Check the status of the service:
|
||||
* 查看服务状态:
|
||||
|
||||
```bash
|
||||
./svc.sh status
|
||||
```
|
||||
|
||||
You can read more about it in the official guide: [Configuring the self-hosted runner application as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service).
|
||||
更多细节可参考官方文档:
|
||||
「Configuring the self-hosted runner application as a service」
|
||||
<https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service>
|
||||
|
||||
### Set Secrets
|
||||
### 配置 Secrets
|
||||
|
||||
On your repository, configure secrets for the environment variables you need, the same ones described above, including `SECRET_KEY`, etc. Follow the [official GitHub guide for setting repository secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository).
|
||||
在 GitHub 仓库中,为部署需要的环境变量配置 Secrets,包括前面提到的 `SECRET_KEY` 等。可参考官方文档:
|
||||
「Using secrets in GitHub Actions」
|
||||
<https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository>
|
||||
|
||||
The current Github Actions workflows expect these secrets:
|
||||
当前的 GitHub Actions 工作流期望存在以下 Secrets:
|
||||
|
||||
* `DOMAIN_PRODUCTION`
|
||||
* `DOMAIN_STAGING`
|
||||
@@ -271,39 +278,39 @@ The current Github Actions workflows expect these secrets:
|
||||
* `LATEST_CHANGES`
|
||||
* `SMOKESHOW_AUTH_KEY`
|
||||
|
||||
## GitHub Action Deployment Workflows
|
||||
## GitHub Actions 部署工作流
|
||||
|
||||
There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels):
|
||||
在 `.github/workflows` 目录中已经包含了一些用于部署到不同环境(基于 Runner 标签)的 GitHub Actions 工作流:
|
||||
|
||||
* `staging`: after pushing (or merging) to the branch `master`.
|
||||
* `production`: after publishing a release.
|
||||
* `staging`:在推送(或合并)到 `master` 分支后触发。
|
||||
* `production`:在发布 Release 后触发。
|
||||
|
||||
If you need to add extra environments you could use those as a starting point.
|
||||
如果你需要更多环境,可以以上述工作流为模板进行扩展。
|
||||
|
||||
## URLs
|
||||
|
||||
Replace `fastapi-project.example.com` with your domain.
|
||||
下面示例中出现的 `fastapi-project.example.com` 请替换为你自己的域名。
|
||||
|
||||
### Main Traefik Dashboard
|
||||
### 主 Traefik 控制台
|
||||
|
||||
Traefik UI: `https://traefik.fastapi-project.example.com`
|
||||
Traefik UI:`https://traefik.fastapi-project.example.com`
|
||||
|
||||
### Production
|
||||
### 生产环境(Production)
|
||||
|
||||
Frontend: `https://dashboard.fastapi-project.example.com`
|
||||
前端:`https://dashboard.fastapi-project.example.com`
|
||||
|
||||
Backend API docs: `https://api.fastapi-project.example.com/docs`
|
||||
后端 API 文档:`https://api.fastapi-project.example.com/docs`
|
||||
|
||||
Backend API base URL: `https://api.fastapi-project.example.com`
|
||||
后端 API 基础地址:`https://api.fastapi-project.example.com`
|
||||
|
||||
Adminer: `https://adminer.fastapi-project.example.com`
|
||||
Adminer:`https://adminer.fastapi-project.example.com`
|
||||
|
||||
### Staging
|
||||
### 预发布环境(Staging)
|
||||
|
||||
Frontend: `https://dashboard.staging.fastapi-project.example.com`
|
||||
前端:`https://dashboard.staging.fastapi-project.example.com`
|
||||
|
||||
Backend API docs: `https://api.staging.fastapi-project.example.com/docs`
|
||||
后端 API 文档:`https://api.staging.fastapi-project.example.com/docs`
|
||||
|
||||
Backend API base URL: `https://api.staging.fastapi-project.example.com`
|
||||
后端 API 基础地址:`https://api.staging.fastapi-project.example.com`
|
||||
|
||||
Adminer: `https://adminer.staging.fastapi-project.example.com`
|
||||
Adminer:`https://adminer.staging.fastapi-project.example.com`
|
||||
|
||||
+78
-78
@@ -1,34 +1,34 @@
|
||||
# FastAPI Project - Development
|
||||
# FastAPI 项目 - 开发(Development)
|
||||
|
||||
## Docker Compose
|
||||
|
||||
* Start the local stack with Docker Compose:
|
||||
* 使用 Docker Compose 启动本地 stack:
|
||||
|
||||
```bash
|
||||
docker compose watch
|
||||
```
|
||||
|
||||
* Now you can open your browser and interact with these URLs:
|
||||
* 启动后,你可以在浏览器中访问以下地址:
|
||||
|
||||
Frontend, built with Docker, with routes handled based on the path: <http://localhost:5173>
|
||||
前端(通过 Docker 构建,基于路径路由):<http://localhost:5173>
|
||||
|
||||
Backend, JSON based web API based on OpenAPI: <http://localhost:8000>
|
||||
后端(基于 OpenAPI 的 JSON Web API):<http://localhost:8000>
|
||||
|
||||
Automatic interactive documentation with Swagger UI (from the OpenAPI backend): <http://localhost:8000/docs>
|
||||
自动生成的 Swagger UI 交互文档:<http://localhost:8000/docs>
|
||||
|
||||
Adminer, database web administration: <http://localhost:8080>
|
||||
Adminer 数据库管理界面:<http://localhost:8080>
|
||||
|
||||
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
|
||||
Traefik UI(查看代理如何路由请求):<http://localhost:8090>
|
||||
|
||||
**Note**: The first time you start your stack, it might take a minute for it to be ready. While the backend waits for the database to be ready and configures everything. You can check the logs to monitor it.
|
||||
**注意**:第一次启动整个 stack 时,后端会等待数据库就绪并完成配置,这可能需要一点时间。你可以通过查看日志来确认进度。
|
||||
|
||||
To check the logs, run (in another terminal):
|
||||
在另一个终端中查看日志:
|
||||
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
To check the logs of a specific service, add the name of the service, e.g.:
|
||||
查看某个特定服务的日志时,可以加上服务名,例如:
|
||||
|
||||
```bash
|
||||
docker compose logs backend
|
||||
@@ -36,136 +36,136 @@ docker compose logs backend
|
||||
|
||||
## Mailcatcher
|
||||
|
||||
Mailcatcher is a simple SMTP server that catches all emails sent by the backend during local development. Instead of sending real emails, they are captured and displayed in a web interface.
|
||||
Mailcatcher 是一个简单的 SMTP 服务器,在本地开发过程中会拦截后端发出的所有邮件。邮件不会真正发送出去,而是通过 Web 界面展示。
|
||||
|
||||
This is useful for:
|
||||
它适用于:
|
||||
|
||||
* Testing email functionality during development
|
||||
* Verifying email content and formatting
|
||||
* Debugging email-related functionality without sending real emails
|
||||
* 在开发阶段测试邮件功能;
|
||||
* 检查邮件内容与排版;
|
||||
* 调试邮件相关逻辑,而无需发送真实邮件。
|
||||
|
||||
The backend is automatically configured to use Mailcatcher when running with Docker Compose locally (SMTP on port 1025). All captured emails can be viewed at <http://localhost:1080>.
|
||||
在本地使用 Docker Compose 运行时,后端会自动配置为使用 Mailcatcher(SMTP 端口为 1025)。所有被捕获的邮件可以在 <http://localhost:1080> 查看。
|
||||
|
||||
## Local Development
|
||||
## 本地开发
|
||||
|
||||
The Docker Compose files are configured so that each of the services is available in a different port in `localhost`.
|
||||
Docker Compose 文件已经配置好,让每个服务在 `localhost` 上使用不同端口。
|
||||
|
||||
For the backend and frontend, they use the same port that would be used by their local development server, so, the backend is at `http://localhost:8000` and the frontend at `http://localhost:5173`.
|
||||
后端与前端使用的端口与其本地开发服务器一致:后端为 `http://localhost:8000`,前端为 `http://localhost:5173`。
|
||||
|
||||
This way, you could turn off a Docker Compose service and start its local development service, and everything would keep working, because it all uses the same ports.
|
||||
这样你可以随时关闭某个 Docker 服务并启动本地开发服务器,其余服务仍能通过相同端口正常访问。
|
||||
|
||||
For example, you can stop that `frontend` service in the Docker Compose, in another terminal, run:
|
||||
例如,想用本地 Vite 开发服务器,可以在另一个终端中停止 Docker 中的 `frontend` 服务:
|
||||
|
||||
```bash
|
||||
docker compose stop frontend
|
||||
```
|
||||
|
||||
And then start the local frontend development server:
|
||||
然后启动本地前端开发服务器:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Or you could stop the `backend` Docker Compose service:
|
||||
同样地,可以停止 Docker 中的 `backend` 服务:
|
||||
|
||||
```bash
|
||||
docker compose stop backend
|
||||
```
|
||||
|
||||
And then you can run the local development server for the backend:
|
||||
接着运行本地后端开发服务器:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
fastapi dev app/main.py
|
||||
```
|
||||
|
||||
## Docker Compose in `localhost.tiangolo.com`
|
||||
## 在 `localhost.tiangolo.com` 下使用 Docker Compose
|
||||
|
||||
When you start the Docker Compose stack, it uses `localhost` by default, with different ports for each service (backend, frontend, adminer, etc).
|
||||
启动 Docker Compose stack 时,默认使用 `localhost`,不同服务(后端、前端、Adminer 等)分别占用不同端口。
|
||||
|
||||
When you deploy it to production (or staging), it will deploy each service in a different subdomain, like `api.example.com` for the backend and `dashboard.example.com` for the frontend.
|
||||
在生产(或预发布)环境中,每个服务通常使用不同子域名,例如后端使用 `api.example.com`,前端使用 `dashboard.example.com`。
|
||||
|
||||
In the guide about [deployment](deployment.md) you can read about Traefik, the configured proxy. That's the component in charge of transmitting traffic to each service based on the subdomain.
|
||||
在 [deployment.md](deployment.md) 中提到的 Traefik 就是用于根据子域名将请求转发到对应服务的反向代理。
|
||||
|
||||
If you want to test that it's all working locally, you can edit the local `.env` file, and change:
|
||||
如果你想在本地完整测试这套子域名路由方案,可以修改本地 `.env` 文件中的配置:
|
||||
|
||||
```dotenv
|
||||
DOMAIN=localhost.tiangolo.com
|
||||
```
|
||||
|
||||
That will be used by the Docker Compose files to configure the base domain for the services.
|
||||
该值会被 Docker Compose 用于配置各服务的基础域名。
|
||||
|
||||
Traefik will use this to transmit traffic at `api.localhost.tiangolo.com` to the backend, and traffic at `dashboard.localhost.tiangolo.com` to the frontend.
|
||||
Traefik 会将 `api.localhost.tiangolo.com` 的请求转发到后端,将 `dashboard.localhost.tiangolo.com` 的请求转发到前端。
|
||||
|
||||
The domain `localhost.tiangolo.com` is a special domain that is configured (with all its subdomains) to point to `127.0.0.1`. This way you can use that for your local development.
|
||||
`localhost.tiangolo.com` 是一个特殊域名,它及其所有子域名都指向 `127.0.0.1`,非常适合作为本地多子域名开发环境。
|
||||
|
||||
After you update it, run again:
|
||||
修改 `.env` 后,重新运行:
|
||||
|
||||
```bash
|
||||
docker compose watch
|
||||
```
|
||||
|
||||
When deploying, for example in production, the main Traefik is configured outside of the Docker Compose files. For local development, there's an included Traefik in `docker-compose.override.yml`, just to let you test that the domains work as expected, for example with `api.localhost.tiangolo.com` and `dashboard.localhost.tiangolo.com`.
|
||||
在生产环境部署时,主 Traefik 一般在 Docker Compose 之外单独配置。本地开发时,`docker-compose.override.yml` 中包含了一个 Traefik 配置,仅用于测试如 `api.localhost.tiangolo.com` 与 `dashboard.localhost.tiangolo.com` 这样的本地域名是否按预期工作。
|
||||
|
||||
## Docker Compose files and env vars
|
||||
## Docker Compose 文件与环境变量
|
||||
|
||||
There is a main `docker-compose.yml` file with all the configurations that apply to the whole stack, it is used automatically by `docker compose`.
|
||||
主配置文件 `docker-compose.yml` 包含整个 stack 的全部配置,`docker compose` 会自动读取它。
|
||||
|
||||
And there's also a `docker-compose.override.yml` with overrides for development, for example to mount the source code as a volume. It is used automatically by `docker compose` to apply overrides on top of `docker-compose.yml`.
|
||||
另外还有一个 `docker-compose.override.yml` 文件,专门用于开发环境的覆盖配置,例如将源代码目录挂载为 volume。`docker compose` 会自动将其作为 `docker-compose.yml` 的覆盖层进行合并。
|
||||
|
||||
These Docker Compose files use the `.env` file containing configurations to be injected as environment variables in the containers.
|
||||
这些 Docker Compose 文件会读取 `.env` 文件中的配置,并将其作为环境变量注入到容器中。
|
||||
|
||||
They also use some additional configurations taken from environment variables set in the scripts before calling the `docker compose` command.
|
||||
此外,在运行 `docker compose` 命令前,某些脚本可能会设置额外的环境变量供配置使用。
|
||||
|
||||
After changing variables, make sure you restart the stack:
|
||||
修改环境变量后,请记得重启 stack:
|
||||
|
||||
```bash
|
||||
docker compose watch
|
||||
```
|
||||
|
||||
## The .env file
|
||||
## .env 文件
|
||||
|
||||
The `.env` file is the one that contains all your configurations, generated keys and passwords, etc.
|
||||
`.env` 文件包含所有配置、生成的密钥和密码等。
|
||||
|
||||
Depending on your workflow, you could want to exclude it from Git, for example if your project is public. In that case, you would have to make sure to set up a way for your CI tools to obtain it while building or deploying your project.
|
||||
根据你的工作流程,你可能希望将其从 Git 中排除(尤其当项目是公开的)。在这种情况下,需要确保 CI 工具在构建或部署时能够获取到这些配置。
|
||||
|
||||
One way to do it could be to add each environment variable to your CI/CD system, and updating the `docker-compose.yml` file to read that specific env var instead of reading the `.env` file.
|
||||
一种做法是:将每个环境变量添加到 CI/CD 系统中,并修改 `docker-compose.yml`,让它读取对应的环境变量,而不是从 `.env` 文件中读取。
|
||||
|
||||
## Pre-commits and code linting
|
||||
## Pre-commit 与代码检查
|
||||
|
||||
we are using a tool called [pre-commit](https://pre-commit.com/) for code linting and formatting.
|
||||
本项目使用 [pre-commit](https://pre-commit.com/) 进行代码检查与格式化。
|
||||
|
||||
When you install it, it runs right before making a commit in git. This way it ensures that the code is consistent and formatted even before it is committed.
|
||||
安装后,它会在每次 git commit 之前自动运行,确保代码在提交前就保持一致的风格和格式。
|
||||
|
||||
You can find a file `.pre-commit-config.yaml` with configurations at the root of the project.
|
||||
项目根目录下有一个 `.pre-commit-config.yaml` 配置文件,定义了所有检查规则。
|
||||
|
||||
#### Install pre-commit to run automatically
|
||||
#### 安装 pre-commit 以自动运行
|
||||
|
||||
`pre-commit` is already part of the dependencies of the project, but you could also install it globally if you prefer to, following [the official pre-commit docs](https://pre-commit.com/).
|
||||
`pre-commit` 已经作为项目依赖的一部分,你也可以按照 [官方文档](https://pre-commit.com/) 全局安装它。
|
||||
|
||||
After having the `pre-commit` tool installed and available, you need to "install" it in the local repository, so that it runs automatically before each commit.
|
||||
安装好 `pre-commit` 工具后,需要在本地仓库"安装"它,使其在每次提交前自动执行。
|
||||
|
||||
Using `uv`, you could do it with:
|
||||
使用 `uv` 的方式:
|
||||
|
||||
```bash
|
||||
❯ uv run pre-commit install
|
||||
pre-commit installed at .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
Now whenever you try to commit, e.g. with:
|
||||
现在每次执行 commit 时,例如:
|
||||
|
||||
```bash
|
||||
git commit
|
||||
```
|
||||
|
||||
...pre-commit will run and check and format the code you are about to commit, and will ask you to add that code (stage it) with git again before committing.
|
||||
...pre-commit 都会运行,检查并格式化即将提交的代码,如果有修改会要求你重新 `git add` 这些文件后再提交。
|
||||
|
||||
Then you can `git add` the modified/fixed files again and now you can commit.
|
||||
然后你可以 `git add` 修改 / 修复的文件,再执行 commit。
|
||||
|
||||
#### Running pre-commit hooks manually
|
||||
#### 手动运行 pre-commit 钩子
|
||||
|
||||
you can also run `pre-commit` manually on all the files, you can do it using `uv` with:
|
||||
你也可以手动对所有文件运行 `pre-commit`,使用 `uv` 的方式:
|
||||
|
||||
```bash
|
||||
❯ uv run pre-commit run --all-files
|
||||
@@ -178,42 +178,42 @@ eslint...................................................................Passed
|
||||
prettier.................................................................Passed
|
||||
```
|
||||
|
||||
## URLs
|
||||
## 访问地址
|
||||
|
||||
The production or staging URLs would use these same paths, but with your own domain.
|
||||
生产或预发布环境使用相同的路径结构,只是替换为你自己的域名。
|
||||
|
||||
### Development URLs
|
||||
### 开发环境地址
|
||||
|
||||
Development URLs, for local development.
|
||||
以下是本地开发环境的地址。
|
||||
|
||||
Frontend: <http://localhost:5173>
|
||||
前端:<http://localhost:5173>
|
||||
|
||||
Backend: <http://localhost:8000>
|
||||
后端:<http://localhost:8000>
|
||||
|
||||
Automatic Interactive Docs (Swagger UI): <http://localhost:8000/docs>
|
||||
自动生成的交互式文档(Swagger UI):<http://localhost:8000/docs>
|
||||
|
||||
Automatic Alternative Docs (ReDoc): <http://localhost:8000/redoc>
|
||||
自动生成的备选文档(ReDoc):<http://localhost:8000/redoc>
|
||||
|
||||
Adminer: <http://localhost:8080>
|
||||
Adminer:<http://localhost:8080>
|
||||
|
||||
Traefik UI: <http://localhost:8090>
|
||||
Traefik UI:<http://localhost:8090>
|
||||
|
||||
MailCatcher: <http://localhost:1080>
|
||||
MailCatcher:<http://localhost:1080>
|
||||
|
||||
### Development URLs with `localhost.tiangolo.com` Configured
|
||||
### 配置 `localhost.tiangolo.com` 后的开发地址
|
||||
|
||||
Development URLs, for local development.
|
||||
以下是本地开发环境的地址。
|
||||
|
||||
Frontend: <http://dashboard.localhost.tiangolo.com>
|
||||
前端:<http://dashboard.localhost.tiangolo.com>
|
||||
|
||||
Backend: <http://api.localhost.tiangolo.com>
|
||||
后端:<http://api.localhost.tiangolo.com>
|
||||
|
||||
Automatic Interactive Docs (Swagger UI): <http://api.localhost.tiangolo.com/docs>
|
||||
自动生成的交互式文档(Swagger UI):<http://api.localhost.tiangolo.com/docs>
|
||||
|
||||
Automatic Alternative Docs (ReDoc): <http://api.localhost.tiangolo.com/redoc>
|
||||
自动生成的备选文档(ReDoc):<http://api.localhost.tiangolo.com/redoc>
|
||||
|
||||
Adminer: <http://localhost.tiangolo.com:8080>
|
||||
Adminer:<http://localhost.tiangolo.com:8080>
|
||||
|
||||
Traefik UI: <http://localhost.tiangolo.com:8090>
|
||||
Traefik UI:<http://localhost.tiangolo.com:8090>
|
||||
|
||||
MailCatcher: <http://localhost.tiangolo.com:1080>
|
||||
MailCatcher:<http://localhost.tiangolo.com:1080>
|
||||
|
||||
+22
-22
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
|
||||
# Local services are available on their ports, but also available on:
|
||||
# http://api.localhost.tiangolo.com: backend
|
||||
# http://dashboard.localhost.tiangolo.com: frontend
|
||||
# etc. To enable it, update .env, set:
|
||||
# 本地服务可通过其端口访问,也可以通过以下地址访问:
|
||||
# http://api.localhost.tiangolo.com: 后端
|
||||
# http://dashboard.localhost.tiangolo.com: 前端
|
||||
# 等等。要启用此功能,请更新 .env,设置:
|
||||
# DOMAIN=localhost.tiangolo.com
|
||||
proxy:
|
||||
image: traefik:3.0
|
||||
@@ -12,34 +12,34 @@ services:
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8090:8080"
|
||||
# Duplicate the command from docker-compose.yml to add --api.insecure=true
|
||||
# 从 docker-compose.yml 复制命令并添加 --api.insecure=true
|
||||
command:
|
||||
# Enable Docker in Traefik, so that it reads labels from Docker services
|
||||
# 在 Traefik 中启用 Docker,以便它能读取 Docker 服务的标签
|
||||
- --providers.docker
|
||||
# Add a constraint to only use services with the label for this stack
|
||||
# 添加约束,只使用具有此堆栈标签的服务
|
||||
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
|
||||
# Do not expose all Docker services, only the ones explicitly exposed
|
||||
# 不暴露所有 Docker 服务,只暴露明确指定的服务
|
||||
- --providers.docker.exposedbydefault=false
|
||||
# Create an entrypoint "http" listening on port 80
|
||||
# 创建一个监听 80 端口的入口点 "http"
|
||||
- --entrypoints.http.address=:80
|
||||
# Create an entrypoint "https" listening on port 443
|
||||
# 创建一个监听 443 端口的入口点 "https"
|
||||
- --entrypoints.https.address=:443
|
||||
# Enable the access log, with HTTP requests
|
||||
# 启用访问日志,记录 HTTP 请求
|
||||
- --accesslog
|
||||
# Enable the Traefik log, for configurations and errors
|
||||
# 启用 Traefik 日志,用于配置和错误
|
||||
- --log
|
||||
# Enable debug logging for local development
|
||||
# 为本地开发启用调试日志
|
||||
- --log.level=DEBUG
|
||||
# Enable the Dashboard and API
|
||||
# 启用仪表板和 API
|
||||
- --api
|
||||
# Enable the Dashboard and API in insecure mode for local development
|
||||
# 为本地开发以不安全模式启用仪表板和 API
|
||||
- --api.insecure=true
|
||||
labels:
|
||||
# Enable Traefik for this service, to make it available in the public network
|
||||
# 为此服务启用 Traefik,使其在公共网络中可用
|
||||
- traefik.enable=true
|
||||
- traefik.constraint-label=traefik-public
|
||||
# Dummy https-redirect middleware that doesn't really redirect, only to
|
||||
# allow running it locally
|
||||
# 虚拟的 https-redirect 中间件,实际上不进行重定向,
|
||||
# 仅用于允许在本地运行
|
||||
- traefik.http.middlewares.https-redirect.contenttype.autodetect=false
|
||||
networks:
|
||||
- traefik-public
|
||||
@@ -61,7 +61,7 @@ services:
|
||||
- "8000:8000"
|
||||
build:
|
||||
context: ./backend
|
||||
# command: sleep infinity # Infinite loop to keep container alive doing nothing
|
||||
# command: sleep infinity # 无限循环以保持容器存活但不执行任何操作
|
||||
command:
|
||||
- fastapi
|
||||
- run
|
||||
@@ -77,7 +77,7 @@ services:
|
||||
- .venv
|
||||
- path: ./backend/pyproject.toml
|
||||
action: rebuild
|
||||
# TODO: remove once coverage is done locally
|
||||
# TODO: 在本地完成覆盖率测试后移除
|
||||
volumes:
|
||||
- ./backend/htmlcov:/app/htmlcov
|
||||
environment:
|
||||
@@ -118,7 +118,7 @@ services:
|
||||
environment:
|
||||
- VITE_API_URL=http://backend:8000
|
||||
- MAILCATCHER_HOST=http://mailcatcher:1080
|
||||
# For the reports when run locally
|
||||
# 用于本地运行时的报告
|
||||
- PLAYWRIGHT_HTML_HOST=0.0.0.0
|
||||
- CI=${CI}
|
||||
volumes:
|
||||
@@ -129,5 +129,5 @@ services:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
# For local dev, don't expect an external Traefik network
|
||||
# 对于本地开发,不要期望有外部 Traefik 网络
|
||||
external: false
|
||||
|
||||
+3
-3
@@ -131,7 +131,7 @@ services:
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
|
||||
|
||||
# Enable redirection for HTTP and HTTPS
|
||||
# 为 HTTP 和 HTTPS 启用重定向
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
|
||||
|
||||
frontend:
|
||||
@@ -160,12 +160,12 @@ services:
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le
|
||||
|
||||
# Enable redirection for HTTP and HTTPS
|
||||
# 为 HTTP 和 HTTPS 启用重定向
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
|
||||
volumes:
|
||||
app-db-data:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
# Allow setting it to false for testing
|
||||
# 允许在测试时将其设置为 false
|
||||
external: true
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Stage 0, "build-stage", based on Node.js, to build and compile the frontend
|
||||
# 阶段 0, "build-stage", 基于 Node.js, 用于构建和编译前端
|
||||
FROM node:24 AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
@@ -14,7 +14,7 @@ ARG VITE_API_URL=${VITE_API_URL}
|
||||
RUN npm run build
|
||||
|
||||
|
||||
# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
|
||||
# 阶段 1, 基于 Nginx, 只包含已编译的应用, 可通过 Nginx 进行生产部署
|
||||
FROM nginx:1
|
||||
|
||||
COPY --from=build-stage /app/dist/ /usr/share/nginx/html
|
||||
|
||||
+54
-55
@@ -1,153 +1,152 @@
|
||||
# FastAPI Project - Frontend
|
||||
# FastAPI 项目 - 前端(Frontend)
|
||||
|
||||
The frontend is built with [Vite](https://vitejs.dev/), [React](https://reactjs.org/), [TypeScript](https://www.typescriptlang.org/), [TanStack Query](https://tanstack.com/query), [TanStack Router](https://tanstack.com/router) and [Tailwind CSS](https://tailwindcss.com/).
|
||||
前端基于 [Vite](https://vitejs.dev/)、[React](https://reactjs.org/)、[TypeScript](https://www.typescriptlang.org/)、[TanStack Query](https://tanstack.com/query)、[TanStack Router](https://tanstack.com/router) 与 [Tailwind CSS](https://tailwindcss.com/) 构建。
|
||||
|
||||
## Frontend development
|
||||
## 前端开发
|
||||
|
||||
Before you begin, ensure that you have either the Node Version Manager (nvm) or Fast Node Manager (fnm) installed on your system.
|
||||
在开始之前,请确保系统中安装了 Node 版本管理工具:Node Version Manager(nvm)或 Fast Node Manager(fnm)。
|
||||
|
||||
* To install fnm follow the [official fnm guide](https://github.com/Schniz/fnm#installation). If you prefer nvm, you can install it using the [official nvm guide](https://github.com/nvm-sh/nvm#installing-and-updating).
|
||||
* 安装 fnm 可参考 [官方文档](https://github.com/Schniz/fnm#installation);如果你更喜欢 nvm,可参考 [nvm 官方文档](https://github.com/nvm-sh/nvm#installing-and-updating)。
|
||||
|
||||
* After installing either nvm or fnm, proceed to the `frontend` directory:
|
||||
* 安装好 nvm 或 fnm 后,进入 `frontend` 目录:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
* If the Node.js version specified in the `.nvmrc` file isn't installed on your system, you can install it using the appropriate command:
|
||||
|
||||
* 如果系统尚未安装 `.nvmrc` 中指定的 Node.js 版本,可以运行对应命令进行安装:
|
||||
|
||||
```bash
|
||||
# If using fnm
|
||||
# 使用 fnm 时
|
||||
fnm install
|
||||
|
||||
# If using nvm
|
||||
# 使用 nvm 时
|
||||
nvm install
|
||||
```
|
||||
|
||||
* Once the installation is complete, switch to the installed version:
|
||||
* 安装完成后,切换到该版本的 Node.js:
|
||||
|
||||
```bash
|
||||
# If using fnm
|
||||
# 使用 fnm 时
|
||||
fnm use
|
||||
|
||||
# If using nvm
|
||||
# 使用 nvm 时
|
||||
nvm use
|
||||
```
|
||||
|
||||
* Within the `frontend` directory, install the necessary NPM packages:
|
||||
* 在 `frontend` 目录中安装依赖:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
* And start the live server with the following `npm` script:
|
||||
* 使用下面的脚本启动本地开发服务器(支持热更新):
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
* Then open your browser at http://localhost:5173/.
|
||||
* 然后在浏览器中打开 `http://localhost:5173/`。
|
||||
|
||||
Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.
|
||||
注意:这个开发服务器是在本机直接运行的,不在 Docker 容器中。这是推荐的前端开发方式:在本地使用 Vite 的热更新体验进行快速迭代;当前端就绪后,再构建前端 Docker 镜像,在接近生产环境的 Docker 中做最终验证。频繁为每次改动重建 Docker 镜像,会远不如直接用本地开发服务器高效。
|
||||
|
||||
Check the file `package.json` to see other available options.
|
||||
更多可用的 npm 脚本与命令可以查看 `package.json`。
|
||||
|
||||
### Removing the frontend
|
||||
### 移除前端
|
||||
|
||||
If you are developing an API-only app and want to remove the frontend, you can do it easily:
|
||||
如果你只需要开发纯 API(仅后端),想要彻底移除前端,可以按照下面步骤操作:
|
||||
|
||||
* Remove the `./frontend` directory.
|
||||
* 删除整个 `./frontend` 目录。
|
||||
|
||||
* In the `docker-compose.yml` file, remove the whole service / section `frontend`.
|
||||
* 在 `docker-compose.yml` 中删除 `frontend` 服务(整个 service 区块)。
|
||||
|
||||
* In the `docker-compose.override.yml` file, remove the whole service / section `frontend` and `playwright`.
|
||||
* 在 `docker-compose.override.yml` 中删除 `frontend` 与 `playwright` 两个服务的配置。
|
||||
|
||||
Done, you have a frontend-less (api-only) app. 🤓
|
||||
完成后,你就获得了一个“无前端”(仅 API)的应用。🤓
|
||||
|
||||
---
|
||||
|
||||
If you want, you can also remove the `FRONTEND` environment variables from:
|
||||
如果你愿意,还可以从以下位置删除与 `FRONTEND` 相关的环境变量,仅用于清理配置(保留它们也不会产生实际影响):
|
||||
|
||||
* `.env`
|
||||
* `./scripts/*.sh`
|
||||
|
||||
But it would be only to clean them up, leaving them won't really have any effect either way.
|
||||
## 生成前端客户端(API Client)
|
||||
|
||||
## Generate Client
|
||||
### 自动方式
|
||||
|
||||
### Automatically
|
||||
|
||||
* Activate the backend virtual environment.
|
||||
* From the top level project directory, run the script:
|
||||
* 激活后端虚拟环境。
|
||||
* 在项目根目录执行脚本:
|
||||
|
||||
```bash
|
||||
./scripts/generate-client.sh
|
||||
```
|
||||
|
||||
* Commit the changes.
|
||||
* 将生成的变更提交到仓库。
|
||||
|
||||
### Manually
|
||||
### 手动方式
|
||||
|
||||
* Start the Docker Compose stack.
|
||||
* 启动 Docker Compose stack。
|
||||
|
||||
* Download the OpenAPI JSON file from `http://localhost/api/v1/openapi.json` and copy it to a new file `openapi.json` at the root of the `frontend` directory.
|
||||
* 从 `http://localhost/api/v1/openapi.json` 下载 OpenAPI JSON 文件,并将其保存为 `frontend` 目录根下的 `openapi.json`。
|
||||
|
||||
* To generate the frontend client, run:
|
||||
* 生成前端客户端:
|
||||
|
||||
```bash
|
||||
npm run generate-client
|
||||
```
|
||||
|
||||
* Commit the changes.
|
||||
* 将生成的变更提交到仓库。
|
||||
|
||||
Notice that everytime the backend changes (changing the OpenAPI schema), you should follow these steps again to update the frontend client.
|
||||
请注意,每当后端发生影响 OpenAPI Schema 的变更时,都需要重新执行上述步骤以更新前端客户端。
|
||||
|
||||
## Using a Remote API
|
||||
## 使用远程 API
|
||||
|
||||
If you want to use a remote API, you can set the environment variable `VITE_API_URL` to the URL of the remote API. For example, you can set it in the `frontend/.env` file:
|
||||
如果你希望前端调用远程环境的 API,可以设置环境变量 `VITE_API_URL` 为远程 API 的基础 URL,例如在 `frontend/.env` 中配置:
|
||||
|
||||
```env
|
||||
VITE_API_URL=https://api.my-domain.example.com
|
||||
```
|
||||
|
||||
Then, when you run the frontend, it will use that URL as the base URL for the API.
|
||||
之后启动前端时,它会将该地址作为 API 请求的基础 URL。
|
||||
|
||||
## Code Structure
|
||||
## 代码结构
|
||||
|
||||
The frontend code is structured as follows:
|
||||
前端代码结构大致如下:
|
||||
|
||||
* `frontend/src` - The main frontend code.
|
||||
* `frontend/src/assets` - Static assets.
|
||||
* `frontend/src/client` - The generated OpenAPI client.
|
||||
* `frontend/src/components` - The different components of the frontend.
|
||||
* `frontend/src/hooks` - Custom hooks.
|
||||
* `frontend/src/routes` - The different routes of the frontend which include the pages.
|
||||
* `frontend/src` - 前端主要源代码。
|
||||
* `frontend/src/assets` - 静态资源。
|
||||
* `frontend/src/client` - 通过 OpenAPI 生成的前端客户端代码。
|
||||
* `frontend/src/components` - 各种通用组件与业务组件。
|
||||
* `frontend/src/hooks` - 自定义 Hooks。
|
||||
* `frontend/src/routes` - 路由定义及页面组件。
|
||||
|
||||
## End-to-End Testing with Playwright
|
||||
## 使用 Playwright 做端到端测试(E2E)
|
||||
|
||||
The frontend includes initial end-to-end tests using Playwright. To run the tests, you need to have the Docker Compose stack running. Start the stack with the following command:
|
||||
前端中预置了基于 Playwright 的端到端测试。要运行测试,需要先启动 Docker Compose stack。可以使用如下命令启动(至少需要后端):
|
||||
|
||||
```bash
|
||||
docker compose up -d --wait backend
|
||||
```
|
||||
|
||||
Then, you can run the tests with the following command:
|
||||
然后运行 Playwright 测试:
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
You can also run your tests in UI mode to see the browser and interact with it running:
|
||||
你也可以使用 UI 模式运行测试,以便查看浏览器界面并与之交互:
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
To stop and remove the Docker Compose stack and clean the data created in tests, use the following command:
|
||||
测试完成后,如果希望停止并移除 Docker Compose stack,同时清理测试期间创建的数据,可运行:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
To update the tests, navigate to the tests directory and modify the existing test files or add new ones as needed.
|
||||
如需更新或新增 E2E 测试用例,只需到 `frontend/tests` 目录中修改现有测试文件或新增文件。
|
||||
|
||||
For more information on writing and running Playwright tests, refer to the official [Playwright documentation](https://playwright.dev/docs/intro).
|
||||
更多关于编写与运行 Playwright 测试的说明,可参考官方文档:[Playwright documentation](https://playwright.dev/docs/intro)。
|
||||
|
||||
@@ -88,7 +88,7 @@ const EditUser = ({ user, onSuccess }: EditUserProps) => {
|
||||
})
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
// exclude confirm_password from submission data and remove password if empty
|
||||
// 从提交数据中排除 confirm_password,如果密码为空则移除密码字段
|
||||
const { confirm_password: _, ...submitData } = data
|
||||
if (!submitData.password) {
|
||||
delete submitData.password
|
||||
|
||||
@@ -10,17 +10,17 @@ const ErrorComponent = () => {
|
||||
<div className="flex items-center z-10">
|
||||
<div className="flex flex-col ml-4 items-center justify-center p-4">
|
||||
<span className="text-6xl md:text-8xl font-bold leading-none mb-4">
|
||||
Error
|
||||
错误
|
||||
</span>
|
||||
<span className="text-2xl font-bold mb-2">Oops!</span>
|
||||
<span className="text-2xl font-bold mb-2">糟糕!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-lg text-muted-foreground mb-4 text-center z-10">
|
||||
Something went wrong. Please try again.
|
||||
出现了错误。请重试。
|
||||
</p>
|
||||
<Link to="/">
|
||||
<Button>Go Home</Button>
|
||||
<Button>返回首页</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ const UserInformation = () => {
|
||||
const onSubmit = (data: FormData) => {
|
||||
const updateData: UserUpdateMe = {}
|
||||
|
||||
// only include fields that have changed
|
||||
// 仅包含已更改的字段
|
||||
if (data.full_name !== currentUser?.full_name) {
|
||||
updateData.full_name = data.full_name
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ function SidebarProvider({
|
||||
return cookie.split("=")[1] === "true"
|
||||
}
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
// 这是侧边栏的内部状态。
|
||||
// 我们使用 openProp 和 setOpenProp 从组件外部进行控制。
|
||||
const [_open, _setOpen] = React.useState(getInitialOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
@@ -92,18 +92,18 @@ function SidebarProvider({
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
// 设置 cookie 以保持侧边栏状态。
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open],
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
// 切换侧边栏的辅助函数。
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
// 添加键盘快捷键来切换侧边栏。
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
@@ -119,8 +119,8 @@ function SidebarProvider({
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
// 添加状态以便使用 data-state="expanded" 或 "collapsed"。
|
||||
// 这样可以更容易地使用 Tailwind 类来设置侧边栏样式。
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
@@ -243,7 +243,7 @@ function Sidebar({
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
// 为浮动和内嵌变体调整内边距。
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
@@ -438,7 +438,7 @@ function SidebarGroupAction({
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
// 增加移动端按钮的点击区域。
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
@@ -573,7 +573,7 @@ function SidebarMenuAction({
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
// 增加移动端按钮的点击区域。
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
@@ -617,7 +617,7 @@ function SidebarMenuSkeleton({
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
// 50% 到 90% 之间的随机宽度。
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// source: https://usehooks-ts.com/react-hook/use-copy-to-clipboard
|
||||
// 来源: https://usehooks-ts.com/react-hook/use-copy-to-clipboard
|
||||
import { useCallback, useState } from "react"
|
||||
|
||||
type CopiedValue = string | null
|
||||
|
||||
@@ -2,13 +2,13 @@ import { toast } from "sonner"
|
||||
|
||||
const useCustomToast = () => {
|
||||
const showSuccessToast = (description: string) => {
|
||||
toast.success("Success!", {
|
||||
toast.success("成功!", {
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
const showErrorToast = (description: string) => {
|
||||
toast.error("Something went wrong!", {
|
||||
toast.error("出现了错误!", {
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function SignUp() {
|
||||
const onSubmit = (data: FormData) => {
|
||||
if (signUpMutation.isPending) return
|
||||
|
||||
// exclude confirm_password from submission data
|
||||
// 从提交数据中排除 confirm_password
|
||||
const { confirm_password: _confirm_password, ...submitData } = data
|
||||
signUpMutation.mutate(submitData)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ function extractErrorMessage(err: ApiError): string {
|
||||
if (Array.isArray(errDetail) && errDetail.length > 0) {
|
||||
return errDetail[0].msg
|
||||
}
|
||||
return errDetail || "Something went wrong."
|
||||
return errDetail || "出现了错误。"
|
||||
}
|
||||
|
||||
export const handleError = function (
|
||||
|
||||
@@ -75,13 +75,13 @@ test("Sign up with existing email", async ({ page }) => {
|
||||
const email = randomEmail()
|
||||
const password = randomPassword()
|
||||
|
||||
// Sign up with an email
|
||||
// 使用邮箱注册
|
||||
await page.goto("/signup")
|
||||
|
||||
await fillForm(page, fullName, email, password, password)
|
||||
await page.getByRole("button", { name: "Sign Up" }).click()
|
||||
|
||||
// Sign up again with the same email
|
||||
// 使用相同邮箱再次注册
|
||||
await page.goto("/signup")
|
||||
|
||||
await fillForm(page, fullName, email, password, password)
|
||||
|
||||
@@ -41,7 +41,7 @@ export function findLastEmail({
|
||||
}) {
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error("Timeout while trying to get latest email")),
|
||||
() => reject(new Error("获取最新邮件时超时")),
|
||||
timeout,
|
||||
),
|
||||
)
|
||||
@@ -53,7 +53,7 @@ export function findLastEmail({
|
||||
if (emailData) {
|
||||
return emailData
|
||||
}
|
||||
// Wait for 100ms before checking again
|
||||
// 等待 100 毫秒后再次检查
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Note: the `PrivateService` is only available when generating the client
|
||||
// for local environments
|
||||
// 注意:`PrivateService` 仅在为本地环境生成客户端时可用
|
||||
import { OpenAPI, PrivateService } from "../../src/client"
|
||||
|
||||
OpenAPI.BASE = `${process.env.VITE_API_URL}`
|
||||
|
||||
@@ -4,7 +4,7 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite"
|
||||
import react from "@vitejs/plugin-react-swc"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
// Vite 配置文件:https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
+43
-43
@@ -1,8 +1,8 @@
|
||||
# Release Notes
|
||||
# 发布日志(Release Notes)
|
||||
|
||||
## Latest Changes
|
||||
## 最新更改
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* ⬆ Bump @tanstack/react-router from 1.140.0 to 1.141.2 in /frontend. PR [#2045](https://github.com/fastapi/full-stack-fastapi-template/pull/2045) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ⬆ Bump actions/download-artifact from 6 to 7. PR [#2051](https://github.com/fastapi/full-stack-fastapi-template/pull/2051) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Features
|
||||
### 新功能
|
||||
|
||||
* ✨ Add meta title support to all pages. PR [#2039](https://github.com/fastapi/full-stack-fastapi-template/pull/2039) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 🛂 Migrate frontend to Shadcn. PR [#2010](https://github.com/fastapi/full-stack-fastapi-template/pull/2010) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -26,7 +26,7 @@
|
||||
* 🐛 Close sidebar drawer on user selection. PR [#1515](https://github.com/fastapi/full-stack-fastapi-template/pull/1515) by [@dtellz](https://github.com/dtellz).
|
||||
* 🐛 Fix required password validation when editing user fields. PR [#1508](https://github.com/fastapi/full-stack-fastapi-template/pull/1508) by [@jpizquierdo](https://github.com/jpizquierdo).
|
||||
|
||||
### Refactors
|
||||
### 代码重构
|
||||
|
||||
* ♻️ Update password max length. PR [#1447](https://github.com/fastapi/full-stack-fastapi-template/pull/1447) by [@michaelAlvarino](https://github.com/michaelAlvarino).
|
||||
* 🚚 Move backend tests outside the `app` directory. PR [#1862](https://github.com/fastapi/full-stack-fastapi-template/pull/1862) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
@@ -38,7 +38,7 @@
|
||||
* 🎨 Add minor UI tweaks in Skeletons and other components. PR [#1507](https://github.com/fastapi/full-stack-fastapi-template/pull/1507) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 🎨 Add minor UI tweaks. PR [#1506](https://github.com/fastapi/full-stack-fastapi-template/pull/1506) by [@alejsdev](https://github.com/alejsdev).
|
||||
|
||||
### Upgrades
|
||||
### 升级
|
||||
|
||||
* ⬆ Bump @types/react from 19.1.12 to 19.1.13 in /frontend. PR [#1888](https://github.com/fastapi/full-stack-fastapi-template/pull/1888) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* ⬆ Bump @tanstack/router-plugin from 1.131.41 to 1.131.43 in /frontend. PR [#1887](https://github.com/fastapi/full-stack-fastapi-template/pull/1887) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
@@ -65,7 +65,7 @@
|
||||
* ⬆️ Update Playwright. PR [#1793](https://github.com/fastapi/full-stack-fastapi-template/pull/1793) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ⬆️ Upgrade React and related dependencies. PR [#1843](https://github.com/fastapi/full-stack-fastapi-template/pull/1843) by [@alejsdev](https://github.com/alejsdev).
|
||||
|
||||
### Docs
|
||||
### 文档
|
||||
|
||||
* 📝 Add Mailcatcher setup instructions for local email testing. PR [#2038](https://github.com/fastapi/full-stack-fastapi-template/pull/2038) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 📝 Update `README` to include link for Vite. PR [#2037](https://github.com/fastapi/full-stack-fastapi-template/pull/2037) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -73,7 +73,7 @@
|
||||
* 📝 Update docs. PR [#2036](https://github.com/fastapi/full-stack-fastapi-template/pull/2036) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ✏️ Fix small typo in `deployment.md`. PR [#1679](https://github.com/fastapi/full-stack-fastapi-template/pull/1679) by [@cassmtnr](https://github.com/cassmtnr).
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* 🔥 Remove unused dependencies. PR [#2035](https://github.com/fastapi/full-stack-fastapi-template/pull/2035) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ⬆ Bump react-dom from 19.2.0 to 19.2.1 in /frontend. PR [#2032](https://github.com/fastapi/full-stack-fastapi-template/pull/2032) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Features
|
||||
### 新功能
|
||||
|
||||
* 🛂 Migrate to Chakra UI v3 . PR [#1496](https://github.com/fastapi/full-stack-fastapi-template/pull/1496) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ✨ Add private, local only, API for usage in E2E tests. PR [#1429](https://github.com/fastapi/full-stack-fastapi-template/pull/1429) by [@patrick91](https://github.com/patrick91).
|
||||
@@ -208,7 +208,7 @@
|
||||
|
||||
* 🧑🔧 Replace correct value for 'htmlFor'. PR [#1456](https://github.com/fastapi/full-stack-fastapi-template/pull/1456) by [@wesenbergg](https://github.com/wesenbergg).
|
||||
|
||||
### Refactors
|
||||
### 代码重构
|
||||
|
||||
* ♻️ Redirect the user to `login` if we get 401/403. PR [#1501](https://github.com/fastapi/full-stack-fastapi-template/pull/1501) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 🐛 Refactor reset password test to create normal user instead of using super user. PR [#1499](https://github.com/fastapi/full-stack-fastapi-template/pull/1499) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -225,11 +225,11 @@
|
||||
* 🔧 Add `ENV PYTHONUNBUFFERED=1` to log output directly to Docker. PR [#1378](https://github.com/fastapi/full-stack-fastapi-template/pull/1378) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 💡 Remove unnecessary comment. PR [#1260](https://github.com/fastapi/full-stack-fastapi-template/pull/1260) by [@sebhani](https://github.com/sebhani).
|
||||
|
||||
### Upgrades
|
||||
### 升级
|
||||
|
||||
* ⬆️ Update Dockerfile to use uv version 0.5.11. PR [#1454](https://github.com/fastapi/full-stack-fastapi-template/pull/1454) by [@alejsdev](https://github.com/alejsdev).
|
||||
|
||||
### Docs
|
||||
### 文档
|
||||
|
||||
* 📝 Removing deprecated manual client SDK step. PR [#1494](https://github.com/fastapi/full-stack-fastapi-template/pull/1494) by [@chandy](https://github.com/chandy).
|
||||
* 📝 Update Frontend README.md. PR [#1462](https://github.com/fastapi/full-stack-fastapi-template/pull/1462) by [@getmarkus](https://github.com/getmarkus).
|
||||
@@ -237,7 +237,7 @@
|
||||
* 📝 Update `deployment.md`, instructions to install GitHub Runner in non-root VMs. PR [#1412](https://github.com/fastapi/full-stack-fastapi-template/pull/1412) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Add MailCatcher to `development.md`. PR [#1387](https://github.com/fastapi/full-stack-fastapi-template/pull/1387) by [@tobiase](https://github.com/tobiase).
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* 🔧 Configure path alias for cleaner imports. PR [#1497](https://github.com/fastapi/full-stack-fastapi-template/pull/1497) by [@alejsdev](https://github.com/alejsdev).
|
||||
* Bump vite from 5.0.13 to 5.4.14 in /frontend. PR [#1469](https://github.com/fastapi/full-stack-fastapi-template/pull/1469) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
@@ -258,18 +258,18 @@
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Highlights
|
||||
### 亮点
|
||||
|
||||
* Migrate from Poetry to [`uv`](https://github.com/astral-sh/uv).
|
||||
* Simplifications and improvements for Docker Compose files, Traefik Dockerfiles.
|
||||
* Make the API use its own domain `api.example.com` and the frontend use `dashboard.example.com`. This would make it easier to deploy them separately if you needed that.
|
||||
* The backend and frontend on Docker Compose now listen on the same port as the local development servers, this way you can stop the Docker Compose services and run the local development servers without changing the frontend configuration.
|
||||
|
||||
### Features
|
||||
### 新功能
|
||||
|
||||
* 🩺 Add DB healthcheck. PR [#1342](https://github.com/fastapi/full-stack-fastapi-template/pull/1342) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Refactors
|
||||
### 代码重构
|
||||
|
||||
* ♻️ Update settings to use top level `.env` file. PR [#1359](https://github.com/fastapi/full-stack-fastapi-template/pull/1359) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆️ Migrate from Poetry to uv. PR [#1356](https://github.com/fastapi/full-stack-fastapi-template/pull/1356) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -284,16 +284,16 @@
|
||||
* 🔥 Enable support for Arm Docker images in Mac, remove old patch. PR [#1341](https://github.com/fastapi/full-stack-fastapi-template/pull/1341) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ♻️ Remove duplicate information in the ItemCreate model. PR [#1287](https://github.com/fastapi/full-stack-fastapi-template/pull/1287) by [@jjaakko](https://github.com/jjaakko).
|
||||
|
||||
### Upgrades
|
||||
### 升级
|
||||
|
||||
* ⬆️ Upgrade FastAPI. PR [#1349](https://github.com/fastapi/full-stack-fastapi-template/pull/1349) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Docs
|
||||
### 文档
|
||||
|
||||
* 💡 Add comments to Dockerfile with uv references. PR [#1357](https://github.com/fastapi/full-stack-fastapi-template/pull/1357) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Add Email Templates to `backend/README.md`. PR [#1311](https://github.com/fastapi/full-stack-fastapi-template/pull/1311) by [@alejsdev](https://github.com/alejsdev).
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* 👷 Do not sync labels as it overrides manually added labels. PR [#1307](https://github.com/fastapi/full-stack-fastapi-template/pull/1307) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Use uv cache on GitHub Actions. PR [#1366](https://github.com/fastapi/full-stack-fastapi-template/pull/1366) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -321,18 +321,18 @@
|
||||
|
||||
## 0.7.0
|
||||
|
||||
Lots of new things! 🎁
|
||||
很多新功能!🎁
|
||||
|
||||
* E2E tests with Playwright.
|
||||
* Mailcatcher configuration, to develop and test email handling.
|
||||
* Pagination.
|
||||
* UUIDs for database keys.
|
||||
* New user sign up.
|
||||
* Support for deploying to multiple environments (staging, prod).
|
||||
* Many refactors and improvements.
|
||||
* Several dependency upgrades.
|
||||
* 使用 Playwright 进行端到端测试。
|
||||
* Mailcatcher 配置,用于开发和测试邮件处理。
|
||||
* 分页功能。
|
||||
* 数据库主键使用 UUID。
|
||||
* 新用户注册。
|
||||
* 支持部署到多个环境(staging、production)。
|
||||
* 大量重构和改进。
|
||||
* 多个依赖升级。
|
||||
|
||||
### Features
|
||||
### 新功能
|
||||
|
||||
* ✨ Add User Settings e2e tests. PR [#1271](https://github.com/tiangolo/full-stack-fastapi-template/pull/1271) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ✨ Add Reset Password e2e tests. PR [#1270](https://github.com/tiangolo/full-stack-fastapi-template/pull/1270) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -347,7 +347,7 @@ Lots of new things! 🎁
|
||||
* ✨ Add support for deploying multiple environments (staging, production) to the same server. PR [#1128](https://github.com/tiangolo/full-stack-fastapi-template/pull/1128) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 👷 Update CI GitHub Actions to allow running in private repos. PR [#1125](https://github.com/tiangolo/full-stack-fastapi-template/pull/1125) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Fixes
|
||||
### 问题修复
|
||||
|
||||
* 🐛 Fix welcome page to show logged-in user. PR [#1218](https://github.com/tiangolo/full-stack-fastapi-template/pull/1218) by [@tomerb](https://github.com/tomerb).
|
||||
* 🐛 Fix local Traefik proxy network config to fix Gateway Timeouts. PR [#1184](https://github.com/tiangolo/full-stack-fastapi-template/pull/1184) by [@JoelGotsch](https://github.com/JoelGotsch).
|
||||
@@ -355,7 +355,7 @@ Lots of new things! 🎁
|
||||
* 🐛 Fix bug when resetting password. PR [#1171](https://github.com/tiangolo/full-stack-fastapi-template/pull/1171) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 🐛 Fix 403 when the frontend has a directory without an index.html. PR [#1094](https://github.com/tiangolo/full-stack-fastapi-template/pull/1094) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Refactors
|
||||
### 代码重构
|
||||
|
||||
* 🚨 Fix Docker build warning. PR [#1283](https://github.com/tiangolo/full-stack-fastapi-template/pull/1283) by [@erip](https://github.com/erip).
|
||||
* ♻️ Regenerate client to use UUID instead of id integers and update frontend. PR [#1281](https://github.com/tiangolo/full-stack-fastapi-template/pull/1281) by [@rehanabdul](https://github.com/rehanabdul).
|
||||
@@ -411,7 +411,7 @@ Lots of new things! 🎁
|
||||
* ♻ Update Docker image to prevent errors in M1 Macs. PR [#710](https://github.com/tiangolo/full-stack-fastapi-template/pull/710) by [@dudil](https://github.com/dudil).
|
||||
* ✏ Fix typo in variable names in `backend/app/api/routes/items.py` and `backend/app/api/routes/users.py`. PR [#711](https://github.com/tiangolo/full-stack-fastapi-template/pull/711) by [@disrupted](https://github.com/disrupted).
|
||||
|
||||
### Upgrades
|
||||
### 升级
|
||||
|
||||
* ⬆️ Update SQLModel to version `>=0.0.21`. PR [#1275](https://github.com/tiangolo/full-stack-fastapi-template/pull/1275) by [@alejsdev](https://github.com/alejsdev).
|
||||
* ⬆️ Upgrade Traefik. PR [#1241](https://github.com/tiangolo/full-stack-fastapi-template/pull/1241) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -423,7 +423,7 @@ Lots of new things! 🎁
|
||||
* Bump vite from 5.0.12 to 5.0.13 in /frontend. PR [#1149](https://github.com/tiangolo/full-stack-fastapi-template/pull/1149) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
* Bump follow-redirects from 1.15.5 to 1.15.6 in /frontend. PR [#734](https://github.com/tiangolo/full-stack-fastapi-template/pull/734) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
|
||||
### Docs
|
||||
### 文档
|
||||
|
||||
* 📝 Update links from tiangolo repo to fastapi org repo. PR [#1285](https://github.com/fastapi/full-stack-fastapi-template/pull/1285) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 📝 Add End-to-End Testing with Playwright to frontend `README.md`. PR [#1279](https://github.com/tiangolo/full-stack-fastapi-template/pull/1279) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -441,7 +441,7 @@ Lots of new things! 🎁
|
||||
* ✏ Update header titles for consistency. PR [#708](https://github.com/tiangolo/full-stack-fastapi-template/pull/708) by [@codesmith-emmy](https://github.com/codesmith-emmy).
|
||||
* 📝 Update `README.md`, dark mode screenshot position. PR [#706](https://github.com/tiangolo/full-stack-fastapi-template/pull/706) by [@alejsdev](https://github.com/alejsdev).
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* 🔧 Update deploy workflows to exclude the main repository. PR [#1284](https://github.com/tiangolo/full-stack-fastapi-template/pull/1284) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 👷 Update issue-manager.yml GitHub Action permissions. PR [#1278](https://github.com/tiangolo/full-stack-fastapi-template/pull/1278) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -466,15 +466,15 @@ Lots of new things! 🎁
|
||||
|
||||
## 0.6.0
|
||||
|
||||
Latest FastAPI, Pydantic, SQLModel 🚀
|
||||
最新的 FastAPI、Pydantic、SQLModel 🚀
|
||||
|
||||
Brand new frontend with React, TS, Vite, Chakra UI, TanStack Query/Router, generated client/SDK 🎨
|
||||
全新的前端:React、TS、Vite、Chakra UI、TanStack Query/Router、自动生成的客户端/SDK 🎨
|
||||
|
||||
CI/CD - GitHub Actions 🤖
|
||||
|
||||
Test cov > 90% ✅
|
||||
测试覆盖率 > 90% ✅
|
||||
|
||||
### Features
|
||||
### 新功能
|
||||
|
||||
* ✨ Adopt SQLModel, create models, start using it. PR [#559](https://github.com/tiangolo/full-stack-fastapi-template/pull/559) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ✨ Upgrade items router with new SQLModel models, simplified logic, and new FastAPI Annotated dependencies. PR [#560](https://github.com/tiangolo/full-stack-fastapi-template/pull/560) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -522,7 +522,7 @@ Test cov > 90% ✅
|
||||
* Upgrade Traefik to version 2, keeping in sync with DockerSwarm.rocks. PR [#199](https://github.com/tiangolo/full-stack-fastapi-template/pull/199).
|
||||
* Run tests with `TestClient`. PR [#160](https://github.com/tiangolo/full-stack-fastapi-template/pull/160).
|
||||
|
||||
### Fixes
|
||||
### 问题修复
|
||||
|
||||
* 🐛 Fix copier to handle string vars with spaces in quotes. PR [#631](https://github.com/tiangolo/full-stack-fastapi-template/pull/631) by [@estebanx64](https://github.com/estebanx64).
|
||||
* 🐛 Fix allowing a user to update the email to the same email they already have. PR [#696](https://github.com/tiangolo/full-stack-fastapi-template/pull/696) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -535,7 +535,7 @@ Test cov > 90% ✅
|
||||
* 🐛 Fix Celery worker command. PR [#443](https://github.com/tiangolo/full-stack-fastapi-template/pull/443) by [@bechtold](https://github.com/bechtold).
|
||||
* 🐛 Fix Poetry installation in Dockerfile and upgrade Python version and packages to fix Docker build. PR [#480](https://github.com/tiangolo/full-stack-fastapi-template/pull/480) by [@little7Li](https://github.com/little7Li).
|
||||
|
||||
### Refactors
|
||||
### 代码重构
|
||||
|
||||
* 🔧 Add missing dotenv variables. PR [#554](https://github.com/tiangolo/full-stack-fastapi-template/pull/554) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⏪ Revert "⚙️ Add Prettier and ESLint config with pre-commit". PR [#644](https://github.com/tiangolo/full-stack-fastapi-template/pull/644) by [@alejsdev](https://github.com/alejsdev).
|
||||
@@ -594,7 +594,7 @@ Test cov > 90% ✅
|
||||
* Simplify `docker-compose.*.yml` files, refactor deployment to reduce config files. PR [#153](https://github.com/tiangolo/full-stack-fastapi-template/pull/153).
|
||||
* Simplify env var files, merge to a single `.env` file. PR [#151](https://github.com/tiangolo/full-stack-fastapi-template/pull/151).
|
||||
|
||||
### Upgrades
|
||||
### 升级
|
||||
|
||||
* 📌 Upgrade Poetry lock dependencies. PR [#702](https://github.com/tiangolo/full-stack-fastapi-template/pull/702) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆️ Upgrade Python version and dependencies. PR [#558](https://github.com/tiangolo/full-stack-fastapi-template/pull/558) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -606,7 +606,7 @@ Test cov > 90% ✅
|
||||
* ⬆ Add `uvicorn[standard]` to include `watchgod` and `uvloop`. PR [#438](https://github.com/tiangolo/full-stack-fastapi-template/pull/438) by [@alonme](https://github.com/alonme).
|
||||
* ⬆ Upgrade code to support pydantic V2. PR [#615](https://github.com/tiangolo/full-stack-fastapi-template/pull/615) by [@estebanx64](https://github.com/estebanx64).
|
||||
|
||||
### Docs
|
||||
### 文档
|
||||
|
||||
* 🦇 Add dark mode to `README.md`. PR [#703](https://github.com/tiangolo/full-stack-fastapi-template/pull/703) by [@alejsdev](https://github.com/alejsdev).
|
||||
* 🍱 Update GitHub image. PR [#701](https://github.com/tiangolo/full-stack-fastapi-template/pull/701) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -628,7 +628,7 @@ Test cov > 90% ✅
|
||||
* Add docs about reporting test coverage in HTML. PR [#161](https://github.com/tiangolo/full-stack-fastapi-template/pull/161).
|
||||
* Add docs about removing the frontend, for an API-only app. PR [#156](https://github.com/tiangolo/full-stack-fastapi-template/pull/156).
|
||||
|
||||
### Internal
|
||||
### 内部更改
|
||||
|
||||
* 👷 Add Lint to GitHub Actions outside of tests. PR [#688](https://github.com/tiangolo/full-stack-fastapi-template/pull/688) by [@tiangolo](https://github.com/tiangolo).
|
||||
* ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.1.2. PR [#643](https://github.com/tiangolo/full-stack-fastapi-template/pull/643) by [@dependabot[bot]](https://github.com/apps/dependabot).
|
||||
|
||||
Reference in New Issue
Block a user