fastapi-fullstack 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/PKG-INFO +1 -1
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/pyproject.toml +1 -1
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +151 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +69 -3
- fastapi_fullstack-0.2.4/template/{{cookiecutter.project_slug}}/backend/tests/test_ssrf.py +207 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/.gitignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/LICENSE +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/README.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/cli.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/config.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/generator.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/prompts.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/VARIABLES.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/cookiecutter.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/hooks/post_gen_project.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/add-endpoint.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/fix-issue.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/review.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/api-conventions.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/architecture.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/code-style.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/exceptions-security.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/frontend.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/schemas-models.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/testing.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/settings.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.env.prod.example +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/AGENTS.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/CLAUDE.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.dockerignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.env.example +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/Dockerfile +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic.ini +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/admin.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/rag_tool.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/web_search.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/files.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/rag.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/rag.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/logging.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/chat_file.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/rag_document.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/sync_log.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/sync_source.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/main.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/config.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/google_drive.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/s3.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/documents.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/embeddings.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/image_describer.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/ingestion.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/models.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/reranker.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/retrieval.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/base.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/google_drive.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/s3.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/vectorstore.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/chat_file.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/rag_document.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/sync_log.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/sync_source.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/file.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/rag.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/sync_source.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/file_storage.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/file_upload.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/rag_document.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/rag_sync.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/sync_source.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/rag_tasks.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_openapi.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_migrations.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_services_conversation.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/adding_features.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/architecture.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/commands.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/configuration.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/file-processing.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-agent-tool.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-api-endpoint.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-background-task.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-rag-source.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-sync-connector.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/configure-sync-sources.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/customize-agent-prompt.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/patterns.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/permissions.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/testing.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.env.example +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.gitignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/README.md +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/eslint.config.mjs +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/package.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/rag/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/settings/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/error.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/export/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/files/[id]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/files/upload/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/users/avatar/[userId]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/users/me/avatar/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/agent/models/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/[name]/documents/[documentId]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/[name]/documents/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/[name]/info/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/[name]/ingest/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/[name]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/collections/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/documents/[docId]/download/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/documents/[docId]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/documents/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/search/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/supported-formats/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/[syncId]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/connectors/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/local/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/sources/[sourceId]/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/sources/[sourceId]/trigger/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/v1/rag/sync/sources/route.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/global-error.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/not-found.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/auth-guard.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/breadcrumb.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/landing-nav.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/page-transition.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/accordion.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/alert-dialog.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/avatar.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/checkbox.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/dialog.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/dropdown-menu.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/popover.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/progress.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/radio-group.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/scroll-area.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/select.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/separator.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/skeleton.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/spinner.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/switch.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/table.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/tabs.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/textarea.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/tooltip.tsx +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/file-api.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/rag-api.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/speech.d.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vercel.json +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +0 -0
- {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-fullstack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Full-stack FastAPI + Next.js template generator with PydanticAI/LangChain agents, WebSocket streaming, 20+ enterprise integrations, and Logfire/LangSmith observability. Ship AI apps fast. CLI tool to generate production-ready FastAPI + Next.js projects with AI agents, auth, and observability.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vstorm-co/full-stack-ai-agent-template
|
|
6
6
|
Project-URL: Documentation, https://github.com/vstorm-co/full-stack-ai-agent-template#readme
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-fullstack"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Full-stack FastAPI + Next.js template generator with PydanticAI/LangChain agents, WebSocket streaming, 20+ enterprise integrations, and Logfire/LangSmith observability. Ship AI apps fast. CLI tool to generate production-ready FastAPI + Next.js projects with AI agents, auth, and observability."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
This module provides security-focused input sanitization functions:
|
|
4
4
|
- HTML sanitization to prevent XSS attacks
|
|
5
5
|
- Path traversal prevention for file operations
|
|
6
|
+
- Webhook URL validation to prevent SSRF attacks
|
|
6
7
|
- Common input cleaning utilities
|
|
7
8
|
|
|
8
9
|
Note: SQL injection is prevented by using SQLAlchemy ORM with parameterized queries.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
import html
|
|
13
|
+
import ipaddress
|
|
12
14
|
import os
|
|
13
15
|
import re
|
|
16
|
+
import socket
|
|
14
17
|
import unicodedata
|
|
15
18
|
from pathlib import Path
|
|
16
19
|
from typing import TypeVar
|
|
20
|
+
from urllib.parse import urlparse
|
|
17
21
|
|
|
18
22
|
# Default allowed HTML tags for rich text content
|
|
19
23
|
DEFAULT_ALLOWED_TAGS = frozenset({
|
|
@@ -28,6 +32,23 @@ DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
|
28
32
|
"acronym": frozenset({"title"}),
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
# Allowed URL schemes for webhook URLs
|
|
36
|
+
WEBHOOK_ALLOWED_SCHEMES = frozenset({"http", "https"})
|
|
37
|
+
|
|
38
|
+
# Shared Address Space (RFC 6598) — CGNAT range.
|
|
39
|
+
# Python 3.11+ no longer classifies 100.64.0.0/10 as private or reserved,
|
|
40
|
+
# so we block it explicitly. Covers cloud metadata endpoints like
|
|
41
|
+
# Alibaba Cloud's 100.100.100.200.
|
|
42
|
+
_CGNAT_NETWORK = ipaddress.ip_network("100.64.0.0/10")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SSRFBlockedError(ValueError):
|
|
46
|
+
"""Raised when a URL is blocked by SSRF protection.
|
|
47
|
+
|
|
48
|
+
Dedicated exception type to avoid fragile string matching when
|
|
49
|
+
distinguishing SSRF blocks from other ValueErrors.
|
|
50
|
+
"""
|
|
51
|
+
|
|
31
52
|
|
|
32
53
|
def sanitize_html(
|
|
33
54
|
content: str,
|
|
@@ -150,6 +171,136 @@ def validate_safe_path(
|
|
|
150
171
|
return full_path
|
|
151
172
|
|
|
152
173
|
|
|
174
|
+
def _is_ip_blocked(ip_str: str) -> bool:
|
|
175
|
+
"""Check if an IP address is private, reserved, loopback, or link-local.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
ip_str: The IP address string to check.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if the address should be blocked, False if it's safe.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
addr = ipaddress.ip_address(ip_str)
|
|
185
|
+
except ValueError:
|
|
186
|
+
# If we can't parse it, block it to be safe
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
addr.is_private
|
|
191
|
+
or addr.is_reserved
|
|
192
|
+
or addr.is_loopback
|
|
193
|
+
or addr.is_link_local
|
|
194
|
+
or addr.is_multicast
|
|
195
|
+
or addr.is_unspecified
|
|
196
|
+
or addr in _CGNAT_NETWORK
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def validate_webhook_url(
|
|
201
|
+
url: str,
|
|
202
|
+
allowed_schemes: frozenset[str] | None = None,
|
|
203
|
+
) -> str:
|
|
204
|
+
"""Validate a webhook URL to prevent SSRF attacks.
|
|
205
|
+
|
|
206
|
+
Checks that the URL:
|
|
207
|
+
- Uses an allowed scheme (http/https only by default)
|
|
208
|
+
- Does not contain userinfo (credentials in the URL)
|
|
209
|
+
- Does not point to private, reserved, loopback, or link-local IP addresses
|
|
210
|
+
- Resolves via DNS to a public IP (prevents DNS rebinding attacks)
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
url: The webhook URL to validate.
|
|
214
|
+
allowed_schemes: Allowed URL schemes. Defaults to {"http", "https"}.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
The validated URL string.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
SSRFBlockedError: If the URL is blocked by SSRF protection.
|
|
221
|
+
ValueError: If the URL is malformed.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> validate_webhook_url("https://example.com/webhook")
|
|
225
|
+
"https://example.com/webhook"
|
|
226
|
+
>>> validate_webhook_url("http://169.254.169.254/latest/meta-data/")
|
|
227
|
+
Raises SSRFBlockedError
|
|
228
|
+
"""
|
|
229
|
+
if allowed_schemes is None:
|
|
230
|
+
allowed_schemes = WEBHOOK_ALLOWED_SCHEMES
|
|
231
|
+
|
|
232
|
+
# Parse the URL
|
|
233
|
+
try:
|
|
234
|
+
parsed = urlparse(url)
|
|
235
|
+
except Exception as err:
|
|
236
|
+
raise ValueError(f"Invalid webhook URL: {url!r}") from err
|
|
237
|
+
|
|
238
|
+
# Validate scheme
|
|
239
|
+
if parsed.scheme not in allowed_schemes:
|
|
240
|
+
raise SSRFBlockedError(
|
|
241
|
+
f"URL scheme {parsed.scheme!r} is not allowed. "
|
|
242
|
+
f"Allowed schemes: {', '.join(sorted(allowed_schemes))}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Extract hostname
|
|
246
|
+
hostname = parsed.hostname
|
|
247
|
+
if not hostname:
|
|
248
|
+
raise ValueError(f"Invalid webhook URL: no hostname found in {url!r}")
|
|
249
|
+
|
|
250
|
+
# Reject URLs with userinfo (credentials) to prevent URL parsing ambiguities
|
|
251
|
+
# e.g. http://user:pass@host/ or http://foo@169.254.169.254%00@public.com/
|
|
252
|
+
if parsed.username is not None or parsed.password is not None:
|
|
253
|
+
raise SSRFBlockedError(
|
|
254
|
+
"Webhook URL must not contain credentials (userinfo). "
|
|
255
|
+
"Remove the user:password@ portion from the URL."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Try to parse hostname directly as an IP address
|
|
259
|
+
try:
|
|
260
|
+
addr = ipaddress.ip_address(hostname)
|
|
261
|
+
if _is_ip_blocked(str(addr)):
|
|
262
|
+
raise SSRFBlockedError(
|
|
263
|
+
f"Webhook URL blocked: {hostname!r} resolves to a private/internal "
|
|
264
|
+
f"address. SSRF protection does not allow requests to internal networks."
|
|
265
|
+
)
|
|
266
|
+
return url
|
|
267
|
+
except SSRFBlockedError:
|
|
268
|
+
raise
|
|
269
|
+
except ValueError:
|
|
270
|
+
# Not an IP literal — continue to DNS resolution below
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
# Determine the correct default port based on the scheme
|
|
274
|
+
default_port = 443 if parsed.scheme == "https" else 80
|
|
275
|
+
port = parsed.port or default_port
|
|
276
|
+
|
|
277
|
+
# Resolve hostname via DNS and check all returned addresses
|
|
278
|
+
# TODO: socket.getaddrinfo() is blocking I/O — in async code paths
|
|
279
|
+
# (PostgreSQL, MongoDB) consider using loop.getaddrinfo() or run_in_executor.
|
|
280
|
+
try:
|
|
281
|
+
addr_infos = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP)
|
|
282
|
+
except socket.gaierror as err:
|
|
283
|
+
raise SSRFBlockedError(
|
|
284
|
+
f"Webhook URL blocked: unable to resolve hostname {hostname!r}"
|
|
285
|
+
) from err
|
|
286
|
+
|
|
287
|
+
if not addr_infos:
|
|
288
|
+
raise SSRFBlockedError(
|
|
289
|
+
f"Webhook URL blocked: hostname {hostname!r} did not resolve to any address"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
for _family, _type, _proto, _canonname, sockaddr in addr_infos:
|
|
293
|
+
ip_str = sockaddr[0]
|
|
294
|
+
if _is_ip_blocked(ip_str):
|
|
295
|
+
raise SSRFBlockedError(
|
|
296
|
+
f"Webhook URL blocked: {hostname!r} resolves to private/internal "
|
|
297
|
+
f"address {ip_str!r}. SSRF protection does not allow requests to "
|
|
298
|
+
f"internal networks."
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return url
|
|
302
|
+
|
|
303
|
+
|
|
153
304
|
def sanitize_string(
|
|
154
305
|
value: str,
|
|
155
306
|
max_length: int | None = None,
|
|
@@ -13,7 +13,8 @@ from uuid import UUID
|
|
|
13
13
|
import httpx
|
|
14
14
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
15
|
|
|
16
|
-
from app.core.exceptions import NotFoundError
|
|
16
|
+
from app.core.exceptions import NotFoundError, ValidationError
|
|
17
|
+
from app.core.sanitize import SSRFBlockedError, validate_webhook_url
|
|
17
18
|
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
18
19
|
from app.repositories import webhook_repo
|
|
19
20
|
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
@@ -21,6 +22,14 @@ from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _validate_url_or_raise_422(url: str) -> str:
|
|
26
|
+
"""Validate a webhook URL; convert SSRF/ValueError into ValidationError (422)."""
|
|
27
|
+
try:
|
|
28
|
+
return validate_webhook_url(url)
|
|
29
|
+
except (SSRFBlockedError, ValueError) as exc:
|
|
30
|
+
raise ValidationError(message=str(exc)) from exc
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
class WebhookService:
|
|
25
34
|
"""Service for webhook management and delivery."""
|
|
26
35
|
|
|
@@ -33,6 +42,9 @@ class WebhookService:
|
|
|
33
42
|
user_id: UUID | None = None,
|
|
34
43
|
) -> Webhook:
|
|
35
44
|
"""Create a new webhook subscription."""
|
|
45
|
+
# Validate URL against SSRF before storing
|
|
46
|
+
_validate_url_or_raise_422(str(data.url))
|
|
47
|
+
|
|
36
48
|
# Generate a secure secret for HMAC signing
|
|
37
49
|
secret = secrets.token_urlsafe(32)
|
|
38
50
|
|
|
@@ -70,6 +82,10 @@ class WebhookService:
|
|
|
70
82
|
data: WebhookUpdate,
|
|
71
83
|
) -> Webhook:
|
|
72
84
|
"""Update a webhook."""
|
|
85
|
+
# Validate new URL against SSRF if provided
|
|
86
|
+
if data.url is not None:
|
|
87
|
+
_validate_url_or_raise_422(str(data.url))
|
|
88
|
+
|
|
73
89
|
webhook = await self.get_webhook(webhook_id)
|
|
74
90
|
return await webhook_repo.update(self.db, webhook, data)
|
|
75
91
|
|
|
@@ -129,6 +145,9 @@ class WebhookService:
|
|
|
129
145
|
payload: dict,
|
|
130
146
|
) -> dict:
|
|
131
147
|
"""Deliver a payload to a webhook with HMAC signature."""
|
|
148
|
+
# Re-validate URL at delivery time to prevent DNS rebinding attacks
|
|
149
|
+
_validate_url_or_raise_422(webhook.url)
|
|
150
|
+
|
|
132
151
|
payload_json = json.dumps(payload, default=str)
|
|
133
152
|
|
|
134
153
|
# Create HMAC signature
|
|
@@ -149,6 +168,9 @@ class WebhookService:
|
|
|
149
168
|
await self.db.flush()
|
|
150
169
|
|
|
151
170
|
try:
|
|
171
|
+
# SECURITY: follow_redirects defaults to False in httpx.
|
|
172
|
+
# Do NOT enable it — redirects could bypass SSRF validation
|
|
173
|
+
# by redirecting to an internal IP after the URL check passes.
|
|
152
174
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
153
175
|
response = await client.post(
|
|
154
176
|
webhook.url,
|
|
@@ -229,7 +251,8 @@ from datetime import UTC, datetime
|
|
|
229
251
|
import httpx
|
|
230
252
|
from sqlalchemy.orm import Session as DBSession
|
|
231
253
|
|
|
232
|
-
from app.core.exceptions import NotFoundError
|
|
254
|
+
from app.core.exceptions import NotFoundError, ValidationError
|
|
255
|
+
from app.core.sanitize import SSRFBlockedError, validate_webhook_url
|
|
233
256
|
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
234
257
|
from app.repositories import webhook_repo
|
|
235
258
|
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
@@ -237,6 +260,14 @@ from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
|
237
260
|
logger = logging.getLogger(__name__)
|
|
238
261
|
|
|
239
262
|
|
|
263
|
+
def _validate_url_or_raise_422(url: str) -> str:
|
|
264
|
+
"""Validate a webhook URL; convert SSRF/ValueError into ValidationError (422)."""
|
|
265
|
+
try:
|
|
266
|
+
return validate_webhook_url(url)
|
|
267
|
+
except (SSRFBlockedError, ValueError) as exc:
|
|
268
|
+
raise ValidationError(message=str(exc)) from exc
|
|
269
|
+
|
|
270
|
+
|
|
240
271
|
class WebhookService:
|
|
241
272
|
"""Service for webhook management and delivery."""
|
|
242
273
|
|
|
@@ -249,6 +280,9 @@ class WebhookService:
|
|
|
249
280
|
user_id: str | None = None,
|
|
250
281
|
) -> Webhook:
|
|
251
282
|
"""Create a new webhook subscription."""
|
|
283
|
+
# Validate URL against SSRF before storing
|
|
284
|
+
_validate_url_or_raise_422(str(data.url))
|
|
285
|
+
|
|
252
286
|
secret = secrets.token_urlsafe(32)
|
|
253
287
|
|
|
254
288
|
return webhook_repo.create(
|
|
@@ -285,6 +319,10 @@ class WebhookService:
|
|
|
285
319
|
data: WebhookUpdate,
|
|
286
320
|
) -> Webhook:
|
|
287
321
|
"""Update a webhook."""
|
|
322
|
+
# Validate new URL against SSRF if provided
|
|
323
|
+
if data.url is not None:
|
|
324
|
+
_validate_url_or_raise_422(str(data.url))
|
|
325
|
+
|
|
288
326
|
webhook = self.get_webhook(webhook_id)
|
|
289
327
|
return webhook_repo.update(self.db, webhook, data)
|
|
290
328
|
|
|
@@ -323,6 +361,9 @@ class WebhookService:
|
|
|
323
361
|
payload: dict,
|
|
324
362
|
) -> dict:
|
|
325
363
|
"""Deliver a payload to a webhook with HMAC signature."""
|
|
364
|
+
# Re-validate URL at delivery time to prevent DNS rebinding attacks
|
|
365
|
+
_validate_url_or_raise_422(webhook.url)
|
|
366
|
+
|
|
326
367
|
payload_json = json.dumps(payload, default=str)
|
|
327
368
|
signature = self._create_signature(webhook.secret, payload_json)
|
|
328
369
|
|
|
@@ -341,6 +382,9 @@ class WebhookService:
|
|
|
341
382
|
self.db.flush()
|
|
342
383
|
|
|
343
384
|
try:
|
|
385
|
+
# SECURITY: follow_redirects defaults to False in httpx.
|
|
386
|
+
# Do NOT enable it — redirects could bypass SSRF validation
|
|
387
|
+
# by redirecting to an internal IP after the URL check passes.
|
|
344
388
|
with httpx.Client(timeout=30.0) as client:
|
|
345
389
|
response = client.post(
|
|
346
390
|
webhook.url,
|
|
@@ -399,7 +443,8 @@ from datetime import UTC, datetime
|
|
|
399
443
|
|
|
400
444
|
import httpx
|
|
401
445
|
|
|
402
|
-
from app.core.exceptions import NotFoundError
|
|
446
|
+
from app.core.exceptions import NotFoundError, ValidationError
|
|
447
|
+
from app.core.sanitize import SSRFBlockedError, validate_webhook_url
|
|
403
448
|
from app.db.models.webhook import Webhook, WebhookDelivery
|
|
404
449
|
from app.repositories import webhook_repo
|
|
405
450
|
from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
@@ -407,6 +452,14 @@ from app.schemas.webhook import WebhookCreate, WebhookUpdate
|
|
|
407
452
|
logger = logging.getLogger(__name__)
|
|
408
453
|
|
|
409
454
|
|
|
455
|
+
def _validate_url_or_raise_422(url: str) -> str:
|
|
456
|
+
"""Validate a webhook URL; convert SSRF/ValueError into ValidationError (422)."""
|
|
457
|
+
try:
|
|
458
|
+
return validate_webhook_url(url)
|
|
459
|
+
except (SSRFBlockedError, ValueError) as exc:
|
|
460
|
+
raise ValidationError(message=str(exc)) from exc
|
|
461
|
+
|
|
462
|
+
|
|
410
463
|
class WebhookService:
|
|
411
464
|
"""Service for webhook management and delivery."""
|
|
412
465
|
|
|
@@ -416,6 +469,9 @@ class WebhookService:
|
|
|
416
469
|
user_id: str | None = None,
|
|
417
470
|
) -> Webhook:
|
|
418
471
|
"""Create a new webhook subscription."""
|
|
472
|
+
# Validate URL against SSRF before storing
|
|
473
|
+
_validate_url_or_raise_422(str(data.url))
|
|
474
|
+
|
|
419
475
|
secret = secrets.token_urlsafe(32)
|
|
420
476
|
|
|
421
477
|
return await webhook_repo.create(
|
|
@@ -449,6 +505,10 @@ class WebhookService:
|
|
|
449
505
|
data: WebhookUpdate,
|
|
450
506
|
) -> Webhook:
|
|
451
507
|
"""Update a webhook."""
|
|
508
|
+
# Validate new URL against SSRF if provided
|
|
509
|
+
if data.url is not None:
|
|
510
|
+
_validate_url_or_raise_422(str(data.url))
|
|
511
|
+
|
|
452
512
|
webhook = await self.get_webhook(webhook_id)
|
|
453
513
|
return await webhook_repo.update(webhook, data)
|
|
454
514
|
|
|
@@ -487,6 +547,9 @@ class WebhookService:
|
|
|
487
547
|
payload: dict,
|
|
488
548
|
) -> dict:
|
|
489
549
|
"""Deliver a payload to a webhook with HMAC signature."""
|
|
550
|
+
# Re-validate URL at delivery time to prevent DNS rebinding attacks
|
|
551
|
+
_validate_url_or_raise_422(webhook.url)
|
|
552
|
+
|
|
490
553
|
payload_json = json.dumps(payload, default=str)
|
|
491
554
|
signature = self._create_signature(webhook.secret, payload_json)
|
|
492
555
|
|
|
@@ -504,6 +567,9 @@ class WebhookService:
|
|
|
504
567
|
await delivery.insert()
|
|
505
568
|
|
|
506
569
|
try:
|
|
570
|
+
# SECURITY: follow_redirects defaults to False in httpx.
|
|
571
|
+
# Do NOT enable it — redirects could bypass SSRF validation
|
|
572
|
+
# by redirecting to an internal IP after the URL check passes.
|
|
507
573
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
508
574
|
response = await client.post(
|
|
509
575
|
webhook.url,
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Tests for SSRF protection in webhook URL validation.
|
|
2
|
+
|
|
3
|
+
Covers validate_webhook_url() and _is_ip_blocked() from app.core.sanitize.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from app.core.sanitize import (
|
|
11
|
+
SSRFBlockedError,
|
|
12
|
+
_is_ip_blocked,
|
|
13
|
+
validate_webhook_url,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# _is_ip_blocked
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestIsIpBlocked:
|
|
23
|
+
"""Tests for the _is_ip_blocked helper."""
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"ip",
|
|
27
|
+
[
|
|
28
|
+
"127.0.0.1",
|
|
29
|
+
"10.0.0.1",
|
|
30
|
+
"172.16.0.1",
|
|
31
|
+
"192.168.1.1",
|
|
32
|
+
"169.254.169.254",
|
|
33
|
+
"0.0.0.0",
|
|
34
|
+
"::1",
|
|
35
|
+
"fe80::1",
|
|
36
|
+
"fc00::1",
|
|
37
|
+
"100.100.100.200", # CGNAT / Alibaba Cloud metadata
|
|
38
|
+
"100.64.0.1", # CGNAT range start (RFC 6598)
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
def test_blocked_ips(self, ip: str):
|
|
42
|
+
assert _is_ip_blocked(ip) is True
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize(
|
|
45
|
+
"ip",
|
|
46
|
+
[
|
|
47
|
+
"8.8.8.8",
|
|
48
|
+
"1.1.1.1",
|
|
49
|
+
"93.184.216.34", # example.com
|
|
50
|
+
"2606:4700:4700::1111", # Cloudflare public DNS IPv6
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_allowed_ips(self, ip: str):
|
|
54
|
+
assert _is_ip_blocked(ip) is False
|
|
55
|
+
|
|
56
|
+
def test_unparseable_ip_is_blocked(self):
|
|
57
|
+
"""If we can't parse it, we block it (fail-closed)."""
|
|
58
|
+
assert _is_ip_blocked("not-an-ip") is True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# validate_webhook_url — scheme validation
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestSchemeValidation:
|
|
67
|
+
"""Blocked schemes must raise SSRFBlockedError."""
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize(
|
|
70
|
+
"url",
|
|
71
|
+
[
|
|
72
|
+
"file:///etc/passwd",
|
|
73
|
+
"ftp://mirror.example.com/pub",
|
|
74
|
+
"gopher://evil.com/",
|
|
75
|
+
"data:text/html,<h1>hi</h1>",
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
def test_blocked_schemes(self, url: str):
|
|
79
|
+
with pytest.raises(SSRFBlockedError):
|
|
80
|
+
validate_webhook_url(url)
|
|
81
|
+
|
|
82
|
+
def test_empty_scheme_is_rejected(self):
|
|
83
|
+
with pytest.raises((SSRFBlockedError, ValueError)):
|
|
84
|
+
validate_webhook_url("://example.com/hook")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# validate_webhook_url — IP-literal URLs
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestDirectIpUrls:
|
|
93
|
+
"""URLs with IP-address hostnames (no DNS involved)."""
|
|
94
|
+
|
|
95
|
+
@pytest.mark.parametrize(
|
|
96
|
+
"url",
|
|
97
|
+
[
|
|
98
|
+
"http://127.0.0.1/hook",
|
|
99
|
+
"https://169.254.169.254/latest/meta-data/",
|
|
100
|
+
"http://10.0.0.1:8080/callback",
|
|
101
|
+
"http://192.168.1.1/hook",
|
|
102
|
+
"http://[::1]/hook",
|
|
103
|
+
"http://0.0.0.0/hook",
|
|
104
|
+
"http://100.100.100.200/latest/meta-data/", # CGNAT / Alibaba metadata
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
def test_private_ip_blocked(self, url: str):
|
|
108
|
+
with pytest.raises(SSRFBlockedError):
|
|
109
|
+
validate_webhook_url(url)
|
|
110
|
+
|
|
111
|
+
def test_public_ip_allowed(self):
|
|
112
|
+
"""A public IP should pass validation (DNS resolution is skipped)."""
|
|
113
|
+
url = "https://93.184.216.34/webhook"
|
|
114
|
+
assert validate_webhook_url(url) == url
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# validate_webhook_url — DNS resolution to private IP
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestDnsResolution:
|
|
123
|
+
"""DNS resolving to a private IP must be blocked."""
|
|
124
|
+
|
|
125
|
+
def _mock_getaddrinfo_private(self, *args, **kwargs):
|
|
126
|
+
"""Return a private IP for any hostname."""
|
|
127
|
+
return [
|
|
128
|
+
(2, 1, 6, "", ("127.0.0.1", 443)),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
def _mock_getaddrinfo_public(self, *args, **kwargs):
|
|
132
|
+
"""Return a public IP for any hostname."""
|
|
133
|
+
return [
|
|
134
|
+
(2, 1, 6, "", ("93.184.216.34", 443)),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
def test_dns_resolves_to_private_ip(self):
|
|
138
|
+
with (
|
|
139
|
+
patch("app.core.sanitize.socket.getaddrinfo", self._mock_getaddrinfo_private),
|
|
140
|
+
pytest.raises(SSRFBlockedError),
|
|
141
|
+
):
|
|
142
|
+
validate_webhook_url("https://evil.attacker.com/hook")
|
|
143
|
+
|
|
144
|
+
def test_dns_resolves_to_public_ip(self):
|
|
145
|
+
with patch("app.core.sanitize.socket.getaddrinfo", self._mock_getaddrinfo_public):
|
|
146
|
+
result = validate_webhook_url("https://example.com/webhook")
|
|
147
|
+
assert result == "https://example.com/webhook"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# validate_webhook_url — edge cases
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestEdgeCases:
|
|
156
|
+
"""Edge cases: empty URL, missing hostname, credentials in URL."""
|
|
157
|
+
|
|
158
|
+
def test_empty_url(self):
|
|
159
|
+
with pytest.raises((SSRFBlockedError, ValueError)):
|
|
160
|
+
validate_webhook_url("")
|
|
161
|
+
|
|
162
|
+
def test_no_hostname(self):
|
|
163
|
+
with pytest.raises(ValueError):
|
|
164
|
+
validate_webhook_url("https:///path")
|
|
165
|
+
|
|
166
|
+
def test_url_with_credentials_rejected(self):
|
|
167
|
+
"""URLs with userinfo (user:pass@) should be rejected."""
|
|
168
|
+
with pytest.raises(SSRFBlockedError):
|
|
169
|
+
validate_webhook_url("http://user:pass@internal.example.com/hook")
|
|
170
|
+
|
|
171
|
+
def test_url_with_username_only_rejected(self):
|
|
172
|
+
with pytest.raises(SSRFBlockedError):
|
|
173
|
+
validate_webhook_url("http://admin@169.254.169.254/")
|
|
174
|
+
|
|
175
|
+
def test_allowed_https_url(self):
|
|
176
|
+
"""A normal public URL should pass (mock DNS to avoid network calls)."""
|
|
177
|
+
with patch(
|
|
178
|
+
"app.core.sanitize.socket.getaddrinfo",
|
|
179
|
+
return_value=[(2, 1, 6, "", ("93.184.216.34", 443))],
|
|
180
|
+
):
|
|
181
|
+
result = validate_webhook_url("https://example.com/webhook")
|
|
182
|
+
assert result == "https://example.com/webhook"
|
|
183
|
+
|
|
184
|
+
def test_allowed_http_url(self):
|
|
185
|
+
"""An http:// URL to a public IP should also pass."""
|
|
186
|
+
with patch(
|
|
187
|
+
"app.core.sanitize.socket.getaddrinfo",
|
|
188
|
+
return_value=[(2, 1, 6, "", ("93.184.216.34", 80))],
|
|
189
|
+
):
|
|
190
|
+
result = validate_webhook_url("http://example.com/webhook")
|
|
191
|
+
assert result == "http://example.com/webhook"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# SSRFBlockedError is a subclass of ValueError
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestSSRFBlockedError:
|
|
200
|
+
"""The dedicated exception type preserves backward compatibility."""
|
|
201
|
+
|
|
202
|
+
def test_is_value_error_subclass(self):
|
|
203
|
+
assert issubclass(SSRFBlockedError, ValueError)
|
|
204
|
+
|
|
205
|
+
def test_catchable_as_value_error(self):
|
|
206
|
+
with pytest.raises(ValueError):
|
|
207
|
+
raise SSRFBlockedError("blocked")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/AGENTS.md
RENAMED
|
File without changes
|
{fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/CLAUDE.md
RENAMED
|
File without changes
|
{fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/Makefile
RENAMED
|
File without changes
|
{fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/README.md
RENAMED
|
File without changes
|
|
File without changes
|