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,246 @@
|
|
|
1
|
+
{%- if cookiecutter.include_example_crud and cookiecutter.use_postgresql %}
|
|
2
|
+
"""Item service (PostgreSQL async).
|
|
3
|
+
|
|
4
|
+
Contains business logic for item operations. Uses ItemRepository for database access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.core.exceptions import NotFoundError
|
|
12
|
+
from app.db.models.item import Item
|
|
13
|
+
from app.repositories import item_repo
|
|
14
|
+
from app.schemas.item import ItemCreate, ItemUpdate
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ItemService:
|
|
18
|
+
"""Service for item-related business logic.
|
|
19
|
+
|
|
20
|
+
This is an example service demonstrating the service layer pattern.
|
|
21
|
+
Services contain business logic and use repositories for database operations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, db: AsyncSession):
|
|
25
|
+
self.db = db
|
|
26
|
+
|
|
27
|
+
async def get_by_id(self, item_id: UUID) -> Item:
|
|
28
|
+
"""Get item by ID.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
NotFoundError: If item does not exist.
|
|
32
|
+
"""
|
|
33
|
+
item = await item_repo.get_by_id(self.db, item_id)
|
|
34
|
+
if not item:
|
|
35
|
+
raise NotFoundError(
|
|
36
|
+
message="Item not found",
|
|
37
|
+
details={"item_id": str(item_id)},
|
|
38
|
+
)
|
|
39
|
+
return item
|
|
40
|
+
|
|
41
|
+
async def get_multi(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
skip: int = 0,
|
|
45
|
+
limit: int = 100,
|
|
46
|
+
active_only: bool = False,
|
|
47
|
+
) -> list[Item]:
|
|
48
|
+
"""Get multiple items with pagination."""
|
|
49
|
+
return await item_repo.get_multi(
|
|
50
|
+
self.db, skip=skip, limit=limit, active_only=active_only
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def create(self, item_in: ItemCreate) -> Item:
|
|
54
|
+
"""Create a new item."""
|
|
55
|
+
return await item_repo.create(
|
|
56
|
+
self.db,
|
|
57
|
+
title=item_in.title,
|
|
58
|
+
description=item_in.description,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def update(self, item_id: UUID, item_in: ItemUpdate) -> Item:
|
|
62
|
+
"""Update an item.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
NotFoundError: If item does not exist.
|
|
66
|
+
"""
|
|
67
|
+
item = await self.get_by_id(item_id)
|
|
68
|
+
update_data = item_in.model_dump(exclude_unset=True)
|
|
69
|
+
return await item_repo.update(self.db, db_item=item, update_data=update_data)
|
|
70
|
+
|
|
71
|
+
async def delete(self, item_id: UUID) -> Item:
|
|
72
|
+
"""Delete an item.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
NotFoundError: If item does not exist.
|
|
76
|
+
"""
|
|
77
|
+
item = await item_repo.delete(self.db, item_id)
|
|
78
|
+
if not item:
|
|
79
|
+
raise NotFoundError(
|
|
80
|
+
message="Item not found",
|
|
81
|
+
details={"item_id": str(item_id)},
|
|
82
|
+
)
|
|
83
|
+
return item
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
{%- elif cookiecutter.include_example_crud and cookiecutter.use_sqlite %}
|
|
87
|
+
"""Item service (SQLite sync).
|
|
88
|
+
|
|
89
|
+
Contains business logic for item operations. Uses ItemRepository for database access.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
from sqlalchemy.orm import Session
|
|
93
|
+
|
|
94
|
+
from app.core.exceptions import NotFoundError
|
|
95
|
+
from app.db.models.item import Item
|
|
96
|
+
from app.repositories import item_repo
|
|
97
|
+
from app.schemas.item import ItemCreate, ItemUpdate
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ItemService:
|
|
101
|
+
"""Service for item-related business logic.
|
|
102
|
+
|
|
103
|
+
This is an example service demonstrating the service layer pattern.
|
|
104
|
+
Services contain business logic and use repositories for database operations.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, db: Session):
|
|
108
|
+
self.db = db
|
|
109
|
+
|
|
110
|
+
def get_by_id(self, item_id: str) -> Item:
|
|
111
|
+
"""Get item by ID.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
NotFoundError: If item does not exist.
|
|
115
|
+
"""
|
|
116
|
+
item = item_repo.get_by_id(self.db, item_id)
|
|
117
|
+
if not item:
|
|
118
|
+
raise NotFoundError(
|
|
119
|
+
message="Item not found",
|
|
120
|
+
details={"item_id": item_id},
|
|
121
|
+
)
|
|
122
|
+
return item
|
|
123
|
+
|
|
124
|
+
def get_multi(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
skip: int = 0,
|
|
128
|
+
limit: int = 100,
|
|
129
|
+
active_only: bool = False,
|
|
130
|
+
) -> list[Item]:
|
|
131
|
+
"""Get multiple items with pagination."""
|
|
132
|
+
return item_repo.get_multi(
|
|
133
|
+
self.db, skip=skip, limit=limit, active_only=active_only
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def create(self, item_in: ItemCreate) -> Item:
|
|
137
|
+
"""Create a new item."""
|
|
138
|
+
return item_repo.create(
|
|
139
|
+
self.db,
|
|
140
|
+
title=item_in.title,
|
|
141
|
+
description=item_in.description,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def update(self, item_id: str, item_in: ItemUpdate) -> Item:
|
|
145
|
+
"""Update an item.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
NotFoundError: If item does not exist.
|
|
149
|
+
"""
|
|
150
|
+
item = self.get_by_id(item_id)
|
|
151
|
+
update_data = item_in.model_dump(exclude_unset=True)
|
|
152
|
+
return item_repo.update(self.db, db_item=item, update_data=update_data)
|
|
153
|
+
|
|
154
|
+
def delete(self, item_id: str) -> Item:
|
|
155
|
+
"""Delete an item.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
NotFoundError: If item does not exist.
|
|
159
|
+
"""
|
|
160
|
+
item = item_repo.delete(self.db, item_id)
|
|
161
|
+
if not item:
|
|
162
|
+
raise NotFoundError(
|
|
163
|
+
message="Item not found",
|
|
164
|
+
details={"item_id": item_id},
|
|
165
|
+
)
|
|
166
|
+
return item
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
{%- elif cookiecutter.include_example_crud and cookiecutter.use_mongodb %}
|
|
170
|
+
"""Item service (MongoDB).
|
|
171
|
+
|
|
172
|
+
Contains business logic for item operations. Uses ItemRepository for database access.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
from app.core.exceptions import NotFoundError
|
|
176
|
+
from app.db.models.item import Item
|
|
177
|
+
from app.repositories import item_repo
|
|
178
|
+
from app.schemas.item import ItemCreate, ItemUpdate
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ItemService:
|
|
182
|
+
"""Service for item-related business logic.
|
|
183
|
+
|
|
184
|
+
This is an example service demonstrating the service layer pattern.
|
|
185
|
+
Services contain business logic and use repositories for database operations.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
async def get_by_id(self, item_id: str) -> Item:
|
|
189
|
+
"""Get item by ID.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
NotFoundError: If item does not exist.
|
|
193
|
+
"""
|
|
194
|
+
item = await item_repo.get_by_id(item_id)
|
|
195
|
+
if not item:
|
|
196
|
+
raise NotFoundError(
|
|
197
|
+
message="Item not found",
|
|
198
|
+
details={"item_id": item_id},
|
|
199
|
+
)
|
|
200
|
+
return item
|
|
201
|
+
|
|
202
|
+
async def get_multi(
|
|
203
|
+
self,
|
|
204
|
+
*,
|
|
205
|
+
skip: int = 0,
|
|
206
|
+
limit: int = 100,
|
|
207
|
+
active_only: bool = False,
|
|
208
|
+
) -> list[Item]:
|
|
209
|
+
"""Get multiple items with pagination."""
|
|
210
|
+
return await item_repo.get_multi(skip=skip, limit=limit, active_only=active_only)
|
|
211
|
+
|
|
212
|
+
async def create(self, item_in: ItemCreate) -> Item:
|
|
213
|
+
"""Create a new item."""
|
|
214
|
+
return await item_repo.create(
|
|
215
|
+
title=item_in.title,
|
|
216
|
+
description=item_in.description,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def update(self, item_id: str, item_in: ItemUpdate) -> Item:
|
|
220
|
+
"""Update an item.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
NotFoundError: If item does not exist.
|
|
224
|
+
"""
|
|
225
|
+
item = await self.get_by_id(item_id)
|
|
226
|
+
update_data = item_in.model_dump(exclude_unset=True)
|
|
227
|
+
return await item_repo.update(db_item=item, update_data=update_data)
|
|
228
|
+
|
|
229
|
+
async def delete(self, item_id: str) -> Item:
|
|
230
|
+
"""Delete an item.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
NotFoundError: If item does not exist.
|
|
234
|
+
"""
|
|
235
|
+
item = await item_repo.delete(item_id)
|
|
236
|
+
if not item:
|
|
237
|
+
raise NotFoundError(
|
|
238
|
+
message="Item not found",
|
|
239
|
+
details={"item_id": item_id},
|
|
240
|
+
)
|
|
241
|
+
return item
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
{%- else %}
|
|
245
|
+
"""Item service - not configured."""
|
|
246
|
+
{%- endif %}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
{%- if cookiecutter.enable_session_management and cookiecutter.use_jwt %}
|
|
2
|
+
{%- if cookiecutter.use_postgresql %}
|
|
3
|
+
"""Session service (PostgreSQL async)."""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.core.config import settings
|
|
12
|
+
from app.core.exceptions import NotFoundError
|
|
13
|
+
from app.db.models.session import Session
|
|
14
|
+
from app.repositories import session_repo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _hash_token(token: str) -> str:
|
|
18
|
+
"""Hash a token for storage."""
|
|
19
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_user_agent(user_agent: str | None) -> tuple[str | None, str | None]:
|
|
23
|
+
"""Parse user agent to extract device name and type."""
|
|
24
|
+
if not user_agent:
|
|
25
|
+
return None, None
|
|
26
|
+
|
|
27
|
+
user_agent_lower = user_agent.lower()
|
|
28
|
+
|
|
29
|
+
# Detect device type
|
|
30
|
+
if "mobile" in user_agent_lower or "android" in user_agent_lower:
|
|
31
|
+
device_type = "mobile"
|
|
32
|
+
elif "tablet" in user_agent_lower or "ipad" in user_agent_lower:
|
|
33
|
+
device_type = "tablet"
|
|
34
|
+
else:
|
|
35
|
+
device_type = "desktop"
|
|
36
|
+
|
|
37
|
+
# Extract browser/device name
|
|
38
|
+
if "chrome" in user_agent_lower:
|
|
39
|
+
device_name = "Chrome"
|
|
40
|
+
elif "firefox" in user_agent_lower:
|
|
41
|
+
device_name = "Firefox"
|
|
42
|
+
elif "safari" in user_agent_lower:
|
|
43
|
+
device_name = "Safari"
|
|
44
|
+
elif "edge" in user_agent_lower:
|
|
45
|
+
device_name = "Edge"
|
|
46
|
+
else:
|
|
47
|
+
device_name = "Unknown Browser"
|
|
48
|
+
|
|
49
|
+
return device_name, device_type
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SessionService:
|
|
53
|
+
"""Service for session management."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, db: AsyncSession):
|
|
56
|
+
self.db = db
|
|
57
|
+
|
|
58
|
+
async def create_session(
|
|
59
|
+
self,
|
|
60
|
+
user_id: UUID,
|
|
61
|
+
refresh_token: str,
|
|
62
|
+
ip_address: str | None = None,
|
|
63
|
+
user_agent: str | None = None,
|
|
64
|
+
) -> Session:
|
|
65
|
+
"""Create a new session for a user."""
|
|
66
|
+
device_name, device_type = _parse_user_agent(user_agent)
|
|
67
|
+
expires_at = datetime.utcnow() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
68
|
+
|
|
69
|
+
return await session_repo.create(
|
|
70
|
+
self.db,
|
|
71
|
+
user_id=user_id,
|
|
72
|
+
refresh_token_hash=_hash_token(refresh_token),
|
|
73
|
+
expires_at=expires_at,
|
|
74
|
+
device_name=device_name,
|
|
75
|
+
device_type=device_type,
|
|
76
|
+
ip_address=ip_address,
|
|
77
|
+
user_agent=user_agent,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def get_user_sessions(self, user_id: UUID) -> list[Session]:
|
|
81
|
+
"""Get all active sessions for a user."""
|
|
82
|
+
return await session_repo.get_user_sessions(self.db, user_id, active_only=True)
|
|
83
|
+
|
|
84
|
+
async def validate_refresh_token(self, refresh_token: str) -> Session | None:
|
|
85
|
+
"""Validate a refresh token and return the session if valid."""
|
|
86
|
+
token_hash = _hash_token(refresh_token)
|
|
87
|
+
session = await session_repo.get_by_refresh_token_hash(self.db, token_hash)
|
|
88
|
+
|
|
89
|
+
if session and session.expires_at > datetime.utcnow():
|
|
90
|
+
await session_repo.update_last_used(self.db, session.id)
|
|
91
|
+
return session
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def logout_session(self, session_id: UUID, user_id: UUID) -> Session:
|
|
96
|
+
"""Logout a specific session."""
|
|
97
|
+
session = await session_repo.get_by_id(self.db, session_id)
|
|
98
|
+
if not session or session.user_id != user_id:
|
|
99
|
+
raise NotFoundError(message="Session not found")
|
|
100
|
+
|
|
101
|
+
await session_repo.deactivate(self.db, session_id)
|
|
102
|
+
return session
|
|
103
|
+
|
|
104
|
+
async def logout_all_sessions(self, user_id: UUID) -> int:
|
|
105
|
+
"""Logout all sessions for a user. Returns count of logged out sessions."""
|
|
106
|
+
return await session_repo.deactivate_all_user_sessions(self.db, user_id)
|
|
107
|
+
|
|
108
|
+
async def logout_by_refresh_token(self, refresh_token: str) -> Session | None:
|
|
109
|
+
"""Logout session by refresh token."""
|
|
110
|
+
token_hash = _hash_token(refresh_token)
|
|
111
|
+
return await session_repo.deactivate_by_refresh_token_hash(self.db, token_hash)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
{%- elif cookiecutter.use_sqlite %}
|
|
115
|
+
"""Session service (SQLite sync)."""
|
|
116
|
+
|
|
117
|
+
import hashlib
|
|
118
|
+
from datetime import datetime, timedelta
|
|
119
|
+
|
|
120
|
+
from sqlalchemy.orm import Session as DBSession
|
|
121
|
+
|
|
122
|
+
from app.core.config import settings
|
|
123
|
+
from app.core.exceptions import NotFoundError
|
|
124
|
+
from app.db.models.session import Session
|
|
125
|
+
from app.repositories import session_repo
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _hash_token(token: str) -> str:
|
|
129
|
+
"""Hash a token for storage."""
|
|
130
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_user_agent(user_agent: str | None) -> tuple[str | None, str | None]:
|
|
134
|
+
"""Parse user agent to extract device name and type."""
|
|
135
|
+
if not user_agent:
|
|
136
|
+
return None, None
|
|
137
|
+
|
|
138
|
+
user_agent_lower = user_agent.lower()
|
|
139
|
+
|
|
140
|
+
# Detect device type
|
|
141
|
+
if "mobile" in user_agent_lower or "android" in user_agent_lower:
|
|
142
|
+
device_type = "mobile"
|
|
143
|
+
elif "tablet" in user_agent_lower or "ipad" in user_agent_lower:
|
|
144
|
+
device_type = "tablet"
|
|
145
|
+
else:
|
|
146
|
+
device_type = "desktop"
|
|
147
|
+
|
|
148
|
+
# Extract browser/device name
|
|
149
|
+
if "chrome" in user_agent_lower:
|
|
150
|
+
device_name = "Chrome"
|
|
151
|
+
elif "firefox" in user_agent_lower:
|
|
152
|
+
device_name = "Firefox"
|
|
153
|
+
elif "safari" in user_agent_lower:
|
|
154
|
+
device_name = "Safari"
|
|
155
|
+
elif "edge" in user_agent_lower:
|
|
156
|
+
device_name = "Edge"
|
|
157
|
+
else:
|
|
158
|
+
device_name = "Unknown Browser"
|
|
159
|
+
|
|
160
|
+
return device_name, device_type
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class SessionService:
|
|
164
|
+
"""Service for session management."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, db: DBSession):
|
|
167
|
+
self.db = db
|
|
168
|
+
|
|
169
|
+
def create_session(
|
|
170
|
+
self,
|
|
171
|
+
user_id: str,
|
|
172
|
+
refresh_token: str,
|
|
173
|
+
ip_address: str | None = None,
|
|
174
|
+
user_agent: str | None = None,
|
|
175
|
+
) -> Session:
|
|
176
|
+
"""Create a new session for a user."""
|
|
177
|
+
device_name, device_type = _parse_user_agent(user_agent)
|
|
178
|
+
expires_at = datetime.utcnow() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
179
|
+
|
|
180
|
+
return session_repo.create(
|
|
181
|
+
self.db,
|
|
182
|
+
user_id=user_id,
|
|
183
|
+
refresh_token_hash=_hash_token(refresh_token),
|
|
184
|
+
expires_at=expires_at,
|
|
185
|
+
device_name=device_name,
|
|
186
|
+
device_type=device_type,
|
|
187
|
+
ip_address=ip_address,
|
|
188
|
+
user_agent=user_agent,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def get_user_sessions(self, user_id: str) -> list[Session]:
|
|
192
|
+
"""Get all active sessions for a user."""
|
|
193
|
+
return session_repo.get_user_sessions(self.db, user_id, active_only=True)
|
|
194
|
+
|
|
195
|
+
def validate_refresh_token(self, refresh_token: str) -> Session | None:
|
|
196
|
+
"""Validate a refresh token and return the session if valid."""
|
|
197
|
+
token_hash = _hash_token(refresh_token)
|
|
198
|
+
session = session_repo.get_by_refresh_token_hash(self.db, token_hash)
|
|
199
|
+
|
|
200
|
+
if session and session.expires_at > datetime.utcnow():
|
|
201
|
+
session_repo.update_last_used(self.db, session.id)
|
|
202
|
+
return session
|
|
203
|
+
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def logout_session(self, session_id: str, user_id: str) -> Session:
|
|
207
|
+
"""Logout a specific session."""
|
|
208
|
+
session = session_repo.get_by_id(self.db, session_id)
|
|
209
|
+
if not session or session.user_id != user_id:
|
|
210
|
+
raise NotFoundError(message="Session not found")
|
|
211
|
+
|
|
212
|
+
session_repo.deactivate(self.db, session_id)
|
|
213
|
+
return session
|
|
214
|
+
|
|
215
|
+
def logout_all_sessions(self, user_id: str) -> int:
|
|
216
|
+
"""Logout all sessions for a user. Returns count of logged out sessions."""
|
|
217
|
+
return session_repo.deactivate_all_user_sessions(self.db, user_id)
|
|
218
|
+
|
|
219
|
+
def logout_by_refresh_token(self, refresh_token: str) -> Session | None:
|
|
220
|
+
"""Logout session by refresh token."""
|
|
221
|
+
token_hash = _hash_token(refresh_token)
|
|
222
|
+
return session_repo.deactivate_by_refresh_token_hash(self.db, token_hash)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
{%- elif cookiecutter.use_mongodb %}
|
|
226
|
+
"""Session service (MongoDB)."""
|
|
227
|
+
|
|
228
|
+
import hashlib
|
|
229
|
+
from datetime import UTC, datetime, timedelta
|
|
230
|
+
|
|
231
|
+
from app.core.config import settings
|
|
232
|
+
from app.core.exceptions import NotFoundError
|
|
233
|
+
from app.db.models.session import Session
|
|
234
|
+
from app.repositories import session_repo
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _hash_token(token: str) -> str:
|
|
238
|
+
"""Hash a token for storage."""
|
|
239
|
+
return hashlib.sha256(token.encode()).hexdigest()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _parse_user_agent(user_agent: str | None) -> tuple[str | None, str | None]:
|
|
243
|
+
"""Parse user agent to extract device name and type."""
|
|
244
|
+
if not user_agent:
|
|
245
|
+
return None, None
|
|
246
|
+
|
|
247
|
+
user_agent_lower = user_agent.lower()
|
|
248
|
+
|
|
249
|
+
# Detect device type
|
|
250
|
+
if "mobile" in user_agent_lower or "android" in user_agent_lower:
|
|
251
|
+
device_type = "mobile"
|
|
252
|
+
elif "tablet" in user_agent_lower or "ipad" in user_agent_lower:
|
|
253
|
+
device_type = "tablet"
|
|
254
|
+
else:
|
|
255
|
+
device_type = "desktop"
|
|
256
|
+
|
|
257
|
+
# Extract browser/device name
|
|
258
|
+
if "chrome" in user_agent_lower:
|
|
259
|
+
device_name = "Chrome"
|
|
260
|
+
elif "firefox" in user_agent_lower:
|
|
261
|
+
device_name = "Firefox"
|
|
262
|
+
elif "safari" in user_agent_lower:
|
|
263
|
+
device_name = "Safari"
|
|
264
|
+
elif "edge" in user_agent_lower:
|
|
265
|
+
device_name = "Edge"
|
|
266
|
+
else:
|
|
267
|
+
device_name = "Unknown Browser"
|
|
268
|
+
|
|
269
|
+
return device_name, device_type
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class SessionService:
|
|
273
|
+
"""Service for session management."""
|
|
274
|
+
|
|
275
|
+
async def create_session(
|
|
276
|
+
self,
|
|
277
|
+
user_id: str,
|
|
278
|
+
refresh_token: str,
|
|
279
|
+
ip_address: str | None = None,
|
|
280
|
+
user_agent: str | None = None,
|
|
281
|
+
) -> Session:
|
|
282
|
+
"""Create a new session for a user."""
|
|
283
|
+
device_name, device_type = _parse_user_agent(user_agent)
|
|
284
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
|
285
|
+
|
|
286
|
+
return await session_repo.create(
|
|
287
|
+
user_id=user_id,
|
|
288
|
+
refresh_token_hash=_hash_token(refresh_token),
|
|
289
|
+
expires_at=expires_at,
|
|
290
|
+
device_name=device_name,
|
|
291
|
+
device_type=device_type,
|
|
292
|
+
ip_address=ip_address,
|
|
293
|
+
user_agent=user_agent,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def get_user_sessions(self, user_id: str) -> list[Session]:
|
|
297
|
+
"""Get all active sessions for a user."""
|
|
298
|
+
return await session_repo.get_user_sessions(user_id, active_only=True)
|
|
299
|
+
|
|
300
|
+
async def validate_refresh_token(self, refresh_token: str) -> Session | None:
|
|
301
|
+
"""Validate a refresh token and return the session if valid."""
|
|
302
|
+
token_hash = _hash_token(refresh_token)
|
|
303
|
+
session = await session_repo.get_by_refresh_token_hash(token_hash)
|
|
304
|
+
|
|
305
|
+
if session and session.expires_at > datetime.now(UTC):
|
|
306
|
+
await session_repo.update_last_used(str(session.id))
|
|
307
|
+
return session
|
|
308
|
+
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
async def logout_session(self, session_id: str, user_id: str) -> Session:
|
|
312
|
+
"""Logout a specific session."""
|
|
313
|
+
session = await session_repo.get_by_id(session_id)
|
|
314
|
+
if not session or session.user_id != user_id:
|
|
315
|
+
raise NotFoundError(message="Session not found")
|
|
316
|
+
|
|
317
|
+
await session_repo.deactivate(session_id)
|
|
318
|
+
return session
|
|
319
|
+
|
|
320
|
+
async def logout_all_sessions(self, user_id: str) -> int:
|
|
321
|
+
"""Logout all sessions for a user. Returns count of logged out sessions."""
|
|
322
|
+
return await session_repo.deactivate_all_user_sessions(user_id)
|
|
323
|
+
|
|
324
|
+
async def logout_by_refresh_token(self, refresh_token: str) -> Session | None:
|
|
325
|
+
"""Logout session by refresh token."""
|
|
326
|
+
token_hash = _hash_token(refresh_token)
|
|
327
|
+
return await session_repo.deactivate_by_refresh_token_hash(token_hash)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
{%- endif %}
|
|
331
|
+
{%- else %}
|
|
332
|
+
"""Session service - not configured."""
|
|
333
|
+
{%- endif %}
|