Repository: vintasoftware/nextjs-fastapi-template Branch: main Commit: 62b67456e8f0 Files: 144 Total size: 425.3 KB Directory structure: gitextract_r3jbnx09/ ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yml │ ├── pre-commit.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.docker.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── docker-compose.yml ├── docs/ │ ├── additional-settings.md │ ├── contributing.md │ ├── deployment.md │ ├── get-started.md │ ├── stylesheets/ │ │ └── extra.css │ ├── support.md │ └── technology-selection.md ├── fastapi_backend/ │ ├── .gitignore │ ├── Dockerfile │ ├── alembic.ini │ ├── alembic_migrations/ │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions/ │ │ ├── 402d067a8b92_added_user_table.py │ │ └── b389592974f8_add_item_model.py │ ├── api/ │ │ └── index.py │ ├── app/ │ │ ├── __init__.py │ │ ├── config.py │ │ ├── database.py │ │ ├── email.py │ │ ├── email_templates/ │ │ │ ├── __init__.py │ │ │ └── password_reset.html │ │ ├── main.py │ │ ├── models.py │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ └── items.py │ │ ├── schemas.py │ │ ├── users.py │ │ └── utils.py │ ├── commands/ │ │ ├── __init__.py │ │ └── generate_openapi_schema.py │ ├── mypy.ini │ ├── pyproject.toml │ ├── pytest.ini │ ├── requirements.txt │ ├── start.sh │ ├── tests/ │ │ ├── __init__.py │ │ ├── commands/ │ │ │ ├── __init__.py │ │ │ ├── files/ │ │ │ │ ├── openapi_test.json │ │ │ │ └── openapi_test_output.json │ │ │ └── test_generate_openapi_schema.py │ │ ├── conftest.py │ │ ├── main/ │ │ │ ├── __init__.py │ │ │ └── test_main.py │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ └── test_items.py │ │ ├── test_database.py │ │ ├── test_email.py │ │ └── utils/ │ │ ├── __init__.py │ │ └── test_utils.py │ ├── vercel.json │ ├── vercel.prod.json │ └── watcher.py ├── local-shared-data/ │ └── openapi.json ├── mkdocs.yml ├── nextjs-frontend/ │ ├── .gitignore │ ├── .prettierignore │ ├── Dockerfile │ ├── __tests__/ │ │ ├── login.test.tsx │ │ ├── loginPage.test.tsx │ │ ├── passwordReset.test.tsx │ │ ├── passwordResetConfirm.test.tsx │ │ ├── passwordResetConfirmPage.test.tsx │ │ ├── passwordResetPage.test.tsx │ │ ├── register.test.ts │ │ └── registerPage.test.tsx │ ├── app/ │ │ ├── clientService.ts │ │ ├── dashboard/ │ │ │ ├── add-item/ │ │ │ │ └── page.tsx │ │ │ ├── deleteButton.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── login/ │ │ │ └── page.tsx │ │ ├── openapi-client/ │ │ │ ├── client/ │ │ │ │ ├── client.gen.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.gen.ts │ │ │ │ └── utils.gen.ts │ │ │ ├── client.gen.ts │ │ │ ├── core/ │ │ │ │ ├── auth.gen.ts │ │ │ │ ├── bodySerializer.gen.ts │ │ │ │ ├── params.gen.ts │ │ │ │ ├── pathSerializer.gen.ts │ │ │ │ ├── serverSentEvents.gen.ts │ │ │ │ ├── types.gen.ts │ │ │ │ └── utils.gen.ts │ │ │ ├── index.ts │ │ │ ├── sdk.gen.ts │ │ │ └── types.gen.ts │ │ ├── page.tsx │ │ ├── password-recovery/ │ │ │ ├── confirm/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── register/ │ │ └── page.tsx │ ├── components/ │ │ ├── actions/ │ │ │ ├── items-action.ts │ │ │ ├── login-action.ts │ │ │ ├── logout-action.ts │ │ │ ├── password-reset-action.ts │ │ │ └── register-action.ts │ │ ├── page-pagination.tsx │ │ ├── page-size-selector.tsx │ │ └── ui/ │ │ ├── FormError.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── submitButton.tsx │ │ ├── table.tsx │ │ └── tabs.tsx │ ├── components.json │ ├── eslint.config.mjs │ ├── jest.config.ts │ ├── next.config.mjs │ ├── openapi-ts.config.ts │ ├── openapi.json │ ├── package.json │ ├── pnpm-workspace.yaml │ ├── postcss.config.js │ ├── proxy.ts │ ├── start.sh │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── vercel.json │ └── watcher.js ├── overrides/ │ └── main.html ├── prod-backend-deploy.yml └── prod-frontend-deploy.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Description ## Motivation and Context ## Screenshots (if appropriate): ## Steps to reproduce (if appropriate): ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] My code follows the code style of this project. - [ ] My change requires documentation updates. - [ ] I have updated the documentation accordingly. - [ ] My change requires dependencies updates. - [ ] I have updated the dependencies accordingly. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: pull_request: jobs: build-fastapi: name: FastAPI CI runs-on: ubuntu-latest env: DATABASE_URL: ${{ secrets.DATABASE_URL }} TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} ACCESS_SECRET_KEY: ${{ secrets.ACCESS_SECRET_KEY }} RESET_PASSWORD_SECRET_KEY: ${{ secrets.RESET_PASSWORD_SECRET_KEY }} VERIFICATION_SECRET_KEY: ${{ secrets.VERIFICATION_SECRET_KEY }} CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }} services: postgres: image: postgres:17 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: testdatabase ports: - 5433:5432 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install the project working-directory: ./fastapi_backend run: uv sync --all-extras --dev - name: Run tests working-directory: ./fastapi_backend run: uv run coverage run -m pytest - name: Generate XML coverage report working-directory: ./fastapi_backend run: uv run coverage xml -o coverage.xml - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: python-coverage parallel: true path-to-lcov: fastapi_backend/coverage.xml build-frontend: name: Next.js CI runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 9 run_install: false - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: Install Node dependencies working-directory: ./nextjs-frontend run: pnpm install - name: Run tests working-directory: ./nextjs-frontend run: pnpm run coverage - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: node-coverage parallel: true finish: name: Coveralls needs: [ build-fastapi, build-frontend ] runs-on: ubuntu-latest steps: - name: Close parallel build uses: coverallsapp/github-action@v2.3.4 with: parallel-finished: true carryforward: "python-coverage,node-coverage" ================================================ FILE: .github/workflows/pre-commit.yml ================================================ name: pre-commit on: push: pull_request: jobs: pre-commit: runs-on: ubuntu-latest env: DATABASE_URL: ${{ secrets.DATABASE_URL }} TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} ACCESS_SECRET_KEY: ${{ secrets.ACCESS_SECRET_KEY }} RESET_PASSWORD_SECRET_KEY: ${{ secrets.RESET_PASSWORD_SECRET_KEY }} VERIFICATION_SECRET_KEY: ${{ secrets.VERIFICATION_SECRET_KEY }} OPENAPI_OUTPUT_FILE: ${{ secrets.OPENAPI_OUTPUT_FILE }} CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install the project working-directory: ./fastapi_backend run: uv sync --all-extras --dev - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 9 run_install: false - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: Install Node dependencies working-directory: ./nextjs-frontend run: pnpm install - name: Run pre-commit uses: pre-commit/action@v3.0.1 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release draft creation on: workflow_dispatch: inputs: version: description: "Version of the release" required: true type: number jobs: release: name: Release runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Extract changelog for version run: | VERSION=${{ github.event.inputs.version }} # Extract changelog for version CHANGELOG=$(awk -v version="$VERSION" 'BEGIN{RS="## "; FS="\n"} $0 ~ version {print "## "$0}' CHANGELOG.md) # Remove the first line (version title) CHANGELOG=$(echo "$CHANGELOG" | sed '1d') # Verify if changelog was found if [ -z "$CHANGELOG" ]; then echo "Changelog for version $VERSION not found" exit 1 fi # Set output echo "CHANGELOG<> $GITHUB_ENV echo "$CHANGELOG" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - name: Create GitHub Release Draft uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.version }} name: ${{ github.event.inputs.version }} body: ${{ env.CHANGELOG }} draft: true prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Summary run: | echo "## 🚀 Release Summary" >> $GITHUB_STEP_SUMMARY echo "Release draft created for version ${{ github.event.inputs.version }}." >> $GITHUB_STEP_SUMMARY echo "Visit the [Releases section](https://github.com/vintasoftware/nextjs-fastapi-template/releases) to review and publish the release." >> $GITHUB_STEP_SUMMARY echo "Once the draft is published, another action will automatically be triggered to publish the packages." >> $GITHUB_STEP_SUMMARY ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ ================================================ FILE: .pre-commit-config.docker.yaml ================================================ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-added-large-files args: ["--maxkb=500"] exclude: > (?x)^( package-lock\.json )$ - id: fix-byte-order-marker - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: debug-statements - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.1.8 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format - repo: local hooks: - id: generate-openapi-schema name: generate OpenAPI schema entry: sh -c 'docker compose run --rm --no-deps -T backend uv run python -m commands.generate_openapi_schema' language: system # Only run OpenAPI schema generation if schemas.py, main.py or package version have changed: files: (main\.py$|schemas\.py$|pyproject\.toml) pass_filenames: false - id: generate-frontend-client name: generate frontend client entry: sh -c 'docker compose run --rm --no-deps -T frontend pnpm run generate-client' language: system # Only run frontend client generation if frontend files have changed: files: ^local-shared-data/openapi\.json$ pass_filenames: false ================================================ FILE: .pre-commit-config.yaml ================================================ fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-added-large-files args: ["--maxkb=500"] exclude: > (?x)^( package-lock\.json )$ - id: fix-byte-order-marker - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: debug-statements - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.12.2 hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format - repo: local hooks: - id: frontend-lint name: run frontend lint entry: sh -c 'cd nextjs-frontend && pnpm run lint' language: system types: [ file ] files: ^nextjs-frontend/.*\.(js|jsx|ts|tsx)$ pass_filenames: true - id: frontend-prettier name: Run Prettier on frontend files entry: sh -c 'cd nextjs-frontend && pnpm run prettier' language: system types: [ file ] files: ^nextjs-frontend/.*\.(js|jsx|ts|tsx)$ pass_filenames: true - id: frontend-tsc name: run frontend tsc entry: sh -c 'cd nextjs-frontend && pnpm run tsc' language: system types: [ file ] files: ^nextjs-frontend/.*\.(ts|tsx)$ pass_filenames: false - id: generate-openapi-schema name: generate OpenAPI schema entry: sh -c 'cd fastapi_backend && uv run python -m commands.generate_openapi_schema' language: system # Only run OpenAPI schema generation if schemas.py, main.py or package version have changed: files: (main\.py$|schemas\.py$|pyproject\.toml) pass_filenames: false - id: generate-frontend-client name: generate frontend client entry: sh -c 'cd nextjs-frontend && pnpm run generate-client' language: system # Only run frontend client generation if frontend files have changed: files: openapi\.json$ pass_filenames: false ================================================ FILE: CHANGELOG.md ================================================ # Changelog This changelog references changes made both to the FastAPI backend, `fastapi_backend`, and the frontend TypeScript client, `nextjs-frontend`. !!! note The backend and the frontend are versioned together, that is, they have the same version number. When you update the backend, you should also update the frontend to the same version. ## 0.0.8 December 17, 2025 {id="0.0.8"} - Upgrade Next.js version to latest version ## 0.0.7 October 24, 2025 {id="0.0.7"} - Upgrade @hey-api/openapi-ts version to ^0.83.1 ## 0.0.6 September 1, 2025 {id="0.0.6"} - Upgrade Next.js version to 15.5.0 ## 0.0.5 July 9, 2025 {id="0.0.5"} - Items Pagination ## 0.0.4 July 9, 2025 {id="0.0.4"} - Fix ESlint missing for pre-commit ## 0.0.3 April 23, 2025 {id="0.0.3"} - Created docs ## 0.0.2 March 12, 2025 {id="0.0.2"} - Generate release draft using github actions ## 0.0.1 March 12, 2025 {id="0.0.1"} - Initial release ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Makefile # Variables BACKEND_DIR=fastapi_backend FRONTEND_DIR=nextjs-frontend DOCKER_COMPOSE=docker compose # Help .PHONY: help help: @echo "Available commands:" @awk '/^[a-zA-Z_-]+:/{split($$1, target, ":"); print " " target[1] "\t" substr($$0, index($$0,$$2))}' $(MAKEFILE_LIST) # Backend commands .PHONY: start-backend test-backend start-backend: ## Start the backend server with FastAPI and hot reload cd $(BACKEND_DIR) && ./start.sh test-backend: ## Run backend tests using pytest cd $(BACKEND_DIR) && uv run pytest # Frontend commands .PHONY: start-frontend test-frontend start-frontend: ## Start the frontend server with pnpm and hot reload cd $(FRONTEND_DIR) && ./start.sh test-frontend: ## Run frontend tests using npm cd $(FRONTEND_DIR) && pnpm run test # Docker commands .PHONY: docker-backend-shell docker-frontend-shell docker-build docker-build-backend \ docker-build-frontend docker-start-backend docker-start-frontend docker-up-test-db \ docker-migrate-db docker-db-schema docker-test-backend docker-test-frontend docker-backend-shell: ## Access the backend container shell $(DOCKER_COMPOSE) run --rm backend sh docker-frontend-shell: ## Access the frontend container shell $(DOCKER_COMPOSE) run --rm frontend sh docker-build: ## Build all the services $(DOCKER_COMPOSE) build --no-cache docker-build-backend: ## Build the backend container with no cache $(DOCKER_COMPOSE) build backend --no-cache docker-build-frontend: ## Build the frontend container with no cache $(DOCKER_COMPOSE) build frontend --no-cache docker-start-backend: ## Start the backend container $(DOCKER_COMPOSE) up backend docker-start-frontend: ## Start the frontend container $(DOCKER_COMPOSE) up frontend docker-up-test-db: ## Start the test database container $(DOCKER_COMPOSE) up db_test docker-migrate-db: ## Run database migrations using Alembic $(DOCKER_COMPOSE) run --rm backend alembic upgrade head docker-db-schema: ## Generate a new migration schema. Usage: make docker-db-schema migration_name="add users" $(DOCKER_COMPOSE) run --rm backend alembic revision --autogenerate -m "$(migration_name)" docker-test-backend: ## Run tests for the backend $(DOCKER_COMPOSE) run --rm backend pytest docker-test-frontend: ## Run tests for the frontend $(DOCKER_COMPOSE) run --rm frontend pnpm run test docker-up-mailhog: ## Start mailhog server $(DOCKER_COMPOSE) up mailhog ================================================ FILE: README.md ================================================ ## Next.js FastAPI Template Next.js FastAPI Template

Next.js FastAPI Template: Python + Modern TypeScript stack with Zod validation.

CI Coverage

--- **Documentation**: https://vintasoftware.github.io/nextjs-fastapi-template/ **Source Code**: https://github.com/vintasoftware/nextjs-fastapi-template/ --- The Next.js FastAPI Template provides a solid foundation for scalable, high-performance web applications, following clean architecture and best practices. It simplifies development by integrating FastAPI, Pydantic, and Next.js with TypeScript and Zod, ensuring end-to-end type safety and schema validation between frontend and backend. The FastAPI backend supports fully asynchronous operations, optimizing database queries, API routes, and test execution for better performance. Deployment is seamless, with both backend and frontend fully deployable to Vercel, enabling quick product releases with minimal configuration. ### Key features ✔ End-to-end type safety – Automatically generated typed clients from the OpenAPI schema ensure seamless API contracts between frontend and backend. ✔ Hot-reload updates – The client updates automatically when backend routes change, keeping FastAPI and Next.js in sync. ✔ Versatile foundation – Designed for MVPs and production-ready applications, with a pre-configured authentication system and API layer. ✔ Quick deployment – Deploys a full-stack application—including authentication flow and a dashboard—on Vercel in just a few steps. ✔ Production-ready authentication – Includes a pre-configured authentication system and dashboard interface, allowing you to immediately start development with user management features. ## Technology stack This template features a carefully selected set of technologies to ensure efficiency, scalability, and ease of use: - Zod + TypeScript – Type safety and schema validation across the stack. - fastapi-users – Complete authentication system with: - Secure password hashing - JWT authentication - Email-based password recovery - shadcn/ui – Prebuilt React components with Tailwind CSS. - OpenAPI-fetch – Fully typed client generation from the OpenAPI schema. - UV – Simplified dependency management and packaging. - Docker Compose – Consistent environments for development and production. - Pre-commit hooks – Automated code linting, formatting, and validation before commits. - Vercel Deployment – Serverless backend and scalable frontend, deployable with minimal configuration. This is a partial list of the technologies included in the template. For a complete overview, visit our [Technology selection](https://vintasoftware.github.io/nextjs-fastapi-template/technology-selection/) page. ## Get Started To use this template, visit our [Get Started](https://vintasoftware.github.io/nextjs-fastapi-template/get-started/) and follow the steps. ## Using the template? Let's talk! We’re always curious to see how the community builds on top of it and where it’s being used. To collaborate: - Join the conversation on [GitHub Discussions](https://github.com/vintasoftware/nextjs-fastapi-template/discussions) - Report bugs or suggest improvements via [issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues) - Check the [Contributing](https://vintasoftware.github.io/nextjs-fastapi-template/contributing/) guide to get involved This project is maintained by [Vinta Software](https://www.vinta.com.br/) and is actively used in production systems we build for clients. Talk to our expert consultants — get a free technical review: contact@vinta.com.br. *Disclaimer: This project is not affiliated with Vercel.* ================================================ FILE: docker-compose.yml ================================================ services: backend: build: context: fastapi_backend environment: - OPENAPI_OUTPUT_FILE=./shared-data/openapi.json - DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/mydatabase - TEST_DATABASE_URL=postgresql+asyncpg://postgres:password@db:5433/testdatabase - MAIL_SERVER=mailhog ports: - "8000:8000" networks: - my_network volumes: - ./fastapi_backend:/app - fastapi-venv:/app/.venv - ./local-shared-data:/app/shared-data depends_on: - db db: image: postgres:17 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: mydatabase ports: - "5432:5432" networks: - my_network volumes: - postgres_data:/var/lib/postgresql/data db_test: image: postgres:17 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: testdatabase ports: - "5433:5432" networks: - my_network restart: always frontend: build: context: ./nextjs-frontend user: node ports: - "3000:3000" networks: - my_network environment: NODE_ENV: development API_BASE_URL: http://backend:8000 OPENAPI_OUTPUT_FILE: ./shared-data/openapi.json volumes: - ./nextjs-frontend:/app - nextjs-node-modules:/app/node_modules - ./local-shared-data:/app/shared-data mailhog: image: mailhog/mailhog ports: - "1025:1025" # SMTP server - "8025:8025" # Web UI networks: - my_network volumes: postgres_data: nextjs-node-modules: fastapi-venv: networks: my_network: driver: bridge ================================================ FILE: docs/additional-settings.md ================================================ ### Production-Ready Authentication & Dashboard features This template comes with a pre-configured authentication system and a simple dashboard interface, allowing you to start building your application with user management features immediately. ### Hot Reload on development The project includes two hot reloads running the application, one for the backend and one for the frontend. These automatically restart local servers when they detect changes, ensuring that the application is always up to date without needing manual restarts. - The **backend hot reload** monitors changes to the backend code. - The **frontend hot reload** monitors changes to the frontend code and the `openapi.json` schema generated by the backend. ### Manual Execution of Hot Reload Commands You can manually execute the same commands that the hot reloads call when they detect a change: 1. To export the `openapi.json` schema: ```bash cd fastapi_backend && uv run python -m commands.generate_openapi_schema ``` or using Docker: ```bash docker compose run --rm --no-deps -T backend uv run python -m commands.generate_openapi_schema ``` 2. To generate the frontend client: ```bash cd nextjs-frontend && npm run generate-client ``` or using Docker: ```bash docker compose run --rm --no-deps -T frontend npm run generate-client ``` ### Testing To run the tests, you need to run the test database container: ```bash make docker-up-test-db ``` Then run the tests locally: ```bash make test-backend make test-frontend ``` Or using Docker: ```bash make docker-test-backend make docker-test-frontend ``` ### Pre-Commit Setup To maintain code quality and consistency, the project includes two separate pre-commit configuration files: - `.pre-commit-config.yaml` is used to run pre-commit checks locally. - `.pre-commit-config.docker.yaml` is used to run pre-commit checks within Docker. ### Installing and Activating Pre-Commit Hooks To activate pre-commit hooks, run the following commands for each configuration file: - **For the local configuration file**: ```bash pre-commit install -c .pre-commit-config.yaml ``` - **For the Docker configuration file**: ```bash pre-commit install -c .pre-commit-config.docker.yaml ``` ### Localhost Email Server Setup To set up the email server locally, you need to start [MailHog](https://github.com/mailhog/MailHog) by running the following command: ```bash make docker-up-mailhog ``` - **Email client**: Access the email at `http://localhost:8025`. ### Running Pre-Commit Checks To manually run the pre-commit checks on all files, use: ```bash pre-commit run --all-files -c .pre-commit-config.yaml ``` or ```bash pre-commit run --all-files -c .pre-commit-config.docker.yaml ``` ### Updating Pre-Commit Hooks To update the hooks to their latest versions, run: ```bash pre-commit autoupdate ``` ### Alembic Database Migrations If you need to create a new Database Migration: ```bash make docker-db-schema migration_name="add users" ``` then apply the migration to the database: ```bash make docker-migrate-db ``` ### GitHub Actions This project has a pre-configured GitHub Actions setup to enable CI/CD. The workflow configuration files are inside the .github/workflows directory. You can customize these workflows to suit your project's needs better. ### Secrets Configuration For the workflows to function correctly, add the secret keys to your GitHub repository's settings. Navigate to Settings > Secrets and variables > Actions and add the following keys: ``` DATABASE_URL: The connection string for your primary database. TEST_DATABASE_URL: The connection string for your test database. ACCESS_SECRET_KEY: The secret key for access token generation. RESET_PASSWORD_SECRET_KEY: The secret key for reset password functionality. VERIFICATION_SECRET_KEY: The secret key for email or user verification. ``` ## Makefile This project includes a `Makefile` that provides a set of commands to simplify everyday tasks such as starting the backend and frontend servers, running tests, building Docker containers, and more. ### Available Commands You can see all available commands and their descriptions by running the following command in your terminal: ```bash make help ``` ================================================ FILE: docs/contributing.md ================================================ # Contributing We can always use your help to improve Next.js FastAPI Template! Please feel free to tackle existing [issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues). If you have a new idea, please create a thread on [Discussions](https://github.com/vintasoftware/django-ai-assistant/discussions). Please follow this guide to learn more about how to develop and test the project locally, before opening a pull request. ## Local Dev Setup ### Clone the repo ```bash git clone git@github.com:vintasoftware/nextjs-fastapi-template.git ``` Check the [Get Started](get-started.md#setup) page to complete the setup. ## Install pre-commit hooks Check the [Additional Settings - Install pre-commit hooks](additional-settings.md#pre-commit-setup) section to complete the setup. It's critical to run the pre-commit hooks before pushing your code to follow the project's code style, and avoid linting errors. ## Updating the OpenAPI schema It's critical to update the OpenAPI schema when you make changes to the FastAPI routes or related files: Check the [Additional Settings - Manual execution of hot reload commands](additional-settings.md#manual-execution-of-hot-reload-commands) section to run the command. ## Tests Check the [Additional Settings - Testing](additional-settings.md#testing) section to run the tests. ## Documentation We use [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) to generate the documentation from markdown files. Check the files in the `docs` directory. To run the documentation locally, you need to run: ```bash uv run mkdocs serve ``` ## Release !!! info The backend and the frontend are versioned together, that is, they should have the same version number. To release and publish a new version, follow these steps: 1. Update the version in `fastapi_backend/pyproject.toml`, `nextjs-frontend/package.json`. 2. Update the changelog in `CHANGELOG.md`. 3. Open a PR with the changes. 4. Once the PR is merged, run the [Release GitHub Action](https://github.com/vintasoftware/nextjs-fastapi-template/actions/workflows/release.yml) to create a draft release. 5. Review the draft release, ensure the description has at least the associated changelog entry, and publish it. ================================================ FILE: docs/deployment.md ================================================ ### Overview Deploying to **Vercel** is supported, with dedicated buttons for the **Frontend** and **Backend** applications. Both require specific configurations during and after deployment to ensure proper functionality. --- ### Frontend Deployment [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvintasoftware%2Fnextjs-fastapi-template%2Ftree%2Fmain%2Fnextjs-frontend&env=API_BASE_URL&envDescription=The%20API_BASE_URL%20is%20the%20backend%20URL%20where%20the%20frontend%20sends%20requests.) - Click the **Frontend** button above to start the deployment process. - During deployment, you will be prompted to set the `API_BASE_URL`. Use a placeholder value (e.g., `https://`) for now, as this will be updated with the backend URL later. - Complete the deployment process [here](#post-deployment-configuration). ### Backend Deployment [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvintasoftware%2Fnextjs-fastapi-template%2Ftree%2Fmain%2Ffastapi_backend&env=CORS_ORIGINS,ACCESS_SECRET_KEY,RESET_PASSWORD_SECRET_KEY,VERIFICATION_SECRET_KEY&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D) - Click the **Backend** button above to begin deployment. - First, set up the database. The connection is automatically configured, so follow the steps, and it should work by default. - During the deployment process, you will be prompted to configure the following environment variables: - **CORS_ORIGINS** - Set this to `["*"]` initially to allow all origins. Later, you can update this with the frontend URL. - **ACCESS_SECRET_KEY**, **RESET_PASSWORD_SECRET_KEY**, **VERIFICATION_SECRET_KEY** - During deployment, you can temporarily set these secret keys as plain strings (e.g., `examplekey`). However, you should generate secure keys and update them after the deployment in the **Post-Deployment Configuration** section. - Complete the deployment process [here](#post-deployment-configuration). ## CI (GitHub Actions) Setup for Production Deployment We provide the **prod-backend-deploy.yml** and **prod-frontend-deploy.yml** files to enable continuous integration through Github Actions. To connect them to GitHub, simply move them to the .github/workflows/ directory. You can do it with the following commands: ```bash mv prod-backend-deploy.yml .github/workflows/prod-backend-deploy.yml mv prod-frontend-deploy.yml .github/workflows/prod-frontend-deploy.yml ``` ### Prerequisites 1. **Create a Vercel Token**: - Generate your [Vercel Access Token](https://vercel.com/account/tokens). - Save the token as `VERCEL_TOKEN` in your GitHub secrets. 2. **Install Vercel CLI**: ```bash pnpm i -g vercel@latest ``` 3. Authenticate your account. ```bash vercel login ``` ### Database Creation (Required) 1. **Choosing a Database** - You can use your database hosted on a different service or opt for the [Neon](https://neon.tech/docs/introduction) database, which integrates seamlessly with Vercel. 2. **Setting Up a Neon Database via Vercel** - In the **Projects dashboard** page on Vercel, navigate to the **Storage** section. - Select the option to **Create a Database** to provision a Neon database. 3. **Configuring the Database URL** - After creating the database, retrieve the **Database URL** provided by Neon. - Include this URL in your **Environment Variables** under `DATABASE_URL`. 4. **Migrating the Database** - The database migration will happen automatically during the GitHub action deployment, setting up the necessary tables and schema. ### Frontend Setup 1. Link the nextjs-frontend Project 2. Navigate to the nextjs-frontend directory and run: ```bash cd nextjs-frontend vercel link ``` 3. Follow the prompts: - Link to existing project? No - Modify settings? No 4. Save Project IDs and Add GitHub Secrets: - Open `nextjs-frontend/.vercel/project.json` and add the following to your GitHub repository secrets: - `projectId` → `VERCEL_PROJECT_ID_FRONTEND` - `orgId` → `VERCEL_ORG_ID` ### Backend Setup 1. Link the fastapi_backend Project 2. Navigate to the fastapi_backend directory and run: ```bash cd fastapi_backend vercel link --local-config=vercel.prod.json ``` - We use a specific configuration file to set the --local-config value. 3. Follow the prompts: - Link to existing project? No - Modify settings? No 4. Save Project IDs and Add GitHub Secrets: - Open `fastapi_backend/.vercel/project.json` and add the following to your GitHub repository secrets: - `projectId` → `VERCEL_PROJECT_ID_BACKEND` - `orgId` → `VERCEL_ORG_ID` (Only in case you haven't added that before) 5. Update requirements.txt file: ```bash cd fastapi_backend uv export > requirements.txt ``` - Export a new requirements.txt file is required to vercel deploy when the uv.lock is modified. ### Notes - Once everything is set up, run `git push`, and the deployment will automatically occur. - Please ensure you complete the setup for both the frontend and backend separately. - Refer to the [Vercel CLI Documentation](https://vercel.com/docs/cli) for more details. - You can find the project_id into the vercel web project settings. - You can find the organization_id into the vercel web organization settings. ## Post-Deployment Configuration ### Frontend - Navigate to the **Settings** page of the deployed frontend project. - Access the **Environment Variables** section. - Update the `API_BASE_URL` variable with the backend URL once the backend deployment is complete. ### Backend - Access the **Settings** page of the deployed backend project. - Navigate to the **Environment Variables** section and update the following variables with secure values: - **CORS_ORIGINS** - Once the frontend is deployed, replace `["*"]` with the actual frontend URL. - **ACCESS_SECRET_KEY** - Generate a secure key for API access and set it here. - **RESET_PASSWORD_SECRET_KEY** - Generate a secure key for password reset functionality and set it. - **VERIFICATION_SECRET_KEY** - Generate a secure key for user verification and configure it. - For detailed instructions on setting these secret keys, please look at the section on [Setting up Environment Variables](get-started.md#setting-up-environment-variables). ### Fluid serverless activation [Fluid](https://vercel.com/docs/functions/fluid-compute) is Vercel's new concurrency model for serverless functions, allowing them to handle multiple requests per execution instead of spinning up a new instance for each request. This improves performance, reduces cold starts, and optimizes resource usage, making serverless workloads more efficient. Follow this [guide](https://vercel.com/docs/functions/fluid-compute#how-to-enable-fluid-compute) to activate Fluid. ================================================ FILE: docs/get-started.md ================================================ To use this template for your own project: 1. Create a new repository using this template by following GitHub's [template repository guide](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template#creating-a-repository-from-a-template) 2. Clone your new repository and navigate to it: `cd your-project-name` 3. Make sure you have Python 3.12 installed Once completed, proceed to the [Setup](#setup) section below. ## Setup ### Installing Required Tools #### 1. uv uv is used to manage Python dependencies in the backend. Install uv by following the [official installation guide](https://docs.astral.sh/uv/getting-started/installation/). #### 2. Node.js, npm, and pnpm To run the frontend, ensure Node.js and npm are installed. Follow the [Node.js installation guide](https://nodejs.org/en/download/). After that, install pnpm by running: ```bash npm install -g pnpm ``` #### 3. Docker Docker is needed to run the project in a containerized environment. Follow the appropriate installation guide: - [Install Docker for Mac](https://docs.docker.com/docker-for-mac/install/) - [Install Docker for Windows](https://docs.docker.com/docker-for-windows/install/) - [Get Docker CE for Linux](https://docs.docker.com/install/linux/docker-ce/) #### 4. Docker Compose Ensure `docker-compose` is installed. Refer to the [Docker Compose installation guide](https://docs.docker.com/compose/install/). ### Setting Up Environment Variables **Backend (`fastapi_backend/.env`):** Copy the `.env.example` files to `.env` and update the variables with your own values. ```bash cd fastapi_backend && cp .env.example .env ``` You will only need to update the secret keys. You can use the following command to generate a new secret key: ```bash python3 -c "import secrets; print(secrets.token_hex(32))" ``` - The DATABASE, MAIL, OPENAPI, CORS, and FRONTEND_URL settings are ready to use locally. - The DATABASE and MAIL settings are already configured in Docker Compose if you're using Docker. - The OPENAPI_URL setting is commented out. Uncommenting it will hide the /docs and openapi.json URLs, which is ideal for production. You can check the .env.example file for more information about the variables. **Frontend (`nextjs-frontend/.env.local`):** Copy the `.env.example` files to `.env.local`. These values are unlikely to change, so you can leave them as they are. ```bash cd nextjs-frontend && cp .env.example .env.local ``` ### Running the Database Use Docker to run the database to avoid local installation issues. Build and start the database container: ```bash docker compose build db docker compose up -d db ``` Run the following command to apply database migrations: ```bash make docker-migrate-db ``` ### Build the project (without Docker): To set the project environment locally, use the following commands: #### Backend Navigate to the `fastapi_backend` directory and run: ```bash uv sync ``` #### Frontend Navigate to the `nextjs-frontend` directory and run: ```bash pnpm install ``` ### Build the project (with Docker): Build the backend and frontend containers: ```bash make docker-build ``` ## Running the Application **If you are not using Docker:** Start the FastAPI server: ```bash make start-backend ``` Start the Next.js development server: ```bash make start-frontend ``` **If you are using Docker:** Start the FastAPI server container: ```bash make docker-start-backend ``` Start the Next.js development server container: ```bash make docker-start-frontend ``` - **Backend**: Access the API at `http://localhost:8000`. - **Frontend**: Access the web application at `http://localhost:3000`. ## Important Considerations - **Environment Variables**: Ensure your `.env` files are up-to-date. - **Database Setup**: It is recommended to use Docker to run the database, even when running the backend and frontend locally, to simplify configuration and avoid potential conflicts. - **Consistency**: It is **not recommended** to switch between running the project locally and using Docker, as this may cause permission issues or unexpected problems. You can choose one method and stick with it. ================================================ FILE: docs/stylesheets/extra.css ================================================ :root > * { --md-primary-fg-color: #004BC9; } ================================================ FILE: docs/support.md ================================================ # Support If you have any questions or need help, feel free to create a thread on [GitHub Discussions](https://github.com/vintasoftware/nextjs-fastapi-template/discussions). In case you're facing a bug, please [check existing issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues) and create a new one if needed. ## Commercial Support [![alt text](images/vinta-logo.png "Vinta Logo")](https://www.vintasoftware.com/) This is an open-source project maintained by [Vinta Software](https://www.vinta.com.br/). We are always looking for exciting work! If you need any commercial support, feel free to get in touch: contact@vinta.com.br ================================================ FILE: docs/technology-selection.md ================================================ This template streamlines building APIs with [FastAPI](https://fastapi.tiangolo.com/) and dynamic frontends with [Next.js](https://nextjs.org/). It integrates the backend and frontend using [@hey-api/openapi-ts](https://github.com/hey-ai/openapi-ts) to generate a type-safe client, with automated watchers to keep the OpenAPI schema and client updated, ensuring a smooth and synchronized development workflow. - [Next.js](https://nextjs.org/): Fast, SEO-friendly frontend framework - [FastAPI](https://fastapi.tiangolo.com/): High-performance Python backend - [SQLAlchemy](https://www.sqlalchemy.org/): Powerful Python SQL toolkit and ORM - [PostgreSQL](https://www.postgresql.org/): Advanced open-source relational database - [Pydantic](https://docs.pydantic.dev/): Data validation and settings management using Python type annotations - [Zod](https://zod.dev/) + [TypeScript](https://www.typescriptlang.org/): End-to-end type safety and schema validation - [fastapi-users](https://fastapi-users.github.io/fastapi-users/): Complete authentication system with: - Secure password hashing by default - JWT (JSON Web Token) authentication - Email-based password recovery - [Shadcn/ui](https://ui.shadcn.com/): Beautiful and customizable React components - [OpenAPI-fetch](https://github.com/Hey-AI/openapi-fetch): Fully typed client generation from OpenAPI schema - [fastapi-mail](https://sabuhish.github.io/fastapi-mail/): Efficient email handling for FastAPI applications - [uv](https://docs.astral.sh/uv/): An extremely fast Python package and project manager - [Pytest](https://docs.pytest.org/): Powerful Python testing framework - Code Quality Tools: - [Ruff](https://github.com/astral-sh/ruff): Fast Python linter - [ESLint](https://eslint.org/): JavaScript/TypeScript code quality - Hot reload watchers: - Backend: [Watchdog](https://github.com/gorakhargosh/watchdog) for monitoring file changes - Frontend: [Chokidar](https://github.com/paulmillr/chokidar) for live updates - [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/): Consistent environments for development and production - [MailHog](https://github.com/mailhog/MailHog): Email server for development - [Pre-commit hooks](https://pre-commit.com/): Enforce code quality with automated checks - [OpenAPI JSON schema](https://swagger.io/specification/): Centralized API documentation and client generation With this setup, you'll save time and maintain a seamless connection between your backend and frontend, boosting productivity and reliability. ================================================ FILE: fastapi_backend/.gitignore ================================================ .vercel ================================================ FILE: fastapi_backend/Dockerfile ================================================ FROM python:3.12-bookworm # Set the working directory WORKDIR /app # The uv installer requires curl (and certificates) to download the release archive RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates # Download the latest uv installer ADD https://astral.sh/uv/install.sh /uv-installer.sh # Run the uv installer then remove it RUN sh /uv-installer.sh && rm /uv-installer.sh # Ensure the installed binary is on the `PATH` ENV PATH="/root/.local/bin/:$PATH" # Copy dependency files first to leverage Docker caching COPY pyproject.toml uv.lock ./ # Install dependencies using uv RUN uv sync --frozen ENV PATH="/app/.venv/bin:$PATH" # Copy the rest of the application code COPY . . # Expose the application port EXPOSE 8000 # Command to run the application CMD ["./start.sh"] ================================================ FILE: fastapi_backend/alembic.ini ================================================ # A generic, single database configuration. [alembic] # path to migration scripts. # Use forward slashes (/) also on windows to provide an os agnostic path script_location = alembic_migrations # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. # defaults to the current working directory. prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. # If specified, requires the python>=3.9 or backports.zoneinfo library. # Any required deps can installed by adding `alembic[tz]` to the pip requirements # string value is passed to ZoneInfo() # leave blank for localtime # timezone = # max length of characters to apply to the "slug" field # truncate_slug_length = 40 # set to 'true' to run the environment during # the 'revision' command, regardless of autogenerate # 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 # sourceless = false # version location specification; This defaults # to alembic/versions. When using multiple version # directories, initial revisions must be specified with --version-path. # The path separator used here should be the separator specified by "version_path_separator" below. # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions # version path separator; As mentioned above, this is the character used to split # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. # Valid values for version_path_separator are: # # version_path_separator = : # version_path_separator = ; # version_path_separator = space # version_path_separator = newline version_path_separator = os # Use os.pathsep. Default configuration used for new projects. # set to 'true' to search source files recursively # in each "version_locations" directory # new in Alembic version 1.10 # recursive_version_locations = false # the output encoding used when revision files # are written from script.py.mako # output_encoding = utf-8 # We are including this url in the env.py file # then we can use the .env file to set the url # sqlalchemy.url = postgresql+asyncpg://postgres:password@localhost:5432/mydatabase [post_write_hooks] # post_write_hooks defines scripts or Python functions that are run # on newly generated revision scripts. See the documentation for further # detail and examples # format using "black" - use the console_scripts runner, against the "black" entrypoint # hooks = black # black.type = console_scripts # black.entrypoint = black # black.options = -l 79 REVISION_SCRIPT_FILENAME # lint with attempts to fix using "ruff" - use the exec runner, execute a binary # hooks = ruff # ruff.type = exec # ruff.executable = %(here)s/.venv/bin/ruff # ruff.options = --fix REVISION_SCRIPT_FILENAME # Logging configuration [loggers] keys = root,sqlalchemy,alembic [handlers] keys = console [formatters] keys = generic [logger_root] level = WARN handlers = console qualname = [logger_sqlalchemy] level = WARN handlers = qualname = sqlalchemy.engine [logger_alembic] level = INFO handlers = qualname = alembic [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S ================================================ FILE: fastapi_backend/alembic_migrations/README ================================================ Generic single-database configuration with an async dbapi. ================================================ FILE: fastapi_backend/alembic_migrations/env.py ================================================ import asyncio import os from urllib.parse import urlparse from logging.config import fileConfig from sqlalchemy import pool from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import async_engine_from_config from alembic import context from app.models import Base from dotenv import load_dotenv load_dotenv() # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support target_metadata = Base.metadata # target_metadata = None # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. # Retrieve the database URL from the environment # set it during execution database_url = os.getenv("DATABASE_URL") if not database_url: raise ValueError("DATABASE_URL environment variable is not set!") parsed_db_url = urlparse(database_url) async_db_connection_url = ( f"postgresql+asyncpg://{parsed_db_url.username}:{parsed_db_url.password}@" f"{parsed_db_url.hostname}{':' + str(parsed_db_url.port) if parsed_db_url.port else ''}" f"{parsed_db_url.path}" ) config.set_main_option("sqlalchemy.url", async_db_connection_url) def run_migrations_offline() -> None: """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. Calls to context.execute() here emit the given string to the script output. """ url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() def do_run_migrations(connection: Connection) -> None: context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() async def run_async_migrations() -> None: """In this scenario we need to create an Engine and associate a connection with the context. """ connectable = async_engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool, ) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) await connectable.dispose() def run_migrations_online() -> None: """Run migrations in 'online' mode.""" asyncio.run(run_async_migrations()) if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() ================================================ FILE: fastapi_backend/alembic_migrations/script.py.mako ================================================ """${message} Revision ID: ${up_revision} Revises: ${down_revision | comma,n} Create Date: ${create_date} """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa import fastapi_users_db_sqlalchemy ${imports if imports else ""} # revision identifiers, used by Alembic. revision: str = ${repr(up_revision)} down_revision: Union[str, None] = ${repr(down_revision)} branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} def upgrade() -> None: ${upgrades if upgrades else "pass"} def downgrade() -> None: ${downgrades if downgrades else "pass"} ================================================ FILE: fastapi_backend/alembic_migrations/versions/402d067a8b92_added_user_table.py ================================================ """Added user table Revision ID: 402d067a8b92 Revises: Create Date: 2024-09-27 14:01:44.155160 """ from typing import Sequence, Union import fastapi_users_db_sqlalchemy import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = "402d067a8b92" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( "user", sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), sa.Column("email", sa.String(length=320), nullable=False), sa.Column("hashed_password", sa.String(length=1024), nullable=False), sa.Column("is_active", sa.Boolean(), nullable=False), sa.Column("is_superuser", sa.Boolean(), nullable=False), sa.Column("is_verified", sa.Boolean(), nullable=False), sa.PrimaryKeyConstraint("id"), ) op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f("ix_user_email"), table_name="user") op.drop_table("user") # ### end Alembic commands ### ================================================ FILE: fastapi_backend/alembic_migrations/versions/b389592974f8_add_item_model.py ================================================ """Add item model Revision ID: b389592974f8 Revises: 402d067a8b92 Create Date: 2024-12-06 17:52:50.698249 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = "b389592974f8" down_revision: Union[str, None] = "402d067a8b92" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table( "items", sa.Column("id", sa.UUID(), nullable=False), sa.Column("name", sa.String(), nullable=False), sa.Column("description", sa.String(), nullable=True), sa.Column("quantity", sa.Integer(), nullable=True), sa.Column("user_id", sa.UUID(), nullable=False), sa.ForeignKeyConstraint( ["user_id"], ["user.id"], ), sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_table("items") # ### end Alembic commands ### ================================================ FILE: fastapi_backend/api/index.py ================================================ from app.main import app # noqa: F401 ================================================ FILE: fastapi_backend/app/__init__.py ================================================ ================================================ FILE: fastapi_backend/app/config.py ================================================ from typing import Set from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): # OpenAPI docs OPENAPI_URL: str = "/openapi.json" # Database DATABASE_URL: str TEST_DATABASE_URL: str | None = None EXPIRE_ON_COMMIT: bool = False # User ACCESS_SECRET_KEY: str RESET_PASSWORD_SECRET_KEY: str VERIFICATION_SECRET_KEY: str ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_SECONDS: int = 3600 # Email MAIL_USERNAME: str | None = None MAIL_PASSWORD: str | None = None MAIL_FROM: str | None = None MAIL_SERVER: str | None = None MAIL_PORT: int | None = None MAIL_FROM_NAME: str = "FastAPI template" MAIL_STARTTLS: bool = True MAIL_SSL_TLS: bool = False USE_CREDENTIALS: bool = True VALIDATE_CERTS: bool = True TEMPLATE_DIR: str = "email_templates" # Frontend FRONTEND_URL: str = "http://localhost:3000" # CORS CORS_ORIGINS: Set[str] model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore" ) settings = Settings() ================================================ FILE: fastapi_backend/app/database.py ================================================ from typing import AsyncGenerator from urllib.parse import urlparse from fastapi import Depends from fastapi_users.db import SQLAlchemyUserDatabase from sqlalchemy import NullPool from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from .config import settings from .models import Base, User parsed_db_url = urlparse(settings.DATABASE_URL) async_db_connection_url = ( f"postgresql+asyncpg://{parsed_db_url.username}:{parsed_db_url.password}@" f"{parsed_db_url.hostname}{':' + str(parsed_db_url.port) if parsed_db_url.port else ''}" f"{parsed_db_url.path}" ) # Disable connection pooling for serverless environments like Vercel engine = create_async_engine(async_db_connection_url, poolclass=NullPool) async_session_maker = async_sessionmaker( engine, expire_on_commit=settings.EXPIRE_ON_COMMIT ) async def create_db_and_tables(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async with async_session_maker() as session: yield session async def get_user_db(session: AsyncSession = Depends(get_async_session)): yield SQLAlchemyUserDatabase(session, User) ================================================ FILE: fastapi_backend/app/email.py ================================================ from pathlib import Path import urllib.parse from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType from .config import settings from .models import User def get_email_config(): conf = ConnectionConfig( MAIL_USERNAME=settings.MAIL_USERNAME, MAIL_PASSWORD=settings.MAIL_PASSWORD, MAIL_FROM=settings.MAIL_FROM, MAIL_PORT=settings.MAIL_PORT, MAIL_SERVER=settings.MAIL_SERVER, MAIL_FROM_NAME=settings.MAIL_FROM_NAME, MAIL_STARTTLS=settings.MAIL_STARTTLS, MAIL_SSL_TLS=settings.MAIL_SSL_TLS, USE_CREDENTIALS=settings.USE_CREDENTIALS, VALIDATE_CERTS=settings.VALIDATE_CERTS, TEMPLATE_FOLDER=Path(__file__).parent / settings.TEMPLATE_DIR, ) return conf async def send_reset_password_email(user: User, token: str): conf = get_email_config() email = user.email base_url = f"{settings.FRONTEND_URL}/password-recovery/confirm?" params = {"token": token} encoded_params = urllib.parse.urlencode(params) link = f"{base_url}{encoded_params}" message = MessageSchema( subject="Password recovery", recipients=[email], template_body={"username": email, "link": link}, subtype=MessageType.html, ) fm = FastMail(conf) await fm.send_message(message, template_name="password_reset.html") ================================================ FILE: fastapi_backend/app/email_templates/__init__.py ================================================ ================================================ FILE: fastapi_backend/app/email_templates/password_reset.html ================================================

Hello {{ username }},

We received a request to reset your password. You can reset your password by clicking the link below:

Reset password

If you did not request this, please ignore this email.

Best regards,
YourCompany

================================================ FILE: fastapi_backend/app/main.py ================================================ from fastapi import FastAPI from fastapi_pagination import add_pagination from .schemas import UserCreate, UserRead, UserUpdate from .users import auth_backend, fastapi_users, AUTH_URL_PATH from fastapi.middleware.cors import CORSMiddleware from .utils import simple_generate_unique_route_id from app.routes.items import router as items_router from app.config import settings app = FastAPI( generate_unique_id_function=simple_generate_unique_route_id, openapi_url=settings.OPENAPI_URL, ) # Middleware for CORS configuration app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include authentication and user management routes app.include_router( fastapi_users.get_auth_router(auth_backend), prefix=f"/{AUTH_URL_PATH}/jwt", tags=["auth"], ) app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), prefix=f"/{AUTH_URL_PATH}", tags=["auth"], ) app.include_router( fastapi_users.get_reset_password_router(), prefix=f"/{AUTH_URL_PATH}", tags=["auth"], ) app.include_router( fastapi_users.get_verify_router(UserRead), prefix=f"/{AUTH_URL_PATH}", tags=["auth"], ) app.include_router( fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"], ) # Include items routes app.include_router(items_router, prefix="/items") add_pagination(app) ================================================ FILE: fastapi_backend/app/models.py ================================================ from fastapi_users.db import SQLAlchemyBaseUserTableUUID from sqlalchemy.orm import DeclarativeBase from sqlalchemy import Column, String, Integer, ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.dialects.postgresql import UUID from uuid import uuid4 class Base(DeclarativeBase): pass class User(SQLAlchemyBaseUserTableUUID, Base): items = relationship("Item", back_populates="user", cascade="all, delete-orphan") class Item(Base): __tablename__ = "items" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) name = Column(String, nullable=False) description = Column(String, nullable=True) quantity = Column(Integer, nullable=True) user_id = Column(UUID(as_uuid=True), ForeignKey("user.id"), nullable=False) user = relationship("User", back_populates="items") ================================================ FILE: fastapi_backend/app/routes/__init__.py ================================================ ================================================ FILE: fastapi_backend/app/routes/items.py ================================================ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_pagination import Page, Params from fastapi_pagination.ext.sqlalchemy import apaginate from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.database import User, get_async_session from app.models import Item from app.schemas import ItemRead, ItemCreate from app.users import current_active_user router = APIRouter(tags=["item"]) def transform_items(items): return [ItemRead.model_validate(item) for item in items] @router.get("/", response_model=Page[ItemRead]) async def read_item( db: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), page: int = Query(1, ge=1, description="Page number"), size: int = Query(10, ge=1, le=100, description="Page size"), ): params = Params(page=page, size=size) query = select(Item).filter(Item.user_id == user.id) return await apaginate(db, query, params, transformer=transform_items) @router.post("/", response_model=ItemRead) async def create_item( item: ItemCreate, db: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): db_item = Item(**item.model_dump(), user_id=user.id) db.add(db_item) await db.commit() await db.refresh(db_item) return db_item @router.delete("/{item_id}") async def delete_item( item_id: UUID, db: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): result = await db.execute( select(Item).filter(Item.id == item_id, Item.user_id == user.id) ) item = result.scalars().first() if not item: raise HTTPException(status_code=404, detail="Item not found or not authorized") await db.delete(item) await db.commit() return {"message": "Item successfully deleted"} ================================================ FILE: fastapi_backend/app/schemas.py ================================================ import uuid from fastapi_users import schemas from pydantic import BaseModel from uuid import UUID class UserRead(schemas.BaseUser[uuid.UUID]): pass class UserCreate(schemas.BaseUserCreate): pass class UserUpdate(schemas.BaseUserUpdate): pass class ItemBase(BaseModel): name: str description: str | None = None quantity: int | None = None class ItemCreate(ItemBase): pass class ItemRead(ItemBase): id: UUID user_id: UUID model_config = {"from_attributes": True} ================================================ FILE: fastapi_backend/app/users.py ================================================ import uuid import re from typing import Optional from fastapi import Depends, Request from fastapi_users import ( BaseUserManager, FastAPIUsers, UUIDIDMixin, InvalidPasswordException, ) from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, JWTStrategy, ) from fastapi_users.db import SQLAlchemyUserDatabase from .config import settings from .database import get_user_db from .email import send_reset_password_email from .models import User from .schemas import UserCreate AUTH_URL_PATH = "auth" class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = settings.RESET_PASSWORD_SECRET_KEY verification_token_secret = settings.VERIFICATION_SECRET_KEY async def on_after_register(self, user: User, request: Optional[Request] = None): print(f"User {user.id} has registered.") async def on_after_forgot_password( self, user: User, token: str, request: Optional[Request] = None ): await send_reset_password_email(user, token) async def on_after_request_verify( self, user: User, token: str, request: Optional[Request] = None ): print(f"Verification requested for user {user.id}. Verification token: {token}") async def validate_password( self, password: str, user: UserCreate, ) -> None: errors = [] if len(password) < 8: errors.append("Password should be at least 8 characters.") if user.email in password: errors.append("Password should not contain e-mail.") if not any(char.isupper() for char in password): errors.append("Password should contain at least one uppercase letter.") if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): errors.append("Password should contain at least one special character.") if errors: raise InvalidPasswordException(reason=errors) async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): yield UserManager(user_db) bearer_transport = BearerTransport(tokenUrl=f"{AUTH_URL_PATH}/jwt/login") def get_jwt_strategy() -> JWTStrategy: return JWTStrategy( secret=settings.ACCESS_SECRET_KEY, lifetime_seconds=settings.ACCESS_TOKEN_EXPIRE_SECONDS, ) auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, get_strategy=get_jwt_strategy, ) fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) current_active_user = fastapi_users.current_user(active=True) ================================================ FILE: fastapi_backend/app/utils.py ================================================ from fastapi.routing import APIRoute def simple_generate_unique_route_id(route: APIRoute): return f"{route.tags[0]}-{route.name}" ================================================ FILE: fastapi_backend/commands/__init__.py ================================================ ================================================ FILE: fastapi_backend/commands/generate_openapi_schema.py ================================================ import json from pathlib import Path from app.main import app import os from dotenv import load_dotenv load_dotenv() OUTPUT_FILE = os.getenv("OPENAPI_OUTPUT_FILE") def generate_openapi_schema(output_file): schema = app.openapi() output_path = Path(output_file) updated_schema = remove_operation_id_tag(schema) output_path.write_text(json.dumps(updated_schema, indent=2)) print(f"OpenAPI schema saved to {output_file}") def remove_operation_id_tag(schema): """ Removes the tag prefix from the operation IDs in the OpenAPI schema. This cleans up the OpenAPI operation IDs that are used by the frontend client generator to create the names of the functions. The modified schema is then returned. """ for path_data in schema["paths"].values(): for operation in path_data.values(): tag = operation["tags"][0] operation_id = operation["operationId"] to_remove = f"{tag}-" new_operation_id = operation_id[len(to_remove) :] operation["operationId"] = new_operation_id return schema if __name__ == "__main__": generate_openapi_schema(OUTPUT_FILE) ================================================ FILE: fastapi_backend/mypy.ini ================================================ [mypy] python_version = 3.12 files = src/**/*.py follow_imports = skip ignore_missing_imports = True strict = True ================================================ FILE: fastapi_backend/pyproject.toml ================================================ [project] name = "app" version = "0.0.6" description = "" authors = [{ name = "Anderson Resende", email = "anderson@vinta.com.br" }] requires-python = ">=3.12,<3.13" readme = "README.md" dependencies = [ "fastapi[standard]>=0.115.0,<0.116", "asyncpg>=0.29.0,<0.30", "fastapi-users[sqlalchemy]>=13.0.0,<14", "pydantic-settings>=2.5.2,<3", "fastapi-mail>=1.4.1,<2", "fastapi-pagination==0.13.3" ] [dependency-groups] dev = [ "pre-commit>=3.4.0,<4", "ruff>=0.1.0,<0.2", "watchdog>=5.0.3,<6", "python-dotenv>=1.0.1,<2", "pytest>=8.3.3,<9", "pytest-mock>=3.14.0,<4", "mypy>=1.13.0,<2", "coveralls>=4.0.1,<5", "alembic>=1.14.0,<2", "pytest-asyncio>=0.24.0,<0.25", "mkdocs-material>=9.6.9", "mkdocs-material[imaging]>=9.6.9", ] [tool.uv] package = false [tool.hatch.build.targets.sdist] include = ["commands"] [tool.hatch.build.targets.wheel] include = ["commands"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ================================================ FILE: fastapi_backend/pytest.ini ================================================ [pytest] asyncio_mode = auto ================================================ FILE: fastapi_backend/requirements.txt ================================================ # This file was autogenerated by uv via the following command: # uv export aiosmtplib==2.0.2 \ --hash=sha256:138599a3227605d29a9081b646415e9e793796ca05322a78f69179f0135016a3 \ --hash=sha256:1e631a7a3936d3e11c6a144fb8ffd94bb4a99b714f2cb433e825d88b698e37bc alembic==1.14.0 \ --hash=sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25 \ --hash=sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 anyio==4.8.0 \ --hash=sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a \ --hash=sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a argon2-cffi==23.1.0 \ --hash=sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08 \ --hash=sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea argon2-cffi-bindings==21.2.0 \ --hash=sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c \ --hash=sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082 \ --hash=sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f \ --hash=sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d \ --hash=sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f \ --hash=sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae \ --hash=sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3 \ --hash=sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86 \ --hash=sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367 \ --hash=sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93 \ --hash=sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e asyncpg==0.29.0 \ --hash=sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe \ --hash=sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175 \ --hash=sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106 \ --hash=sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178 \ --hash=sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb \ --hash=sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02 \ --hash=sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59 \ --hash=sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e \ --hash=sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364 babel==2.17.0 \ --hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \ --hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2 backrefs==5.8 \ --hash=sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd \ --hash=sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b \ --hash=sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc \ --hash=sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486 \ --hash=sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d bcrypt==4.1.2 \ --hash=sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f \ --hash=sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5 \ --hash=sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb \ --hash=sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258 \ --hash=sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4 \ --hash=sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc \ --hash=sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2 \ --hash=sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326 \ --hash=sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483 \ --hash=sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a \ --hash=sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966 \ --hash=sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63 \ --hash=sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c \ --hash=sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551 \ --hash=sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e \ --hash=sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0 \ --hash=sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c \ --hash=sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1 \ --hash=sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42 \ --hash=sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1 \ --hash=sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c \ --hash=sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7 \ --hash=sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369 blinker==1.9.0 \ --hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \ --hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc cairocffi==1.7.1 \ --hash=sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b \ --hash=sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f cairosvg==2.7.1 \ --hash=sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0 \ --hash=sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ --hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db cffi==1.17.1 \ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 cfgv==3.4.0 \ --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 charset-normalizer==3.4.1 \ --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 click==8.1.8 \ --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 coverage==7.6.10 \ --hash=sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50 \ --hash=sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853 \ --hash=sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359 \ --hash=sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0 \ --hash=sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23 \ --hash=sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852 \ --hash=sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078 \ --hash=sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0 \ --hash=sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247 \ --hash=sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022 \ --hash=sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b coveralls==4.0.1 \ --hash=sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809 \ --hash=sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69 cryptography==44.0.0 \ --hash=sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7 \ --hash=sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b \ --hash=sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc \ --hash=sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543 \ --hash=sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591 \ --hash=sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede \ --hash=sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb \ --hash=sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f \ --hash=sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123 \ --hash=sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c \ --hash=sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285 \ --hash=sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd \ --hash=sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092 \ --hash=sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289 \ --hash=sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02 \ --hash=sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64 \ --hash=sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053 \ --hash=sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417 \ --hash=sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e \ --hash=sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e \ --hash=sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7 cssselect2==0.8.0 \ --hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \ --hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 dnspython==2.7.0 \ --hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 \ --hash=sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1 docopt==0.6.2 \ --hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 email-validator==2.1.2 \ --hash=sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c \ --hash=sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115 fastapi==0.115.6 \ --hash=sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654 \ --hash=sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305 fastapi-cli==0.0.7 \ --hash=sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e \ --hash=sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4 fastapi-mail==1.4.1 \ --hash=sha256:9095b713bd9d3abb02fe6d7abb637502aaf680b52e177d60f96273ef6bc8bb70 \ --hash=sha256:fa5ef23b2dea4d3ba4587f4bbb53f8f15274124998fb4e40629b3b636c76c398 fastapi-users==13.0.0 \ --hash=sha256:b397c815b7051c8fd4b560fbeee707acd28e00bd3e8f25c292ad158a1e47e884 \ --hash=sha256:e6246529e3080a5b50e5afeed1e996663b661f1dc791a1ac478925cb5bfc0fa0 fastapi-users-db-sqlalchemy==7.0.0 \ --hash=sha256:5fceac018e7cfa69efc70834dd3035b3de7988eb4274154a0dbe8b14f5aa001e \ --hash=sha256:6823eeedf8a92f819276a2b2210ef1dcfd71fe8b6e37f7b4da8d1c60e3dfd595 filelock==3.16.1 \ --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 ghp-import==2.1.0 \ --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 greenlet==3.1.1 \ --hash=sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9 \ --hash=sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942 \ --hash=sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441 \ --hash=sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d \ --hash=sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467 \ --hash=sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01 \ --hash=sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36 \ --hash=sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0 \ --hash=sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa \ --hash=sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79 h11==0.14.0 \ --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 httpcore==1.0.7 \ --hash=sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c \ --hash=sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd httptools==0.6.4 \ --hash=sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2 \ --hash=sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c \ --hash=sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1 \ --hash=sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44 \ --hash=sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970 \ --hash=sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2 \ --hash=sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81 \ --hash=sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad identify==2.6.5 \ --hash=sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566 \ --hash=sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 iniconfig==2.0.0 \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 jinja2==3.1.5 \ --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb makefun==1.15.6 \ --hash=sha256:26bc63442a6182fb75efed8b51741dd2d1db2f176bec8c64e20a586256b8f149 \ --hash=sha256:e69b870f0bb60304765b1e3db576aaecf2f9b3e5105afe8cfeff8f2afe6ad067 mako==1.3.8 \ --hash=sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627 \ --hash=sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8 markdown==3.7 \ --hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \ --hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803 markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb markupsafe==3.0.2 \ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba mergedeep==1.3.4 \ --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 mkdocs==1.6.1 \ --hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \ --hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e mkdocs-get-deps==0.2.0 \ --hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \ --hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134 mkdocs-material==9.6.9 \ --hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \ --hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 mypy==1.14.1 \ --hash=sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14 \ --hash=sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e \ --hash=sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6 \ --hash=sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11 \ --hash=sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b \ --hash=sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1 \ --hash=sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9 \ --hash=sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89 mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 nodeenv==1.9.1 \ --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \ --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 packaging==24.2 \ --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f paginate==0.5.7 \ --hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \ --hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 pathspec==0.12.1 \ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 pillow==10.4.0 \ --hash=sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06 \ --hash=sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a \ --hash=sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80 \ --hash=sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9 \ --hash=sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94 \ --hash=sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b \ --hash=sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42 \ --hash=sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597 \ --hash=sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a \ --hash=sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca \ --hash=sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9 \ --hash=sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb pluggy==1.5.0 \ --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 pre-commit==3.8.0 \ --hash=sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af \ --hash=sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f pwdlib==0.2.0 \ --hash=sha256:b1bdafc064310eb6d3d07144a210267063ab4f45ac73a97be948e6589f74e861 \ --hash=sha256:be53812012ab66795a57ac9393a59716ae7c2b60841ed453eb1262017fdec144 pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc pydantic==2.10.4 \ --hash=sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d \ --hash=sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06 pydantic-core==2.27.2 \ --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \ --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \ --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \ --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \ --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \ --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \ --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \ --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \ --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \ --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \ --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \ --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \ --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \ --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \ --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 pydantic-settings==2.7.1 \ --hash=sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93 \ --hash=sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd pygments==2.19.0 \ --hash=sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b \ --hash=sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783 pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 pymdown-extensions==10.14.3 \ --hash=sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9 \ --hash=sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b pytest==8.3.4 \ --hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6 \ --hash=sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761 pytest-asyncio==0.24.0 \ --hash=sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b \ --hash=sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276 pytest-mock==3.14.0 \ --hash=sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f \ --hash=sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0 python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 python-dotenv==1.0.1 \ --hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \ --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a python-multipart==0.0.9 \ --hash=sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026 \ --hash=sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215 pyyaml==6.0.2 \ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 pyyaml-env-tag==0.1 \ --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 rich==13.9.4 \ --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \ --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90 rich-toolkit==0.12.0 \ --hash=sha256:a2da4416384410ae871e890db7edf8623e1f5e983341dbbc8cc03603ce24f0ab \ --hash=sha256:facb0b40418010309f77abd44e2583b4936656f6ee5c8625da807564806a6c40 ruff==0.1.15 \ --hash=sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447 \ --hash=sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f \ --hash=sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587 \ --hash=sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df \ --hash=sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852 \ --hash=sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f \ --hash=sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5 \ --hash=sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e \ --hash=sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807 \ --hash=sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360 \ --hash=sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2 \ --hash=sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1 \ --hash=sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec \ --hash=sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5 \ --hash=sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8 \ --hash=sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e \ --hash=sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b shellingham==1.5.4 \ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc sqlalchemy==2.0.36 \ --hash=sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588 \ --hash=sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855 \ --hash=sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e \ --hash=sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5 \ --hash=sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686 \ --hash=sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a \ --hash=sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5 \ --hash=sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4 \ --hash=sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e \ --hash=sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53 starlette==0.41.3 \ --hash=sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835 \ --hash=sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7 tinycss2==1.4.0 \ --hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \ --hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 typer==0.15.1 \ --hash=sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847 \ --hash=sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d uvicorn==0.34.0 \ --hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \ --hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9 uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \ --hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \ --hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \ --hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \ --hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \ --hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \ --hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \ --hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2 virtualenv==20.28.1 \ --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 watchdog==5.0.3 \ --hash=sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7 \ --hash=sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176 \ --hash=sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c \ --hash=sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97 \ --hash=sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05 \ --hash=sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926 \ --hash=sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45 \ --hash=sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e \ --hash=sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c \ --hash=sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221 \ --hash=sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8 \ --hash=sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49 \ --hash=sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91 \ --hash=sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9 watchfiles==1.0.3 \ --hash=sha256:0d1ec043f02ca04bf21b1b32cab155ce90c651aaf5540db8eb8ad7f7e645cba8 \ --hash=sha256:1df924ba82ae9e77340101c28d56cbaff2c991bd6fe8444a545d24075abb0a87 \ --hash=sha256:48681c86f2cb08348631fed788a116c89c787fdf1e6381c5febafd782f6c3b44 \ --hash=sha256:49bc1bc26abf4f32e132652f4b3bfeec77d8f8f62f57652703ef127e85a3e38d \ --hash=sha256:632a52dcaee44792d0965c17bdfe5dc0edad5b86d6a29e53d6ad4bf92dc0ff49 \ --hash=sha256:65ab1fb635476f6170b07e8e21db0424de94877e4b76b7feabfe11f9a5fc12b5 \ --hash=sha256:6a5bc3ca468bb58a2ef50441f953e1f77b9a61bd1b8c347c8223403dc9b4ac9a \ --hash=sha256:80bf4b459d94a0387617a1b499f314aa04d8a64b7a0747d15d425b8c8b151da0 \ --hash=sha256:93436ed550e429da007fbafb723e0769f25bae178fbb287a94cb4ccdf42d3af3 \ --hash=sha256:9e080cf917b35b20c889225a13f290f2716748362f6071b859b60b8847a6aa43 \ --hash=sha256:c18f3502ad0737813c7dad70e3e1cc966cc147fbaeef47a09463bbffe70b0a00 \ --hash=sha256:ca94c85911601b097d53caeeec30201736ad69a93f30d15672b967558df02885 \ --hash=sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56 \ --hash=sha256:f58d3bfafecf3d81c15d99fc0ecf4319e80ac712c77cf0ce2661c8cf8bf84066 webencodings==0.5.1 \ --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 websockets==14.1 \ --hash=sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0 \ --hash=sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8 \ --hash=sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4 \ --hash=sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e \ --hash=sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058 \ --hash=sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d \ --hash=sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f \ --hash=sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a \ --hash=sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58 \ --hash=sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45 \ --hash=sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707 \ --hash=sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05 \ --hash=sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed fastapi-pagination==0.13.3 ================================================ FILE: fastapi_backend/start.sh ================================================ #!/bin/bash if [ -f /.dockerenv ]; then echo "Running in Docker" fastapi dev app/main.py --host 0.0.0.0 --port 8000 --reload & python watcher.py else echo "Running locally with uv" uv run fastapi dev app/main.py --host 0.0.0.0 --port 8000 --reload & uv run python watcher.py fi wait ================================================ FILE: fastapi_backend/tests/__init__.py ================================================ ================================================ FILE: fastapi_backend/tests/commands/__init__.py ================================================ ================================================ FILE: fastapi_backend/tests/commands/files/openapi_test.json ================================================ { "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/auth/jwt/login": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Login", "operationId": "auth-auth:jwt.login_post", "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/login_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BearerResponse" }, "example": { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", "token_type": "bearer" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "LOGIN_BAD_CREDENTIALS": { "summary": "Bad credentials or the user is inactive.", "value": { "detail": "LOGIN_BAD_CREDENTIALS" } }, "LOGIN_USER_NOT_VERIFIED": { "summary": "The user is not verified.", "value": { "detail": "LOGIN_USER_NOT_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/jwt/logout": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Logout", "operationId": "auth-auth:jwt.logout_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/auth/register": { "post": { "tags": [ "auth" ], "summary": "Register:Register", "operationId": "auth-register:register_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreate" } } }, "required": true }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "REGISTER_USER_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "REGISTER_USER_ALREADY_EXISTS" } }, "REGISTER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "REGISTER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/forgot-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Forgot Password", "operationId": "auth-reset:forgot_password_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_forgot_password_post" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/reset-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Reset Password", "operationId": "auth-reset:reset_password_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_reset_password_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "RESET_PASSWORD_BAD_TOKEN": { "summary": "Bad or expired token.", "value": { "detail": "RESET_PASSWORD_BAD_TOKEN" } }, "RESET_PASSWORD_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "RESET_PASSWORD_INVALID_PASSWORD", "reason": "Password should be at least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/request-verify-token": { "post": { "tags": [ "auth" ], "summary": "Verify:Request-Token", "operationId": "auth-verify:request-token_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_request-token_post" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/verify": { "post": { "tags": [ "auth" ], "summary": "Verify:Verify", "operationId": "auth-verify:verify_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_verify_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "VERIFY_USER_BAD_TOKEN": { "summary": "Bad token, not existing user ornot the e-mail currently set for the user.", "value": { "detail": "VERIFY_USER_BAD_TOKEN" } }, "VERIFY_USER_ALREADY_VERIFIED": { "summary": "The user is already verified.", "value": { "detail": "VERIFY_USER_ALREADY_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/users/me": { "get": { "tags": [ "users" ], "summary": "Users:Current User", "operationId": "users-users:current_user_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] }, "patch": { "tags": [ "users" ], "summary": "Users:Patch Current User", "operationId": "users-users:patch_current_user_patch", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/users/{id}": { "get": { "tags": [ "users" ], "summary": "Users:User", "operationId": "users-users:user_get", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "patch": { "tags": [ "users" ], "summary": "Users:Patch User", "operationId": "users-users:patch_user_patch", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "400": { "content": { "application/json": { "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } }, "schema": { "$ref": "#/components/schemas/ErrorModel" } } }, "description": "Bad Request" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "users" ], "summary": "Users:Delete User", "operationId": "users-users:delete_user_delete", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "204": { "description": "Successful Response" }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/authenticated-route": { "get": { "tags": [ "custom-auth" ], "summary": "Authenticated Route", "operationId": "custom-auth-authenticated_route_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } }, "security": [ { "OAuth2PasswordBearer": [] } ] } } }, "components": { "schemas": { "BearerResponse": { "properties": { "access_token": { "type": "string", "title": "Access Token" }, "token_type": { "type": "string", "title": "Token Type" } }, "type": "object", "required": [ "access_token", "token_type" ], "title": "BearerResponse" }, "Body_auth-reset_forgot_password_post": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-reset:forgot_password_post" }, "Body_auth-reset_reset_password_post": { "properties": { "token": { "type": "string", "title": "Token" }, "password": { "type": "string", "title": "Password" } }, "type": "object", "required": [ "token", "password" ], "title": "Body_auth-reset:reset_password_post" }, "Body_auth-verify_request-token_post": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-verify:request-token_post" }, "Body_auth-verify_verify_post": { "properties": { "token": { "type": "string", "title": "Token" } }, "type": "object", "required": [ "token" ], "title": "Body_auth-verify:verify_post" }, "ErrorModel": { "properties": { "detail": { "anyOf": [ { "type": "string" }, { "additionalProperties": { "type": "string" }, "type": "object" } ], "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ErrorModel" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "UserCreate": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" }, "password": { "type": "string", "title": "Password" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active", "default": true }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser", "default": false }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified", "default": false } }, "type": "object", "required": [ "email", "password" ], "title": "UserCreate" }, "UserRead": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, "email": { "type": "string", "format": "email", "title": "Email" }, "is_active": { "type": "boolean", "title": "Is Active", "default": true }, "is_superuser": { "type": "boolean", "title": "Is Superuser", "default": false }, "is_verified": { "type": "boolean", "title": "Is Verified", "default": false } }, "type": "object", "required": [ "id", "email" ], "title": "UserRead" }, "UserUpdate": { "properties": { "password": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Password" }, "email": { "anyOf": [ { "type": "string", "format": "email" }, { "type": "null" } ], "title": "Email" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active" }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser" }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified" } }, "type": "object", "title": "UserUpdate" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" }, "login_post": { "properties": { "grant_type": { "anyOf": [ { "type": "string", "pattern": "password" }, { "type": "null" } ], "title": "Grant Type" }, "username": { "type": "string", "title": "Username" }, "password": { "type": "string", "title": "Password" }, "scope": { "type": "string", "title": "Scope", "default": "" }, "client_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Id" }, "client_secret": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Secret" } }, "type": "object", "required": [ "username", "password" ], "title": "Body_auth-auth:jwt.login_post" } }, "securitySchemes": { "OAuth2PasswordBearer": { "type": "oauth2", "flows": { "password": { "scopes": {}, "tokenUrl": "auth/jwt/login" } } } } } } ================================================ FILE: fastapi_backend/tests/commands/files/openapi_test_output.json ================================================ { "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/auth/jwt/login": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Login", "operationId": "auth:jwt.login_post", "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/login_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BearerResponse" }, "example": { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", "token_type": "bearer" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "LOGIN_BAD_CREDENTIALS": { "summary": "Bad credentials or the user is inactive.", "value": { "detail": "LOGIN_BAD_CREDENTIALS" } }, "LOGIN_USER_NOT_VERIFIED": { "summary": "The user is not verified.", "value": { "detail": "LOGIN_USER_NOT_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/jwt/logout": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Logout", "operationId": "auth:jwt.logout_post", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/auth/register": { "post": { "tags": [ "auth" ], "summary": "Register:Register", "operationId": "register:register_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreate" } } }, "required": true }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "REGISTER_USER_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "REGISTER_USER_ALREADY_EXISTS" } }, "REGISTER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "REGISTER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/forgot-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Forgot Password", "operationId": "reset:forgot_password_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_forgot_password_post" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/reset-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Reset Password", "operationId": "reset:reset_password_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_reset_password_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "RESET_PASSWORD_BAD_TOKEN": { "summary": "Bad or expired token.", "value": { "detail": "RESET_PASSWORD_BAD_TOKEN" } }, "RESET_PASSWORD_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "RESET_PASSWORD_INVALID_PASSWORD", "reason": "Password should be at least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/request-verify-token": { "post": { "tags": [ "auth" ], "summary": "Verify:Request-Token", "operationId": "verify:request-token_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_request-token_post" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/verify": { "post": { "tags": [ "auth" ], "summary": "Verify:Verify", "operationId": "verify:verify_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_verify_post" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "VERIFY_USER_BAD_TOKEN": { "summary": "Bad token, not existing user ornot the e-mail currently set for the user.", "value": { "detail": "VERIFY_USER_BAD_TOKEN" } }, "VERIFY_USER_ALREADY_VERIFIED": { "summary": "The user is already verified.", "value": { "detail": "VERIFY_USER_ALREADY_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/users/me": { "get": { "tags": [ "users" ], "summary": "Users:Current User", "operationId": "users:current_user_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] }, "patch": { "tags": [ "users" ], "summary": "Users:Patch Current User", "operationId": "users:patch_current_user_patch", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/users/{id}": { "get": { "tags": [ "users" ], "summary": "Users:User", "operationId": "users:user_get", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "patch": { "tags": [ "users" ], "summary": "Users:Patch User", "operationId": "users:patch_user_patch", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "400": { "content": { "application/json": { "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } }, "schema": { "$ref": "#/components/schemas/ErrorModel" } } }, "description": "Bad Request" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "users" ], "summary": "Users:Delete User", "operationId": "users:delete_user_delete", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "204": { "description": "Successful Response" }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/authenticated-route": { "get": { "tags": [ "custom-auth" ], "summary": "Authenticated Route", "operationId": "authenticated_route_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } } }, "security": [ { "OAuth2PasswordBearer": [] } ] } } }, "components": { "schemas": { "BearerResponse": { "properties": { "access_token": { "type": "string", "title": "Access Token" }, "token_type": { "type": "string", "title": "Token Type" } }, "type": "object", "required": [ "access_token", "token_type" ], "title": "BearerResponse" }, "Body_auth-reset_forgot_password_post": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-reset:forgot_password_post" }, "Body_auth-reset_reset_password_post": { "properties": { "token": { "type": "string", "title": "Token" }, "password": { "type": "string", "title": "Password" } }, "type": "object", "required": [ "token", "password" ], "title": "Body_auth-reset:reset_password_post" }, "Body_auth-verify_request-token_post": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-verify:request-token_post" }, "Body_auth-verify_verify_post": { "properties": { "token": { "type": "string", "title": "Token" } }, "type": "object", "required": [ "token" ], "title": "Body_auth-verify:verify_post" }, "ErrorModel": { "properties": { "detail": { "anyOf": [ { "type": "string" }, { "additionalProperties": { "type": "string" }, "type": "object" } ], "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ErrorModel" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "UserCreate": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" }, "password": { "type": "string", "title": "Password" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active", "default": true }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser", "default": false }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified", "default": false } }, "type": "object", "required": [ "email", "password" ], "title": "UserCreate" }, "UserRead": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, "email": { "type": "string", "format": "email", "title": "Email" }, "is_active": { "type": "boolean", "title": "Is Active", "default": true }, "is_superuser": { "type": "boolean", "title": "Is Superuser", "default": false }, "is_verified": { "type": "boolean", "title": "Is Verified", "default": false } }, "type": "object", "required": [ "id", "email" ], "title": "UserRead" }, "UserUpdate": { "properties": { "password": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Password" }, "email": { "anyOf": [ { "type": "string", "format": "email" }, { "type": "null" } ], "title": "Email" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active" }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser" }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified" } }, "type": "object", "title": "UserUpdate" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" }, "login_post": { "properties": { "grant_type": { "anyOf": [ { "type": "string", "pattern": "password" }, { "type": "null" } ], "title": "Grant Type" }, "username": { "type": "string", "title": "Username" }, "password": { "type": "string", "title": "Password" }, "scope": { "type": "string", "title": "Scope", "default": "" }, "client_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Id" }, "client_secret": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Secret" } }, "type": "object", "required": [ "username", "password" ], "title": "Body_auth-auth:jwt.login_post" } }, "securitySchemes": { "OAuth2PasswordBearer": { "type": "oauth2", "flows": { "password": { "scopes": {}, "tokenUrl": "auth/jwt/login" } } } } } } ================================================ FILE: fastapi_backend/tests/commands/test_generate_openapi_schema.py ================================================ import json import os import pytest from pathlib import Path from commands.generate_openapi_schema import ( generate_openapi_schema, remove_operation_id_tag, ) def load_json_file(filename): test_dir = os.path.dirname(__file__) file_path = os.path.join(test_dir, "files", filename) with open(file_path, "r") as f: return json.load(f) @pytest.fixture def sample_openapi_schema(): return load_json_file("openapi_test.json") @pytest.fixture def expected_output_schema(): return load_json_file("openapi_test_output.json") def test_remove_operation_id_tag(sample_openapi_schema, expected_output_schema): cleaned_schema = remove_operation_id_tag(sample_openapi_schema) assert cleaned_schema == expected_output_schema @pytest.fixture def mock_app(mocker): app = mocker.patch("commands.generate_openapi_schema.app") app.openapi.return_value = { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": {}, } return app def test_generate_openapi_schema(mocker, mock_app): mock_remove_operation_id_tag = mocker.patch( "commands.generate_openapi_schema.remove_operation_id_tag" ) mock_remove_operation_id_tag.return_value = {"mocked_schema": True} output_file = "openapi_test.json" expected_output = json.dumps({"mocked_schema": True}, indent=2) generate_openapi_schema(output_file) mock_app.openapi.assert_called_once() mock_remove_operation_id_tag.assert_called_once_with(mock_app.openapi.return_value) output_path = Path(output_file) assert output_path.is_file() with open(output_file, "r") as f: content = f.read() assert content == expected_output output_path.unlink() ================================================ FILE: fastapi_backend/tests/conftest.py ================================================ from httpx import AsyncClient, ASGITransport import pytest_asyncio from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from fastapi_users.db import SQLAlchemyUserDatabase from fastapi_users.password import PasswordHelper import uuid from app.config import settings from app.models import User, Base from app.database import get_user_db, get_async_session from app.main import app from app.users import get_jwt_strategy @pytest_asyncio.fixture(scope="function") async def engine(): """Create a fresh test database engine for each test function.""" engine = create_async_engine(settings.TEST_DATABASE_URL, echo=True) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture(scope="function") async def db_session(engine): """Create a fresh database session for each test.""" async_session_maker = async_sessionmaker( engine, class_=AsyncSession, expire_on_commit=False ) async with async_session_maker() as session: yield session await session.rollback() await session.close() @pytest_asyncio.fixture(scope="function") async def test_client(db_session): """Fixture to create a test client that uses the test database session.""" # FastAPI-Users database override (wraps session with user operation helpers) async def override_get_user_db(): session = SQLAlchemyUserDatabase(db_session, User) try: yield session finally: await db_session.close() # General database override (raw session access) async def override_get_async_session(): try: yield db_session finally: await db_session.close() # Set up test database overrides app.dependency_overrides[get_user_db] = override_get_user_db app.dependency_overrides[get_async_session] = override_get_async_session async with AsyncClient( transport=ASGITransport(app=app), base_url="http://localhost:8000" ) as client: yield client @pytest_asyncio.fixture(scope="function") async def authenticated_user(test_client, db_session): """Fixture to create and authenticate a test user directly in the database.""" # Create user data user_data = { "id": uuid.uuid4(), "email": "test@example.com", "hashed_password": PasswordHelper().hash("TestPassword123#"), "is_active": True, "is_superuser": False, "is_verified": True, } # Create user directly in database user = User(**user_data) db_session.add(user) await db_session.commit() await db_session.refresh(user) # Generate token using the strategy directly strategy = get_jwt_strategy() access_token = await strategy.write_token(user) # Return both the headers and the user data return { "headers": {"Authorization": f"Bearer {access_token}"}, "user": user, "user_data": {"email": user_data["email"], "password": "TestPassword123#"}, } ================================================ FILE: fastapi_backend/tests/main/__init__.py ================================================ ================================================ FILE: fastapi_backend/tests/main/test_main.py ================================================ import pytest from fastapi import status from fastapi_users.router import ErrorCode from sqlalchemy import select from app.models import User class TestPasswordValidation: @pytest.mark.parametrize( "email, password, expected_status, expected_detail", [ ( "test@example.com", "short", status.HTTP_400_BAD_REQUEST, { "detail": { "code": ErrorCode.REGISTER_INVALID_PASSWORD.value, "reason": ["Password should be at least 8 characters."], } }, ), ( "test@example.com", "test@example.com", status.HTTP_400_BAD_REQUEST, { "detail": { "code": ErrorCode.REGISTER_INVALID_PASSWORD.value, "reason": ["Password should not contain e-mail."], } }, ), ( "test@example.com", "lowercasepassword", status.HTTP_400_BAD_REQUEST, { "detail": { "code": ErrorCode.REGISTER_INVALID_PASSWORD.value, "reason": [ "Password should contain at least one uppercase letter." ], } }, ), ( "test@example.com", "Nosppecialchar1", status.HTTP_400_BAD_REQUEST, { "detail": { "code": ErrorCode.REGISTER_INVALID_PASSWORD.value, "reason": [ "Password should contain at least one special character." ], } }, ), ( "test@example.com", "shorttest", status.HTTP_400_BAD_REQUEST, { "detail": { "code": ErrorCode.REGISTER_INVALID_PASSWORD.value, "reason": [ "Password should be at least 8 characters.", "Password should contain at least one uppercase letter.", "Password should contain at least one special character.", ], } }, ), ], ) @pytest.mark.asyncio(loop_scope="function") async def test_password_validation( self, test_client, email, password, expected_status, expected_detail ): """Test user registration with password validation.""" json = {"email": email, "password": password} response = await test_client.post("/auth/register", json=json) assert response.status_code == expected_status @pytest.mark.asyncio(loop_scope="function") async def test_register_user_with_valid_password(self, test_client, db_session): """Test user registration with success""" json = { "email": "user@1.com", "password": "Sppecialchar1#", } response = await test_client.post("/auth/register", json=json) row = await db_session.execute(select(User)) user = row.scalars().first() assert response.status_code == status.HTTP_201_CREATED assert user is not None assert user.email == "user@1.com" ================================================ FILE: fastapi_backend/tests/routes/__init__.py ================================================ ================================================ FILE: fastapi_backend/tests/routes/test_items.py ================================================ import pytest from fastapi import status from sqlalchemy import select, insert from app.models import Item class TestItems: @pytest.mark.asyncio(loop_scope="function") async def test_create_item(self, test_client, db_session, authenticated_user): """Test creating an item.""" item_data = {"name": "Test Item", "description": "Test Description"} create_response = await test_client.post( "/items/", json=item_data, headers=authenticated_user["headers"] ) assert create_response.status_code == status.HTTP_200_OK created_item = create_response.json() assert created_item["name"] == item_data["name"] assert created_item["description"] == item_data["description"] # Check if the item is in the database item = await db_session.execute( select(Item).where(Item.id == created_item["id"]) ) item = item.scalar() assert item is not None assert item.name == item_data["name"] assert item.description == item_data["description"] @pytest.mark.asyncio(loop_scope="function") async def test_read_items(self, test_client, db_session, authenticated_user): """Test reading items.""" # Create multiple items items_data = [ { "name": "First Item", "description": "First Description", "user_id": authenticated_user["user"].id, }, { "name": "Second Item", "description": "Second Description", "user_id": authenticated_user["user"].id, }, ] # create items in the database for item_data in items_data: await db_session.execute(insert(Item).values(**item_data)) await db_session.commit() # Add commit to ensure items are saved # Read items - test pagination response read_response = await test_client.get( "/items/", headers=authenticated_user["headers"] ) assert read_response.status_code == status.HTTP_200_OK response_data = read_response.json() # Check pagination structure assert "items" in response_data assert "total" in response_data assert "page" in response_data assert "size" in response_data items = response_data["items"] # Filter items created in this test (to avoid interference from other tests) test_items = [ item for item in items if item["name"] in ["First Item", "Second Item"] ] assert len(test_items) == 2 assert any(item["name"] == "First Item" for item in test_items) assert any(item["name"] == "Second Item" for item in test_items) @pytest.mark.asyncio(loop_scope="function") async def test_delete_item(self, test_client, db_session, authenticated_user): """Test deleting an item.""" # Create an item directly in the database item_data = { "name": "Item to Delete", "description": "Will be deleted", "user_id": authenticated_user["user"].id, } await db_session.execute(insert(Item).values(**item_data)) # Get the created item from database db_item = ( await db_session.execute(select(Item).where(Item.name == item_data["name"])) ).scalar() # Delete the item delete_response = await test_client.delete( f"/items/{db_item.id}", headers=authenticated_user["headers"] ) assert delete_response.status_code == status.HTTP_200_OK # Verify item is deleted from database db_check = ( await db_session.execute(select(Item).where(Item.id == db_item.id)) ).scalar() assert db_check is None @pytest.mark.asyncio(loop_scope="function") async def test_delete_nonexistent_item(self, test_client, authenticated_user): """Test deleting an item that doesn't exist.""" # Try to delete non-existent item delete_response = await test_client.delete( "/items/00000000-0000-0000-0000-000000000000", headers=authenticated_user["headers"], ) assert delete_response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio(loop_scope="function") async def test_unauthorized_read_items(self, test_client): """Test reading items without authentication.""" response = await test_client.get("/items/") assert response.status_code == status.HTTP_401_UNAUTHORIZED @pytest.mark.asyncio(loop_scope="function") async def test_unauthorized_create_item(self, test_client): """Test creating item without authentication.""" item_data = {"name": "Unauthorized Item", "description": "Should fail"} response = await test_client.post("/items/", json=item_data) assert response.status_code == status.HTTP_401_UNAUTHORIZED @pytest.mark.asyncio(loop_scope="function") async def test_unauthorized_delete_item(self, test_client): """Test deleting item without authentication.""" response = await test_client.delete( "/items/00000000-0000-0000-0000-000000000000" ) assert response.status_code == status.HTTP_401_UNAUTHORIZED ================================================ FILE: fastapi_backend/tests/test_database.py ================================================ import pytest from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine from fastapi_users.db import SQLAlchemyUserDatabase from app.database import ( async_session_maker, create_db_and_tables, get_async_session, get_user_db, ) from app.models import Base, User @pytest.fixture async def mock_engine(mocker): # Mock the engine mock_engine = mocker.AsyncMock(spec=AsyncEngine) # Create a mock connection mock_conn = mocker.AsyncMock() mock_conn.run_sync = mocker.AsyncMock() # Set up the context manager properly mock_context = mocker.AsyncMock() mock_context.__aenter__.return_value = mock_conn mock_engine.begin.return_value = mock_context return mock_engine @pytest.fixture async def mock_session(mocker): # Create a mock session mock_session = mocker.AsyncMock(spec=AsyncSession) # Mock the session context manager mock_session.__aenter__.return_value = mock_session mock_session.__aexit__.return_value = None # Mock the session maker mock_session_maker = mocker.patch("app.database.async_session_maker") mock_session_maker.return_value = mock_session return mock_session @pytest.mark.asyncio async def test_create_db_and_tables(mock_engine, mocker): # Replace the real engine with our mock mocker.patch("app.database.engine", mock_engine) await create_db_and_tables() # Verify that begin was called mock_engine.begin.assert_called_once() # Verify that create_all was called mock_conn = mock_engine.begin.return_value.__aenter__.return_value mock_conn.run_sync.assert_called_once_with(Base.metadata.create_all) @pytest.mark.asyncio async def test_get_async_session(mock_session): # Test the session generator session_generator = get_async_session() session = await session_generator.__anext__() # Verify we got the mock session assert session == mock_session # Verify the session was created with the expected context mock_session.__aenter__.assert_called_once() @pytest.mark.asyncio async def test_get_user_db(mock_session): # Test the user db generator user_db_generator = get_user_db(mock_session) user_db = await user_db_generator.__anext__() # Verify we got a SQLAlchemyUserDatabase instance assert isinstance(user_db, SQLAlchemyUserDatabase) assert user_db.session == mock_session # Verify the model class is correct assert user_db.user_table == User def test_engine_creation(mocker): # Mock settings mock_settings = mocker.patch("app.database.settings") mock_settings.DATABASE_URL = "sqlite+aiosqlite:///./test.db" mock_settings.EXPIRE_ON_COMMIT = False # Import engine to trigger creation with mocked settings from app.database import engine, async_session_maker # Verify engine is created assert isinstance(engine, AsyncEngine) # Verify session maker is configured assert async_session_maker.kw["expire_on_commit"] is False @pytest.mark.asyncio async def test_session_maker_configuration(): # Create a test session async with async_session_maker() as session: assert isinstance(session, AsyncSession) ================================================ FILE: fastapi_backend/tests/test_email.py ================================================ import pytest from pathlib import Path from fastapi_mail import ConnectionConfig, MessageSchema from app.email import get_email_config, send_reset_password_email from app.models import User @pytest.fixture def mock_settings(mocker): mock = mocker.patch("app.email.settings") # Set up mock settings with test values mock.MAIL_USERNAME = "test_user" mock.MAIL_PASSWORD = "test_pass" mock.MAIL_FROM = "test@example.com" mock.MAIL_PORT = 587 mock.MAIL_SERVER = "smtp.test.com" mock.MAIL_FROM_NAME = "Test Sender" mock.MAIL_STARTTLS = True mock.MAIL_SSL_TLS = False mock.USE_CREDENTIALS = True mock.VALIDATE_CERTS = True mock.TEMPLATE_DIR = "email_templates" mock.FRONTEND_URL = "http://test-frontend.com" return mock @pytest.fixture def mock_user(): return User( email="user@example.com", ) def test_get_email_config(mock_settings): config = get_email_config() assert isinstance(config, ConnectionConfig) assert config.MAIL_USERNAME == "test_user" assert config.MAIL_PASSWORD == "test_pass" assert config.MAIL_FROM == "test@example.com" assert config.MAIL_PORT == 587 assert config.MAIL_SERVER == "smtp.test.com" assert config.MAIL_FROM_NAME == "Test Sender" assert config.MAIL_STARTTLS assert not config.MAIL_SSL_TLS assert config.USE_CREDENTIALS assert config.VALIDATE_CERTS assert isinstance(config.TEMPLATE_FOLDER, Path) @pytest.mark.asyncio async def test_send_reset_password_email(mock_settings, mock_user, mocker): # Mock FastMail mock_fastmail = mocker.patch("app.email.FastMail") mock_fastmail_instance = mock_fastmail.return_value mock_fastmail_instance.send_message = mocker.AsyncMock() # Test data test_token = "test-token-123" # Call the function await send_reset_password_email(mock_user, test_token) # Verify FastMail was instantiated with correct config mock_fastmail.assert_called_once() config_arg = mock_fastmail.call_args[0][0] assert isinstance(config_arg, ConnectionConfig) # Verify send_message was called mock_fastmail_instance.send_message.assert_called_once() # Verify the message schema message_arg = mock_fastmail_instance.send_message.call_args[0][0] assert isinstance(message_arg, MessageSchema) assert message_arg.subject == "Password recovery" assert message_arg.recipients == [mock_user.email] # Verify template body contains correct data expected_link = ( f"http://test-frontend.com/password-recovery/confirm?token={test_token}" ) assert message_arg.template_body == { "username": mock_user.email, "link": expected_link, } # Verify template name template_name = mock_fastmail_instance.send_message.call_args[1]["template_name"] assert template_name == "password_reset.html" ================================================ FILE: fastapi_backend/tests/utils/__init__.py ================================================ ================================================ FILE: fastapi_backend/tests/utils/test_utils.py ================================================ from fastapi.routing import APIRoute from app.utils import simple_generate_unique_route_id def test_simple_generate_unique_route_id(mocker): mock_route = mocker.Mock(spec=APIRoute) mock_route.tags = ["auth"] mock_route.name = "authenticate_user" unique_id = simple_generate_unique_route_id(mock_route) assert unique_id == "auth-authenticate_user" ================================================ FILE: fastapi_backend/vercel.json ================================================ { "buildCommand": "python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt && alembic upgrade head && deactivate && rm -rf venv", "outputDirectory": "api", "git": { "deploymentEnabled": { "main": false } }, "routes": [ { "src": "/(.*)", "dest": "api/index.py" } ] } ================================================ FILE: fastapi_backend/vercel.prod.json ================================================ { "routes": [ { "src": "/(.*)", "dest": "api/index.py" } ] } ================================================ FILE: fastapi_backend/watcher.py ================================================ import time import re import subprocess import os from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from threading import Timer # Updated regex to include main.py, schemas.py, and all .py files in app/routes WATCHER_REGEX_PATTERN = re.compile(r"(main\.py|schemas\.py|routes/.*\.py)$") APP_PATH = "app" class MyHandler(FileSystemEventHandler): def __init__(self): super().__init__() self.debounce_timer = None self.last_modified = 0 def on_modified(self, event): if not event.is_directory and WATCHER_REGEX_PATTERN.search( os.path.relpath(event.src_path, APP_PATH) ): current_time = time.time() if current_time - self.last_modified > 1: self.last_modified = current_time if self.debounce_timer: self.debounce_timer.cancel() self.debounce_timer = Timer(1.0, self.execute_command, [event.src_path]) self.debounce_timer.start() def execute_command(self, file_path): print(f"File {file_path} has been modified and saved.") self.run_mypy_checks() self.run_openapi_schema_generation() def run_mypy_checks(self): """Run mypy type checks and print output.""" print("Running mypy type checks...") result = subprocess.run( ["uv", "run", "mypy", "app"], capture_output=True, text=True, check=False, ) print(result.stdout, result.stderr, sep="\n") print( "Type errors detected! We recommend checking the mypy output for " "more information on the issues." if result.returncode else "No type errors detected." ) def run_openapi_schema_generation(self): """Run the OpenAPI schema generation command.""" print("Proceeding with OpenAPI schema generation...") try: subprocess.run( [ "uv", "run", "python", "-m", "commands.generate_openapi_schema", ], check=True, ) print("OpenAPI schema generation completed successfully.") except subprocess.CalledProcessError as e: print(f"An error occurred while generating OpenAPI schema: {e}") if __name__ == "__main__": observer = Observer() observer.schedule(MyHandler(), APP_PATH, recursive=True) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: observer.stop() observer.join() ================================================ FILE: local-shared-data/openapi.json ================================================ { "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/auth/jwt/login": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Login", "operationId": "auth:jwt.login", "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/login" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BearerResponse" }, "example": { "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI", "token_type": "bearer" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "LOGIN_BAD_CREDENTIALS": { "summary": "Bad credentials or the user is inactive.", "value": { "detail": "LOGIN_BAD_CREDENTIALS" } }, "LOGIN_USER_NOT_VERIFIED": { "summary": "The user is not verified.", "value": { "detail": "LOGIN_USER_NOT_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/jwt/logout": { "post": { "tags": [ "auth" ], "summary": "Auth:Jwt.Logout", "operationId": "auth:jwt.logout", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/auth/register": { "post": { "tags": [ "auth" ], "summary": "Register:Register", "operationId": "register:register", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserCreate" } } }, "required": true }, "responses": { "201": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "REGISTER_USER_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "REGISTER_USER_ALREADY_EXISTS" } }, "REGISTER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "REGISTER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/forgot-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Forgot Password", "operationId": "reset:forgot_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_forgot_password" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/reset-password": { "post": { "tags": [ "auth" ], "summary": "Reset:Reset Password", "operationId": "reset:reset_password", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-reset_reset_password" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "RESET_PASSWORD_BAD_TOKEN": { "summary": "Bad or expired token.", "value": { "detail": "RESET_PASSWORD_BAD_TOKEN" } }, "RESET_PASSWORD_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "RESET_PASSWORD_INVALID_PASSWORD", "reason": "Password should be at least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/request-verify-token": { "post": { "tags": [ "auth" ], "summary": "Verify:Request-Token", "operationId": "verify:request-token", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_request-token" } } }, "required": true }, "responses": { "202": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/auth/verify": { "post": { "tags": [ "auth" ], "summary": "Verify:Verify", "operationId": "verify:verify", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Body_auth-verify_verify" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "VERIFY_USER_BAD_TOKEN": { "summary": "Bad token, not existing user ornot the e-mail currently set for the user.", "value": { "detail": "VERIFY_USER_BAD_TOKEN" } }, "VERIFY_USER_ALREADY_VERIFIED": { "summary": "The user is already verified.", "value": { "detail": "VERIFY_USER_ALREADY_VERIFIED" } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/users/me": { "get": { "tags": [ "users" ], "summary": "Users:Current User", "operationId": "users:current_user", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." } }, "security": [ { "OAuth2PasswordBearer": [] } ] }, "patch": { "tags": [ "users" ], "summary": "Users:Patch Current User", "operationId": "users:patch_current_user", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorModel" }, "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } }, "security": [ { "OAuth2PasswordBearer": [] } ] } }, "/users/{id}": { "get": { "tags": [ "users" ], "summary": "Users:User", "operationId": "users:user", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "patch": { "tags": [ "users" ], "summary": "Users:Patch User", "operationId": "users:patch_user", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserUpdate" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserRead" } } } }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "400": { "content": { "application/json": { "examples": { "UPDATE_USER_EMAIL_ALREADY_EXISTS": { "summary": "A user with this email already exists.", "value": { "detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS" } }, "UPDATE_USER_INVALID_PASSWORD": { "summary": "Password validation failed.", "value": { "detail": { "code": "UPDATE_USER_INVALID_PASSWORD", "reason": "Password should beat least 3 characters" } } } }, "schema": { "$ref": "#/components/schemas/ErrorModel" } } }, "description": "Bad Request" }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "delete": { "tags": [ "users" ], "summary": "Users:Delete User", "operationId": "users:delete_user", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "title": "Id" } } ], "responses": { "204": { "description": "Successful Response" }, "401": { "description": "Missing token or inactive user." }, "403": { "description": "Not a superuser." }, "404": { "description": "The user does not exist." }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/items/": { "get": { "tags": [ "item" ], "summary": "Read Item", "operationId": "read_item", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "description": "Page number", "default": 1, "title": "Page" }, "description": "Page number" }, { "name": "size", "in": "query", "required": false, "schema": { "type": "integer", "maximum": 100, "minimum": 1, "description": "Page size", "default": 50, "title": "Size" }, "description": "Page size" } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Page_ItemRead_" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } }, "post": { "tags": [ "item" ], "summary": "Create Item", "operationId": "create_item", "security": [ { "OAuth2PasswordBearer": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ItemCreate" } } } }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ItemRead" } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/items/{item_id}": { "delete": { "tags": [ "item" ], "summary": "Delete Item", "operationId": "delete_item", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ { "name": "item_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", "title": "Item Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": {} } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } } }, "components": { "schemas": { "BearerResponse": { "properties": { "access_token": { "type": "string", "title": "Access Token" }, "token_type": { "type": "string", "title": "Token Type" } }, "type": "object", "required": [ "access_token", "token_type" ], "title": "BearerResponse" }, "Body_auth-reset_forgot_password": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-reset:forgot_password" }, "Body_auth-reset_reset_password": { "properties": { "token": { "type": "string", "title": "Token" }, "password": { "type": "string", "title": "Password" } }, "type": "object", "required": [ "token", "password" ], "title": "Body_auth-reset:reset_password" }, "Body_auth-verify_request-token": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" } }, "type": "object", "required": [ "email" ], "title": "Body_auth-verify:request-token" }, "Body_auth-verify_verify": { "properties": { "token": { "type": "string", "title": "Token" } }, "type": "object", "required": [ "token" ], "title": "Body_auth-verify:verify" }, "ErrorModel": { "properties": { "detail": { "anyOf": [ { "type": "string" }, { "additionalProperties": { "type": "string" }, "type": "object" } ], "title": "Detail" } }, "type": "object", "required": [ "detail" ], "title": "ErrorModel" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "ItemCreate": { "properties": { "name": { "type": "string", "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Description" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "title": "Quantity" } }, "type": "object", "required": [ "name" ], "title": "ItemCreate" }, "ItemRead": { "properties": { "name": { "type": "string", "title": "Name" }, "description": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Description" }, "quantity": { "anyOf": [ { "type": "integer" }, { "type": "null" } ], "title": "Quantity" }, "id": { "type": "string", "format": "uuid", "title": "Id" }, "user_id": { "type": "string", "format": "uuid", "title": "User Id" } }, "type": "object", "required": [ "name", "id", "user_id" ], "title": "ItemRead" }, "Page_ItemRead_": { "properties": { "items": { "items": { "$ref": "#/components/schemas/ItemRead" }, "type": "array", "title": "Items" }, "total": { "anyOf": [ { "type": "integer", "minimum": 0.0 }, { "type": "null" } ], "title": "Total" }, "page": { "anyOf": [ { "type": "integer", "minimum": 1.0 }, { "type": "null" } ], "title": "Page" }, "size": { "anyOf": [ { "type": "integer", "minimum": 1.0 }, { "type": "null" } ], "title": "Size" }, "pages": { "anyOf": [ { "type": "integer", "minimum": 0.0 }, { "type": "null" } ], "title": "Pages" } }, "type": "object", "required": [ "items", "page", "size" ], "title": "Page[ItemRead]" }, "UserCreate": { "properties": { "email": { "type": "string", "format": "email", "title": "Email" }, "password": { "type": "string", "title": "Password" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active", "default": true }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser", "default": false }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified", "default": false } }, "type": "object", "required": [ "email", "password" ], "title": "UserCreate" }, "UserRead": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, "email": { "type": "string", "format": "email", "title": "Email" }, "is_active": { "type": "boolean", "title": "Is Active", "default": true }, "is_superuser": { "type": "boolean", "title": "Is Superuser", "default": false }, "is_verified": { "type": "boolean", "title": "Is Verified", "default": false } }, "type": "object", "required": [ "id", "email" ], "title": "UserRead" }, "UserUpdate": { "properties": { "password": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Password" }, "email": { "anyOf": [ { "type": "string", "format": "email" }, { "type": "null" } ], "title": "Email" }, "is_active": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Active" }, "is_superuser": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Superuser" }, "is_verified": { "anyOf": [ { "type": "boolean" }, { "type": "null" } ], "title": "Is Verified" } }, "type": "object", "title": "UserUpdate" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" }, "login": { "properties": { "grant_type": { "anyOf": [ { "type": "string", "pattern": "password" }, { "type": "null" } ], "title": "Grant Type" }, "username": { "type": "string", "title": "Username" }, "password": { "type": "string", "title": "Password" }, "scope": { "type": "string", "title": "Scope", "default": "" }, "client_id": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Id" }, "client_secret": { "anyOf": [ { "type": "string" }, { "type": "null" } ], "title": "Client Secret" } }, "type": "object", "required": [ "username", "password" ], "title": "Body_auth-auth:jwt.login" } }, "securitySchemes": { "OAuth2PasswordBearer": { "type": "oauth2", "flows": { "password": { "scopes": {}, "tokenUrl": "auth/jwt/login" } } } } } } ================================================ FILE: mkdocs.yml ================================================ site_name: Next.js FastAPI Template site_description: Kickstart scalable apps with our Next.js FastAPI Template. Includes auth, type safety (Zod), hot reload, Docker, and Vercel-ready deployment. site_url: https://vintasoftware.github.io/nextjs-fastapi-template/ repo_name: vintasoftware/nextjs-fastapi-template repo_url: https://github.com/vintasoftware/nextjs-fastapi-template/ edit_uri: blob/main/docs/ copyright: From Vinta Software to the community with 💙 theme: name: material custom_dir: overrides logo: images/nav-logo.png favicon: images/github-favicon.png features: - navigation.footer - navigation.indexes - navigation.sections - navigation.tabs - navigation.top - navigation.tracking - search.highlight - search.share - search.suggest - toc.follow palette: # Palette toggle for automatic mode - media: "(prefers-color-scheme)" primary: custom toggle: icon: material/brightness-auto name: Switch to light mode # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default primary: custom toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: custom toggle: icon: material/brightness-4 name: Switch to system preference markdown_extensions: - admonition - pymdownx.highlight: use_pygments: true - pymdownx.inlinehilite - pymdownx.superfences - pymdownx.snippets: check_paths: true - toc: permalink: true - attr_list - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg nav: - Home: README.md - Get Started: get-started.md - Additional Settings: additional-settings.md - Technology Selection: technology-selection.md - Deployment: deployment.md - Changelog: CHANGELOG.md - Contributing: contributing.md - Support: support.md plugins: - search extra: social: - icon: fontawesome/brands/github link: https://github.com/vintasoftware/nextjs-fastapi-template/ name: Nextjs FastAPI Template GitHub - icon: fontawesome/brands/x-twitter link: https://x.com/vintasoftware name: Vinta Software X - icon: fontawesome/brands/linkedin link: https://linkedin.com/company/vintasoftware name: Vinta Software LinkedIn version: provider: mike analytics: provider: google property: GTM-M9GMGBHR extra_css: - stylesheets/extra.css ================================================ FILE: nextjs-frontend/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .yarn/install-state.gz # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env .env.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts !/lib/ ================================================ FILE: nextjs-frontend/.prettierignore ================================================ openapi.json ================================================ FILE: nextjs-frontend/Dockerfile ================================================ FROM node:20-bookworm # Set the working directory WORKDIR /app # Install pnpm globally RUN npm install -g pnpm # Copy package files and install dependencies COPY package*.json ./ # Install dependencies as root (or myappuser, depending on your structure) RUN pnpm install ## Switch to the non-root user USER node # Copy the rest of your application code COPY . . EXPOSE 3000 # Start the application CMD ["./start.sh"] ================================================ FILE: nextjs-frontend/__tests__/login.test.tsx ================================================ import { login } from "@/components/actions/login-action"; import { authJwtLogin } from "@/app/clientService"; import { cookies } from "next/headers"; jest.mock("../app/clientService", () => ({ authJwtLogin: jest.fn(), })); jest.mock("next/headers", () => { const mockSet = jest.fn(); return { cookies: jest.fn().mockResolvedValue({ set: mockSet }) }; }); jest.mock("next/navigation", () => ({ redirect: jest.fn(), })); describe("login action", () => { it("should call login service action with the correct input", async () => { const formData = new FormData(); formData.set("username", "a@a.com"); formData.set("password", "Q12341414#"); const mockSet = (await cookies()).set; // Mock a successful login (authJwtLogin as jest.Mock).mockResolvedValue({ data: { access_token: "1245token" }, }); await login({}, formData); expect(authJwtLogin).toHaveBeenCalledWith({ body: { username: "a@a.com", password: "Q12341414#", }, }); expect(cookies).toHaveBeenCalled(); expect(mockSet).toHaveBeenCalledWith("accessToken", "1245token"); }); it("should should return an error if the server validation fails", async () => { const formData = new FormData(); formData.set("username", "invalid@invalid.com"); formData.set("password", "Q12341414#"); // Mock a failed login (authJwtLogin as jest.Mock).mockResolvedValue({ error: { detail: "LOGIN_BAD_CREDENTIALS", }, }); const result = await login(undefined, formData); expect(authJwtLogin).toHaveBeenCalledWith({ body: { username: "invalid@invalid.com", password: "Q12341414#", }, }); expect(result).toEqual({ server_validation_error: "LOGIN_BAD_CREDENTIALS", }); expect(cookies).not.toHaveBeenCalled(); }); it("should should return an error if either the password or username is not sent", async () => { const formData = new FormData(); formData.set("username", ""); formData.set("password", ""); const result = await login({}, formData); expect(authJwtLogin).not.toHaveBeenCalledWith(); expect(result).toEqual({ errors: { password: ["Password is required"], username: ["Username is required"], }, }); expect(cookies).not.toHaveBeenCalled(); }); it("should handle unexpected errors and return server error message", async () => { // Mock the authJwtLogin to throw an error const mockError = new Error("Network error"); (authJwtLogin as jest.Mock).mockRejectedValue(mockError); const formData = new FormData(); formData.append("username", "testuser"); formData.append("password", "password123"); const result = await login(undefined, formData); expect(result).toEqual({ server_error: "An unexpected error occurred. Please try again later.", }); }); }); ================================================ FILE: nextjs-frontend/__tests__/loginPage.test.tsx ================================================ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import Page from "@/app/login/page"; import { login } from "@/components/actions/login-action"; jest.mock("../components/actions/login-action", () => ({ login: jest.fn(), })); describe("Login Page", () => { afterEach(() => { jest.clearAllMocks(); }); it("renders the form with username and password input and submit button", () => { render(); expect(screen.getByLabelText(/username/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect( screen.getByRole("button", { name: /sign in/i }), ).toBeInTheDocument(); }); it("calls login in successful form submission", async () => { (login as jest.Mock).mockResolvedValue({}); render(); const usernameInput = screen.getByLabelText(/username/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(usernameInput, { target: { value: "testuser@example.com" }, }); fireEvent.change(passwordInput, { target: { value: "#123176a@" } }); fireEvent.click(submitButton); await waitFor(() => { const formData = new FormData(); formData.set("username", "testuser@example.com"); formData.set("password", "#123176a@"); expect(login).toHaveBeenCalledWith(undefined, formData); }); }); it("displays error message if login fails", async () => { // Mock a failed login (login as jest.Mock).mockResolvedValue({ server_validation_error: "LOGIN_BAD_CREDENTIALS", }); render(); const usernameInput = screen.getByLabelText(/username/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(usernameInput, { target: { value: "wrong@example.com" } }); fireEvent.change(passwordInput, { target: { value: "wrongpass" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText("LOGIN_BAD_CREDENTIALS")).toBeInTheDocument(); }); }); it("displays server error for unexpected errors", async () => { (login as jest.Mock).mockResolvedValue({ server_error: "An unexpected error occurred. Please try again later.", }); render(); const usernameInput = screen.getByLabelText(/username/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign in/i }); fireEvent.change(usernameInput, { target: { value: "test@test.com" } }); fireEvent.change(passwordInput, { target: { value: "password123" } }); fireEvent.click(submitButton); await waitFor(() => { expect( screen.getByText( "An unexpected error occurred. Please try again later.", ), ).toBeInTheDocument(); }); }); }); ================================================ FILE: nextjs-frontend/__tests__/passwordReset.test.tsx ================================================ import { passwordReset } from "@/components/actions/password-reset-action"; import { resetForgotPassword } from "@/app/clientService"; jest.mock("../app/openapi-client/sdk.gen", () => ({ resetForgotPassword: jest.fn(), })); jest.mock("../lib/clientConfig", () => ({ client: { setConfig: jest.fn(), }, })); describe("passwordReset action", () => { afterEach(() => { jest.clearAllMocks(); }); it("should call resetForgotPassword with the correct input and return success message", async () => { const formData = new FormData(); formData.set("email", "testuser@example.com"); // Mock a successful password reset (resetForgotPassword as jest.Mock).mockResolvedValue({}); const result = await passwordReset({}, formData); expect(resetForgotPassword).toHaveBeenCalledWith({ body: { email: "testuser@example.com" }, }); expect(result).toEqual({ message: "Password reset instructions sent to your email.", }); }); it("should return a server validation error if the server call fails", async () => { const formData = new FormData(); formData.set("email", "testuser@example.com"); // Mock a failed password reset (resetForgotPassword as jest.Mock).mockResolvedValue({ error: { detail: "User not found" }, }); const result = await passwordReset({}, formData); expect(result).toEqual({ server_validation_error: "User not found" }); expect(resetForgotPassword).toHaveBeenCalledWith({ body: { email: "testuser@example.com" }, }); }); it("should handle unexpected errors and return server error message", async () => { // Mock the resetForgotPassword to throw an error const mockError = new Error("Network error"); (resetForgotPassword as jest.Mock).mockRejectedValue(mockError); const formData = new FormData(); formData.append("email", "testuser@example.com"); const result = await passwordReset(undefined, formData); expect(result).toEqual({ server_error: "An unexpected error occurred. Please try again later.", }); }); }); ================================================ FILE: nextjs-frontend/__tests__/passwordResetConfirm.test.tsx ================================================ import { passwordResetConfirm } from "@/components/actions/password-reset-action"; import { resetResetPassword } from "@/app/clientService"; import { redirect } from "next/navigation"; jest.mock("next/navigation", () => ({ redirect: jest.fn(), })); jest.mock("../app/openapi-client/sdk.gen", () => ({ resetResetPassword: jest.fn(), })); jest.mock("../lib/clientConfig", () => ({ client: { setConfig: jest.fn(), }, })); describe("passwordReset action", () => { afterEach(() => { jest.clearAllMocks(); }); it("should call resetPassword with the correct input", async () => { const formData = new FormData(); formData.set("resetToken", "token"); formData.set("password", "P12345678#"); formData.set("passwordConfirm", "P12345678#"); // Mock a successful password reset confirm (resetResetPassword as jest.Mock).mockResolvedValue({}); await passwordResetConfirm({}, formData); expect(resetResetPassword).toHaveBeenCalledWith({ body: { token: "token", password: "P12345678#" }, }); expect(redirect).toHaveBeenCalled(); }); it("should return an error message if password reset fails", async () => { const formData = new FormData(); formData.set("resetToken", "invalid_token"); formData.set("password", "P12345678#"); formData.set("passwordConfirm", "P12345678#"); // Mock a failed password reset (resetResetPassword as jest.Mock).mockResolvedValue({ error: { detail: "Invalid token" }, }); const result = await passwordResetConfirm(undefined, formData); expect(result).toEqual({ server_validation_error: "Invalid token" }); expect(resetResetPassword).toHaveBeenCalledWith({ body: { token: "invalid_token", password: "P12345678#" }, }); }); it("should return an validation error if passwords are invalid and don't match", async () => { const formData = new FormData(); formData.set("resetToken", "token"); formData.set("password", "12345678#"); formData.set("passwordConfirm", "45678#"); const result = await passwordResetConfirm(undefined, formData); expect(result).toEqual({ errors: { password: ["Password should contain at least one uppercase letter."], passwordConfirm: ["Passwords must match."], }, }); expect(resetResetPassword).not.toHaveBeenCalledWith(); }); it("should handle unexpected errors and return server error message", async () => { // Mock the resetResetPassword to throw an error const mockError = new Error("Network error"); (resetResetPassword as jest.Mock).mockRejectedValue(mockError); const formData = new FormData(); formData.append("resetToken", "token"); formData.append("password", "P12345678#"); formData.append("passwordConfirm", "P12345678#"); const result = await passwordResetConfirm(undefined, formData); expect(result).toEqual({ server_error: "An unexpected error occurred. Please try again later.", }); }); }); ================================================ FILE: nextjs-frontend/__tests__/passwordResetConfirmPage.test.tsx ================================================ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import Page from "@/app/password-recovery/confirm/page"; import { passwordResetConfirm } from "@/components/actions/password-reset-action"; import { useSearchParams, notFound } from "next/navigation"; jest.mock("next/navigation", () => ({ ...jest.requireActual("next/navigation"), useSearchParams: jest.fn(), notFound: jest.fn(), })); jest.mock("../components/actions/password-reset-action", () => ({ passwordResetConfirm: jest.fn(), })); describe("Password Reset Confirm Page", () => { afterEach(() => { jest.clearAllMocks(); }); it("renders the form with password and confirm password input and submit button", () => { (useSearchParams as jest.Mock).mockImplementation(() => ({ get: (key: string) => (key === "token" ? "mock-token" : null), })); render(); expect(screen.getByLabelText("Password")).toBeInTheDocument(); expect(screen.getByLabelText("Password Confirm")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument(); }); it("renders the 404 page in case there is not a token", () => { (useSearchParams as jest.Mock).mockImplementation(() => ({ get: (key: string) => (key === "token" ? "" : undefined), })); render(); expect(notFound).toHaveBeenCalled(); }); it("displays error message if password reset fails", async () => { (useSearchParams as jest.Mock).mockImplementation(() => ({ get: (key: string) => (key === "token" ? "invalid-mock-token" : null), })); // Mock a successful password reset (passwordResetConfirm as jest.Mock).mockResolvedValue({ server_validation_error: "Invalid Token", }); render(); const password = screen.getByLabelText("Password"); const passwordConfirm = screen.getByLabelText("Password Confirm"); const submitButton = screen.getByRole("button", { name: /send/i }); fireEvent.change(password, { target: { value: "P12345678#" } }); fireEvent.change(passwordConfirm, { target: { value: "P12345678#" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText("Invalid Token")).toBeInTheDocument(); }); const formData = new FormData(); formData.set("password", "P12345678#"); formData.set("passwordConfirm", "P12345678#"); formData.set("resetToken", "invalid-mock-token"); expect(passwordResetConfirm).toHaveBeenCalledWith(undefined, formData); }); it("displays validation errors if password is invalid and don't match", async () => { (useSearchParams as jest.Mock).mockImplementation(() => ({ get: (key: string) => (key === "token" ? "mock-token" : null), })); // Mock a successful password reset (passwordResetConfirm as jest.Mock).mockResolvedValue({ errors: { password: ["Password should contain at least one uppercase letter."], passwordConfirm: ["Passwords must match."], }, }); render(); const password = screen.getByLabelText("Password"); const passwordConfirm = screen.getByLabelText("Password Confirm"); const submitButton = screen.getByRole("button", { name: /send/i }); fireEvent.change(password, { target: { value: "12345678#" } }); fireEvent.change(passwordConfirm, { target: { value: "45678#" } }); fireEvent.click(submitButton); await waitFor(() => { expect( screen.getByText( "Password should contain at least one uppercase letter.", ), ).toBeInTheDocument(); expect(screen.getByText("Passwords must match.")).toBeInTheDocument(); }); const formData = new FormData(); formData.set("password", "12345678#"); formData.set("passwordConfirm", "45678#"); formData.set("resetToken", "mock-token"); expect(passwordResetConfirm).toHaveBeenCalledWith(undefined, formData); }); }); ================================================ FILE: nextjs-frontend/__tests__/passwordResetPage.test.tsx ================================================ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import Page from "@/app/password-recovery/page"; import { passwordReset } from "@/components/actions/password-reset-action"; jest.mock("../components/actions/password-reset-action", () => ({ passwordReset: jest.fn(), })); describe("Password Reset Page", () => { afterEach(() => { jest.clearAllMocks(); }); it("renders the form with email input and submit button", () => { render(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument(); }); it("displays success message on successful form submission", async () => { // Mock a successful password reset (passwordReset as jest.Mock).mockResolvedValue({ message: "Password reset instructions sent to your email.", }); render(); const emailInput = screen.getByLabelText(/email/i); const submitButton = screen.getByRole("button", { name: /send/i }); fireEvent.change(emailInput, { target: { value: "testuser@example.com" } }); fireEvent.click(submitButton); await waitFor(() => { expect( screen.getByText("Password reset instructions sent to your email."), ).toBeInTheDocument(); }); const formData = new FormData(); formData.set("email", "testuser@example.com"); expect(passwordReset).toHaveBeenCalledWith(undefined, formData); }); it("displays error message if password reset fails", async () => { // Mock a failed password reset (passwordReset as jest.Mock).mockResolvedValue({ server_validation_error: "User not found", }); render(); const emailInput = screen.getByLabelText(/email/i); const submitButton = screen.getByRole("button", { name: /send/i }); fireEvent.change(emailInput, { target: { value: "invaliduser@example.com" }, }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText("User not found")).toBeInTheDocument(); }); const formData = new FormData(); formData.set("email", "invaliduser@example.com"); expect(passwordReset).toHaveBeenCalledWith(undefined, formData); }); }); ================================================ FILE: nextjs-frontend/__tests__/register.test.ts ================================================ import { register } from "@/components/actions/register-action"; import { redirect } from "next/navigation"; import { registerRegister } from "@/app/clientService"; jest.mock("next/navigation", () => ({ redirect: jest.fn(), })); jest.mock("../app/clientService", () => ({ registerRegister: jest.fn(), })); describe("register action", () => { it("should call register service action with the correct input", async () => { const formData = new FormData(); formData.set("email", "a@a.com"); formData.set("password", "Q12341414#"); // Mock a successful register (registerRegister as jest.Mock).mockResolvedValue({}); await register({}, formData); expect(registerRegister).toHaveBeenCalledWith({ body: { email: "a@a.com", password: "Q12341414#", }, }); expect(redirect).toHaveBeenCalled(); }); it("should should return an error if the server call fails", async () => { const formData = new FormData(); formData.set("email", "a@a.com"); formData.set("password", "Q12341414#"); // Mock a failed register (registerRegister as jest.Mock).mockResolvedValue({ error: { detail: "REGISTER_USER_ALREADY_EXISTS", }, }); const result = await register({}, formData); expect(registerRegister).toHaveBeenCalledWith({ body: { email: "a@a.com", password: "Q12341414#", }, }); expect(result).toEqual({ server_validation_error: "REGISTER_USER_ALREADY_EXISTS", }); }); it("should return an validation error if the form is invalid", async () => { const formData = new FormData(); formData.set("email", "email"); formData.set("password", "invalid_password"); const result = await register({}, formData); expect(result).toEqual({ errors: { email: ["Invalid email address"], password: [ "Password should contain at least one uppercase letter.", "Password should contain at least one special character.", ], }, }); expect(registerRegister).not.toHaveBeenCalledWith(); }); it("should handle unexpected errors and return server error message", async () => { // Mock the registerRegister to throw an error const mockError = new Error("Network error"); (registerRegister as jest.Mock).mockRejectedValue(mockError); const formData = new FormData(); formData.append("email", "testuser@example.com"); formData.append("password", "Password123#"); const result = await register(undefined, formData); expect(result).toEqual({ server_error: "An unexpected error occurred. Please try again later.", }); }); }); ================================================ FILE: nextjs-frontend/__tests__/registerPage.test.tsx ================================================ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import Page from "@/app/register/page"; import { register } from "@/components/actions/register-action"; jest.mock("../components/actions/register-action", () => ({ register: jest.fn(), })); describe("Register Page", () => { afterEach(() => { jest.clearAllMocks(); }); it("renders the form with email and password input and submit button", () => { render(); expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); expect( screen.getByRole("button", { name: /sign up/i }), ).toBeInTheDocument(); }); it("displays success message on successful form submission", async () => { // Mock a successful register (register as jest.Mock).mockResolvedValue({}); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign up/i }); fireEvent.change(emailInput, { target: { value: "testuser@example.com" } }); fireEvent.change(passwordInput, { target: { value: "@1231231%a" } }); fireEvent.click(submitButton); await waitFor(() => { const formData = new FormData(); formData.set("email", "testuser@example.com"); formData.set("password", "@1231231%a"); expect(register).toHaveBeenCalledWith(undefined, formData); }); }); it("displays server validation error if register fails", async () => { (register as jest.Mock).mockResolvedValue({ server_validation_error: "User already exists", }); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign up/i }); fireEvent.change(emailInput, { target: { value: "already@already.com" } }); fireEvent.change(passwordInput, { target: { value: "@1231231%a" } }); fireEvent.click(submitButton); await waitFor(() => { expect(screen.getByText("User already exists")).toBeInTheDocument(); }); }); it("displays server error for unexpected errors", async () => { (register as jest.Mock).mockResolvedValue({ server_error: "An unexpected error occurred. Please try again later.", }); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign up/i }); fireEvent.change(emailInput, { target: { value: "test@test.com" } }); fireEvent.change(passwordInput, { target: { value: "@1231231%a" } }); fireEvent.click(submitButton); await waitFor(() => { expect( screen.getByText( "An unexpected error occurred. Please try again later.", ), ).toBeInTheDocument(); }); const formData = new FormData(); formData.set("email", "test@test.com"); formData.set("password", "@1231231%a"); expect(register).toHaveBeenCalledWith(undefined, formData); }); it("displays validation errors if password and email are invalid", async () => { // Mock a successful password register (register as jest.Mock).mockResolvedValue({ errors: { email: ["Invalid email address"], password: [ "Password should contain at least one uppercase letter.", "Password should contain at least one special character.", ], }, }); render(); const emailInput = screen.getByLabelText(/email/i); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole("button", { name: /sign up/i }); fireEvent.change(emailInput, { target: { value: "email@email.com" } }); fireEvent.change(passwordInput, { target: { value: "invalid_password" } }); fireEvent.click(submitButton); await waitFor(() => { expect( screen.getByText( "Password should contain at least one uppercase letter.", ), ).toBeInTheDocument(); expect( screen.getByText( "Password should contain at least one special character.", ), ).toBeInTheDocument(); expect(screen.getByText("Invalid email address")).toBeInTheDocument(); }); const formData = new FormData(); formData.set("email", "email@email.com"); formData.set("password", "invalid_password"); expect(register).toHaveBeenCalledWith(undefined, formData); }); }); ================================================ FILE: nextjs-frontend/app/clientService.ts ================================================ export * from "./openapi-client"; import "@/lib/clientConfig"; ================================================ FILE: nextjs-frontend/app/dashboard/add-item/page.tsx ================================================ "use client"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { addItem } from "@/components/actions/items-action"; import { useActionState } from "react"; import { SubmitButton } from "@/components/ui/submitButton"; const initialState = { message: "" }; export default function CreateItemPage() { const [state, dispatch] = useActionState(addItem, initialState); return (

Create New Item

Enter the details of the new item below.

{state.errors?.name && (

{state.errors.name}

)}
{state.errors?.description && (

{state.errors.description}

)}
{state.errors?.quantity && (

{state.errors.quantity}

)}
{state?.message && (

{state.message}

)}
); } ================================================ FILE: nextjs-frontend/app/dashboard/deleteButton.tsx ================================================ "use client"; import { removeItem } from "@/components/actions/items-action"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; interface DeleteButtonProps { itemId: string; } export function DeleteButton({ itemId }: DeleteButtonProps) { const handleDelete = async () => { await removeItem(itemId); }; return ( Delete ); } ================================================ FILE: nextjs-frontend/app/dashboard/layout.tsx ================================================ import Link from "next/link"; import { Home, Users2, List } from "lucide-react"; import Image from "next/image"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { logout } from "@/components/actions/logout-action"; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return (
Home / Dashboard
Support
{children}
); } ================================================ FILE: nextjs-frontend/app/dashboard/page.tsx ================================================ import { Table, TableBody, TableCell, TableHead, TableRow, TableHeader, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { fetchItems } from "@/components/actions/items-action"; import { DeleteButton } from "./deleteButton"; import { ReadItemResponse } from "@/app/openapi-client"; import { Button } from "@/components/ui/button"; import Link from "next/link"; import { PageSizeSelector } from "@/components/page-size-selector"; import { PagePagination } from "@/components/page-pagination"; interface DashboardPageProps { searchParams: Promise<{ page?: string; size?: string; }>; } export default async function DashboardPage({ searchParams, }: DashboardPageProps) { const params = await searchParams; const page = Number(params.page) || 1; const size = Number(params.size) || 10; const items = (await fetchItems(page, size)) as ReadItemResponse; const totalPages = Math.ceil((items.total || 0) / size); return (

Welcome to your Dashboard

Here, you can see the overview of your items and manage them.

Items

Name Description Quantity Actions {!items.items?.length ? ( No results. ) : ( items.items.map((item, index) => ( {item.name} {item.description} {item.quantity} ... Edit )) )}
{/* Pagination Controls */}
); } ================================================ FILE: nextjs-frontend/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 0 0% 3.9%; --card: 0 0% 100%; --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; --primary: 0 0% 9%; --primary-foreground: 0 0% 98%; --secondary: 0 0% 96.1%; --secondary-foreground: 0 0% 9%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; --accent: 0 0% 96.1%; --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; --ring: 0 0% 3.9%; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; } .dark { --background: 0 0% 3.9%; --foreground: 0 0% 98%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; --primary: 0 0% 98%; --primary-foreground: 0 0% 9%; --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; --muted: 0 0% 14.9%; --muted-foreground: 0 0% 63.9%; --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; --border: 0 0% 14.9%; --input: 0 0% 14.9%; --ring: 0 0% 83.1%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ================================================ FILE: nextjs-frontend/app/layout.tsx ================================================ import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", weight: "100 900", }); const geistMono = localFont({ src: "./fonts/GeistMonoVF.woff", variable: "--font-geist-mono", weight: "100 900", }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( {children} ); } ================================================ FILE: nextjs-frontend/app/login/page.tsx ================================================ "use client"; import Link from "next/link"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { login } from "@/components/actions/login-action"; import { useActionState } from "react"; import { SubmitButton } from "@/components/ui/submitButton"; import { FieldError, FormError } from "@/components/ui/FormError"; export default function Page() { const [state, dispatch] = useActionState(login, undefined); return (
Login Enter your email below to log in to your account.
Forgot your password?
Don't have an account?{" "} Sign up
); } ================================================ FILE: nextjs-frontend/app/openapi-client/client/client.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from "axios"; import axios from "axios"; import { createSseClient } from "../core/serverSentEvents.gen"; import type { HttpMethod } from "../core/types.gen"; import { getValidRequestBody } from "../core/utils.gen"; import type { Client, Config, RequestOptions } from "./types.gen"; import { buildUrl, createConfig, mergeConfigs, mergeHeaders, setAuthParams, } from "./utils.gen"; export const createClient = (config: Config = {}): Client => { let _config = mergeConfigs(createConfig(), config); let instance: AxiosInstance; if (_config.axios && !("Axios" in _config.axios)) { instance = _config.axios; } else { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { auth, ...configWithoutAuth } = _config; instance = axios.create(configWithoutAuth); } const getConfig = (): Config => ({ ..._config }); const setConfig = (config: Config): Config => { _config = mergeConfigs(_config, config); instance.defaults = { ...instance.defaults, ..._config, // @ts-expect-error headers: mergeHeaders(instance.defaults.headers, _config.headers), }; return getConfig(); }; const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, axios: options.axios ?? _config.axios ?? instance, headers: mergeHeaders(_config.headers, options.headers), }; if (opts.security) { await setAuthParams({ ...opts, security: opts.security, }); } if (opts.requestValidator) { await opts.requestValidator(opts); } if (opts.body !== undefined && opts.bodySerializer) { opts.body = opts.bodySerializer(opts.body); } const url = buildUrl(opts); return { opts, url }; }; // @ts-expect-error const request: Client["request"] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); try { // assign Axios here for consistency with fetch const _axios = opts.axios!; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { auth, ...optsWithoutAuth } = opts; const response = await _axios({ ...optsWithoutAuth, baseURL: opts.baseURL as string, data: getValidRequestBody(opts), headers: opts.headers as RawAxiosRequestHeaders, // let `paramsSerializer()` handle query params if it exists params: opts.paramsSerializer ? opts.query : undefined, url, }); let { data } = response; if (opts.responseType === "json") { if (opts.responseValidator) { await opts.responseValidator(data); } if (opts.responseTransformer) { data = await opts.responseTransformer(data); } } return { ...response, data: data ?? {}, }; } catch (error) { const e = error as AxiosError; if (opts.throwOnError) { throw e; } // @ts-expect-error e.error = e.response?.data ?? {}; return e; } }; const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }); const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { const { opts, url } = await beforeRequest(options); return createSseClient({ ...opts, body: opts.body as BodyInit | null | undefined, headers: opts.headers as Record, method, // @ts-expect-error signal: opts.signal, url, }); }; return { buildUrl, connect: makeMethodFn("CONNECT"), delete: makeMethodFn("DELETE"), get: makeMethodFn("GET"), getConfig, head: makeMethodFn("HEAD"), instance, options: makeMethodFn("OPTIONS"), patch: makeMethodFn("PATCH"), post: makeMethodFn("POST"), put: makeMethodFn("PUT"), request, setConfig, sse: { connect: makeSseFn("CONNECT"), delete: makeSseFn("DELETE"), get: makeSseFn("GET"), head: makeSseFn("HEAD"), options: makeSseFn("OPTIONS"), patch: makeSseFn("PATCH"), post: makeSseFn("POST"), put: makeSseFn("PUT"), trace: makeSseFn("TRACE"), }, trace: makeMethodFn("TRACE"), } as Client; }; ================================================ FILE: nextjs-frontend/app/openapi-client/client/index.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts export type { Auth } from "../core/auth.gen"; export type { QuerySerializerOptions } from "../core/bodySerializer.gen"; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, } from "../core/bodySerializer.gen"; export { buildClientParams } from "../core/params.gen"; export { createClient } from "./client.gen"; export type { Client, ClientOptions, Config, CreateClientConfig, Options, OptionsLegacyParser, RequestOptions, RequestResult, TDataShape, } from "./types.gen"; export { createConfig } from "./utils.gen"; ================================================ FILE: nextjs-frontend/app/openapi-client/client/types.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { AxiosError, AxiosInstance, AxiosRequestHeaders, AxiosResponse, AxiosStatic, CreateAxiosDefaults, } from "axios"; import type { Auth } from "../core/auth.gen"; import type { ServerSentEventsOptions, ServerSentEventsResult, } from "../core/serverSentEvents.gen"; import type { Client as CoreClient, Config as CoreConfig, } from "../core/types.gen"; export interface Config extends Omit, CoreConfig { /** * Axios implementation. You can use this option to provide either an * `AxiosStatic` or an `AxiosInstance`. * * @default axios */ axios?: AxiosStatic | AxiosInstance; /** * Base URL for all requests made by this client. */ baseURL?: T["baseURL"]; /** * An object containing any HTTP headers that you want to pre-populate your * `Headers` object with. * * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} */ headers?: | AxiosRequestHeaders | Record< string, | string | number | boolean | (string | number | boolean)[] | null | undefined | unknown >; /** * Throw an error instead of returning it in the response? * * @default false */ throwOnError?: T["throwOnError"]; } export interface RequestOptions< TData = unknown, ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ throwOnError: ThrowOnError; }>, Pick< ServerSentEventsOptions, | "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" > { /** * Any body that you want to add to your request. * * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} */ body?: unknown; path?: Record; query?: Record; /** * Security mechanism(s) to use for the request. */ security?: ReadonlyArray; url: Url; } export interface ClientOptions { baseURL?: string; throwOnError?: boolean; } export type RequestResult< TData = unknown, TError = unknown, ThrowOnError extends boolean = boolean, > = ThrowOnError extends true ? Promise< AxiosResponse< TData extends Record ? TData[keyof TData] : TData > > : Promise< | (AxiosResponse< TData extends Record ? TData[keyof TData] : TData > & { error: undefined }) | (AxiosError< TError extends Record ? TError[keyof TError] : TError > & { data: undefined; error: TError extends Record ? TError[keyof TError] : TError; }) >; type MethodFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( options: Omit, "method">, ) => RequestResult; type SseFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( options: Omit, "method">, ) => Promise>; type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( options: Omit, "method"> & Pick>, "method">, ) => RequestResult; type BuildUrlFn = < TData extends { body?: unknown; path?: Record; query?: Record; url: string; }, >( options: Pick & Omit, "axios">, ) => string; export type Client = CoreClient< RequestFn, Config, MethodFn, BuildUrlFn, SseFn > & { instance: AxiosInstance; }; /** * The `createClientConfig()` function will be called on client initialization * and the returned object will become the client's initial configuration. * * You may want to initialize your client this way instead of calling * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ export type CreateClientConfig = ( override?: Config, ) => Config & T>; export interface TDataShape { body?: unknown; headers?: unknown; path?: unknown; query?: unknown; url: string; } type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, TResponse = unknown, > = OmitKeys< RequestOptions, "body" | "path" | "query" | "url" > & Omit; export type OptionsLegacyParser< TData = unknown, ThrowOnError extends boolean = boolean, > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< RequestOptions, "body" | "headers" | "url" > & TData : OmitKeys, "body" | "url"> & TData & Pick, "headers"> : TData extends { headers?: any } ? OmitKeys, "headers" | "url"> & TData & Pick, "body"> : OmitKeys, "url"> & TData; ================================================ FILE: nextjs-frontend/app/openapi-client/client/utils.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from "../core/auth.gen"; import type { QuerySerializerOptions } from "../core/bodySerializer.gen"; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from "../core/pathSerializer.gen"; import { getUrl } from "../core/utils.gen"; import type { Client, ClientOptions, Config, RequestOptions, } from "./types.gen"; export const createQuerySerializer = ({ allowReserved, array, object, }: QuerySerializerOptions = {}) => { const querySerializer = (queryParams: T) => { const search: string[] = []; if (queryParams && typeof queryParams === "object") { for (const name in queryParams) { const value = queryParams[name]; if (value === undefined || value === null) { continue; } if (Array.isArray(value)) { const serializedArray = serializeArrayParam({ allowReserved, explode: true, name, style: "form", value, ...array, }); if (serializedArray) search.push(serializedArray); } else if (typeof value === "object") { const serializedObject = serializeObjectParam({ allowReserved, explode: true, name, style: "deepObject", value: value as Record, ...object, }); if (serializedObject) search.push(serializedObject); } else { const serializedPrimitive = serializePrimitiveParam({ allowReserved, name, value: value as string, }); if (serializedPrimitive) search.push(serializedPrimitive); } } } return search.join("&"); }; return querySerializer; }; const checkForExistence = ( options: Pick & { headers: Record; }, name?: string, ): boolean => { if (!name) { return false; } if (name in options.headers || options.query?.[name]) { return true; } if ( "Cookie" in options.headers && options.headers["Cookie"] && typeof options.headers["Cookie"] === "string" ) { return options.headers["Cookie"].includes(`${name}=`); } return false; }; export const setAuthParams = async ({ security, ...options }: Pick, "security"> & Pick & { headers: Record; }) => { for (const auth of security) { if (checkForExistence(options, auth.name)) { continue; } const token = await getAuthToken(auth, options.auth); if (!token) { continue; } const name = auth.name ?? "Authorization"; switch (auth.in) { case "query": if (!options.query) { options.query = {}; } options.query[name] = token; break; case "cookie": { const value = `${name}=${token}`; if ("Cookie" in options.headers && options.headers["Cookie"]) { options.headers["Cookie"] = `${options.headers["Cookie"]}; ${value}`; } else { options.headers["Cookie"] = value; } break; } case "header": default: options.headers[name] = token; break; } } }; export const buildUrl: Client["buildUrl"] = (options) => getUrl({ baseUrl: options.baseURL as string, path: options.path, // let `paramsSerializer()` handle query params if it exists query: !options.paramsSerializer ? options.query : undefined, querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer), url: options.url, }); export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; config.headers = mergeHeaders(a.headers, b.headers); return config; }; /** * Special Axios headers keywords allowing to set headers by request method. */ export const axiosHeadersKeywords = [ "common", "delete", "get", "head", "patch", "post", "put", ] as const; export const mergeHeaders = ( ...headers: Array["headers"] | undefined> ): Record => { const mergedHeaders: Record = {}; for (const header of headers) { if (!header || typeof header !== "object") { continue; } const iterator = Object.entries(header); for (const [key, value] of iterator) { if ( axiosHeadersKeywords.includes( key as (typeof axiosHeadersKeywords)[number], ) && typeof value === "object" ) { mergedHeaders[key] = { ...(mergedHeaders[key] as Record), ...value, }; } else if (value === null) { delete mergedHeaders[key]; } else if (Array.isArray(value)) { for (const v of value) { // @ts-expect-error mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string]; } } else if (value !== undefined) { // assume object headers are meant to be JSON stringified, i.e. their // content value in OpenAPI specification is 'application/json' mergedHeaders[key] = typeof value === "object" ? JSON.stringify(value) : (value as string); } } } return mergedHeaders; }; export const createConfig = ( override: Config & T> = {}, ): Config & T> => ({ ...override, }); ================================================ FILE: nextjs-frontend/app/openapi-client/client.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { ClientOptions } from "./types.gen"; import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig, } from "./client"; /** * The `createClientConfig()` function will be called on client initialization * and the returned object will become the client's initial configuration. * * You may want to initialize your client this way instead of calling * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ export type CreateClientConfig = ( override?: Config, ) => Config & T>; export const client = createClient(createConfig()); ================================================ FILE: nextjs-frontend/app/openapi-client/core/auth.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts export type AuthToken = string | undefined; export interface Auth { /** * Which part of the request do we use to send the auth? * * @default 'header' */ in?: "header" | "query" | "cookie"; /** * Header or query parameter name. * * @default 'Authorization' */ name?: string; scheme?: "basic" | "bearer"; type: "apiKey" | "http"; } export const getAuthToken = async ( auth: Auth, callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { const token = typeof callback === "function" ? await callback(auth) : callback; if (!token) { return; } if (auth.scheme === "bearer") { return `Bearer ${token}`; } if (auth.scheme === "basic") { return `Basic ${btoa(token)}`; } return token; }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/bodySerializer.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { ArrayStyle, ObjectStyle, SerializerOptions, } from "./pathSerializer.gen"; export type QuerySerializer = (query: Record) => string; export type BodySerializer = (body: any) => any; export interface QuerySerializerOptions { allowReserved?: boolean; array?: SerializerOptions; object?: SerializerOptions; } const serializeFormDataPair = ( data: FormData, key: string, value: unknown, ): void => { if (typeof value === "string" || value instanceof Blob) { data.append(key, value); } else if (value instanceof Date) { data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } }; const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, ): void => { if (typeof value === "string") { data.append(key, value); } else { data.append(key, JSON.stringify(value)); } }; export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { if (value === undefined || value === null) { return; } if (Array.isArray(value)) { value.forEach((v) => serializeFormDataPair(data, key, v)); } else { serializeFormDataPair(data, key, value); } }); return data; }, }; export const jsonBodySerializer = { bodySerializer: (body: T): string => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value, ), }; export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { if (value === undefined || value === null) { return; } if (Array.isArray(value)) { value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); } else { serializeUrlSearchParamsPair(data, key, value); } }); return data.toString(); }, }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/params.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts type Slot = "body" | "headers" | "path" | "query"; export type Field = | { in: Exclude; /** * Field name. This is the name we want the user to see and use. */ key: string; /** * Field mapped name. This is the name we want to use in the request. * If omitted, we use the same value as `key`. */ map?: string; } | { in: Extract; /** * Key isn't required for bodies. */ key?: string; map?: string; }; export interface Fields { allowExtra?: Partial>; args?: ReadonlyArray; } export type FieldsConfig = ReadonlyArray; const extraPrefixesMap: Record = { $body_: "body", $headers_: "headers", $path_: "path", $query_: "query", }; const extraPrefixes = Object.entries(extraPrefixesMap); type KeyMap = Map< string, { in: Slot; map?: string; } >; const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { if (!map) { map = new Map(); } for (const config of fields) { if ("in" in config) { if (config.key) { map.set(config.key, { in: config.in, map: config.map, }); } } else if (config.args) { buildKeyMap(config.args, map); } } return map; }; interface Params { body: unknown; headers: Record; path: Record; query: Record; } const stripEmptySlots = (params: Params) => { for (const [slot, value] of Object.entries(params)) { if (value && typeof value === "object" && !Object.keys(value).length) { delete params[slot as Slot]; } } }; export const buildClientParams = ( args: ReadonlyArray, fields: FieldsConfig, ) => { const params: Params = { body: {}, headers: {}, path: {}, query: {}, }; const map = buildKeyMap(fields); let config: FieldsConfig[number] | undefined; for (const [index, arg] of args.entries()) { if (fields[index]) { config = fields[index]; } if (!config) { continue; } if ("in" in config) { if (config.key) { const field = map.get(config.key)!; const name = field.map || config.key; (params[field.in] as Record)[name] = arg; } else { params.body = arg; } } else { for (const [key, value] of Object.entries(arg ?? {})) { const field = map.get(key); if (field) { const name = field.map || key; (params[field.in] as Record)[name] = value; } else { const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix), ); if (extra) { const [prefix, slot] = extra; (params[slot] as Record)[ key.slice(prefix.length) ] = value; } else { for (const [slot, allowed] of Object.entries( config.allowExtra ?? {}, )) { if (allowed) { (params[slot as Slot] as Record)[key] = value; break; } } } } } } } stripEmptySlots(params); return params; }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/pathSerializer.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { allowReserved?: boolean; name: string; } export interface SerializerOptions { /** * @default true */ explode: boolean; style: T; } export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; type MatrixStyle = "label" | "matrix" | "simple"; export type ObjectStyle = "form" | "deepObject"; type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; interface SerializePrimitiveParam extends SerializePrimitiveOptions { value: string; } export const separatorArrayExplode = (style: ArraySeparatorStyle) => { switch (style) { case "label": return "."; case "matrix": return ";"; case "simple": return ","; default: return "&"; } }; export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { switch (style) { case "form": return ","; case "pipeDelimited": return "|"; case "spaceDelimited": return "%20"; default: return ","; } }; export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { switch (style) { case "label": return "."; case "matrix": return ";"; case "simple": return ","; default: return "&"; } }; export const serializeArrayParam = ({ allowReserved, explode, name, style, value, }: SerializeOptions & { value: unknown[]; }) => { if (!explode) { const joinedValues = ( allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) ).join(separatorArrayNoExplode(style)); switch (style) { case "label": return `.${joinedValues}`; case "matrix": return `;${name}=${joinedValues}`; case "simple": return joinedValues; default: return `${name}=${joinedValues}`; } } const separator = separatorArrayExplode(style); const joinedValues = value .map((v) => { if (style === "label" || style === "simple") { return allowReserved ? v : encodeURIComponent(v as string); } return serializePrimitiveParam({ allowReserved, name, value: v as string, }); }) .join(separator); return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; export const serializePrimitiveParam = ({ allowReserved, name, value, }: SerializePrimitiveParam) => { if (value === undefined || value === null) { return ""; } if (typeof value === "object") { throw new Error( "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", ); } return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; }; export const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly, }: SerializeOptions & { value: Record | Date; valueOnly?: boolean; }) => { if (value instanceof Date) { return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; } if (style !== "deepObject" && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { values = [ ...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string), ]; }); const joinedValues = values.join(","); switch (style) { case "form": return `${name}=${joinedValues}`; case "label": return `.${joinedValues}`; case "matrix": return `;${name}=${joinedValues}`; default: return joinedValues; } } const separator = separatorObjectExplode(style); const joinedValues = Object.entries(value) .map(([key, v]) => serializePrimitiveParam({ allowReserved, name: style === "deepObject" ? `${name}[${key}]` : key, value: v as string, }), ) .join(separator); return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/serverSentEvents.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { Config } from "./types.gen"; export type ServerSentEventsOptions = Omit< RequestInit, "method" > & Pick & { /** * Fetch API implementation. You can use this option to provide a custom * fetch instance. * * @default globalThis.fetch */ fetch?: typeof fetch; /** * Implementing clients can call request interceptors inside this hook. */ onRequest?: (url: string, init: RequestInit) => Promise; /** * Callback invoked when a network or parsing error occurs during streaming. * * This option applies only if the endpoint returns a stream of events. * * @param error The error that occurred. */ onSseError?: (error: unknown) => void; /** * Callback invoked when an event is streamed from the server. * * This option applies only if the endpoint returns a stream of events. * * @param event Event streamed from the server. * @returns Nothing (void). */ onSseEvent?: (event: StreamEvent) => void; serializedBody?: RequestInit["body"]; /** * Default retry delay in milliseconds. * * This option applies only if the endpoint returns a stream of events. * * @default 3000 */ sseDefaultRetryDelay?: number; /** * Maximum number of retry attempts before giving up. */ sseMaxRetryAttempts?: number; /** * Maximum retry delay in milliseconds. * * Applies only when exponential backoff is used. * * This option applies only if the endpoint returns a stream of events. * * @default 30000 */ sseMaxRetryDelay?: number; /** * Optional sleep function for retry backoff. * * Defaults to using `setTimeout`. */ sseSleepFn?: (ms: number) => Promise; url: string; }; export interface StreamEvent { data: TData; event?: string; id?: string; retry?: number; } export type ServerSentEventsResult< TData = unknown, TReturn = void, TNext = unknown, > = { stream: AsyncGenerator< TData extends Record ? TData[keyof TData] : TData, TReturn, TNext >; }; export const createSseClient = ({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }: ServerSentEventsOptions): ServerSentEventsResult => { let lastEventId: string | undefined; const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); const createStream = async function* () { let retryDelay: number = sseDefaultRetryDelay ?? 3000; let attempt = 0; const signal = options.signal ?? new AbortController().signal; while (true) { if (signal.aborted) break; attempt++; const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers as Record | undefined); if (lastEventId !== undefined) { headers.set("Last-Event-ID", lastEventId); } try { const requestInit: RequestInit = { redirect: "follow", ...options, body: options.serializedBody, headers, signal, }; let request = new Request(url, requestInit); if (onRequest) { request = await onRequest(url, requestInit); } // fetch must be assigned here, otherwise it would throw the error: // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation const _fetch = options.fetch ?? globalThis.fetch; const response = await _fetch(request); if (!response.ok) throw new Error( `SSE failed: ${response.status} ${response.statusText}`, ); if (!response.body) throw new Error("No body in SSE response"); const reader = response.body .pipeThrough(new TextDecoderStream()) .getReader(); let buffer = ""; const abortHandler = () => { try { reader.cancel(); } catch { // noop } }; signal.addEventListener("abort", abortHandler); try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += value; const chunks = buffer.split("\n\n"); buffer = chunks.pop() ?? ""; for (const chunk of chunks) { const lines = chunk.split("\n"); const dataLines: Array = []; let eventName: string | undefined; for (const line of lines) { if (line.startsWith("data:")) { dataLines.push(line.replace(/^data:\s*/, "")); } else if (line.startsWith("event:")) { eventName = line.replace(/^event:\s*/, ""); } else if (line.startsWith("id:")) { lastEventId = line.replace(/^id:\s*/, ""); } else if (line.startsWith("retry:")) { const parsed = Number.parseInt( line.replace(/^retry:\s*/, ""), 10, ); if (!Number.isNaN(parsed)) { retryDelay = parsed; } } } let data: unknown; let parsedJson = false; if (dataLines.length) { const rawData = dataLines.join("\n"); try { data = JSON.parse(rawData); parsedJson = true; } catch { data = rawData; } } if (parsedJson) { if (responseValidator) { await responseValidator(data); } if (responseTransformer) { data = await responseTransformer(data); } } onSseEvent?.({ data, event: eventName, id: lastEventId, retry: retryDelay, }); if (dataLines.length) { yield data as any; } } } } finally { signal.removeEventListener("abort", abortHandler); reader.releaseLock(); } break; // exit loop on normal completion } catch (error) { // connection failed or aborted; retry after delay onSseError?.(error); if ( sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts ) { break; // stop after firing error } // exponential backoff: double retry each attempt, cap at 30s const backoff = Math.min( retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000, ); await sleep(backoff); } } }; const stream = createStream(); return { stream }; }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/types.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { Auth, AuthToken } from "./auth.gen"; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, } from "./bodySerializer.gen"; export type HttpMethod = | "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never, > = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; getConfig: () => Config; request: RequestFn; setConfig: (config: Config) => Config; } & { [K in HttpMethod]: MethodFn; } & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** * Auth token or a function returning auth token. The resolved value will be * added to the request payload as defined by its `security` array. */ auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; /** * A function for serializing request body parameter. By default, * {@link JSON.stringify()} will be used. */ bodySerializer?: BodySerializer | null; /** * An object containing any HTTP headers that you want to pre-populate your * `Headers` object with. * * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} */ headers?: | RequestInit["headers"] | Record< string, | string | number | boolean | (string | number | boolean)[] | null | undefined | unknown >; /** * The request method. * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject * style, and reserved characters are percent-encoded. * * This method will have no effect if the native `paramsSerializer()` Axios * API function is used. * * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; /** * A function validating request data. This is useful if you want to ensure * the request conforms to the desired shape, so it can be safely sent to * the server. */ requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. */ responseTransformer?: (data: unknown) => Promise; /** * A function validating response data. This is useful if you want to ensure * the response conforms to the desired shape, so it can be safely passed to * the transformers and returned to the user. */ responseValidator?: (data: unknown) => Promise; } type IsExactlyNeverOrNeverUndefined = [T] extends [never] ? true : [T] extends [never | undefined] ? [undefined] extends [T] ? false : true : false; export type OmitNever> = { [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; }; ================================================ FILE: nextjs-frontend/app/openapi-client/core/utils.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen"; import { type ArraySeparatorStyle, serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from "./pathSerializer.gen"; export interface PathSerializer { path: Record; url: string; } export const PATH_PARAM_RE = /\{[^{}]+\}/g; export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { let url = _url; const matches = _url.match(PATH_PARAM_RE); if (matches) { for (const match of matches) { let explode = false; let name = match.substring(1, match.length - 1); let style: ArraySeparatorStyle = "simple"; if (name.endsWith("*")) { explode = true; name = name.substring(0, name.length - 1); } if (name.startsWith(".")) { name = name.substring(1); style = "label"; } else if (name.startsWith(";")) { name = name.substring(1); style = "matrix"; } const value = path[name]; if (value === undefined || value === null) { continue; } if (Array.isArray(value)) { url = url.replace( match, serializeArrayParam({ explode, name, style, value }), ); continue; } if (typeof value === "object") { url = url.replace( match, serializeObjectParam({ explode, name, style, value: value as Record, valueOnly: true, }), ); continue; } if (style === "matrix") { url = url.replace( match, `;${serializePrimitiveParam({ name, value: value as string, })}`, ); continue; } const replaceValue = encodeURIComponent( style === "label" ? `.${value as string}` : (value as string), ); url = url.replace(match, replaceValue); } } return url; }; export const getUrl = ({ baseUrl, path, query, querySerializer, url: _url, }: { baseUrl?: string; path?: Record; query?: Record; querySerializer: QuerySerializer; url: string; }) => { const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; let url = (baseUrl ?? "") + pathUrl; if (path) { url = defaultPathSerializer({ path, url }); } let search = query ? querySerializer(query) : ""; if (search.startsWith("?")) { search = search.substring(1); } if (search) { url += `?${search}`; } return url; }; export function getValidRequestBody(options: { body?: unknown; bodySerializer?: BodySerializer | null; serializedBody?: unknown; }) { const hasBody = options.body !== undefined; const isSerializedBody = hasBody && options.bodySerializer; if (isSerializedBody) { if ("serializedBody" in options) { const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""; return hasSerializedBody ? options.serializedBody : null; } // not all clients implement a serializedBody property (i.e. client-axios) return options.body !== "" ? options.body : null; } // plain/text body if (hasBody) { return options.body; } // no body was provided return undefined; } ================================================ FILE: nextjs-frontend/app/openapi-client/index.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts export * from "./types.gen"; export * from "./sdk.gen"; ================================================ FILE: nextjs-frontend/app/openapi-client/sdk.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, urlSearchParamsBodySerializer, } from "./client"; import type { AuthJwtLoginData, AuthJwtLoginResponses, AuthJwtLoginErrors, AuthJwtLogoutData, AuthJwtLogoutResponses, AuthJwtLogoutErrors, RegisterRegisterData, RegisterRegisterResponses, RegisterRegisterErrors, ResetForgotPasswordData, ResetForgotPasswordResponses, ResetForgotPasswordErrors, ResetResetPasswordData, ResetResetPasswordResponses, ResetResetPasswordErrors, VerifyRequestTokenData, VerifyRequestTokenResponses, VerifyRequestTokenErrors, VerifyVerifyData, VerifyVerifyResponses, VerifyVerifyErrors, UsersCurrentUserData, UsersCurrentUserResponses, UsersCurrentUserErrors, UsersPatchCurrentUserData, UsersPatchCurrentUserResponses, UsersPatchCurrentUserErrors, UsersDeleteUserData, UsersDeleteUserResponses, UsersDeleteUserErrors, UsersUserData, UsersUserResponses, UsersUserErrors, UsersPatchUserData, UsersPatchUserResponses, UsersPatchUserErrors, ReadItemData, ReadItemResponses, ReadItemErrors, CreateItemData, CreateItemResponses, CreateItemErrors, DeleteItemData, DeleteItemResponses, DeleteItemErrors, } from "./types.gen"; import { client } from "./client.gen"; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, > = ClientOptions & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a * custom client. */ client?: Client; /** * You can pass arbitrary values through the `meta` object. This can be * used to access values that aren't defined as part of the SDK function. */ meta?: Record; }; /** * Auth:Jwt.Login */ export const authJwtLogin = ( options: Options, ) => { return (options.client ?? client).post< AuthJwtLoginResponses, AuthJwtLoginErrors, ThrowOnError >({ ...urlSearchParamsBodySerializer, responseType: "json", url: "/auth/jwt/login", ...options, headers: { "Content-Type": "application/x-www-form-urlencoded", ...options.headers, }, }); }; /** * Auth:Jwt.Logout */ export const authJwtLogout = ( options?: Options, ) => { return (options?.client ?? client).post< AuthJwtLogoutResponses, AuthJwtLogoutErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/auth/jwt/logout", ...options, }); }; /** * Register:Register */ export const registerRegister = ( options: Options, ) => { return (options.client ?? client).post< RegisterRegisterResponses, RegisterRegisterErrors, ThrowOnError >({ responseType: "json", url: "/auth/register", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Reset:Forgot Password */ export const resetForgotPassword = ( options: Options, ) => { return (options.client ?? client).post< ResetForgotPasswordResponses, ResetForgotPasswordErrors, ThrowOnError >({ responseType: "json", url: "/auth/forgot-password", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Reset:Reset Password */ export const resetResetPassword = ( options: Options, ) => { return (options.client ?? client).post< ResetResetPasswordResponses, ResetResetPasswordErrors, ThrowOnError >({ responseType: "json", url: "/auth/reset-password", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Verify:Request-Token */ export const verifyRequestToken = ( options: Options, ) => { return (options.client ?? client).post< VerifyRequestTokenResponses, VerifyRequestTokenErrors, ThrowOnError >({ responseType: "json", url: "/auth/request-verify-token", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Verify:Verify */ export const verifyVerify = ( options: Options, ) => { return (options.client ?? client).post< VerifyVerifyResponses, VerifyVerifyErrors, ThrowOnError >({ responseType: "json", url: "/auth/verify", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Users:Current User */ export const usersCurrentUser = ( options?: Options, ) => { return (options?.client ?? client).get< UsersCurrentUserResponses, UsersCurrentUserErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/users/me", ...options, }); }; /** * Users:Patch Current User */ export const usersPatchCurrentUser = ( options: Options, ) => { return (options.client ?? client).patch< UsersPatchCurrentUserResponses, UsersPatchCurrentUserErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/users/me", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Users:Delete User */ export const usersDeleteUser = ( options: Options, ) => { return (options.client ?? client).delete< UsersDeleteUserResponses, UsersDeleteUserErrors, ThrowOnError >({ security: [ { scheme: "bearer", type: "http", }, ], url: "/users/{id}", ...options, }); }; /** * Users:User */ export const usersUser = ( options: Options, ) => { return (options.client ?? client).get< UsersUserResponses, UsersUserErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/users/{id}", ...options, }); }; /** * Users:Patch User */ export const usersPatchUser = ( options: Options, ) => { return (options.client ?? client).patch< UsersPatchUserResponses, UsersPatchUserErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/users/{id}", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Read Item */ export const readItem = ( options?: Options, ) => { return (options?.client ?? client).get< ReadItemResponses, ReadItemErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/items/", ...options, }); }; /** * Create Item */ export const createItem = ( options: Options, ) => { return (options.client ?? client).post< CreateItemResponses, CreateItemErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/items/", ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); }; /** * Delete Item */ export const deleteItem = ( options: Options, ) => { return (options.client ?? client).delete< DeleteItemResponses, DeleteItemErrors, ThrowOnError >({ responseType: "json", security: [ { scheme: "bearer", type: "http", }, ], url: "/items/{item_id}", ...options, }); }; ================================================ FILE: nextjs-frontend/app/openapi-client/types.gen.ts ================================================ // This file is auto-generated by @hey-api/openapi-ts /** * BearerResponse */ export type BearerResponse = { /** * Access Token */ access_token: string; /** * Token Type */ token_type: string; }; /** * Body_auth-reset:forgot_password */ export type BodyAuthResetForgotPassword = { /** * Email */ email: string; }; /** * Body_auth-reset:reset_password */ export type BodyAuthResetResetPassword = { /** * Token */ token: string; /** * Password */ password: string; }; /** * Body_auth-verify:request-token */ export type BodyAuthVerifyRequestToken = { /** * Email */ email: string; }; /** * Body_auth-verify:verify */ export type BodyAuthVerifyVerify = { /** * Token */ token: string; }; /** * ErrorModel */ export type ErrorModel = { /** * Detail */ detail: | string | { [key: string]: string; }; }; /** * HTTPValidationError */ export type HttpValidationError = { /** * Detail */ detail?: Array; }; /** * ItemCreate */ export type ItemCreate = { /** * Name */ name: string; /** * Description */ description?: string | null; /** * Quantity */ quantity?: number | null; }; /** * ItemRead */ export type ItemRead = { /** * Name */ name: string; /** * Description */ description?: string | null; /** * Quantity */ quantity?: number | null; /** * Id */ id: string; /** * User Id */ user_id: string; }; /** * Page[ItemRead] */ export type PageItemRead = { /** * Items */ items: Array; /** * Total */ total?: number | null; /** * Page */ page: number | null; /** * Size */ size: number | null; /** * Pages */ pages?: number | null; }; /** * UserCreate */ export type UserCreate = { /** * Email */ email: string; /** * Password */ password: string; /** * Is Active */ is_active?: boolean | null; /** * Is Superuser */ is_superuser?: boolean | null; /** * Is Verified */ is_verified?: boolean | null; }; /** * UserRead */ export type UserRead = { /** * Id */ id: string; /** * Email */ email: string; /** * Is Active */ is_active?: boolean; /** * Is Superuser */ is_superuser?: boolean; /** * Is Verified */ is_verified?: boolean; }; /** * UserUpdate */ export type UserUpdate = { /** * Password */ password?: string | null; /** * Email */ email?: string | null; /** * Is Active */ is_active?: boolean | null; /** * Is Superuser */ is_superuser?: boolean | null; /** * Is Verified */ is_verified?: boolean | null; }; /** * ValidationError */ export type ValidationError = { /** * Location */ loc: Array; /** * Message */ msg: string; /** * Error Type */ type: string; }; /** * Body_auth-auth:jwt.login */ export type Login = { /** * Grant Type */ grant_type?: string | null; /** * Username */ username: string; /** * Password */ password: string; /** * Scope */ scope?: string; /** * Client Id */ client_id?: string | null; /** * Client Secret */ client_secret?: string | null; }; export type AuthJwtLoginData = { body: Login; path?: never; query?: never; url: "/auth/jwt/login"; }; export type AuthJwtLoginErrors = { /** * Bad Request */ 400: ErrorModel; /** * Validation Error */ 422: HttpValidationError; }; export type AuthJwtLoginError = AuthJwtLoginErrors[keyof AuthJwtLoginErrors]; export type AuthJwtLoginResponses = { /** * Successful Response */ 200: BearerResponse; }; export type AuthJwtLoginResponse = AuthJwtLoginResponses[keyof AuthJwtLoginResponses]; export type AuthJwtLogoutData = { body?: never; path?: never; query?: never; url: "/auth/jwt/logout"; }; export type AuthJwtLogoutErrors = { /** * Missing token or inactive user. */ 401: unknown; }; export type AuthJwtLogoutResponses = { /** * Successful Response */ 200: unknown; }; export type RegisterRegisterData = { body: UserCreate; path?: never; query?: never; url: "/auth/register"; }; export type RegisterRegisterErrors = { /** * Bad Request */ 400: ErrorModel; /** * Validation Error */ 422: HttpValidationError; }; export type RegisterRegisterError = RegisterRegisterErrors[keyof RegisterRegisterErrors]; export type RegisterRegisterResponses = { /** * Successful Response */ 201: UserRead; }; export type RegisterRegisterResponse = RegisterRegisterResponses[keyof RegisterRegisterResponses]; export type ResetForgotPasswordData = { body: BodyAuthResetForgotPassword; path?: never; query?: never; url: "/auth/forgot-password"; }; export type ResetForgotPasswordErrors = { /** * Validation Error */ 422: HttpValidationError; }; export type ResetForgotPasswordError = ResetForgotPasswordErrors[keyof ResetForgotPasswordErrors]; export type ResetForgotPasswordResponses = { /** * Successful Response */ 202: unknown; }; export type ResetResetPasswordData = { body: BodyAuthResetResetPassword; path?: never; query?: never; url: "/auth/reset-password"; }; export type ResetResetPasswordErrors = { /** * Bad Request */ 400: ErrorModel; /** * Validation Error */ 422: HttpValidationError; }; export type ResetResetPasswordError = ResetResetPasswordErrors[keyof ResetResetPasswordErrors]; export type ResetResetPasswordResponses = { /** * Successful Response */ 200: unknown; }; export type VerifyRequestTokenData = { body: BodyAuthVerifyRequestToken; path?: never; query?: never; url: "/auth/request-verify-token"; }; export type VerifyRequestTokenErrors = { /** * Validation Error */ 422: HttpValidationError; }; export type VerifyRequestTokenError = VerifyRequestTokenErrors[keyof VerifyRequestTokenErrors]; export type VerifyRequestTokenResponses = { /** * Successful Response */ 202: unknown; }; export type VerifyVerifyData = { body: BodyAuthVerifyVerify; path?: never; query?: never; url: "/auth/verify"; }; export type VerifyVerifyErrors = { /** * Bad Request */ 400: ErrorModel; /** * Validation Error */ 422: HttpValidationError; }; export type VerifyVerifyError = VerifyVerifyErrors[keyof VerifyVerifyErrors]; export type VerifyVerifyResponses = { /** * Successful Response */ 200: UserRead; }; export type VerifyVerifyResponse = VerifyVerifyResponses[keyof VerifyVerifyResponses]; export type UsersCurrentUserData = { body?: never; path?: never; query?: never; url: "/users/me"; }; export type UsersCurrentUserErrors = { /** * Missing token or inactive user. */ 401: unknown; }; export type UsersCurrentUserResponses = { /** * Successful Response */ 200: UserRead; }; export type UsersCurrentUserResponse = UsersCurrentUserResponses[keyof UsersCurrentUserResponses]; export type UsersPatchCurrentUserData = { body: UserUpdate; path?: never; query?: never; url: "/users/me"; }; export type UsersPatchCurrentUserErrors = { /** * Bad Request */ 400: ErrorModel; /** * Missing token or inactive user. */ 401: unknown; /** * Validation Error */ 422: HttpValidationError; }; export type UsersPatchCurrentUserError = UsersPatchCurrentUserErrors[keyof UsersPatchCurrentUserErrors]; export type UsersPatchCurrentUserResponses = { /** * Successful Response */ 200: UserRead; }; export type UsersPatchCurrentUserResponse = UsersPatchCurrentUserResponses[keyof UsersPatchCurrentUserResponses]; export type UsersDeleteUserData = { body?: never; path: { /** * Id */ id: string; }; query?: never; url: "/users/{id}"; }; export type UsersDeleteUserErrors = { /** * Missing token or inactive user. */ 401: unknown; /** * Not a superuser. */ 403: unknown; /** * The user does not exist. */ 404: unknown; /** * Validation Error */ 422: HttpValidationError; }; export type UsersDeleteUserError = UsersDeleteUserErrors[keyof UsersDeleteUserErrors]; export type UsersDeleteUserResponses = { /** * Successful Response */ 204: void; }; export type UsersDeleteUserResponse = UsersDeleteUserResponses[keyof UsersDeleteUserResponses]; export type UsersUserData = { body?: never; path: { /** * Id */ id: string; }; query?: never; url: "/users/{id}"; }; export type UsersUserErrors = { /** * Missing token or inactive user. */ 401: unknown; /** * Not a superuser. */ 403: unknown; /** * The user does not exist. */ 404: unknown; /** * Validation Error */ 422: HttpValidationError; }; export type UsersUserError = UsersUserErrors[keyof UsersUserErrors]; export type UsersUserResponses = { /** * Successful Response */ 200: UserRead; }; export type UsersUserResponse = UsersUserResponses[keyof UsersUserResponses]; export type UsersPatchUserData = { body: UserUpdate; path: { /** * Id */ id: string; }; query?: never; url: "/users/{id}"; }; export type UsersPatchUserErrors = { /** * Bad Request */ 400: ErrorModel; /** * Missing token or inactive user. */ 401: unknown; /** * Not a superuser. */ 403: unknown; /** * The user does not exist. */ 404: unknown; /** * Validation Error */ 422: HttpValidationError; }; export type UsersPatchUserError = UsersPatchUserErrors[keyof UsersPatchUserErrors]; export type UsersPatchUserResponses = { /** * Successful Response */ 200: UserRead; }; export type UsersPatchUserResponse = UsersPatchUserResponses[keyof UsersPatchUserResponses]; export type ReadItemData = { body?: never; path?: never; query?: { /** * Page * Page number */ page?: number; /** * Size * Page size */ size?: number; }; url: "/items/"; }; export type ReadItemErrors = { /** * Validation Error */ 422: HttpValidationError; }; export type ReadItemError = ReadItemErrors[keyof ReadItemErrors]; export type ReadItemResponses = { /** * Successful Response */ 200: PageItemRead; }; export type ReadItemResponse = ReadItemResponses[keyof ReadItemResponses]; export type CreateItemData = { body: ItemCreate; path?: never; query?: never; url: "/items/"; }; export type CreateItemErrors = { /** * Validation Error */ 422: HttpValidationError; }; export type CreateItemError = CreateItemErrors[keyof CreateItemErrors]; export type CreateItemResponses = { /** * Successful Response */ 200: ItemRead; }; export type CreateItemResponse = CreateItemResponses[keyof CreateItemResponses]; export type DeleteItemData = { body?: never; path: { /** * Item Id */ item_id: string; }; query?: never; url: "/items/{item_id}"; }; export type DeleteItemErrors = { /** * Validation Error */ 422: HttpValidationError; }; export type DeleteItemError = DeleteItemErrors[keyof DeleteItemErrors]; export type DeleteItemResponses = { /** * Successful Response */ 200: unknown; }; export type ClientOptions = { baseURL: `${string}://openapi.json` | (string & {}); }; ================================================ FILE: nextjs-frontend/app/page.tsx ================================================ import { Button } from "@/components/ui/button"; import Link from "next/link"; import { FaGithub } from "react-icons/fa"; import { Badge } from "@/components/ui/badge"; export default function Home() { return (

Welcome to the Next.js & FastAPI Boilerplate

A simple and powerful template to get started with full-stack development using Next.js and FastAPI.

{/* Link to Dashboard */} {/* GitHub Badge */}
View on GitHub
); } ================================================ FILE: nextjs-frontend/app/password-recovery/confirm/page.tsx ================================================ "use client"; import { useActionState } from "react"; import { notFound, useSearchParams } from "next/navigation"; import { passwordResetConfirm } from "@/components/actions/password-reset-action"; import { SubmitButton } from "@/components/ui/submitButton"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Suspense } from "react"; import { FieldError, FormError } from "@/components/ui/FormError"; function ResetPasswordForm() { const [state, dispatch] = useActionState(passwordResetConfirm, undefined); const searchParams = useSearchParams(); const token = searchParams.get("token"); if (!token) { notFound(); } return (
Reset your Password Enter the new password and confirm it.
); } export default function Page() { return (
Loading reset form...
}> ); } ================================================ FILE: nextjs-frontend/app/password-recovery/page.tsx ================================================ "use client"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { passwordReset } from "@/components/actions/password-reset-action"; import { useActionState } from "react"; import { SubmitButton } from "@/components/ui/submitButton"; import Link from "next/link"; import { FormError } from "@/components/ui/FormError"; export default function Page() { const [state, dispatch] = useActionState(passwordReset, undefined); return (
Password Recovery Enter your email to receive instructions to reset your password.
{state?.message &&

{state.message}

}
Back to login
); } ================================================ FILE: nextjs-frontend/app/register/page.tsx ================================================ "use client"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { register } from "@/components/actions/register-action"; import { useActionState } from "react"; import { SubmitButton } from "@/components/ui/submitButton"; import Link from "next/link"; import { FieldError, FormError } from "@/components/ui/FormError"; export default function Page() { const [state, dispatch] = useActionState(register, undefined); return (
Sign Up Enter your email and password below to create your account.
Back to login
); } ================================================ FILE: nextjs-frontend/components/actions/items-action.ts ================================================ "use server"; import { cookies } from "next/headers"; import { readItem, deleteItem, createItem } from "@/app/clientService"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { itemSchema } from "@/lib/definitions"; export async function fetchItems(page: number = 1, size: number = 10) { const cookieStore = await cookies(); const token = cookieStore.get("accessToken")?.value; if (!token) { return { message: "No access token found" }; } const { data, error } = await readItem({ query: { page: page, size: size, }, headers: { Authorization: `Bearer ${token}`, }, }); if (error) { return { message: error }; } return data; } export async function removeItem(id: string) { const cookieStore = await cookies(); const token = cookieStore.get("accessToken")?.value; if (!token) { return { message: "No access token found" }; } const { error } = await deleteItem({ headers: { Authorization: `Bearer ${token}`, }, path: { item_id: id, }, }); if (error) { return { message: error }; } revalidatePath("/dashboard"); } export async function addItem(prevState: {}, formData: FormData) { const cookieStore = await cookies(); const token = cookieStore.get("accessToken")?.value; if (!token) { return { message: "No access token found" }; } const validatedFields = itemSchema.safeParse({ name: formData.get("name"), description: formData.get("description"), quantity: formData.get("quantity"), }); if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors }; } const { name, description, quantity } = validatedFields.data; const input = { headers: { Authorization: `Bearer ${token}`, }, body: { name, description, quantity, }, }; const { error } = await createItem(input); if (error) { return { message: `${error.detail}` }; } redirect(`/dashboard`); } ================================================ FILE: nextjs-frontend/components/actions/login-action.ts ================================================ "use server"; import { cookies } from "next/headers"; import { authJwtLogin } from "@/app/clientService"; import { redirect } from "next/navigation"; import { loginSchema } from "@/lib/definitions"; import { getErrorMessage } from "@/lib/utils"; export async function login(prevState: unknown, formData: FormData) { const validatedFields = loginSchema.safeParse({ username: formData.get("username") as string, password: formData.get("password") as string, }); if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, }; } const { username, password } = validatedFields.data; const input = { body: { username, password, }, }; try { const { data, error } = await authJwtLogin(input); if (error) { return { server_validation_error: getErrorMessage(error) }; } (await cookies()).set("accessToken", data.access_token); } catch (err) { console.error("Login error:", err); return { server_error: "An unexpected error occurred. Please try again later.", }; } redirect("/dashboard"); } ================================================ FILE: nextjs-frontend/components/actions/logout-action.ts ================================================ "use server"; import { cookies } from "next/headers"; import { authJwtLogout } from "@/app/clientService"; import { redirect } from "next/navigation"; export async function logout() { const cookieStore = await cookies(); const token = cookieStore.get("accessToken")?.value; if (!token) { return { message: "No access token found" }; } const { error } = await authJwtLogout({ headers: { Authorization: `Bearer ${token}`, }, }); if (error) { return { message: error }; } cookieStore.delete("accessToken"); redirect(`/login`); } ================================================ FILE: nextjs-frontend/components/actions/password-reset-action.ts ================================================ "use server"; import { resetForgotPassword, resetResetPassword } from "@/app/clientService"; import { redirect } from "next/navigation"; import { passwordResetConfirmSchema } from "@/lib/definitions"; import { getErrorMessage } from "@/lib/utils"; export async function passwordReset(prevState: unknown, formData: FormData) { const input = { body: { email: formData.get("email") as string, }, }; try { const { error } = await resetForgotPassword(input); if (error) { return { server_validation_error: getErrorMessage(error) }; } return { message: "Password reset instructions sent to your email." }; } catch (err) { console.error("Password reset error:", err); return { server_error: "An unexpected error occurred. Please try again later.", }; } } export async function passwordResetConfirm( prevState: unknown, formData: FormData, ) { const validatedFields = passwordResetConfirmSchema.safeParse({ token: formData.get("resetToken") as string, password: formData.get("password") as string, passwordConfirm: formData.get("passwordConfirm") as string, }); if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, }; } const { token, password } = validatedFields.data; const input = { body: { token, password, }, }; try { const { error } = await resetResetPassword(input); if (error) { return { server_validation_error: getErrorMessage(error) }; } redirect(`/login`); } catch (err) { console.error("Password reset confirmation error:", err); return { server_error: "An unexpected error occurred. Please try again later.", }; } } ================================================ FILE: nextjs-frontend/components/actions/register-action.ts ================================================ "use server"; import { redirect } from "next/navigation"; import { registerRegister } from "@/app/clientService"; import { registerSchema } from "@/lib/definitions"; import { getErrorMessage } from "@/lib/utils"; export async function register(prevState: unknown, formData: FormData) { const validatedFields = registerSchema.safeParse({ email: formData.get("email") as string, password: formData.get("password") as string, }); if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, }; } const { email, password } = validatedFields.data; const input = { body: { email, password, }, }; try { const { error } = await registerRegister(input); if (error) { return { server_validation_error: getErrorMessage(error) }; } } catch (err) { console.error("Registration error:", err); return { server_error: "An unexpected error occurred. Please try again later.", }; } redirect(`/login`); } ================================================ FILE: nextjs-frontend/components/page-pagination.tsx ================================================ import { Button } from "@/components/ui/button"; import Link from "next/link"; import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRightIcon, } from "@radix-ui/react-icons"; interface PagePaginationProps { currentPage: number; totalPages: number; pageSize: number; totalItems: number; basePath?: string; } export function PagePagination({ currentPage, totalPages, pageSize, totalItems, basePath = "/dashboard", }: PagePaginationProps) { const hasNextPage = currentPage < totalPages; const hasPreviousPage = currentPage > 1; const buildUrl = (page: number) => `${basePath}?page=${page}&size=${pageSize}`; return (
{totalItems === 0 ? ( <>Showing 0 of 0 results ) : ( <> Showing {(currentPage - 1) * pageSize + 1} to{" "} {Math.min(currentPage * pageSize, totalItems)} of {totalItems}{" "} results )}
{/* First Page */} {/* Previous Page */} {/* Page Info */} {totalPages > 0 && ( Page {currentPage} of {totalPages} )} {/* Next Page */} {/* Last Page */}
); } ================================================ FILE: nextjs-frontend/components/page-size-selector.tsx ================================================ "use client"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useRouter } from "next/navigation"; interface PageSizeSelectorProps { currentSize: number; } export function PageSizeSelector({ currentSize }: PageSizeSelectorProps) { const router = useRouter(); const pageSizeOptions = [5, 10, 20, 50, 100]; const handleSizeChange = (newSize: string) => { router.push(`/dashboard?page=1&size=${newSize}`); }; return (
Items per page:
); } ================================================ FILE: nextjs-frontend/components/ui/FormError.tsx ================================================ interface ErrorState { errors?: { [key: string]: string | string[]; }; server_validation_error?: string; server_error?: string; } interface FormErrorProps { state?: ErrorState; className?: string; } export function FormError({ state, className = "" }: FormErrorProps) { if (!state) return null; const error = state.server_validation_error || state.server_error; if (!error) return null; return

{error}

; } interface FieldErrorProps { state?: ErrorState; field: string; className?: string; } export function FieldError({ state, field, className = "" }: FieldErrorProps) { if (!state?.errors) return null; const error = state.errors[field]; if (!error) return null; if (Array.isArray(error)) { return (
    {error.map((err) => (
  • {err}
  • ))}
); } return

{error}

; } ================================================ FILE: nextjs-frontend/components/ui/avatar.tsx ================================================ "use client"; import * as React from "react"; import * as AvatarPrimitive from "@radix-ui/react-avatar"; import { cn } from "@/lib/utils"; const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; export { Avatar, AvatarImage, AvatarFallback }; ================================================ FILE: nextjs-frontend/components/ui/badge.tsx ================================================ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", outline: "text-foreground", }, }, defaultVariants: { variant: "default", }, }, ); export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return (
); } export { Badge, badgeVariants }; ================================================ FILE: nextjs-frontend/components/ui/breadcrumb.tsx ================================================ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cn } from "@/lib/utils"; import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { separator?: React.ReactNode; } >(({ ...props }, ref) =>