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.
Files changed (365) hide show
  1. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/PKG-INFO +1 -1
  2. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/pyproject.toml +1 -1
  3. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +151 -0
  4. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +69 -3
  5. fastapi_fullstack-0.2.4/template/{{cookiecutter.project_slug}}/backend/tests/test_ssrf.py +207 -0
  6. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/.gitignore +0 -0
  7. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/LICENSE +0 -0
  8. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/README.md +0 -0
  9. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/__init__.py +0 -0
  10. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/cli.py +0 -0
  11. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/config.py +0 -0
  12. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/generator.py +0 -0
  13. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/fastapi_gen/prompts.py +0 -0
  14. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/VARIABLES.md +0 -0
  15. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/cookiecutter.json +0 -0
  16. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/hooks/post_gen_project.py +0 -0
  17. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/add-endpoint.md +0 -0
  18. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/fix-issue.md +0 -0
  19. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/commands/review.md +0 -0
  20. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/api-conventions.md +0 -0
  21. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/architecture.md +0 -0
  22. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/code-style.md +0 -0
  23. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/exceptions-security.md +0 -0
  24. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/frontend.md +0 -0
  25. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/schemas-models.md +0 -0
  26. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/rules/testing.md +0 -0
  27. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.claude/settings.json +0 -0
  28. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.env.prod.example +0 -0
  29. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +0 -0
  30. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.gitignore +0 -0
  31. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +0 -0
  32. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/AGENTS.md +0 -0
  33. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/CLAUDE.md +0 -0
  34. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/Makefile +0 -0
  35. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/README.md +0 -0
  36. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.dockerignore +0 -0
  37. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.env.example +0 -0
  38. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +0 -0
  39. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/Dockerfile +0 -0
  40. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +0 -0
  41. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +0 -0
  42. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
  43. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/alembic.ini +0 -0
  44. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +0 -0
  45. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/admin.py +0 -0
  46. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +0 -0
  47. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +0 -0
  48. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +0 -0
  49. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +0 -0
  50. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +0 -0
  51. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +0 -0
  52. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +0 -0
  53. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +0 -0
  54. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +0 -0
  55. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/rag_tool.py +0 -0
  56. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/web_search.py +0 -0
  57. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +0 -0
  58. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +0 -0
  59. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +0 -0
  60. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +0 -0
  61. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +0 -0
  62. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +0 -0
  63. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +0 -0
  64. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +0 -0
  65. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +0 -0
  66. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/files.py +0 -0
  67. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +0 -0
  68. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +0 -0
  69. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/rag.py +0 -0
  70. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +0 -0
  71. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +0 -0
  72. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +0 -0
  73. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +0 -0
  74. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +0 -0
  75. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +0 -0
  76. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +0 -0
  77. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +0 -0
  78. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +0 -0
  79. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/rag.py +0 -0
  80. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +0 -0
  81. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +0 -0
  82. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +0 -0
  83. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +0 -0
  84. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +0 -0
  85. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +0 -0
  86. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +0 -0
  87. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/logging.py +0 -0
  88. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +0 -0
  89. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +0 -0
  90. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +0 -0
  91. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +0 -0
  92. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +0 -0
  93. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +0 -0
  94. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +0 -0
  95. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/chat_file.py +0 -0
  96. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +0 -0
  97. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/rag_document.py +0 -0
  98. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +0 -0
  99. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/sync_log.py +0 -0
  100. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/sync_source.py +0 -0
  101. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +0 -0
  102. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +0 -0
  103. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +0 -0
  104. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/main.py +0 -0
  105. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/__init__.py +0 -0
  106. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/config.py +0 -0
  107. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/__init__.py +0 -0
  108. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/google_drive.py +0 -0
  109. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/connectors/s3.py +0 -0
  110. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/documents.py +0 -0
  111. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/embeddings.py +0 -0
  112. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/image_describer.py +0 -0
  113. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/ingestion.py +0 -0
  114. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/models.py +0 -0
  115. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/reranker.py +0 -0
  116. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/retrieval.py +0 -0
  117. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/__init__.py +0 -0
  118. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/base.py +0 -0
  119. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/google_drive.py +0 -0
  120. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/sources/s3.py +0 -0
  121. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/rag/vectorstore.py +0 -0
  122. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +0 -0
  123. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/chat_file.py +0 -0
  124. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +0 -0
  125. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/rag_document.py +0 -0
  126. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +0 -0
  127. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/sync_log.py +0 -0
  128. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/sync_source.py +0 -0
  129. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +0 -0
  130. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +0 -0
  131. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +0 -0
  132. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +0 -0
  133. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +0 -0
  134. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/file.py +0 -0
  135. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/rag.py +0 -0
  136. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +0 -0
  137. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/sync_source.py +0 -0
  138. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +0 -0
  139. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +0 -0
  140. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +0 -0
  141. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +0 -0
  142. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +0 -0
  143. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/file_storage.py +0 -0
  144. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/file_upload.py +0 -0
  145. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/rag_document.py +0 -0
  146. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/rag_sync.py +0 -0
  147. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +0 -0
  148. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/sync_source.py +0 -0
  149. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +0 -0
  150. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +0 -0
  151. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +0 -0
  152. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +0 -0
  153. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +0 -0
  154. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +0 -0
  155. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +0 -0
  156. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/rag_tasks.py +0 -0
  157. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +0 -0
  158. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +0 -0
  159. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +0 -0
  160. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +0 -0
  161. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +0 -0
  162. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
  163. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +0 -0
  164. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +0 -0
  165. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +0 -0
  166. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +0 -0
  167. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +0 -0
  168. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +0 -0
  169. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_openapi.py +0 -0
  170. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +0 -0
  171. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +0 -0
  172. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +0 -0
  173. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +0 -0
  174. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +0 -0
  175. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +0 -0
  176. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +0 -0
  177. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_migrations.py +0 -0
  178. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +0 -0
  179. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +0 -0
  180. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +0 -0
  181. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_services_conversation.py +0 -0
  182. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +0 -0
  183. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +0 -0
  184. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +0 -0
  185. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +0 -0
  186. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  187. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/adding_features.md +0 -0
  188. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/architecture.md +0 -0
  189. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/commands.md +0 -0
  190. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/configuration.md +0 -0
  191. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/file-processing.md +0 -0
  192. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-agent-tool.md +0 -0
  193. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-api-endpoint.md +0 -0
  194. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-background-task.md +0 -0
  195. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-rag-source.md +0 -0
  196. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/add-sync-connector.md +0 -0
  197. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/configure-sync-sources.md +0 -0
  198. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/howto/customize-agent-prompt.md +0 -0
  199. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/patterns.md +0 -0
  200. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/permissions.md +0 -0
  201. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/docs/testing.md +0 -0
  202. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +0 -0
  203. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.env.example +0 -0
  204. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.gitignore +0 -0
  205. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +0 -0
  206. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +0 -0
  207. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +0 -0
  208. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/README.md +0 -0
  209. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +0 -0
  210. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +0 -0
  211. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +0 -0
  212. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +0 -0
  213. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/eslint.config.mjs +0 -0
  214. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +0 -0
  215. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +0 -0
  216. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +0 -0
  217. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +0 -0
  218. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/package.json +0 -0
  219. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +0 -0
  220. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +0 -0
  221. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +0 -0
  222. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +0 -0
  223. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +0 -0
  224. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +0 -0
  225. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +0 -0
  226. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +0 -0
  227. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +0 -0
  228. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/rag/page.tsx +0 -0
  229. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/settings/page.tsx +0 -0
  230. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +0 -0
  231. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/error.tsx +0 -0
  232. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +0 -0
  233. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +0 -0
  234. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +0 -0
  235. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +0 -0
  236. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +0 -0
  237. {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
  238. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +0 -0
  239. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +0 -0
  240. {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
  241. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +0 -0
  242. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/export/route.ts +0 -0
  243. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +0 -0
  244. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/files/[id]/route.ts +0 -0
  245. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/files/upload/route.ts +0 -0
  246. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +0 -0
  247. {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
  248. {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
  249. {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
  250. {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
  251. {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
  252. {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
  253. {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
  254. {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
  255. {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
  256. {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
  257. {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
  258. {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
  259. {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
  260. {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
  261. {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
  262. {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
  263. {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
  264. {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
  265. {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
  266. {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
  267. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/global-error.tsx +0 -0
  268. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +0 -0
  269. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +0 -0
  270. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/not-found.tsx +0 -0
  271. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +0 -0
  272. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +0 -0
  273. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +0 -0
  274. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +0 -0
  275. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +0 -0
  276. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +0 -0
  277. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +0 -0
  278. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +0 -0
  279. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +0 -0
  280. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +0 -0
  281. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +0 -0
  282. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +0 -0
  283. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +0 -0
  284. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +0 -0
  285. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +0 -0
  286. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +0 -0
  287. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +0 -0
  288. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/auth-guard.tsx +0 -0
  289. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/breadcrumb.tsx +0 -0
  290. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +0 -0
  291. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +0 -0
  292. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/landing-nav.tsx +0 -0
  293. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/page-transition.tsx +0 -0
  294. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +0 -0
  295. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +0 -0
  296. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +0 -0
  297. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +0 -0
  298. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/accordion.tsx +0 -0
  299. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/alert-dialog.tsx +0 -0
  300. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/avatar.tsx +0 -0
  301. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +0 -0
  302. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +0 -0
  303. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +0 -0
  304. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +0 -0
  305. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/checkbox.tsx +0 -0
  306. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/dialog.tsx +0 -0
  307. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/dropdown-menu.tsx +0 -0
  308. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +0 -0
  309. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +0 -0
  310. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +0 -0
  311. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/popover.tsx +0 -0
  312. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/progress.tsx +0 -0
  313. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/radio-group.tsx +0 -0
  314. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/scroll-area.tsx +0 -0
  315. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/select.tsx +0 -0
  316. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/separator.tsx +0 -0
  317. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +0 -0
  318. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/skeleton.tsx +0 -0
  319. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/spinner.tsx +0 -0
  320. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/switch.tsx +0 -0
  321. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/table.tsx +0 -0
  322. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/tabs.tsx +0 -0
  323. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/textarea.tsx +0 -0
  324. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/tooltip.tsx +0 -0
  325. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +0 -0
  326. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +0 -0
  327. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +0 -0
  328. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +0 -0
  329. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +0 -0
  330. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +0 -0
  331. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +0 -0
  332. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +0 -0
  333. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/file-api.ts +0 -0
  334. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/rag-api.ts +0 -0
  335. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +0 -0
  336. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +0 -0
  337. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +0 -0
  338. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +0 -0
  339. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +0 -0
  340. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +0 -0
  341. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +0 -0
  342. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +0 -0
  343. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +0 -0
  344. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +0 -0
  345. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +0 -0
  346. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +0 -0
  347. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +0 -0
  348. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +0 -0
  349. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +0 -0
  350. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +0 -0
  351. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +0 -0
  352. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/src/types/speech.d.ts +0 -0
  353. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +0 -0
  354. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vercel.json +0 -0
  355. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +0 -0
  356. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +0 -0
  357. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +0 -0
  358. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +0 -0
  359. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +0 -0
  360. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +0 -0
  361. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +0 -0
  362. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +0 -0
  363. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +0 -0
  364. {fastapi_fullstack-0.2.3 → fastapi_fullstack-0.2.4}/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +0 -0
  365. {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
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"
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")