@weirdfingers/baseboards 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/dist/index.js +561 -469
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -5
  4. package/templates/README.md +0 -122
  5. package/templates/api/.env.example +0 -65
  6. package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
  7. package/templates/api/Dockerfile +0 -32
  8. package/templates/api/README.md +0 -264
  9. package/templates/api/alembic/env.py +0 -114
  10. package/templates/api/alembic/script.py.mako +0 -28
  11. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
  12. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
  13. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
  14. package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
  16. package/templates/api/alembic.ini +0 -36
  17. package/templates/api/config/generators.yaml +0 -237
  18. package/templates/api/config/storage_config.yaml +0 -26
  19. package/templates/api/docs/ADDING_GENERATORS.md +0 -409
  20. package/templates/api/docs/GENERATORS_API.md +0 -502
  21. package/templates/api/docs/MIGRATIONS.md +0 -472
  22. package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
  23. package/templates/api/docs/storage_providers.md +0 -337
  24. package/templates/api/pyproject.toml +0 -205
  25. package/templates/api/src/boards/__init__.py +0 -10
  26. package/templates/api/src/boards/api/app.py +0 -172
  27. package/templates/api/src/boards/api/auth.py +0 -75
  28. package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
  29. package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
  30. package/templates/api/src/boards/api/endpoints/setup.py +0 -505
  31. package/templates/api/src/boards/api/endpoints/sse.py +0 -129
  32. package/templates/api/src/boards/api/endpoints/storage.py +0 -155
  33. package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
  34. package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
  35. package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
  36. package/templates/api/src/boards/auth/__init__.py +0 -15
  37. package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
  38. package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
  39. package/templates/api/src/boards/auth/adapters/base.py +0 -73
  40. package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
  41. package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
  42. package/templates/api/src/boards/auth/adapters/none.py +0 -102
  43. package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
  44. package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
  45. package/templates/api/src/boards/auth/context.py +0 -35
  46. package/templates/api/src/boards/auth/factory.py +0 -129
  47. package/templates/api/src/boards/auth/middleware.py +0 -221
  48. package/templates/api/src/boards/auth/provisioning.py +0 -129
  49. package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
  50. package/templates/api/src/boards/cli.py +0 -354
  51. package/templates/api/src/boards/config.py +0 -131
  52. package/templates/api/src/boards/database/__init__.py +0 -7
  53. package/templates/api/src/boards/database/cli.py +0 -110
  54. package/templates/api/src/boards/database/connection.py +0 -292
  55. package/templates/api/src/boards/database/models.py +0 -19
  56. package/templates/api/src/boards/database/seed_data.py +0 -182
  57. package/templates/api/src/boards/dbmodels/__init__.py +0 -441
  58. package/templates/api/src/boards/generators/__init__.py +0 -57
  59. package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
  60. package/templates/api/src/boards/generators/artifacts.py +0 -53
  61. package/templates/api/src/boards/generators/base.py +0 -144
  62. package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
  63. package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
  64. package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
  65. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
  66. package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
  67. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
  68. package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
  69. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
  70. package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
  71. package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
  72. package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
  73. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
  74. package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
  75. package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
  76. package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
  77. package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
  78. package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
  79. package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
  80. package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
  81. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
  82. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
  83. package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
  84. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
  85. package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
  86. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
  87. package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
  88. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
  89. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
  90. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
  91. package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
  92. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
  93. package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
  94. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
  95. package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
  96. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
  97. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
  98. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
  99. package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
  100. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
  101. package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
  102. package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
  103. package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
  104. package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
  105. package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
  106. package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
  107. package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
  108. package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
  109. package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
  110. package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
  111. package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
  112. package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
  113. package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
  114. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
  115. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
  116. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
  117. package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
  118. package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
  119. package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
  120. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
  121. package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
  122. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
  123. package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
  124. package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
  125. package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
  126. package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
  127. package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
  128. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
  129. package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
  130. package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
  131. package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
  132. package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
  133. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
  134. package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
  135. package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
  136. package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
  137. package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
  138. package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
  139. package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
  140. package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
  141. package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
  142. package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
  143. package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
  144. package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
  145. package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
  146. package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
  147. package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
  148. package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
  149. package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
  150. package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
  151. package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
  152. package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
  153. package/templates/api/src/boards/generators/loader.py +0 -253
  154. package/templates/api/src/boards/generators/registry.py +0 -114
  155. package/templates/api/src/boards/generators/resolution.py +0 -632
  156. package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
  157. package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
  158. package/templates/api/src/boards/graphql/__init__.py +0 -7
  159. package/templates/api/src/boards/graphql/access_control.py +0 -136
  160. package/templates/api/src/boards/graphql/mutations/root.py +0 -148
  161. package/templates/api/src/boards/graphql/queries/root.py +0 -116
  162. package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
  163. package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
  164. package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
  165. package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
  166. package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
  167. package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
  168. package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
  169. package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
  170. package/templates/api/src/boards/graphql/schema.py +0 -81
  171. package/templates/api/src/boards/graphql/types/board.py +0 -102
  172. package/templates/api/src/boards/graphql/types/generation.py +0 -166
  173. package/templates/api/src/boards/graphql/types/generator.py +0 -17
  174. package/templates/api/src/boards/graphql/types/user.py +0 -47
  175. package/templates/api/src/boards/jobs/repository.py +0 -153
  176. package/templates/api/src/boards/logging.py +0 -195
  177. package/templates/api/src/boards/middleware.py +0 -339
  178. package/templates/api/src/boards/progress/__init__.py +0 -4
  179. package/templates/api/src/boards/progress/models.py +0 -25
  180. package/templates/api/src/boards/progress/publisher.py +0 -64
  181. package/templates/api/src/boards/py.typed +0 -0
  182. package/templates/api/src/boards/redis_pool.py +0 -118
  183. package/templates/api/src/boards/storage/__init__.py +0 -52
  184. package/templates/api/src/boards/storage/base.py +0 -363
  185. package/templates/api/src/boards/storage/config.py +0 -187
  186. package/templates/api/src/boards/storage/factory.py +0 -288
  187. package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
  188. package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
  189. package/templates/api/src/boards/storage/implementations/local.py +0 -201
  190. package/templates/api/src/boards/storage/implementations/s3.py +0 -294
  191. package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
  192. package/templates/api/src/boards/tenant_isolation.py +0 -446
  193. package/templates/api/src/boards/validation.py +0 -262
  194. package/templates/api/src/boards/workers/__init__.py +0 -1
  195. package/templates/api/src/boards/workers/actors.py +0 -274
  196. package/templates/api/src/boards/workers/cli.py +0 -125
  197. package/templates/api/src/boards/workers/context.py +0 -348
  198. package/templates/api/src/boards/workers/middleware.py +0 -58
  199. package/templates/api/src/py.typed +0 -0
  200. package/templates/compose.web.yaml +0 -35
  201. package/templates/compose.yaml +0 -116
  202. package/templates/docker/env.example +0 -23
  203. package/templates/web/.env.example +0 -28
  204. package/templates/web/Dockerfile +0 -51
  205. package/templates/web/components.json +0 -22
  206. package/templates/web/imageLoader.js +0 -18
  207. package/templates/web/next-env.d.ts +0 -5
  208. package/templates/web/next.config.js +0 -36
  209. package/templates/web/package.json +0 -41
  210. package/templates/web/postcss.config.mjs +0 -7
  211. package/templates/web/public/favicon.ico +0 -0
  212. package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
  213. package/templates/web/src/app/globals.css +0 -123
  214. package/templates/web/src/app/layout.tsx +0 -31
  215. package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
  216. package/templates/web/src/app/page.tsx +0 -35
  217. package/templates/web/src/app/providers.tsx +0 -18
  218. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
  219. package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
  220. package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
  221. package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
  222. package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
  223. package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
  224. package/templates/web/src/components/header.tsx +0 -32
  225. package/templates/web/src/components/theme-provider.tsx +0 -10
  226. package/templates/web/src/components/theme-toggle.tsx +0 -75
  227. package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
  228. package/templates/web/src/components/ui/button.tsx +0 -58
  229. package/templates/web/src/components/ui/card.tsx +0 -92
  230. package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
  231. package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
  232. package/templates/web/src/components/ui/toast.tsx +0 -128
  233. package/templates/web/src/components/ui/toaster.tsx +0 -35
  234. package/templates/web/src/components/ui/use-toast.ts +0 -187
  235. package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
  236. package/templates/web/src/lib/utils.ts +0 -6
  237. package/templates/web/tsconfig.json +0 -41
@@ -1,505 +0,0 @@
1
- """
2
- Setup endpoints for initial tenant configuration.
3
-
4
- These endpoints help with one-time setup for single-tenant deployments
5
- or initial configuration of multi-tenant environments.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from datetime import UTC
11
- from typing import Any
12
- from uuid import UUID
13
-
14
- from fastapi import APIRouter, HTTPException, Path
15
- from pydantic import BaseModel, Field
16
- from sqlalchemy import delete, select, update
17
-
18
- from ...config import settings
19
- from ...database.connection import get_async_session
20
- from ...database.seed_data import ensure_tenant, seed_tenant_with_data
21
- from ...dbmodels import Tenants
22
- from ...logging import get_logger
23
-
24
- logger = get_logger(__name__)
25
-
26
- router = APIRouter()
27
-
28
-
29
- class TenantSetupRequest(BaseModel):
30
- """Request model for tenant setup."""
31
-
32
- name: str = Field(..., min_length=1, max_length=255, description="Display name for the tenant")
33
- slug: str = Field(
34
- ...,
35
- min_length=1,
36
- max_length=255,
37
- pattern=r"^[a-z0-9-]+$",
38
- description="URL-safe slug for the tenant (lowercase, numbers, hyphens only)",
39
- )
40
- settings: dict[str, Any] = Field(
41
- default_factory=dict, description="Optional tenant-specific settings"
42
- )
43
- include_sample_data: bool = Field(
44
- default=False, description="Whether to include sample boards and data"
45
- )
46
-
47
-
48
- class TenantUpdateRequest(BaseModel):
49
- """Request model for tenant updates."""
50
-
51
- name: str | None = Field(
52
- default=None,
53
- min_length=1,
54
- max_length=255,
55
- description="Display name for the tenant",
56
- )
57
- slug: str | None = Field(
58
- None,
59
- min_length=1,
60
- max_length=255,
61
- pattern=r"^[a-z0-9-]+$",
62
- description="URL-safe slug for the tenant (lowercase, numbers, hyphens only)",
63
- )
64
- settings: dict[str, Any] | None = Field(None, description="Tenant-specific settings")
65
-
66
-
67
- class TenantResponse(BaseModel):
68
- """Response model for tenant operations."""
69
-
70
- tenant_id: str = Field(..., description="UUID of the tenant")
71
- name: str = Field(..., description="Display name of the tenant")
72
- slug: str = Field(..., description="Slug of the tenant")
73
- settings: dict[str, Any] = Field(..., description="Tenant-specific settings")
74
- created_at: str = Field(..., description="Creation timestamp")
75
- updated_at: str = Field(..., description="Last update timestamp")
76
-
77
-
78
- class TenantSetupResponse(BaseModel):
79
- """Response model for tenant setup."""
80
-
81
- tenant_id: str = Field(..., description="UUID of the created tenant")
82
- name: str = Field(..., description="Display name of the tenant")
83
- slug: str = Field(..., description="Slug of the tenant")
84
- message: str = Field(..., description="Success message")
85
- existing: bool = Field(..., description="Whether tenant already existed")
86
-
87
-
88
- class TenantListResponse(BaseModel):
89
- """Response model for tenant list."""
90
-
91
- tenants: list[TenantResponse] = Field(..., description="List of tenants")
92
- total_count: int = Field(..., description="Total number of tenants")
93
-
94
-
95
- @router.post("/tenant", response_model=TenantSetupResponse)
96
- async def setup_tenant(request: TenantSetupRequest) -> TenantSetupResponse:
97
- """
98
- Create or configure a tenant for initial setup.
99
-
100
- This endpoint is useful for:
101
- - Single-tenant initial setup
102
- - Creating new tenants in multi-tenant mode
103
- - Demo/development tenant creation
104
-
105
- In single-tenant mode, this is typically called once during deployment.
106
- In multi-tenant mode, this can be used for admin tenant creation.
107
- """
108
- logger.info(
109
- "Setting up tenant",
110
- name=request.name,
111
- slug=request.slug,
112
- include_sample_data=request.include_sample_data,
113
- )
114
-
115
- try:
116
- async with get_async_session() as db:
117
- # Check if tenant already exists
118
- existing_tenant_id = await ensure_tenant(
119
- db,
120
- name=request.name,
121
- slug=request.slug,
122
- settings_dict=request.settings,
123
- )
124
-
125
- # For now, we'll consider it "existing" if the ensure_tenant call
126
- # found an existing tenant. We could enhance this by checking
127
- # if the tenant was created or updated.
128
- try:
129
- # Try to create with sample data if requested
130
- if request.include_sample_data:
131
- tenant_id = await seed_tenant_with_data(
132
- db,
133
- tenant_name=request.name,
134
- tenant_slug=request.slug,
135
- tenant_settings=request.settings,
136
- include_sample_data=True,
137
- )
138
- existing = False # seed_tenant_with_data creates new tenant
139
- else:
140
- tenant_id = existing_tenant_id
141
- existing = True # assume it existed (could be enhanced)
142
-
143
- except Exception as e:
144
- # If seeding fails, we still have the basic tenant
145
- logger.warning(
146
- "Sample data creation failed, but tenant was created",
147
- error=str(e),
148
- tenant_id=str(existing_tenant_id),
149
- )
150
- tenant_id = existing_tenant_id
151
- existing = True
152
-
153
- logger.info(
154
- "Tenant setup completed",
155
- tenant_id=str(tenant_id),
156
- name=request.name,
157
- slug=request.slug,
158
- existing=existing,
159
- )
160
-
161
- return TenantSetupResponse(
162
- tenant_id=str(tenant_id),
163
- name=request.name,
164
- slug=request.slug,
165
- message=f"Tenant {'configured' if existing else 'created'} successfully",
166
- existing=existing,
167
- )
168
-
169
- except Exception as e:
170
- logger.error("Tenant setup failed", error=str(e))
171
- raise HTTPException(status_code=500, detail=f"Failed to setup tenant: {str(e)}") from e
172
-
173
-
174
- @router.get("/tenant/{tenant_id}", response_model=TenantResponse)
175
- async def get_tenant(
176
- tenant_id: UUID = Path(..., description="UUID of the tenant to retrieve"),
177
- ) -> TenantResponse:
178
- """
179
- Get a specific tenant by ID.
180
-
181
- This endpoint retrieves detailed information about a tenant including
182
- its settings, creation date, and other metadata.
183
- """
184
- logger.info("Retrieving tenant", tenant_id=str(tenant_id))
185
-
186
- try:
187
- async with get_async_session() as db:
188
- # Query for the tenant
189
- stmt = select(Tenants).where(Tenants.id == tenant_id)
190
- result = await db.execute(stmt)
191
- tenant = result.scalar_one_or_none()
192
-
193
- if not tenant:
194
- raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
195
-
196
- logger.info(
197
- "Tenant retrieved successfully",
198
- tenant_id=str(tenant_id),
199
- slug=tenant.slug,
200
- )
201
-
202
- return TenantResponse(
203
- tenant_id=str(tenant.id),
204
- name=tenant.name,
205
- slug=tenant.slug,
206
- settings=tenant.settings or {},
207
- created_at=tenant.created_at.isoformat() if tenant.created_at else "",
208
- updated_at=tenant.updated_at.isoformat() if tenant.updated_at else "",
209
- )
210
-
211
- except HTTPException:
212
- # Re-raise HTTP exceptions as-is
213
- raise
214
- except Exception as e:
215
- logger.error("Failed to retrieve tenant", tenant_id=str(tenant_id), error=str(e))
216
- raise HTTPException(status_code=500, detail=f"Failed to retrieve tenant: {str(e)}") from e
217
-
218
-
219
- @router.get("/tenants", response_model=TenantListResponse)
220
- async def list_tenants() -> TenantListResponse:
221
- """
222
- List all tenants in the system.
223
-
224
- This endpoint is useful for:
225
- - Multi-tenant administration
226
- - Tenant discovery and management
227
- - System overview and monitoring
228
- """
229
- logger.info("Listing all tenants")
230
-
231
- try:
232
- async with get_async_session() as db:
233
- # Query for all tenants, ordered by creation date
234
- stmt = select(Tenants).order_by(Tenants.created_at.desc())
235
- result = await db.execute(stmt)
236
- tenants = result.scalars().all()
237
-
238
- tenant_responses = [
239
- TenantResponse(
240
- tenant_id=str(tenant.id),
241
- name=tenant.name,
242
- slug=tenant.slug,
243
- settings=tenant.settings or {},
244
- created_at=(tenant.created_at.isoformat() if tenant.created_at else ""),
245
- updated_at=(tenant.updated_at.isoformat() if tenant.updated_at else ""),
246
- )
247
- for tenant in tenants
248
- ]
249
-
250
- logger.info(
251
- "Tenants listed successfully",
252
- total_count=len(tenant_responses),
253
- )
254
-
255
- return TenantListResponse(
256
- tenants=tenant_responses,
257
- total_count=len(tenant_responses),
258
- )
259
-
260
- except Exception as e:
261
- logger.error("Failed to list tenants", error=str(e))
262
- raise HTTPException(status_code=500, detail=f"Failed to list tenants: {str(e)}") from e
263
-
264
-
265
- @router.put("/tenant/{tenant_id}", response_model=TenantResponse)
266
- async def update_tenant(
267
- request: TenantUpdateRequest,
268
- tenant_id: UUID = Path(..., description="UUID of the tenant to update"),
269
- ) -> TenantResponse:
270
- """
271
- Update a specific tenant.
272
-
273
- This endpoint allows updating tenant information including:
274
- - Display name
275
- - Slug (URL identifier)
276
- - Settings (JSON metadata)
277
-
278
- Only provided fields will be updated; others remain unchanged.
279
- """
280
- logger.info(
281
- "Updating tenant",
282
- tenant_id=str(tenant_id),
283
- name=request.name,
284
- slug=request.slug,
285
- )
286
-
287
- try:
288
- async with get_async_session() as db:
289
- # First, check if tenant exists
290
- stmt = select(Tenants).where(Tenants.id == tenant_id)
291
- result = await db.execute(stmt)
292
- existing_tenant = result.scalar_one_or_none()
293
-
294
- if not existing_tenant:
295
- raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
296
-
297
- # Check for slug conflicts if slug is being updated
298
- if request.slug and request.slug != existing_tenant.slug:
299
- slug_check = select(Tenants).where(
300
- (Tenants.slug == request.slug) & (Tenants.id != tenant_id)
301
- )
302
- result = await db.execute(slug_check)
303
- if result.scalar_one_or_none():
304
- raise HTTPException(
305
- status_code=409,
306
- detail=f"A tenant with slug '{request.slug}' already exists",
307
- )
308
-
309
- # Build update data - only include provided fields
310
- update_data = {}
311
- if request.name is not None:
312
- update_data["name"] = request.name
313
- if request.slug is not None:
314
- update_data["slug"] = request.slug
315
- if request.settings is not None:
316
- update_data["settings"] = request.settings
317
-
318
- # Add updated_at timestamp
319
- from datetime import datetime
320
-
321
- update_data["updated_at"] = datetime.now(UTC)
322
-
323
- if update_data:
324
- # Perform the update
325
- stmt = update(Tenants).where(Tenants.id == tenant_id).values(**update_data)
326
- await db.execute(stmt)
327
- await db.commit()
328
-
329
- # Fetch the updated tenant
330
- stmt = select(Tenants).where(Tenants.id == tenant_id)
331
- result = await db.execute(stmt)
332
- updated_tenant = result.scalar_one()
333
-
334
- logger.info(
335
- "Tenant updated successfully",
336
- tenant_id=str(tenant_id),
337
- name=updated_tenant.name,
338
- slug=updated_tenant.slug,
339
- )
340
-
341
- return TenantResponse(
342
- tenant_id=str(updated_tenant.id),
343
- name=updated_tenant.name,
344
- slug=updated_tenant.slug,
345
- settings=updated_tenant.settings or {},
346
- created_at=(
347
- updated_tenant.created_at.isoformat() if updated_tenant.created_at else ""
348
- ),
349
- updated_at=(
350
- updated_tenant.updated_at.isoformat() if updated_tenant.updated_at else ""
351
- ),
352
- )
353
-
354
- except HTTPException:
355
- # Re-raise HTTP exceptions as-is
356
- raise
357
- except Exception as e:
358
- logger.error("Failed to update tenant", tenant_id=str(tenant_id), error=str(e))
359
- raise HTTPException(status_code=500, detail=f"Failed to update tenant: {str(e)}") from e
360
-
361
-
362
- @router.delete("/tenant/{tenant_id}")
363
- async def delete_tenant(
364
- tenant_id: UUID = Path(..., description="UUID of the tenant to delete"),
365
- ) -> dict[str, Any]:
366
- """
367
- Delete a specific tenant.
368
-
369
- **WARNING**: This operation is destructive and will cascade delete all related data:
370
- - All users in this tenant
371
- - All boards and their content
372
- - All generations and media
373
- - All provider configurations
374
- - All credit transactions
375
-
376
- This operation cannot be undone. Use with extreme caution.
377
- """
378
- logger.warning(
379
- "Attempting to delete tenant - this is a destructive operation",
380
- tenant_id=str(tenant_id),
381
- )
382
-
383
- try:
384
- async with get_async_session() as db:
385
- # First, check if tenant exists and get its info for logging
386
- stmt = select(Tenants).where(Tenants.id == tenant_id)
387
- result = await db.execute(stmt)
388
- existing_tenant = result.scalar_one_or_none()
389
-
390
- if not existing_tenant:
391
- raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
392
-
393
- # Prevent deletion of default tenant in single-tenant mode
394
- if (
395
- not settings.multi_tenant_mode
396
- and existing_tenant.slug == settings.default_tenant_slug
397
- ):
398
- raise HTTPException(
399
- status_code=400,
400
- detail="Cannot delete the default tenant in single-tenant mode. "
401
- "Enable multi-tenant mode or use a different default tenant first.",
402
- )
403
-
404
- tenant_name = existing_tenant.name
405
- tenant_slug = existing_tenant.slug
406
-
407
- # Perform the deletion (CASCADE will handle related records)
408
- stmt = delete(Tenants).where(Tenants.id == tenant_id)
409
- result = await db.execute(stmt)
410
- await db.commit()
411
-
412
- if result.rowcount == 0:
413
- # This shouldn't happen since we checked existence above
414
- raise HTTPException(status_code=404, detail=f"Tenant with ID {tenant_id} not found")
415
-
416
- logger.warning(
417
- "Tenant deleted successfully - all related data has been removed",
418
- tenant_id=str(tenant_id),
419
- name=tenant_name,
420
- slug=tenant_slug,
421
- deleted_records=result.rowcount,
422
- )
423
-
424
- return {
425
- "message": f"Tenant '{tenant_name}' ({tenant_slug}) deleted successfully",
426
- "tenant_id": str(tenant_id),
427
- "warning": "All related data (users, boards, generations, etc.) has been permanently deleted", # noqa: E501
428
- }
429
-
430
- except HTTPException:
431
- # Re-raise HTTP exceptions as-is
432
- raise
433
- except Exception as e:
434
- logger.error("Failed to delete tenant", tenant_id=str(tenant_id), error=str(e))
435
- raise HTTPException(status_code=500, detail=f"Failed to delete tenant: {str(e)}") from e
436
-
437
-
438
- @router.get("/status")
439
- async def setup_status() -> dict[str, Any]:
440
- """
441
- Get the current setup status of the application.
442
-
443
- This endpoint provides information about:
444
- - Whether a default tenant exists
445
- - Current configuration mode
446
- - Setup recommendations
447
- """
448
- try:
449
- async with get_async_session() as db:
450
- # Check if default tenant exists
451
- try:
452
- default_tenant_id = await ensure_tenant(db, slug=settings.default_tenant_slug)
453
- has_default_tenant = True
454
- default_tenant_uuid = str(default_tenant_id)
455
- except Exception:
456
- has_default_tenant = False
457
- default_tenant_uuid = None
458
-
459
- setup_needed = not has_default_tenant and not settings.multi_tenant_mode
460
-
461
- return {
462
- "setup_needed": setup_needed,
463
- "has_default_tenant": has_default_tenant,
464
- "default_tenant_id": default_tenant_uuid,
465
- "default_tenant_slug": settings.default_tenant_slug,
466
- "multi_tenant_mode": settings.multi_tenant_mode,
467
- "auth_provider": settings.auth_provider,
468
- "recommendations": _get_setup_recommendations(
469
- setup_needed, has_default_tenant, settings.multi_tenant_mode
470
- ),
471
- }
472
-
473
- except Exception as e:
474
- logger.error("Failed to get setup status", error=str(e))
475
- raise HTTPException(status_code=500, detail=f"Failed to get setup status: {str(e)}") from e
476
-
477
-
478
- def _get_setup_recommendations(
479
- setup_needed: bool, has_default_tenant: bool, multi_tenant_mode: bool
480
- ) -> list[str]:
481
- """Get setup recommendations based on current state."""
482
- recommendations = []
483
-
484
- if setup_needed:
485
- recommendations.append(
486
- f"Create a default tenant using POST /api/setup/tenant with slug '{settings.default_tenant_slug}'" # noqa: E501
487
- )
488
-
489
- if not has_default_tenant and not multi_tenant_mode:
490
- recommendations.append(
491
- "Run database migrations to create the default tenant: 'alembic upgrade head'"
492
- )
493
-
494
- if settings.auth_provider == "none" and not multi_tenant_mode:
495
- recommendations.append(
496
- "Consider configuring a proper authentication provider for production use"
497
- )
498
-
499
- if not recommendations:
500
- if multi_tenant_mode:
501
- recommendations.append("System is ready for multi-tenant operation")
502
- else:
503
- recommendations.append("Single-tenant setup is complete and ready to use")
504
-
505
- return recommendations
@@ -1,129 +0,0 @@
1
- """
2
- Server-Sent Events (SSE) endpoints for real-time generation progress.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- import asyncio
8
-
9
- from fastapi import APIRouter, Depends, HTTPException, Request
10
- from fastapi.responses import StreamingResponse
11
- from sqlalchemy.ext.asyncio import AsyncSession
12
-
13
- from ...config import Settings
14
- from ...database.connection import get_db_session
15
- from ...jobs import repository as jobs_repo
16
- from ...logging import get_logger
17
- from ...redis_pool import get_redis_client
18
- from ..auth import AuthenticatedUser, get_current_user
19
-
20
- logger = get_logger(__name__)
21
-
22
-
23
- router = APIRouter()
24
- _settings = Settings()
25
- # Use the shared Redis connection pool
26
- _redis = get_redis_client()
27
-
28
-
29
- @router.get("/generations/{generation_id}/progress")
30
- async def generation_progress_stream(
31
- generation_id: str,
32
- request: Request,
33
- db: AsyncSession = Depends(get_db_session),
34
- current_user: AuthenticatedUser = Depends(get_current_user),
35
- ):
36
- """Server-sent events for job progress, backed by Redis pub/sub.
37
-
38
- Requires authentication. Users can only monitor progress for their own generations
39
- or generations within their tenant (depending on access control policy).
40
- """
41
-
42
- logger.info(
43
- "SSE: generation progress stream requested",
44
- generation_id=generation_id,
45
- url=request.url,
46
- )
47
- # Verify user has access to this generation
48
- try:
49
- generation = await jobs_repo.get_generation(db, generation_id)
50
-
51
- # Check if user owns this generation or belongs to the same tenant
52
- if generation.user_id != current_user.user_id:
53
- # Allow tenant-level access (users in same tenant can see each other's jobs)
54
- # You may want to make this more restrictive based on your requirements
55
- if generation.tenant_id != current_user.tenant_id:
56
- logger.warning(
57
- "User attempted to access generation belonging to different user",
58
- user_id=str(current_user.user_id),
59
- generation_id=generation_id,
60
- owner_user_id=str(generation.user_id),
61
- )
62
- raise HTTPException(
63
- status_code=403,
64
- detail="You don't have permission to access this generation",
65
- )
66
-
67
- logger.info(
68
- "User connected to progress stream",
69
- user_id=str(current_user.user_id),
70
- generation_id=generation_id,
71
- )
72
-
73
- except HTTPException:
74
- raise
75
- except Exception as e:
76
- logger.error(
77
- "Failed to verify access to generation",
78
- generation_id=generation_id,
79
- error=str(e),
80
- )
81
- raise HTTPException(status_code=404, detail="Generation not found") from e
82
-
83
- channel = f"job:{generation_id}:progress"
84
-
85
- async def event_stream():
86
- pubsub = _redis.pubsub()
87
- await pubsub.subscribe(channel)
88
- logger.info(
89
- "SSE: Subscribed to Redis channel",
90
- generation_id=generation_id,
91
- channel=channel,
92
- )
93
- try:
94
- while True:
95
- if await request.is_disconnected():
96
- logger.info(
97
- "Client disconnected from progress stream",
98
- generation_id=generation_id,
99
- )
100
- break
101
- msg = await pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
102
- if msg:
103
- logger.info(
104
- "SSE: pubsub message received",
105
- message=msg,
106
- msg_type=msg.get("type"),
107
- )
108
- if msg and msg.get("type") == "message":
109
- data = msg["data"]
110
- logger.info(
111
- "SSE: sending progress data to client",
112
- generation_id=generation_id,
113
- data_preview=(data[:100] if isinstance(data, str) else str(data)[:100]),
114
- )
115
- yield f"data: {data}\n\n"
116
- else:
117
- # Send keep-alive every 15 seconds to prevent timeout
118
- await asyncio.sleep(15)
119
- logger.debug("SSE: sending keep-alive", generation_id=generation_id)
120
- yield ": keep-alive\n\n"
121
- finally:
122
- logger.info(
123
- "SSE: Cleaning up stream",
124
- generation_id=generation_id,
125
- )
126
- await pubsub.unsubscribe(channel)
127
- await pubsub.close()
128
-
129
- return StreamingResponse(event_stream(), media_type="text/event-stream")