mirror of
https://github.com/fastapi/full-stack-fastapi-template.git
synced 2026-11-03 20:36:27 +00:00
Compare commits
1 Commits
0.11.1
..
0eff31727b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eff31727b |
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
environment: production
|
||||||
|
# Do not deploy in the main repository, only in user projects
|
||||||
|
if: github.repository_owner != 'fastapi'
|
||||||
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- production
|
||||||
|
env:
|
||||||
|
DOMAIN: ${{ secrets.DOMAIN }}
|
||||||
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
|
FIRST_SUPERUSER: ${{ secrets.FIRST_SUPERUSER }}
|
||||||
|
FIRST_SUPERUSER_PASSWORD: ${{ secrets.FIRST_SUPERUSER_PASSWORD }}
|
||||||
|
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||||
|
EMAILS_FROM_EMAIL: ${{ secrets.EMAILS_FROM_EMAIL }}
|
||||||
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- run: docker compose -f compose.yml -f compose.deploy.yml build
|
||||||
|
- run: docker compose -f compose.yml -f compose.deploy.yml run --rm backend bash scripts/prestart.sh
|
||||||
|
- run: docker compose -f compose.yml -f compose.deploy.yml up -d
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
name: Deploy
|
name: Deploy to Staging
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths-ignore:
|
|
||||||
- release-notes.md
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
environment: staging
|
||||||
# Do not deploy in the main repository, only in user projects
|
# Do not deploy in the main repository, only in user projects
|
||||||
if: github.repository_owner != 'fastapi'
|
if: github.repository_owner != 'fastapi'
|
||||||
runs-on: self-hosted
|
runs-on:
|
||||||
|
- self-hosted
|
||||||
|
- staging
|
||||||
env:
|
env:
|
||||||
DOMAIN: ${{ secrets.DOMAIN }}
|
DOMAIN: ${{ secrets.DOMAIN }}
|
||||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
||||||
version: "0.11.18"
|
version: "0.11.18"
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- run: docker compose up -d db mailcatcher
|
- run: docker compose up -d db mailpit
|
||||||
- name: Migrate DB
|
- name: Migrate DB
|
||||||
run: uv run bash scripts/prestart.sh
|
run: uv run bash scripts/prestart.sh
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
- 🔒 Secure password hashing by default.
|
- 🔒 Secure password hashing by default.
|
||||||
- 🔑 JWT (JSON Web Token) authentication.
|
- 🔑 JWT (JSON Web Token) authentication.
|
||||||
- 📫 Email based password recovery.
|
- 📫 Email based password recovery.
|
||||||
- 📬 [Mailcatcher](https://mailcatcher.me) for local email testing during development.
|
- 📬 [Mailpit](https://mailpit.axllent.org) for local email testing during development.
|
||||||
- ✅ Tests with [Pytest](https://pytest.org).
|
- ✅ Tests with [Pytest](https://pytest.org).
|
||||||
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
- 📞 [Traefik](https://traefik.io) as a reverse proxy / load balancer.
|
||||||
- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik.
|
- 🚢 Deployment instructions using Docker Compose with automatic HTTPS provided by Traefik.
|
||||||
|
|||||||
+2
-2
@@ -9,10 +9,10 @@
|
|||||||
|
|
||||||
Run the backend locally and connect it to PostgreSQL in Docker Compose.
|
Run the backend locally and connect it to PostgreSQL in Docker Compose.
|
||||||
|
|
||||||
From the project root, start PostgreSQL and Mailcatcher:
|
From the project root, start PostgreSQL and Mailpit:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker compose up -d db mailcatcher
|
$ docker compose up -d db mailpit
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, from `./backend/`, install the dependencies, prepare the database, and start the development server:
|
Then, from `./backend/`, install the dependencies, prepare the database, and start the development server:
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ services:
|
|||||||
- ./backend/htmlcov:/app/backend/htmlcov
|
- ./backend/htmlcov:/app/backend/htmlcov
|
||||||
environment:
|
environment:
|
||||||
FASTAPI_ENV: "development"
|
FASTAPI_ENV: "development"
|
||||||
SMTP_HOST: "mailcatcher"
|
SMTP_HOST: "mailpit"
|
||||||
SMTP_PORT: "1025"
|
SMTP_PORT: "1025"
|
||||||
SMTP_TLS: "false"
|
SMTP_TLS: "false"
|
||||||
|
|
||||||
mailcatcher:
|
mailpit:
|
||||||
image: schickling/mailcatcher
|
image: axllent/mailpit
|
||||||
ports:
|
ports:
|
||||||
- "1080:1080"
|
- "8025:8025"
|
||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
|
|
||||||
playwright:
|
playwright:
|
||||||
@@ -83,13 +83,13 @@ services:
|
|||||||
ipc: host
|
ipc: host
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
- mailcatcher
|
- mailpit
|
||||||
environment:
|
environment:
|
||||||
- FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set}
|
- FIRST_SUPERUSER=${FIRST_SUPERUSER:?Variable not set}
|
||||||
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set}
|
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD:?Variable not set}
|
||||||
- PLAYWRIGHT_BASE_URL=http://backend:8000
|
- PLAYWRIGHT_BASE_URL=http://backend:8000
|
||||||
- VITE_API_URL=http://backend:8000
|
- VITE_API_URL=http://backend:8000
|
||||||
- MAILCATCHER_HOST=http://mailcatcher:1080
|
- MAILPIT_HOST=http://mailpit:8025
|
||||||
# For the reports when run locally
|
# For the reports when run locally
|
||||||
- PLAYWRIGHT_HTML_HOST=0.0.0.0
|
- PLAYWRIGHT_HTML_HOST=0.0.0.0
|
||||||
- CI=${CI:-}
|
- CI=${CI:-}
|
||||||
|
|||||||
+48
-12
@@ -4,13 +4,13 @@ You can deploy the project using Docker Compose to a remote server.
|
|||||||
|
|
||||||
The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application.
|
The deployment Docker Compose configuration includes Traefik to handle HTTPS and route incoming traffic to the application.
|
||||||
|
|
||||||
The included GitHub Actions workflow can deploy the application automatically.
|
You can use CI/CD (continuous integration and continuous deployment) systems to deploy automatically, there are already configurations to do it with GitHub Actions.
|
||||||
|
|
||||||
But you have to configure a couple things first. 🤓
|
But you have to configure a couple things first. 🤓
|
||||||
|
|
||||||
## Preparation
|
## Preparation
|
||||||
|
|
||||||
* Have a remote server ready and available.
|
* Have a remote server ready and available. Use a separate server for each environment, for example one for staging and one for production.
|
||||||
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`.
|
* Configure DNS records pointing to the server for the application domain and any supporting service subdomains you want to expose, e.g. `fastapi-project.example.com` and `adminer.fastapi-project.example.com`.
|
||||||
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
* Install and configure [Docker](https://docs.docker.com/engine/install/) on the remote server (Docker Engine, not Docker Desktop).
|
||||||
|
|
||||||
@@ -82,15 +82,12 @@ You can set several other environment variables:
|
|||||||
* `EMAILS_FROM_EMAIL`: The email account to send emails from.
|
* `EMAILS_FROM_EMAIL`: The email account to send emails from.
|
||||||
* `SENTRY_DSN`: The DSN for Sentry, if you are using it.
|
* `SENTRY_DSN`: The DSN for Sentry, if you are using it.
|
||||||
|
|
||||||
## GitHub Repository Automation
|
## GitHub Actions Environment Variables
|
||||||
|
|
||||||
Install the following GitHub Apps to enable the included repository automation:
|
There are some environment variables only used by GitHub Actions that you can configure:
|
||||||
|
|
||||||
* [Latest Changes](https://github.com/apps/latest-changes) updates `release-notes.md` when a pull request is merged.
|
* `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.
|
||||||
* [PR Push](https://github.com/apps/pr-push) lets the pre-commit workflow push automated fixes to pull request branches.
|
* `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.
|
||||||
* [PR Submit](https://github.com/apps/pr-submit) lets the **Bump pre-commit hooks** and **Prepare Release** workflows create pull requests.
|
|
||||||
|
|
||||||
To publish code coverage with [Smokeshow](https://github.com/samuelcolvin/smokeshow), add `SMOKESHOW_AUTH_KEY` as a repository secret.
|
|
||||||
|
|
||||||
### Deploy with Docker Compose
|
### Deploy with Docker Compose
|
||||||
|
|
||||||
@@ -109,7 +106,7 @@ The `compose.deploy.yml` file adds the deployment settings to the shared configu
|
|||||||
|
|
||||||
You can use GitHub Actions to deploy your project automatically. 😎
|
You can use GitHub Actions to deploy your project automatically. 😎
|
||||||
|
|
||||||
The included `deploy.yml` workflow deploys the application whenever changes are pushed to `master`, including when a pull request is merged. 🚀
|
There are already two environment deployments configured, `staging` and `production`. Each environment should be deployed to a separate server. 🚀
|
||||||
|
|
||||||
### Install GitHub Actions Runner
|
### Install GitHub Actions Runner
|
||||||
|
|
||||||
@@ -139,6 +136,8 @@ 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).
|
* [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).
|
||||||
|
|
||||||
|
* When asked about labels, add a label for the environment, e.g. `production`. You can also add labels later.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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:
|
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:
|
||||||
@@ -181,9 +180,17 @@ cd /home/github/actions-runner
|
|||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
### Set Repository Secrets
|
### Configure GitHub Environments
|
||||||
|
|
||||||
In your repository, go to **Settings** > **Secrets and variables** > **Actions** and add the following [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository):
|
The deployment workflows use [GitHub Environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) for `staging` and `production`. This enables environment-specific secrets, deployment protection rules (e.g. required reviewers, wait timers), and deployment status tracking.
|
||||||
|
|
||||||
|
To configure them, go to your repository's **Settings** > **Environments** and create the `staging` and `production` environments.
|
||||||
|
|
||||||
|
### Set Secrets
|
||||||
|
|
||||||
|
For each GitHub Environment (`staging` and `production`), configure the required secrets as [environment secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-an-environment). Environment secrets are preferred over [repository secrets](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository) because they are scoped to the specific environment, reducing exposure and aligning with any protection rules you configure.
|
||||||
|
|
||||||
|
The deployment workflows require these secrets:
|
||||||
|
|
||||||
* `DOMAIN`
|
* `DOMAIN`
|
||||||
* `FIRST_SUPERUSER`
|
* `FIRST_SUPERUSER`
|
||||||
@@ -200,12 +207,41 @@ To enable emails, configure these additional secrets with the values from your e
|
|||||||
|
|
||||||
To enable Sentry, configure the `SENTRY_DSN` secret.
|
To enable Sentry, configure the `SENTRY_DSN` secret.
|
||||||
|
|
||||||
|
## GitHub Action Deployment Workflows
|
||||||
|
|
||||||
|
There are GitHub Action workflows in the `.github/workflows` directory already configured for deploying to the environments (GitHub Actions runners with the labels):
|
||||||
|
|
||||||
|
* `staging`: after pushing (or merging) to the branch `master`.
|
||||||
|
* `production`: after publishing a release.
|
||||||
|
|
||||||
|
Both workflows are associated with their respective GitHub Environments, so deployments will be visible in the repository's **Environments** section and will respect any protection rules you configure.
|
||||||
|
|
||||||
|
### Prepare a Release
|
||||||
|
|
||||||
|
Install the [PR Submit GitHub App](https://github.com/apps/pr-submit) in your repository to enable the **Prepare Release** workflow.
|
||||||
|
|
||||||
|
Run the workflow manually from the **Actions** tab and select the version bump. It creates a pull request that updates the release notes. When you merge that pull request, the **Create Draft Release** workflow creates a draft GitHub release with the corresponding release notes.
|
||||||
|
|
||||||
|
Review and publish the draft release to trigger the production deployment.
|
||||||
|
|
||||||
|
If you need to add extra environments you could use those as a starting point.
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
Replace `fastapi-project.example.com` with your domain.
|
Replace `fastapi-project.example.com` with your domain.
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
Application (frontend and API): `https://fastapi-project.example.com`
|
Application (frontend and API): `https://fastapi-project.example.com`
|
||||||
|
|
||||||
Interactive API docs: `https://fastapi-project.example.com/docs`
|
Interactive API docs: `https://fastapi-project.example.com/docs`
|
||||||
|
|
||||||
Adminer: `https://adminer.fastapi-project.example.com`
|
Adminer: `https://adminer.fastapi-project.example.com`
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
Application (frontend and API): `https://staging.fastapi-project.example.com`
|
||||||
|
|
||||||
|
Interactive API docs: `https://staging.fastapi-project.example.com/docs`
|
||||||
|
|
||||||
|
Adminer: `https://adminer.staging.fastapi-project.example.com`
|
||||||
|
|||||||
+8
-8
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
For local development, run PostgreSQL and Mailcatcher with Docker Compose, and run the FastAPI and Vite development servers locally.
|
For local development, run PostgreSQL and Mailpit with Docker Compose, and run the FastAPI and Vite development servers locally.
|
||||||
|
|
||||||
Start the supporting services:
|
Start the supporting services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d db mailcatcher
|
docker compose up -d db mailpit
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, from the `backend` directory, install the dependencies and prepare the database:
|
Then, from the `backend` directory, install the dependencies and prepare the database:
|
||||||
@@ -38,7 +38,7 @@ Backend API: <http://localhost:8000>
|
|||||||
|
|
||||||
Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs>
|
Automatic interactive API documentation with Swagger UI: <http://localhost:8000/docs>
|
||||||
|
|
||||||
Mailcatcher: <http://localhost:1080>
|
Mailpit: <http://localhost:8025>
|
||||||
|
|
||||||
The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`.
|
The frontend development server uses the backend at `http://localhost:8000`, as configured in `frontend/.env`.
|
||||||
|
|
||||||
@@ -71,15 +71,15 @@ Adminer, database web administration: <http://localhost:8080>
|
|||||||
|
|
||||||
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
|
Traefik UI, to see how the routes are being handled by the proxy: <http://localhost:8090>
|
||||||
|
|
||||||
Mailcatcher: <http://localhost:1080>
|
Mailpit: <http://localhost:8025>
|
||||||
|
|
||||||
Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`.
|
Stop a locally running FastAPI server before starting the Compose backend because both use port `8000`.
|
||||||
|
|
||||||
**Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service.
|
**Note**: The first time you start the stack, it might take a minute for all the services to be ready. To monitor it, use `docker compose logs`, or `docker compose logs backend` for the backend service.
|
||||||
|
|
||||||
## Mailcatcher
|
## Mailpit
|
||||||
|
|
||||||
Mailcatcher captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailcatcher` service. Captured emails are available at <http://localhost:1080>.
|
[Mailpit](https://mailpit.axllent.org) captures emails sent during local development instead of delivering them. The local backend connects to it at `localhost:1025`, and the Compose backend connects to the `mailpit` service. Captured emails are available at <http://localhost:8025>.
|
||||||
|
|
||||||
## Docker Compose files and env vars
|
## Docker Compose files and env vars
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ biome check..............................................................Passed
|
|||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
|
|
||||||
The deployed URLs use these same paths, but with your own domain.
|
The production or staging URLs would use these same paths, but with your own domain.
|
||||||
|
|
||||||
### Development URLs
|
### Development URLs
|
||||||
|
|
||||||
@@ -172,4 +172,4 @@ Adminer: <http://localhost:8080>
|
|||||||
|
|
||||||
Traefik UI: <http://localhost:8090>
|
Traefik UI: <http://localhost:8090>
|
||||||
|
|
||||||
MailCatcher: <http://localhost:1080>
|
Mailpit: <http://localhost:8025>
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
VITE_API_URL=http://localhost:8000
|
VITE_API_URL=http://localhost:8000
|
||||||
MAILCATCHER_HOST=http://localhost:1080
|
MAILPIT_HOST=http://localhost:8025
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, test } from "@playwright/test"
|
||||||
import { findLastEmail } from "./utils/mailcatcher"
|
import { emailUrl, findLastEmail } from "./utils/mailpit"
|
||||||
import { randomEmail, randomPassword } from "./utils/random"
|
import { randomEmail, randomPassword } from "./utils/random"
|
||||||
import { logInUser, signUpNewUser } from "./utils/user"
|
import { logInUser, signUpNewUser } from "./utils/user"
|
||||||
|
|
||||||
@@ -46,13 +46,11 @@ test("User can reset password successfully using the link", async ({
|
|||||||
|
|
||||||
const emailData = await findLastEmail({
|
const emailData = await findLastEmail({
|
||||||
request,
|
request,
|
||||||
filter: (e) => e.recipients.includes(`<${email}>`),
|
query: `to:${email}`,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(emailUrl(emailData))
|
||||||
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const selector = 'a[href*="/reset-password?token="]'
|
const selector = 'a[href*="/reset-password?token="]'
|
||||||
|
|
||||||
@@ -99,13 +97,11 @@ test("Weak new password validation", async ({ page, request }) => {
|
|||||||
|
|
||||||
const emailData = await findLastEmail({
|
const emailData = await findLastEmail({
|
||||||
request,
|
request,
|
||||||
filter: (e) => e.recipients.includes(`<${email}>`),
|
query: `to:${email}`,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto(
|
await page.goto(emailUrl(emailData))
|
||||||
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const selector = 'a[href*="/reset-password?token="]'
|
const selector = 'a[href*="/reset-password?token="]'
|
||||||
const url = await page.getAttribute(selector, "href")
|
const url = await page.getAttribute(selector, "href")
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import type { APIRequestContext } from "@playwright/test"
|
|
||||||
|
|
||||||
type Email = {
|
|
||||||
id: number
|
|
||||||
recipients: string[]
|
|
||||||
subject: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findEmail({
|
|
||||||
request,
|
|
||||||
filter,
|
|
||||||
}: {
|
|
||||||
request: APIRequestContext
|
|
||||||
filter?: (email: Email) => boolean
|
|
||||||
}) {
|
|
||||||
const response = await request.get(`${process.env.MAILCATCHER_HOST}/messages`)
|
|
||||||
|
|
||||||
let emails = await response.json()
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
emails = emails.filter(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = emails[emails.length - 1]
|
|
||||||
|
|
||||||
if (email) {
|
|
||||||
return email as Email
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findLastEmail({
|
|
||||||
request,
|
|
||||||
filter,
|
|
||||||
timeout = 5000,
|
|
||||||
}: {
|
|
||||||
request: APIRequestContext
|
|
||||||
filter?: (email: Email) => boolean
|
|
||||||
timeout?: number
|
|
||||||
}) {
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Timeout while trying to get latest email")),
|
|
||||||
timeout,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
const checkEmails = async () => {
|
|
||||||
while (true) {
|
|
||||||
const emailData = await findEmail({ request, filter })
|
|
||||||
|
|
||||||
if (emailData) {
|
|
||||||
return emailData
|
|
||||||
}
|
|
||||||
// Wait for 100ms before checking again
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.race([timeoutPromise, checkEmails()])
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { APIRequestContext } from "@playwright/test"
|
||||||
|
|
||||||
|
type Address = {
|
||||||
|
Name: string
|
||||||
|
Address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Email = {
|
||||||
|
ID: string
|
||||||
|
To: Address[]
|
||||||
|
Subject: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findEmail({
|
||||||
|
request,
|
||||||
|
query,
|
||||||
|
}: {
|
||||||
|
request: APIRequestContext
|
||||||
|
query: string
|
||||||
|
}) {
|
||||||
|
const response = await request.get(
|
||||||
|
`${process.env.MAILPIT_HOST}/api/v1/search`,
|
||||||
|
{
|
||||||
|
params: { query, limit: 1 },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { messages }: { messages: Email[] } = await response.json()
|
||||||
|
|
||||||
|
return messages[0] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findLastEmail({
|
||||||
|
request,
|
||||||
|
query,
|
||||||
|
timeout = 5000,
|
||||||
|
}: {
|
||||||
|
request: APIRequestContext
|
||||||
|
query: string
|
||||||
|
timeout?: number
|
||||||
|
}) {
|
||||||
|
const deadline = Date.now() + timeout
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const email = await findEmail({ request, query })
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timeout while trying to get the latest email for "${query}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emailUrl(email: Email) {
|
||||||
|
return `${process.env.MAILPIT_HOST}/view/${email.ID}.html`
|
||||||
|
}
|
||||||
@@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
## Latest Changes
|
## Latest Changes
|
||||||
|
|
||||||
## 0.11.1 (2026-08-11)
|
|
||||||
|
|
||||||
### Refactors
|
### Refactors
|
||||||
|
|
||||||
* ♻️ Simplify Docker Compose deployment. PR [#2435](https://github.com/fastapi/full-stack-fastapi-template/pull/2435) by [@tiangolo](https://github.com/tiangolo).
|
|
||||||
* ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91).
|
* ♻️ Migrate to `DATABASE_URL` instead of separate variables. PR [#2184](https://github.com/fastapi/full-stack-fastapi-template/pull/2184) by [@patrick91](https://github.com/patrick91).
|
||||||
|
|
||||||
### Internal
|
### Internal
|
||||||
|
|||||||
Reference in New Issue
Block a user