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,227 @@
|
|
|
1
|
+
"""Health check endpoints.
|
|
2
|
+
|
|
3
|
+
Provides Kubernetes-compatible health check endpoints:
|
|
4
|
+
- /health - Simple liveness check
|
|
5
|
+
- /health/live - Detailed liveness probe
|
|
6
|
+
- /health/ready - Readiness probe with dependency checks
|
|
7
|
+
"""
|
|
8
|
+
{%- if cookiecutter.use_database or cookiecutter.enable_redis %}
|
|
9
|
+
# ruff: noqa: I001 - Imports structured for Jinja2 template conditionals
|
|
10
|
+
{%- endif %}
|
|
11
|
+
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from fastapi import APIRouter
|
|
16
|
+
from fastapi.responses import JSONResponse
|
|
17
|
+
{%- if cookiecutter.use_postgresql or cookiecutter.use_sqlite %}
|
|
18
|
+
from sqlalchemy import text
|
|
19
|
+
{%- endif %}
|
|
20
|
+
|
|
21
|
+
from app.core.config import settings
|
|
22
|
+
{%- if cookiecutter.use_database or cookiecutter.enable_redis %}
|
|
23
|
+
from app.api.deps import {% if cookiecutter.use_database %}DBSession{% endif %}{% if cookiecutter.use_database and cookiecutter.enable_redis %}, {% endif %}{% if cookiecutter.enable_redis %}Redis{% endif %}
|
|
24
|
+
|
|
25
|
+
{%- endif %}
|
|
26
|
+
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_health_response(
|
|
31
|
+
status: str,
|
|
32
|
+
checks: dict[str, Any] | None = None,
|
|
33
|
+
details: dict[str, Any] | None = None,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Build a structured health response."""
|
|
36
|
+
response: dict[str, Any] = {
|
|
37
|
+
"status": status,
|
|
38
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
39
|
+
"service": settings.PROJECT_NAME,
|
|
40
|
+
}
|
|
41
|
+
if checks is not None:
|
|
42
|
+
response["checks"] = checks
|
|
43
|
+
if details is not None:
|
|
44
|
+
response["details"] = details
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.get("/health")
|
|
49
|
+
async def health_check() -> dict[str, str]:
|
|
50
|
+
"""Simple liveness probe - check if application is running.
|
|
51
|
+
|
|
52
|
+
This is a lightweight check that should always succeed if the
|
|
53
|
+
application is running. Use this for basic connectivity tests.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
{"status": "healthy"}
|
|
57
|
+
"""
|
|
58
|
+
return {"status": "healthy"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get("/health/live")
|
|
62
|
+
async def liveness_probe() -> dict[str, Any]:
|
|
63
|
+
"""Detailed liveness probe for Kubernetes.
|
|
64
|
+
|
|
65
|
+
This endpoint is designed for Kubernetes liveness probes.
|
|
66
|
+
It checks if the application process is alive and responding.
|
|
67
|
+
Failure indicates the container should be restarted.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Structured response with timestamp and service info.
|
|
71
|
+
"""
|
|
72
|
+
return _build_health_response(
|
|
73
|
+
status="alive",
|
|
74
|
+
details={
|
|
75
|
+
"version": getattr(settings, "VERSION", "1.0.0"),
|
|
76
|
+
"environment": settings.ENVIRONMENT,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.get("/health/ready", response_model=None)
|
|
82
|
+
async def readiness_probe(
|
|
83
|
+
{%- if cookiecutter.use_database %}
|
|
84
|
+
db: DBSession,
|
|
85
|
+
{%- endif %}
|
|
86
|
+
{%- if cookiecutter.enable_redis %}
|
|
87
|
+
redis: Redis,
|
|
88
|
+
{%- endif %}
|
|
89
|
+
) -> dict[str, Any] | JSONResponse:
|
|
90
|
+
"""Readiness probe for Kubernetes.
|
|
91
|
+
|
|
92
|
+
This endpoint checks if all dependencies are ready to handle traffic.
|
|
93
|
+
It verifies database connections, Redis, and other critical services.
|
|
94
|
+
Failure indicates traffic should be temporarily diverted.
|
|
95
|
+
|
|
96
|
+
Checks performed:
|
|
97
|
+
{%- if cookiecutter.use_database %}
|
|
98
|
+
- Database connectivity
|
|
99
|
+
{%- endif %}
|
|
100
|
+
{%- if cookiecutter.enable_redis %}
|
|
101
|
+
- Redis connectivity
|
|
102
|
+
{%- endif %}
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Structured response with individual check results.
|
|
106
|
+
Returns 503 if any critical check fails.
|
|
107
|
+
"""
|
|
108
|
+
checks: dict[str, dict[str, Any]] = {}
|
|
109
|
+
|
|
110
|
+
{%- if cookiecutter.use_postgresql %}
|
|
111
|
+
# Database check
|
|
112
|
+
try:
|
|
113
|
+
start = datetime.now(UTC)
|
|
114
|
+
await db.execute(text("SELECT 1"))
|
|
115
|
+
latency_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
116
|
+
checks["database"] = {
|
|
117
|
+
"status": "healthy",
|
|
118
|
+
"latency_ms": round(latency_ms, 2),
|
|
119
|
+
"type": "postgresql",
|
|
120
|
+
}
|
|
121
|
+
except Exception as e:
|
|
122
|
+
checks["database"] = {
|
|
123
|
+
"status": "unhealthy",
|
|
124
|
+
"error": str(e),
|
|
125
|
+
"type": "postgresql",
|
|
126
|
+
}
|
|
127
|
+
{%- endif %}
|
|
128
|
+
|
|
129
|
+
{%- if cookiecutter.use_mongodb %}
|
|
130
|
+
# Database check
|
|
131
|
+
try:
|
|
132
|
+
start = datetime.now(UTC)
|
|
133
|
+
await db.command("ping")
|
|
134
|
+
latency_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
135
|
+
checks["database"] = {
|
|
136
|
+
"status": "healthy",
|
|
137
|
+
"latency_ms": round(latency_ms, 2),
|
|
138
|
+
"type": "mongodb",
|
|
139
|
+
}
|
|
140
|
+
except Exception as e:
|
|
141
|
+
checks["database"] = {
|
|
142
|
+
"status": "unhealthy",
|
|
143
|
+
"error": str(e),
|
|
144
|
+
"type": "mongodb",
|
|
145
|
+
}
|
|
146
|
+
{%- endif %}
|
|
147
|
+
|
|
148
|
+
{%- if cookiecutter.use_sqlite %}
|
|
149
|
+
# Database check
|
|
150
|
+
try:
|
|
151
|
+
start = datetime.now(UTC)
|
|
152
|
+
db.execute(text("SELECT 1"))
|
|
153
|
+
latency_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
154
|
+
checks["database"] = {
|
|
155
|
+
"status": "healthy",
|
|
156
|
+
"latency_ms": round(latency_ms, 2),
|
|
157
|
+
"type": "sqlite",
|
|
158
|
+
}
|
|
159
|
+
except Exception as e:
|
|
160
|
+
checks["database"] = {
|
|
161
|
+
"status": "unhealthy",
|
|
162
|
+
"error": str(e),
|
|
163
|
+
"type": "sqlite",
|
|
164
|
+
}
|
|
165
|
+
{%- endif %}
|
|
166
|
+
|
|
167
|
+
{%- if cookiecutter.enable_redis %}
|
|
168
|
+
# Redis check
|
|
169
|
+
try:
|
|
170
|
+
start = datetime.now(UTC)
|
|
171
|
+
is_healthy = await redis.ping()
|
|
172
|
+
latency_ms = (datetime.now(UTC) - start).total_seconds() * 1000
|
|
173
|
+
if is_healthy:
|
|
174
|
+
checks["redis"] = {
|
|
175
|
+
"status": "healthy",
|
|
176
|
+
"latency_ms": round(latency_ms, 2),
|
|
177
|
+
}
|
|
178
|
+
else:
|
|
179
|
+
checks["redis"] = {
|
|
180
|
+
"status": "unhealthy",
|
|
181
|
+
"error": "Ping failed",
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
checks["redis"] = {
|
|
185
|
+
"status": "unhealthy",
|
|
186
|
+
"error": str(e),
|
|
187
|
+
}
|
|
188
|
+
{%- endif %}
|
|
189
|
+
|
|
190
|
+
# Determine overall health
|
|
191
|
+
all_healthy = all(
|
|
192
|
+
check.get("status") == "healthy" for check in checks.values()
|
|
193
|
+
) if checks else True
|
|
194
|
+
|
|
195
|
+
response_data = _build_health_response(
|
|
196
|
+
status="ready" if all_healthy else "not_ready",
|
|
197
|
+
checks=checks,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not all_healthy:
|
|
201
|
+
return JSONResponse(status_code=503, content=response_data)
|
|
202
|
+
|
|
203
|
+
return response_data
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Backward compatibility - keep /ready endpoint
|
|
207
|
+
@router.get("/ready", response_model=None)
|
|
208
|
+
async def readiness_check(
|
|
209
|
+
{%- if cookiecutter.use_database %}
|
|
210
|
+
db: DBSession,
|
|
211
|
+
{%- endif %}
|
|
212
|
+
{%- if cookiecutter.enable_redis %}
|
|
213
|
+
redis: Redis,
|
|
214
|
+
{%- endif %}
|
|
215
|
+
) -> dict[str, Any] | JSONResponse:
|
|
216
|
+
"""Readiness check (alias for /health/ready).
|
|
217
|
+
|
|
218
|
+
Deprecated: Use /health/ready instead.
|
|
219
|
+
"""
|
|
220
|
+
return await readiness_probe(
|
|
221
|
+
{%- if cookiecutter.use_database %}
|
|
222
|
+
db=db,
|
|
223
|
+
{%- endif %}
|
|
224
|
+
{%- if cookiecutter.enable_redis %}
|
|
225
|
+
redis=redis,
|
|
226
|
+
{%- endif %}
|
|
227
|
+
)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
{%- if cookiecutter.include_example_crud and cookiecutter.use_database %}
|
|
2
|
+
# ruff: noqa: I001 - Imports structured for Jinja2 template conditionals
|
|
3
|
+
"""Item CRUD routes - example API endpoints.
|
|
4
|
+
|
|
5
|
+
This module demonstrates a complete CRUD API for the Item entity.
|
|
6
|
+
You can use it as a template for creating your own endpoints.
|
|
7
|
+
|
|
8
|
+
The endpoints are:
|
|
9
|
+
- GET /items - List all items (with pagination if enabled)
|
|
10
|
+
- POST /items - Create a new item
|
|
11
|
+
- GET /items/{item_id} - Get a single item by ID
|
|
12
|
+
- PATCH /items/{item_id} - Update an item
|
|
13
|
+
- DELETE /items/{item_id} - Delete an item
|
|
14
|
+
"""
|
|
15
|
+
{%- if cookiecutter.use_postgresql %}
|
|
16
|
+
|
|
17
|
+
from uuid import UUID
|
|
18
|
+
{%- endif %}
|
|
19
|
+
|
|
20
|
+
from fastapi import APIRouter, status
|
|
21
|
+
{%- if cookiecutter.enable_pagination and cookiecutter.use_postgresql %}
|
|
22
|
+
from fastapi_pagination import Page
|
|
23
|
+
from fastapi_pagination.ext.sqlalchemy import paginate
|
|
24
|
+
from sqlalchemy import select
|
|
25
|
+
{%- elif cookiecutter.enable_pagination and cookiecutter.use_sqlite %}
|
|
26
|
+
from fastapi_pagination import Page
|
|
27
|
+
from fastapi_pagination.ext.sqlalchemy import paginate
|
|
28
|
+
from sqlalchemy import select
|
|
29
|
+
{%- endif %}
|
|
30
|
+
|
|
31
|
+
{%- if cookiecutter.use_mongodb %}
|
|
32
|
+
from app.api.deps import ItemSvc
|
|
33
|
+
{%- elif cookiecutter.enable_pagination %}
|
|
34
|
+
from app.api.deps import DBSession, ItemSvc
|
|
35
|
+
{%- else %}
|
|
36
|
+
from app.api.deps import ItemSvc
|
|
37
|
+
{%- endif %}
|
|
38
|
+
{%- if cookiecutter.enable_pagination and (cookiecutter.use_postgresql or cookiecutter.use_sqlite) %}
|
|
39
|
+
from app.db.models.item import Item
|
|
40
|
+
{%- endif %}
|
|
41
|
+
from app.schemas.item import ItemCreate, ItemRead, ItemUpdate
|
|
42
|
+
|
|
43
|
+
router = APIRouter()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
{%- if cookiecutter.use_postgresql %}
|
|
47
|
+
|
|
48
|
+
{%- if cookiecutter.enable_pagination %}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.get("", response_model=Page[ItemRead])
|
|
52
|
+
async def list_items(db: DBSession):
|
|
53
|
+
"""List all items with pagination.
|
|
54
|
+
|
|
55
|
+
Returns a paginated list of items. Use query parameters
|
|
56
|
+
`page` and `size` to control pagination.
|
|
57
|
+
"""
|
|
58
|
+
return await paginate(db, select(Item))
|
|
59
|
+
{%- else %}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("", response_model=list[ItemRead])
|
|
63
|
+
async def list_items(
|
|
64
|
+
item_service: ItemSvc,
|
|
65
|
+
skip: int = 0,
|
|
66
|
+
limit: int = 100,
|
|
67
|
+
):
|
|
68
|
+
"""List all items.
|
|
69
|
+
|
|
70
|
+
Returns a list of items with offset-based pagination.
|
|
71
|
+
"""
|
|
72
|
+
return await item_service.get_multi(skip=skip, limit=limit)
|
|
73
|
+
{%- endif %}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
|
|
77
|
+
async def create_item(
|
|
78
|
+
item_in: ItemCreate,
|
|
79
|
+
item_service: ItemSvc,
|
|
80
|
+
):
|
|
81
|
+
"""Create a new item.
|
|
82
|
+
|
|
83
|
+
Creates an item with the provided title and optional description.
|
|
84
|
+
"""
|
|
85
|
+
return await item_service.create(item_in)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.get("/{item_id}", response_model=ItemRead)
|
|
89
|
+
async def get_item(
|
|
90
|
+
item_id: UUID,
|
|
91
|
+
item_service: ItemSvc,
|
|
92
|
+
):
|
|
93
|
+
"""Get a single item by ID.
|
|
94
|
+
|
|
95
|
+
Raises 404 if the item does not exist.
|
|
96
|
+
"""
|
|
97
|
+
return await item_service.get_by_id(item_id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.patch("/{item_id}", response_model=ItemRead)
|
|
101
|
+
async def update_item(
|
|
102
|
+
item_id: UUID,
|
|
103
|
+
item_in: ItemUpdate,
|
|
104
|
+
item_service: ItemSvc,
|
|
105
|
+
):
|
|
106
|
+
"""Update an item.
|
|
107
|
+
|
|
108
|
+
Supports partial updates - only provided fields are updated.
|
|
109
|
+
Raises 404 if the item does not exist.
|
|
110
|
+
"""
|
|
111
|
+
return await item_service.update(item_id, item_in)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
115
|
+
async def delete_item(
|
|
116
|
+
item_id: UUID,
|
|
117
|
+
item_service: ItemSvc,
|
|
118
|
+
):
|
|
119
|
+
"""Delete an item.
|
|
120
|
+
|
|
121
|
+
Raises 404 if the item does not exist.
|
|
122
|
+
"""
|
|
123
|
+
await item_service.delete(item_id)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
127
|
+
|
|
128
|
+
{%- if cookiecutter.enable_pagination %}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@router.get("", response_model=Page[ItemRead])
|
|
132
|
+
def list_items(db: DBSession):
|
|
133
|
+
"""List all items with pagination.
|
|
134
|
+
|
|
135
|
+
Returns a paginated list of items. Use query parameters
|
|
136
|
+
`page` and `size` to control pagination.
|
|
137
|
+
"""
|
|
138
|
+
return paginate(db, select(Item))
|
|
139
|
+
{%- else %}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@router.get("", response_model=list[ItemRead])
|
|
143
|
+
def list_items(
|
|
144
|
+
item_service: ItemSvc,
|
|
145
|
+
skip: int = 0,
|
|
146
|
+
limit: int = 100,
|
|
147
|
+
):
|
|
148
|
+
"""List all items.
|
|
149
|
+
|
|
150
|
+
Returns a list of items with offset-based pagination.
|
|
151
|
+
"""
|
|
152
|
+
return item_service.get_multi(skip=skip, limit=limit)
|
|
153
|
+
{%- endif %}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
|
|
157
|
+
def create_item(
|
|
158
|
+
item_in: ItemCreate,
|
|
159
|
+
item_service: ItemSvc,
|
|
160
|
+
):
|
|
161
|
+
"""Create a new item.
|
|
162
|
+
|
|
163
|
+
Creates an item with the provided title and optional description.
|
|
164
|
+
"""
|
|
165
|
+
return item_service.create(item_in)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@router.get("/{item_id}", response_model=ItemRead)
|
|
169
|
+
def get_item(
|
|
170
|
+
item_id: str,
|
|
171
|
+
item_service: ItemSvc,
|
|
172
|
+
):
|
|
173
|
+
"""Get a single item by ID.
|
|
174
|
+
|
|
175
|
+
Raises 404 if the item does not exist.
|
|
176
|
+
"""
|
|
177
|
+
return item_service.get_by_id(item_id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.patch("/{item_id}", response_model=ItemRead)
|
|
181
|
+
def update_item(
|
|
182
|
+
item_id: str,
|
|
183
|
+
item_in: ItemUpdate,
|
|
184
|
+
item_service: ItemSvc,
|
|
185
|
+
):
|
|
186
|
+
"""Update an item.
|
|
187
|
+
|
|
188
|
+
Supports partial updates - only provided fields are updated.
|
|
189
|
+
Raises 404 if the item does not exist.
|
|
190
|
+
"""
|
|
191
|
+
return item_service.update(item_id, item_in)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
195
|
+
def delete_item(
|
|
196
|
+
item_id: str,
|
|
197
|
+
item_service: ItemSvc,
|
|
198
|
+
):
|
|
199
|
+
"""Delete an item.
|
|
200
|
+
|
|
201
|
+
Raises 404 if the item does not exist.
|
|
202
|
+
"""
|
|
203
|
+
item_service.delete(item_id)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@router.get("", response_model=list[ItemRead])
|
|
210
|
+
async def list_items(
|
|
211
|
+
item_service: ItemSvc,
|
|
212
|
+
skip: int = 0,
|
|
213
|
+
limit: int = 100,
|
|
214
|
+
):
|
|
215
|
+
"""List all items.
|
|
216
|
+
|
|
217
|
+
Returns a list of items with offset-based pagination.
|
|
218
|
+
"""
|
|
219
|
+
return await item_service.get_multi(skip=skip, limit=limit)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
|
|
223
|
+
async def create_item(
|
|
224
|
+
item_in: ItemCreate,
|
|
225
|
+
item_service: ItemSvc,
|
|
226
|
+
):
|
|
227
|
+
"""Create a new item.
|
|
228
|
+
|
|
229
|
+
Creates an item with the provided title and optional description.
|
|
230
|
+
"""
|
|
231
|
+
return await item_service.create(item_in)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@router.get("/{item_id}", response_model=ItemRead)
|
|
235
|
+
async def get_item(
|
|
236
|
+
item_id: str,
|
|
237
|
+
item_service: ItemSvc,
|
|
238
|
+
):
|
|
239
|
+
"""Get a single item by ID.
|
|
240
|
+
|
|
241
|
+
Raises 404 if the item does not exist.
|
|
242
|
+
"""
|
|
243
|
+
return await item_service.get_by_id(item_id)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.patch("/{item_id}", response_model=ItemRead)
|
|
247
|
+
async def update_item(
|
|
248
|
+
item_id: str,
|
|
249
|
+
item_in: ItemUpdate,
|
|
250
|
+
item_service: ItemSvc,
|
|
251
|
+
):
|
|
252
|
+
"""Update an item.
|
|
253
|
+
|
|
254
|
+
Supports partial updates - only provided fields are updated.
|
|
255
|
+
Raises 404 if the item does not exist.
|
|
256
|
+
"""
|
|
257
|
+
return await item_service.update(item_id, item_in)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
261
|
+
async def delete_item(
|
|
262
|
+
item_id: str,
|
|
263
|
+
item_service: ItemSvc,
|
|
264
|
+
):
|
|
265
|
+
"""Delete an item.
|
|
266
|
+
|
|
267
|
+
Raises 404 if the item does not exist.
|
|
268
|
+
"""
|
|
269
|
+
await item_service.delete(item_id)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
{%- endif %}
|
|
273
|
+
{%- else %}
|
|
274
|
+
"""Item routes - not configured."""
|
|
275
|
+
{%- endif %}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_oauth %}
|
|
2
|
+
"""OAuth2 authentication routes."""
|
|
3
|
+
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Request
|
|
7
|
+
from fastapi.responses import RedirectResponse
|
|
8
|
+
|
|
9
|
+
from app.api.deps import UserSvc
|
|
10
|
+
from app.core.oauth import oauth
|
|
11
|
+
from app.core.security import create_access_token, create_refresh_token
|
|
12
|
+
|
|
13
|
+
router = APIRouter()
|
|
14
|
+
|
|
15
|
+
# Frontend URL for OAuth callback redirect
|
|
16
|
+
FRONTEND_URL = "http://localhost:{{ cookiecutter.frontend_port }}"
|
|
17
|
+
|
|
18
|
+
{%- if cookiecutter.enable_oauth_google %}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/google/login")
|
|
22
|
+
async def google_login(request: Request):
|
|
23
|
+
"""Redirect to Google OAuth2 login page."""
|
|
24
|
+
from app.core.config import settings
|
|
25
|
+
|
|
26
|
+
redirect_uri = settings.GOOGLE_REDIRECT_URI
|
|
27
|
+
return await oauth.google.authorize_redirect(request, redirect_uri)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
{%- if cookiecutter.use_postgresql %}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/google/callback")
|
|
34
|
+
async def google_callback(request: Request, user_service: UserSvc):
|
|
35
|
+
"""Handle Google OAuth2 callback.
|
|
36
|
+
|
|
37
|
+
Creates a new user if one doesn't exist with the Google email,
|
|
38
|
+
or returns tokens for existing user. Redirects to frontend with tokens.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
token = await oauth.google.authorize_access_token(request)
|
|
42
|
+
user_info = token.get("userinfo")
|
|
43
|
+
|
|
44
|
+
if not user_info:
|
|
45
|
+
params = urlencode({"error": "Failed to get user info from Google"})
|
|
46
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
47
|
+
|
|
48
|
+
email = user_info.get("email")
|
|
49
|
+
google_id = user_info.get("sub")
|
|
50
|
+
full_name = user_info.get("name")
|
|
51
|
+
|
|
52
|
+
# Try to find existing user by OAuth ID
|
|
53
|
+
user = await user_service.get_by_oauth("google", google_id)
|
|
54
|
+
|
|
55
|
+
if not user:
|
|
56
|
+
# Try to find by email (link existing account)
|
|
57
|
+
user = await user_service.get_by_email(email)
|
|
58
|
+
if user:
|
|
59
|
+
# Link OAuth to existing account
|
|
60
|
+
user = await user_service.link_oauth(user.id, "google", google_id)
|
|
61
|
+
else:
|
|
62
|
+
# Create new user
|
|
63
|
+
user = await user_service.create_oauth_user(
|
|
64
|
+
email=email,
|
|
65
|
+
full_name=full_name,
|
|
66
|
+
oauth_provider="google",
|
|
67
|
+
oauth_id=google_id,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
access_token = create_access_token(subject=str(user.id))
|
|
71
|
+
refresh_token = create_refresh_token(subject=str(user.id))
|
|
72
|
+
|
|
73
|
+
# Redirect to frontend with tokens
|
|
74
|
+
params = urlencode({
|
|
75
|
+
"access_token": access_token,
|
|
76
|
+
"refresh_token": refresh_token,
|
|
77
|
+
})
|
|
78
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/auth/callback?{params}")
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
params = urlencode({"error": str(e)})
|
|
82
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.get("/google/callback")
|
|
89
|
+
async def google_callback(request: Request, user_service: UserSvc):
|
|
90
|
+
"""Handle Google OAuth2 callback.
|
|
91
|
+
|
|
92
|
+
Creates a new user if one doesn't exist with the Google email,
|
|
93
|
+
or returns tokens for existing user. Redirects to frontend with tokens.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
token = await oauth.google.authorize_access_token(request)
|
|
97
|
+
user_info = token.get("userinfo")
|
|
98
|
+
|
|
99
|
+
if not user_info:
|
|
100
|
+
params = urlencode({"error": "Failed to get user info from Google"})
|
|
101
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
102
|
+
|
|
103
|
+
email = user_info.get("email")
|
|
104
|
+
google_id = user_info.get("sub")
|
|
105
|
+
full_name = user_info.get("name")
|
|
106
|
+
|
|
107
|
+
# Try to find existing user by OAuth ID
|
|
108
|
+
user = await user_service.get_by_oauth("google", google_id)
|
|
109
|
+
|
|
110
|
+
if not user:
|
|
111
|
+
# Try to find by email (link existing account)
|
|
112
|
+
user = await user_service.get_by_email(email)
|
|
113
|
+
if user:
|
|
114
|
+
# Link OAuth to existing account
|
|
115
|
+
user = await user_service.link_oauth(str(user.id), "google", google_id)
|
|
116
|
+
else:
|
|
117
|
+
# Create new user
|
|
118
|
+
user = await user_service.create_oauth_user(
|
|
119
|
+
email=email,
|
|
120
|
+
full_name=full_name,
|
|
121
|
+
oauth_provider="google",
|
|
122
|
+
oauth_id=google_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
access_token = create_access_token(subject=str(user.id))
|
|
126
|
+
refresh_token = create_refresh_token(subject=str(user.id))
|
|
127
|
+
|
|
128
|
+
# Redirect to frontend with tokens
|
|
129
|
+
params = urlencode({
|
|
130
|
+
"access_token": access_token,
|
|
131
|
+
"refresh_token": refresh_token,
|
|
132
|
+
})
|
|
133
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/auth/callback?{params}")
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
params = urlencode({"error": str(e)})
|
|
137
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.get("/google/callback")
|
|
144
|
+
def google_callback(request: Request, user_service: UserSvc):
|
|
145
|
+
"""Handle Google OAuth2 callback.
|
|
146
|
+
|
|
147
|
+
Creates a new user if one doesn't exist with the Google email,
|
|
148
|
+
or returns tokens for existing user. Redirects to frontend with tokens.
|
|
149
|
+
"""
|
|
150
|
+
import asyncio
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Run async OAuth in sync context
|
|
154
|
+
loop = asyncio.new_event_loop()
|
|
155
|
+
token = loop.run_until_complete(oauth.google.authorize_access_token(request))
|
|
156
|
+
loop.close()
|
|
157
|
+
|
|
158
|
+
user_info = token.get("userinfo")
|
|
159
|
+
|
|
160
|
+
if not user_info:
|
|
161
|
+
params = urlencode({"error": "Failed to get user info from Google"})
|
|
162
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
163
|
+
|
|
164
|
+
email = user_info.get("email")
|
|
165
|
+
google_id = user_info.get("sub")
|
|
166
|
+
full_name = user_info.get("name")
|
|
167
|
+
|
|
168
|
+
# Try to find existing user by OAuth ID
|
|
169
|
+
user = user_service.get_by_oauth("google", google_id)
|
|
170
|
+
|
|
171
|
+
if not user:
|
|
172
|
+
# Try to find by email (link existing account)
|
|
173
|
+
user = user_service.get_by_email(email)
|
|
174
|
+
if user:
|
|
175
|
+
# Link OAuth to existing account
|
|
176
|
+
user = user_service.link_oauth(user.id, "google", google_id)
|
|
177
|
+
else:
|
|
178
|
+
# Create new user
|
|
179
|
+
user = user_service.create_oauth_user(
|
|
180
|
+
email=email,
|
|
181
|
+
full_name=full_name,
|
|
182
|
+
oauth_provider="google",
|
|
183
|
+
oauth_id=google_id,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
access_token = create_access_token(subject=user.id)
|
|
187
|
+
refresh_token = create_refresh_token(subject=user.id)
|
|
188
|
+
|
|
189
|
+
# Redirect to frontend with tokens
|
|
190
|
+
params = urlencode({
|
|
191
|
+
"access_token": access_token,
|
|
192
|
+
"refresh_token": refresh_token,
|
|
193
|
+
})
|
|
194
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/auth/callback?{params}")
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
params = urlencode({"error": str(e)})
|
|
198
|
+
return RedirectResponse(url=f"{FRONTEND_URL}/login?{params}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
{%- endif %}
|
|
202
|
+
{%- endif %}
|
|
203
|
+
{%- else %}
|
|
204
|
+
"""OAuth routes - not configured."""
|
|
205
|
+
{%- endif %}
|