Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions .env
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
# Domain
# This would be set to the production domain with an env var on deployment
# used by Traefik to transmit traffic and aqcuire TLS certificates
DOMAIN=localhost
# To test the local Traefik config
# DOMAIN=localhost.tiangolo.com
DOMAIN="op://Environments/My Full-Stack Template/$ENVIRONMENT/DOMAIN"

# Used by the backend to generate links in emails to the frontend
FRONTEND_HOST=http://localhost:5173
FRONTEND_HOST="op://Environments/My Full-Stack Template/$ENVIRONMENT/FRONTEND_HOST"
# In staging and production, set this env var to the frontend host, e.g.
# FRONTEND_HOST=https://dashboard.example.com

# Environment: local, staging, production
ENVIRONMENT=local

PROJECT_NAME="Full Stack FastAPI Project"
STACK_NAME=full-stack-fastapi-project
PROJECT_NAME='Gym Bro'
STACK_NAME="op://Environments/My Full-Stack Template/$ENVIRONMENT/STACK_NAME"

# Backend
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173,http://localhost.tiangolo.com"
SECRET_KEY=changethis
FIRST_SUPERUSER=[email protected]
FIRST_SUPERUSER_PASSWORD=changethis
BACKEND_CORS_ORIGINS="op://Environments/My Full-Stack Template/$ENVIRONMENT/BACKEND_CORS_ORIGINS"
SECRET_KEY="op://Environments/My Full-Stack Template/SECRET_KEY"
FIRST_SUPERUSER="op://Environments/My Full-Stack Template/FIRST_SUPERUSER"
FIRST_SUPERUSER_PASSWORD="op://Environments/My Full-Stack Template/FIRST_SUPERUSER_PASSWORD"

# Emails
SMTP_HOST=
SMTP_USER=
SMTP_PASSWORD=
EMAILS_FROM_EMAIL=[email protected]
SMTP_TLS=True
SMTP_SSL=False
SMTP_PORT=587
SMTP_HOST="op://Environments/Synology SMTP Client/SMPT_HOST"
SMTP_USER="op://Environments/Synology SMTP Client/SMTP_USER"
SMTP_PASSWORD="op://Environments/Synology SMTP Client/SMPT_PASSWORD"
EMAILS_FROM_EMAIL="op://Environments/My Full-Stack Template/EMAILS_FROM_EMAIL"
SMTP_TLS="op://Environments/My Full-Stack Template/SMTP_TLS"
SMTP_SSL="op://Environments/My Full-Stack Template/SMTP_SSL"
SMTP_PORT="op://Environments/My Full-Stack Template/SMTP_PORT"

# Postgres
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=app
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changethis
POSTGRES_SERVER="op://Environments/My Full-Stack Template/POSTGRES_SERVER"
POSTGRES_PORT="op://Environments/My Full-Stack Template/POSTGRES_PORT"
POSTGRES_DB="op://Environments/My Full-Stack Template/POSTGRES_DB"
POSTGRES_USER="op://Environments/My Full-Stack Template/POSTGRES_USER"
POSTGRES_PASSWORD="op://Environments/My Full-Stack Template/POSTGRES_PASSWORD"

SENTRY_DSN=
SENTRY_DSN="op://Environments/My Full-Stack Template/SENTRY_DSN"

# Configure these with your own Docker registry images
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_FRONTEND=frontend
DOCKER_IMAGE_BACKEND="op://Environments/My Full-Stack Template/$ENVIRONMENT/DOCKER_IMAGE_BACKEND"
DOCKER_IMAGE_FRONTEND="op://Environments/My Full-Stack Template/$ENVIRONMENT/DOCKER_IMAGE_FRONTEND"


## NextJS
NEXT_AUTH_SECRET="op://Environments/My Full-Stack Template/NEXT_AUTH_SECRET"
20 changes: 7 additions & 13 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@ jobs:
- staging
env:
ENVIRONMENT: staging
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
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 }}
PROJECT_NAME: my-full-stack-template-staging
steps:
- name: Checkout
uses: actions/checkout@v4
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
- run: docker compose -f docker-compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
- name: Load secrets from 1Password
uses: 1password/load-secrets-action/configure@v2
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- run: op run --env-file=".env" -- docker compose -f docker-compose.yml --project-name ${{ env.PROJECT_NAME }} build
- run: op run --env-file=".env" -- docker compose -f docker-compose.yml --project-name ${{ env.PROJECT_NAME }} up -d
2 changes: 1 addition & 1 deletion .github/workflows/generate-client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
node-version: lts/*
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,27 @@ jobs:
if: ${{ needs.changes.outputs.changed == 'true' }}
timeout-minutes: 60
runs-on: ubuntu-latest
env:
ENVIRONMENT: local
strategy:
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Load secrets from 1Password
uses: 1password/load-secrets-action/configure@v2
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: lts/*
- uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
Expand All @@ -68,13 +76,13 @@ jobs:
working-directory: backend
- run: npm ci
working-directory: frontend
- run: uv run bash scripts/generate-client.sh
- run: op run --env-file=".env" -- uv run bash scripts/generate-client.sh
env:
VIRTUAL_ENV: backend/.venv
- run: docker compose build
- run: op run --env-file=".env" -- docker compose build
- run: docker compose down -v --remove-orphans
- name: Run Playwright tests
run: docker compose run --rm playwright npx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: op run --env-file=".env" -- docker compose run --rm playwright npx playwright test --fail-on-flaky-tests --trace=retain-on-failure --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- run: docker compose down -v --remove-orphans
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/smokeshow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- run: pip install smokeshow
- uses: actions/download-artifact@v4
with:
Expand Down
16 changes: 12 additions & 4 deletions .github/workflows/test-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,33 @@ on:
jobs:
test-backend:
runs-on: ubuntu-latest
env:
ENVIRONMENT: local
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Load secrets from 1Password
uses: 1password/load-secrets-action/configure@v2
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.4.15"
enable-cache: true
- run: docker compose down -v --remove-orphans
- run: docker compose up -d db mailcatcher
- run: op run --no-masking --env-file="./.env" -- docker compose up -d db mailcatcher
- name: Migrate DB
run: uv run bash scripts/prestart.sh
run: op run --no-masking --env-file="../.env" -- uv run bash scripts/prestart.sh
working-directory: backend
- name: Run tests
run: uv run bash scripts/tests-start.sh "Coverage for ${{ github.sha }}"
run: op run --no-masking --env-file="../.env" -- uv run bash scripts/tests-start.sh "Coverage for ${{ github.sha }}"
working-directory: backend
- run: docker compose down -v --remove-orphans
- name: Store coverage files
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/test-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ jobs:

test-docker-compose:
runs-on: ubuntu-latest
env:
ENVIRONMENT: local
steps:
- name: Checkout
uses: actions/checkout@v4
- run: docker compose build
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Load secrets from 1Password
uses: 1password/load-secrets-action/configure@v2
with:
service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- run: op run --env-file=".env" -- docker compose build
- run: docker compose down -v --remove-orphans
- run: docker compose up -d --wait backend frontend adminer
- run: op run --env-file=".env" -- docker compose up -d --wait backend frontend adminer
- name: Test backend is up
run: curl http://localhost:8000/api/v1/utils/health-check
- name: Test frontend is up
Expand Down
23 changes: 23 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export ENVIRONMENT=local


watch:
op run --env-file=".env" -- docker compose watch

up:
op run --env-file=".env" -- docker compose up

build:
op run --env-file=".env" -- docker compose build

playwright:
op run --env-file=".env" -- docker compose build; \
op run --env-file=".env" -- docker compose up -d --wait backend mailcatcher; \
cd frontend; \
op run --env-file="../.env" -- npx playwright test --fail-on-flaky-tests --trace=retain-on-failure;

generate-client:
op run --env-file=".env" -- ./scripts/generate-client.sh


.PHONY: watch up build playwright generate-client
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Full Stack FastAPI Template

<a href="https://github.com/fastapi/full-stack-fastapi-template/actions?query=workflow%3ATest" target="_blank"><img src="https://github.com/fastapi/full-stack-fastapi-template/workflows/Test/badge.svg" alt="Test"></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>
## Disclaimer


This project is a fork from [https://github.com/fastapi/full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template) but customized with my own stack of technologies. It is not intended to be used as a template for everyone, but feel free to cherry-pick any features that interest you.

## Technology Stack and Features

Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.10
FROM python:3.12

ENV PYTHONUNBUFFERED=1

Expand Down
16 changes: 16 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export ENVIRONMENT=local

test:
docker compose down -v --remove-orphans; \
op run --no-masking --env-file="../.env" -- docker compose up -d db mailcatcher; \
op run --no-masking --env-file="../.env" -- uv run bash scripts/prestart.sh; \
op run --no-masking --env-file="../.env" -- uv run bash scripts/tests-start.sh $(MSG); \
docker compose down -v --remove-orphans;

lint:
uv run bash scripts/lint.sh

format:
uv run bash scripts/format.sh

.PHONY: test lint format
14 changes: 0 additions & 14 deletions backend/app/api/main.py

This file was deleted.

File renamed without changes.
17 changes: 17 additions & 0 deletions backend/app/auth/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from sqlmodel import Field, SQLModel


# JSON payload containing access token
class Token(SQLModel):
access_token: str
token_type: str = "bearer"


# Contents of JWT token
class TokenPayload(SQLModel):
sub: str | None = None


class NewPassword(SQLModel):
token: str
new_password: str = Field(min_length=8, max_length=40)
14 changes: 14 additions & 0 deletions backend/app/auth/repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from sqlmodel import Session

from app.core.security import verify_password
from app.users.models import User
from app.users.repository import get_user_by_email


def authenticate(*, session: Session, email: str, password: str) -> User | None:
db_user = get_user_by_email(session=session, email=email)
if not db_user:
return None
if not verify_password(password, db_user.hashed_password):
return None
return db_user
19 changes: 11 additions & 8 deletions backend/app/api/routes/login.py → backend/app/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
from fastapi.responses import HTMLResponse
from fastapi.security import OAuth2PasswordRequestForm

from app import crud
from app.api.deps import CurrentUser, SessionDep, get_current_active_superuser
from app.auth import repository as auth_repository
from app.auth.models import NewPassword, Token
from app.core import security
from app.core.config import settings
from app.core.security import get_password_hash
from app.models import Message, NewPassword, Token, UserPublic
from app.utils import (
from app.models import Message
from app.users import repository as users_repository
from app.users.dependencies import CurrentUser, SessionDep, get_current_active_superuser
from app.users.models import UserPublic
from app.utils.utils import (
generate_password_reset_token,
generate_reset_password_email,
send_email,
Expand All @@ -28,7 +31,7 @@ def login_access_token(
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.authenticate(
user = auth_repository.authenticate(
session=session, email=form_data.username, password=form_data.password
)
if not user:
Expand Down Expand Up @@ -56,7 +59,7 @@ def recover_password(email: str, session: SessionDep) -> Message:
"""
Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
user = users_repository.get_user_by_email(session=session, email=email)

if not user:
raise HTTPException(
Expand All @@ -83,7 +86,7 @@ def reset_password(session: SessionDep, body: NewPassword) -> Message:
email = verify_password_reset_token(token=body.token)
if not email:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud.get_user_by_email(session=session, email=email)
user = users_repository.get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=404,
Expand All @@ -107,7 +110,7 @@ def recover_password_html_content(email: str, session: SessionDep) -> Any:
"""
HTML Content for Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
user = users_repository.get_user_by_email(session=session, email=email)

if not user:
raise HTTPException(
Expand Down
Loading
Loading