fastapi-fullstack 0.1.7__py3-none-any.whl
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.1.7.dist-info/METADATA +739 -0
- fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
- fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
- fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
- fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
- fastapi_gen/__init__.py +3 -0
- fastapi_gen/cli.py +442 -0
- fastapi_gen/config.py +356 -0
- fastapi_gen/generator.py +207 -0
- fastapi_gen/prompts.py +874 -0
- fastapi_gen/template/VARIABLES.md +276 -0
- fastapi_gen/template/cookiecutter.json +93 -0
- fastapi_gen/template/hooks/post_gen_project.py +355 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"""API dependencies.
|
|
2
|
+
|
|
3
|
+
Dependency injection factories for services, repositories, and authentication.
|
|
4
|
+
"""
|
|
5
|
+
{%- if cookiecutter.use_database or cookiecutter.use_jwt or cookiecutter.use_api_key or cookiecutter.enable_redis %}
|
|
6
|
+
# ruff: noqa: I001, E402 - Imports structured for Jinja2 template conditionals
|
|
7
|
+
{%- endif %}
|
|
8
|
+
{%- if cookiecutter.use_database or cookiecutter.use_jwt or cookiecutter.use_api_key or cookiecutter.enable_redis %}
|
|
9
|
+
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
from fastapi import Depends
|
|
13
|
+
{%- endif %}
|
|
14
|
+
{%- if cookiecutter.use_jwt %}
|
|
15
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
16
|
+
{%- endif %}
|
|
17
|
+
{%- if cookiecutter.use_jwt or cookiecutter.use_api_key %}
|
|
18
|
+
|
|
19
|
+
from app.core.config import settings
|
|
20
|
+
{%- endif %}
|
|
21
|
+
{%- if cookiecutter.use_database %}
|
|
22
|
+
from app.db.session import get_db_session
|
|
23
|
+
{%- endif %}
|
|
24
|
+
|
|
25
|
+
{%- if cookiecutter.use_postgresql %}
|
|
26
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
27
|
+
|
|
28
|
+
DBSession = Annotated[AsyncSession, Depends(get_db_session)]
|
|
29
|
+
{%- endif %}
|
|
30
|
+
|
|
31
|
+
{%- if cookiecutter.use_sqlite %}
|
|
32
|
+
from sqlalchemy.orm import Session
|
|
33
|
+
|
|
34
|
+
DBSession = Annotated[Session, Depends(get_db_session)]
|
|
35
|
+
{%- endif %}
|
|
36
|
+
|
|
37
|
+
{%- if cookiecutter.use_mongodb %}
|
|
38
|
+
from motor.motor_asyncio import AsyncIOMotorDatabase
|
|
39
|
+
|
|
40
|
+
DBSession = Annotated[AsyncIOMotorDatabase, Depends(get_db_session)]
|
|
41
|
+
{%- endif %}
|
|
42
|
+
|
|
43
|
+
{%- if cookiecutter.enable_redis %}
|
|
44
|
+
from fastapi import Request
|
|
45
|
+
|
|
46
|
+
from app.clients.redis import RedisClient
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def get_redis(request: Request) -> RedisClient:
|
|
50
|
+
"""Get Redis client from lifespan state."""
|
|
51
|
+
return request.state.redis
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
Redis = Annotated[RedisClient, Depends(get_redis)]
|
|
55
|
+
{%- endif %}
|
|
56
|
+
|
|
57
|
+
{%- if cookiecutter.use_jwt %}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# === Service Dependencies ===
|
|
61
|
+
|
|
62
|
+
from app.services.user import UserService
|
|
63
|
+
{%- if cookiecutter.enable_session_management %}
|
|
64
|
+
from app.services.session import SessionService
|
|
65
|
+
{%- endif %}
|
|
66
|
+
{%- endif %}
|
|
67
|
+
{%- if cookiecutter.enable_webhooks and cookiecutter.use_database %}
|
|
68
|
+
from app.services.webhook import WebhookService
|
|
69
|
+
{%- endif %}
|
|
70
|
+
{%- if cookiecutter.include_example_crud and cookiecutter.use_database %}
|
|
71
|
+
from app.services.item import ItemService
|
|
72
|
+
{%- endif %}
|
|
73
|
+
{%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
|
|
74
|
+
from app.services.conversation import ConversationService
|
|
75
|
+
{%- endif %}
|
|
76
|
+
{%- if cookiecutter.use_jwt %}
|
|
77
|
+
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_user_service(db: DBSession) -> UserService:
|
|
81
|
+
"""Create UserService instance with database session."""
|
|
82
|
+
return UserService(db)
|
|
83
|
+
|
|
84
|
+
{%- if cookiecutter.enable_session_management %}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_session_service(db: DBSession) -> SessionService:
|
|
88
|
+
"""Create SessionService instance with database session."""
|
|
89
|
+
return SessionService(db)
|
|
90
|
+
{%- endif %}
|
|
91
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_user_service() -> UserService:
|
|
95
|
+
"""Create UserService instance."""
|
|
96
|
+
return UserService()
|
|
97
|
+
|
|
98
|
+
{%- if cookiecutter.enable_session_management %}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_session_service() -> SessionService:
|
|
102
|
+
"""Create SessionService instance."""
|
|
103
|
+
return SessionService()
|
|
104
|
+
{%- endif %}
|
|
105
|
+
{%- endif %}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
UserSvc = Annotated[UserService, Depends(get_user_service)]
|
|
109
|
+
{%- if cookiecutter.enable_session_management %}
|
|
110
|
+
SessionSvc = Annotated[SessionService, Depends(get_session_service)]
|
|
111
|
+
{%- endif %}
|
|
112
|
+
{%- endif %}
|
|
113
|
+
|
|
114
|
+
{%- if cookiecutter.enable_webhooks and cookiecutter.use_database %}
|
|
115
|
+
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_webhook_service(db: DBSession) -> WebhookService:
|
|
119
|
+
"""Create WebhookService instance with database session."""
|
|
120
|
+
return WebhookService(db)
|
|
121
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_webhook_service() -> WebhookService:
|
|
125
|
+
"""Create WebhookService instance."""
|
|
126
|
+
return WebhookService()
|
|
127
|
+
{%- endif %}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
WebhookSvc = Annotated[WebhookService, Depends(get_webhook_service)]
|
|
131
|
+
{%- endif %}
|
|
132
|
+
|
|
133
|
+
{%- if cookiecutter.include_example_crud and cookiecutter.use_database %}
|
|
134
|
+
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_item_service(db: DBSession) -> ItemService:
|
|
138
|
+
"""Create ItemService instance with database session."""
|
|
139
|
+
return ItemService(db)
|
|
140
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_item_service() -> ItemService:
|
|
144
|
+
"""Create ItemService instance."""
|
|
145
|
+
return ItemService()
|
|
146
|
+
{%- endif %}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
ItemSvc = Annotated[ItemService, Depends(get_item_service)]
|
|
150
|
+
{%- endif %}
|
|
151
|
+
|
|
152
|
+
{%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
|
|
153
|
+
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_conversation_service(db: DBSession) -> ConversationService:
|
|
157
|
+
"""Create ConversationService instance with database session."""
|
|
158
|
+
return ConversationService(db)
|
|
159
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_conversation_service() -> ConversationService:
|
|
163
|
+
"""Create ConversationService instance."""
|
|
164
|
+
return ConversationService()
|
|
165
|
+
{%- endif %}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
ConversationSvc = Annotated[ConversationService, Depends(get_conversation_service)]
|
|
169
|
+
{%- endif %}
|
|
170
|
+
|
|
171
|
+
{%- if cookiecutter.use_jwt %}
|
|
172
|
+
|
|
173
|
+
# === Authentication Dependencies ===
|
|
174
|
+
|
|
175
|
+
from app.core.exceptions import AuthenticationError, AuthorizationError
|
|
176
|
+
from app.db.models.user import User, UserRole
|
|
177
|
+
|
|
178
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
|
179
|
+
|
|
180
|
+
{%- if cookiecutter.use_postgresql %}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def get_current_user(
|
|
184
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
185
|
+
user_service: UserSvc,
|
|
186
|
+
) -> User:
|
|
187
|
+
"""Get current authenticated user from JWT token.
|
|
188
|
+
|
|
189
|
+
Returns the full User object including role information.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
AuthenticationError: If token is invalid or user not found.
|
|
193
|
+
"""
|
|
194
|
+
from uuid import UUID
|
|
195
|
+
|
|
196
|
+
from app.core.security import verify_token
|
|
197
|
+
|
|
198
|
+
payload = verify_token(token)
|
|
199
|
+
if payload is None:
|
|
200
|
+
raise AuthenticationError(message="Invalid or expired token")
|
|
201
|
+
|
|
202
|
+
# Ensure this is an access token, not a refresh token
|
|
203
|
+
if payload.get("type") != "access":
|
|
204
|
+
raise AuthenticationError(message="Invalid token type")
|
|
205
|
+
|
|
206
|
+
user_id = payload.get("sub")
|
|
207
|
+
if user_id is None:
|
|
208
|
+
raise AuthenticationError(message="Invalid token payload")
|
|
209
|
+
|
|
210
|
+
user = await user_service.get_by_id(UUID(user_id))
|
|
211
|
+
if not user.is_active:
|
|
212
|
+
raise AuthenticationError(message="User account is disabled")
|
|
213
|
+
|
|
214
|
+
return user
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class RoleChecker:
|
|
218
|
+
"""Dependency class for role-based access control.
|
|
219
|
+
|
|
220
|
+
Usage:
|
|
221
|
+
# Require admin role
|
|
222
|
+
@router.get("/admin-only")
|
|
223
|
+
async def admin_endpoint(
|
|
224
|
+
user: Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
225
|
+
):
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
# Require any authenticated user
|
|
229
|
+
@router.get("/users")
|
|
230
|
+
async def users_endpoint(
|
|
231
|
+
user: Annotated[User, Depends(get_current_user)]
|
|
232
|
+
):
|
|
233
|
+
...
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, required_role: UserRole) -> None:
|
|
237
|
+
self.required_role = required_role
|
|
238
|
+
|
|
239
|
+
async def __call__(
|
|
240
|
+
self,
|
|
241
|
+
user: Annotated[User, Depends(get_current_user)],
|
|
242
|
+
) -> User:
|
|
243
|
+
"""Check if user has the required role.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
AuthorizationError: If user doesn't have the required role.
|
|
247
|
+
"""
|
|
248
|
+
if not user.has_role(self.required_role):
|
|
249
|
+
raise AuthorizationError(
|
|
250
|
+
message=f"Role '{self.required_role.value}' required for this action"
|
|
251
|
+
)
|
|
252
|
+
return user
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def get_current_active_superuser(
|
|
256
|
+
current_user: Annotated[User, Depends(get_current_user)],
|
|
257
|
+
) -> User:
|
|
258
|
+
"""Get current user and verify they are a superuser.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
AuthorizationError: If user is not a superuser.
|
|
262
|
+
"""
|
|
263
|
+
if not current_user.is_superuser:
|
|
264
|
+
raise AuthorizationError(message="Superuser privileges required")
|
|
265
|
+
return current_user
|
|
266
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_current_user(
|
|
270
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
271
|
+
user_service: UserSvc,
|
|
272
|
+
) -> User:
|
|
273
|
+
"""Get current authenticated user from JWT token.
|
|
274
|
+
|
|
275
|
+
Returns the full User object including role information.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
AuthenticationError: If token is invalid or user not found.
|
|
279
|
+
"""
|
|
280
|
+
from app.core.security import verify_token
|
|
281
|
+
|
|
282
|
+
payload = verify_token(token)
|
|
283
|
+
if payload is None:
|
|
284
|
+
raise AuthenticationError(message="Invalid or expired token")
|
|
285
|
+
|
|
286
|
+
# Ensure this is an access token, not a refresh token
|
|
287
|
+
if payload.get("type") != "access":
|
|
288
|
+
raise AuthenticationError(message="Invalid token type")
|
|
289
|
+
|
|
290
|
+
user_id = payload.get("sub")
|
|
291
|
+
if user_id is None:
|
|
292
|
+
raise AuthenticationError(message="Invalid token payload")
|
|
293
|
+
|
|
294
|
+
user = user_service.get_by_id(user_id)
|
|
295
|
+
if not user.is_active:
|
|
296
|
+
raise AuthenticationError(message="User account is disabled")
|
|
297
|
+
|
|
298
|
+
return user
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class RoleChecker:
|
|
302
|
+
"""Dependency class for role-based access control.
|
|
303
|
+
|
|
304
|
+
Usage:
|
|
305
|
+
# Require admin role
|
|
306
|
+
@router.get("/admin-only")
|
|
307
|
+
def admin_endpoint(
|
|
308
|
+
user: Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
309
|
+
):
|
|
310
|
+
...
|
|
311
|
+
|
|
312
|
+
# Require any authenticated user
|
|
313
|
+
@router.get("/users")
|
|
314
|
+
def users_endpoint(
|
|
315
|
+
user: Annotated[User, Depends(get_current_user)]
|
|
316
|
+
):
|
|
317
|
+
...
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, required_role: UserRole) -> None:
|
|
321
|
+
self.required_role = required_role
|
|
322
|
+
|
|
323
|
+
def __call__(
|
|
324
|
+
self,
|
|
325
|
+
user: Annotated[User, Depends(get_current_user)],
|
|
326
|
+
) -> User:
|
|
327
|
+
"""Check if user has the required role.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
AuthorizationError: If user doesn't have the required role.
|
|
331
|
+
"""
|
|
332
|
+
if not user.has_role(self.required_role):
|
|
333
|
+
raise AuthorizationError(
|
|
334
|
+
message=f"Role '{self.required_role.value}' required for this action"
|
|
335
|
+
)
|
|
336
|
+
return user
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def get_current_active_superuser(
|
|
340
|
+
current_user: Annotated[User, Depends(get_current_user)],
|
|
341
|
+
) -> User:
|
|
342
|
+
"""Get current user and verify they are a superuser.
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
AuthorizationError: If user is not a superuser.
|
|
346
|
+
"""
|
|
347
|
+
if not current_user.is_superuser:
|
|
348
|
+
raise AuthorizationError(message="Superuser privileges required")
|
|
349
|
+
return current_user
|
|
350
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def get_current_user(
|
|
354
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
355
|
+
user_service: UserSvc,
|
|
356
|
+
) -> User:
|
|
357
|
+
"""Get current authenticated user from JWT token.
|
|
358
|
+
|
|
359
|
+
Returns the full User object including role information.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
AuthenticationError: If token is invalid or user not found.
|
|
363
|
+
"""
|
|
364
|
+
from app.core.security import verify_token
|
|
365
|
+
|
|
366
|
+
payload = verify_token(token)
|
|
367
|
+
if payload is None:
|
|
368
|
+
raise AuthenticationError(message="Invalid or expired token")
|
|
369
|
+
|
|
370
|
+
# Ensure this is an access token, not a refresh token
|
|
371
|
+
if payload.get("type") != "access":
|
|
372
|
+
raise AuthenticationError(message="Invalid token type")
|
|
373
|
+
|
|
374
|
+
user_id = payload.get("sub")
|
|
375
|
+
if user_id is None:
|
|
376
|
+
raise AuthenticationError(message="Invalid token payload")
|
|
377
|
+
|
|
378
|
+
user = await user_service.get_by_id(user_id)
|
|
379
|
+
if not user.is_active:
|
|
380
|
+
raise AuthenticationError(message="User account is disabled")
|
|
381
|
+
|
|
382
|
+
return user
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class RoleChecker:
|
|
386
|
+
"""Dependency class for role-based access control.
|
|
387
|
+
|
|
388
|
+
Usage:
|
|
389
|
+
# Require admin role
|
|
390
|
+
@router.get("/admin-only")
|
|
391
|
+
async def admin_endpoint(
|
|
392
|
+
user: Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
393
|
+
):
|
|
394
|
+
...
|
|
395
|
+
|
|
396
|
+
# Require any authenticated user
|
|
397
|
+
@router.get("/users")
|
|
398
|
+
async def users_endpoint(
|
|
399
|
+
user: Annotated[User, Depends(get_current_user)]
|
|
400
|
+
):
|
|
401
|
+
...
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
def __init__(self, required_role: UserRole) -> None:
|
|
405
|
+
self.required_role = required_role
|
|
406
|
+
|
|
407
|
+
async def __call__(
|
|
408
|
+
self,
|
|
409
|
+
user: Annotated[User, Depends(get_current_user)],
|
|
410
|
+
) -> User:
|
|
411
|
+
"""Check if user has the required role.
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
AuthorizationError: If user doesn't have the required role.
|
|
415
|
+
"""
|
|
416
|
+
if not user.has_role(self.required_role):
|
|
417
|
+
raise AuthorizationError(
|
|
418
|
+
message=f"Role '{self.required_role.value}' required for this action"
|
|
419
|
+
)
|
|
420
|
+
return user
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def get_current_active_superuser(
|
|
424
|
+
current_user: Annotated[User, Depends(get_current_user)],
|
|
425
|
+
) -> User:
|
|
426
|
+
"""Get current user and verify they are a superuser.
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
AuthorizationError: If user is not a superuser.
|
|
430
|
+
"""
|
|
431
|
+
if not current_user.is_superuser:
|
|
432
|
+
raise AuthorizationError(message="Superuser privileges required")
|
|
433
|
+
return current_user
|
|
434
|
+
{%- endif %}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# Type aliases for dependency injection
|
|
438
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
439
|
+
CurrentSuperuser = Annotated[User, Depends(get_current_active_superuser)]
|
|
440
|
+
CurrentAdmin = Annotated[User, Depends(RoleChecker(UserRole.ADMIN))]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# WebSocket authentication dependency
|
|
444
|
+
from fastapi import WebSocket, Query, Cookie
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
async def get_current_user_ws(
|
|
448
|
+
websocket: WebSocket,
|
|
449
|
+
token: str | None = Query(None, alias="token"),
|
|
450
|
+
access_token: str | None = Cookie(None),
|
|
451
|
+
) -> User:
|
|
452
|
+
"""Get current user from WebSocket JWT token.
|
|
453
|
+
|
|
454
|
+
Token can be passed either as:
|
|
455
|
+
- Query parameter: ws://...?token=<jwt>
|
|
456
|
+
- Cookie: access_token cookie (set by HTTP login)
|
|
457
|
+
|
|
458
|
+
Raises:
|
|
459
|
+
AuthenticationError: If token is invalid or user not found.
|
|
460
|
+
"""
|
|
461
|
+
from uuid import UUID
|
|
462
|
+
|
|
463
|
+
from app.core.security import verify_token
|
|
464
|
+
|
|
465
|
+
# Try query parameter first, then cookie
|
|
466
|
+
auth_token = token or access_token
|
|
467
|
+
|
|
468
|
+
if not auth_token:
|
|
469
|
+
await websocket.close(code=4001, reason="Missing authentication token")
|
|
470
|
+
raise AuthenticationError(message="Missing authentication token")
|
|
471
|
+
|
|
472
|
+
payload = verify_token(auth_token)
|
|
473
|
+
if payload is None:
|
|
474
|
+
await websocket.close(code=4001, reason="Invalid or expired token")
|
|
475
|
+
raise AuthenticationError(message="Invalid or expired token")
|
|
476
|
+
|
|
477
|
+
if payload.get("type") != "access":
|
|
478
|
+
await websocket.close(code=4001, reason="Invalid token type")
|
|
479
|
+
raise AuthenticationError(message="Invalid token type")
|
|
480
|
+
|
|
481
|
+
user_id = payload.get("sub")
|
|
482
|
+
if user_id is None:
|
|
483
|
+
await websocket.close(code=4001, reason="Invalid token payload")
|
|
484
|
+
raise AuthenticationError(message="Invalid token payload")
|
|
485
|
+
{%- if cookiecutter.use_postgresql %}
|
|
486
|
+
|
|
487
|
+
from app.db.session import get_db_context
|
|
488
|
+
|
|
489
|
+
async with get_db_context() as db:
|
|
490
|
+
user_service = UserService(db)
|
|
491
|
+
user = await user_service.get_by_id(UUID(user_id))
|
|
492
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
493
|
+
|
|
494
|
+
db = await get_db_session()
|
|
495
|
+
user_service = UserService(db)
|
|
496
|
+
user = await user_service.get_by_id(UUID(user_id))
|
|
497
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
498
|
+
|
|
499
|
+
with get_db_session() as db:
|
|
500
|
+
user_service = UserService(db)
|
|
501
|
+
user = user_service.get_by_id(user_id)
|
|
502
|
+
{%- endif %}
|
|
503
|
+
|
|
504
|
+
if not user.is_active:
|
|
505
|
+
await websocket.close(code=4001, reason="User account is disabled")
|
|
506
|
+
raise AuthenticationError(message="User account is disabled")
|
|
507
|
+
|
|
508
|
+
return user
|
|
509
|
+
{%- endif %}
|
|
510
|
+
|
|
511
|
+
{%- if cookiecutter.use_api_key %}
|
|
512
|
+
|
|
513
|
+
import secrets
|
|
514
|
+
|
|
515
|
+
from fastapi.security import APIKeyHeader
|
|
516
|
+
|
|
517
|
+
from app.core.exceptions import AuthenticationError, AuthorizationError
|
|
518
|
+
|
|
519
|
+
api_key_header = APIKeyHeader(name=settings.API_KEY_HEADER, auto_error=False)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
async def verify_api_key(
|
|
523
|
+
api_key: Annotated[str | None, Depends(api_key_header)],
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Verify API key from header.
|
|
526
|
+
|
|
527
|
+
Uses constant-time comparison to prevent timing attacks.
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
AuthenticationError: If API key is missing.
|
|
531
|
+
AuthorizationError: If API key is invalid.
|
|
532
|
+
"""
|
|
533
|
+
if api_key is None:
|
|
534
|
+
raise AuthenticationError(message="API Key header missing")
|
|
535
|
+
if not secrets.compare_digest(api_key, settings.API_KEY):
|
|
536
|
+
raise AuthorizationError(message="Invalid API Key")
|
|
537
|
+
return api_key
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
ValidAPIKey = Annotated[str, Depends(verify_api_key)]
|
|
541
|
+
{%- endif %}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Exception handlers for FastAPI application.
|
|
2
|
+
|
|
3
|
+
These handlers convert domain exceptions to proper HTTP responses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, Request, WebSocket
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
from app.core.exceptions import AppException
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def app_exception_handler(
|
|
18
|
+
request: Union[Request, WebSocket], exc: AppException
|
|
19
|
+
) -> JSONResponse:
|
|
20
|
+
"""Handle application exceptions.
|
|
21
|
+
|
|
22
|
+
Logs 5xx errors as errors and 4xx as warnings.
|
|
23
|
+
Returns a standardized JSON error response.
|
|
24
|
+
|
|
25
|
+
Note: For WebSocket connections, this handler may not be able to return
|
|
26
|
+
a response if the connection was already closed.
|
|
27
|
+
"""
|
|
28
|
+
# WebSocket objects don't have a method attribute
|
|
29
|
+
method = getattr(request, "method", "WEBSOCKET")
|
|
30
|
+
|
|
31
|
+
log_extra = {
|
|
32
|
+
"error_code": exc.code,
|
|
33
|
+
"status_code": exc.status_code,
|
|
34
|
+
"details": exc.details,
|
|
35
|
+
"path": request.url.path,
|
|
36
|
+
"method": method,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if exc.status_code >= 500:
|
|
40
|
+
logger.error(f"{exc.code}: {exc.message}", extra=log_extra)
|
|
41
|
+
else:
|
|
42
|
+
logger.warning(f"{exc.code}: {exc.message}", extra=log_extra)
|
|
43
|
+
|
|
44
|
+
headers: dict[str, str] = {}
|
|
45
|
+
if exc.status_code == 401:
|
|
46
|
+
headers["WWW-Authenticate"] = "Bearer"
|
|
47
|
+
|
|
48
|
+
return JSONResponse(
|
|
49
|
+
status_code=exc.status_code,
|
|
50
|
+
content={
|
|
51
|
+
"error": {
|
|
52
|
+
"code": exc.code,
|
|
53
|
+
"message": exc.message,
|
|
54
|
+
"details": exc.details or None,
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
headers=headers,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def unhandled_exception_handler(
|
|
62
|
+
request: Union[Request, WebSocket], exc: Exception
|
|
63
|
+
) -> JSONResponse:
|
|
64
|
+
"""Handle unexpected exceptions.
|
|
65
|
+
|
|
66
|
+
Logs the full exception but returns a generic error to the client
|
|
67
|
+
to avoid leaking sensitive information.
|
|
68
|
+
"""
|
|
69
|
+
method = getattr(request, "method", "WEBSOCKET")
|
|
70
|
+
|
|
71
|
+
logger.exception(
|
|
72
|
+
"Unhandled exception",
|
|
73
|
+
extra={
|
|
74
|
+
"path": request.url.path,
|
|
75
|
+
"method": method,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return JSONResponse(
|
|
80
|
+
status_code=500,
|
|
81
|
+
content={
|
|
82
|
+
"error": {
|
|
83
|
+
"code": "INTERNAL_ERROR",
|
|
84
|
+
"message": "An unexpected error occurred",
|
|
85
|
+
"details": None,
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
92
|
+
"""Register all exception handlers on the FastAPI app.
|
|
93
|
+
|
|
94
|
+
Call this after creating the FastAPI application instance.
|
|
95
|
+
"""
|
|
96
|
+
app.add_exception_handler(AppException, app_exception_handler)
|
|
97
|
+
# Uncomment to catch all unhandled exceptions:
|
|
98
|
+
# app.add_exception_handler(Exception, unhandled_exception_handler)
|