@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,218 +0,0 @@
1
- """Supabase storage provider with integrated auth and CDN support."""
2
-
3
- import os
4
- import tempfile
5
- from collections.abc import AsyncIterator
6
- from datetime import UTC, datetime, timedelta
7
- from typing import TYPE_CHECKING, Any
8
-
9
- import aiofiles
10
-
11
- if TYPE_CHECKING:
12
- from supabase import AsyncClient, create_async_client
13
-
14
- try:
15
- from supabase import AsyncClient, create_async_client
16
-
17
- _supabase_available = True
18
- except ImportError:
19
- # Handle case where supabase is not installed
20
- create_async_client = None
21
- # AsyncClient = None
22
- _supabase_available = False
23
-
24
- from ...logging import get_logger
25
- from ..base import StorageException, StorageProvider
26
-
27
- logger = get_logger(__name__)
28
-
29
-
30
- class SupabaseStorageProvider(StorageProvider):
31
- """Supabase storage with integrated auth, CDN, and proper async patterns."""
32
-
33
- def __init__(self, url: str, key: str, bucket: str):
34
- if not _supabase_available:
35
- raise ImportError("supabase-py is required for SupabaseStorageProvider")
36
-
37
- self.url = url
38
- self.key = key
39
- self.bucket = bucket
40
- self._client: AsyncClient | None = None
41
-
42
- async def _get_client(self) -> "AsyncClient":
43
- """Get or create the async Supabase client."""
44
- if self._client is None:
45
- if create_async_client is None:
46
- raise ImportError("Async Supabase client not available")
47
- self._client = await create_async_client(self.url, self.key)
48
- return self._client
49
-
50
- async def upload(
51
- self,
52
- key: str,
53
- content: bytes | AsyncIterator[bytes],
54
- content_type: str,
55
- metadata: dict[str, Any] | None = None,
56
- ) -> str:
57
- try:
58
- client = await self._get_client()
59
-
60
- # Handle streaming content for large files
61
- if isinstance(content, bytes):
62
- file_content = content
63
- else:
64
- # Stream to temp file to avoid memory issues
65
- with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
66
- tmp_file_path = tmp_file.name
67
-
68
- # Use async file operations for streaming content
69
- async with aiofiles.open(tmp_file_path, "wb") as f:
70
- async for chunk in content:
71
- await f.write(chunk)
72
-
73
- # Read the temp file asynchronously and upload
74
- async with aiofiles.open(tmp_file_path, "rb") as f:
75
- file_content = await f.read()
76
-
77
- # Clean up temp file
78
- os.unlink(tmp_file_path)
79
-
80
- # Use async Supabase client methods
81
- response = await client.storage.from_(self.bucket).upload(
82
- path=key,
83
- file=file_content,
84
- file_options={
85
- "content-type": content_type,
86
- "upsert": "false", # Prevent accidental overwrites
87
- },
88
- )
89
-
90
- return response.path
91
-
92
- except Exception as e:
93
- if isinstance(e, StorageException):
94
- raise
95
- logger.error(f"Unexpected error uploading {key} to Supabase: {e}")
96
- raise StorageException(f"Supabase upload failed: {e}") from e
97
-
98
- async def download(self, key: str) -> bytes:
99
- """Download file content from Supabase storage."""
100
- try:
101
- client = await self._get_client()
102
- response = await client.storage.from_(self.bucket).download(key)
103
-
104
- return response
105
-
106
- except Exception as e:
107
- if isinstance(e, StorageException):
108
- raise
109
- logger.error(f"Failed to download {key} from Supabase: {e}")
110
- raise StorageException(f"Download failed: {e}") from e
111
-
112
- async def get_presigned_upload_url(
113
- self,
114
- key: str,
115
- content_type: str,
116
- expires_in: timedelta | None = None,
117
- ) -> dict[str, Any]:
118
- """Generate presigned URL for direct client uploads."""
119
- if expires_in is None:
120
- expires_in = timedelta(hours=1)
121
-
122
- try:
123
- client = await self._get_client()
124
- response = await client.storage.from_(self.bucket).create_signed_upload_url(path=key)
125
-
126
- return {
127
- "url": response["signed_url"],
128
- "fields": {}, # Supabase doesn't use form fields like S3
129
- "expires_at": (datetime.now(UTC) + expires_in).isoformat(),
130
- }
131
- except Exception as e:
132
- if isinstance(e, StorageException):
133
- raise
134
- logger.error(f"Failed to create presigned upload URL for {key}: {e}")
135
- raise StorageException(f"Presigned URL creation failed: {e}") from e
136
-
137
- async def get_presigned_download_url(
138
- self, key: str, expires_in: timedelta | None = None
139
- ) -> str:
140
- """Generate presigned URL for secure downloads."""
141
- if expires_in is None:
142
- expires_in = timedelta(hours=1)
143
-
144
- try:
145
- client = await self._get_client()
146
- response = await client.storage.from_(self.bucket).create_signed_url(
147
- path=key, expires_in=int(expires_in.total_seconds())
148
- )
149
-
150
- return response["signedURL"]
151
-
152
- except Exception as e:
153
- if isinstance(e, StorageException):
154
- raise
155
- logger.error(f"Failed to create presigned download URL for {key}: {e}")
156
- raise StorageException(f"Presigned download URL creation failed: {e}") from e
157
-
158
- async def delete(self, key: str) -> bool:
159
- """Delete file by storage key."""
160
- try:
161
- client = await self._get_client()
162
- await client.storage.from_(self.bucket).remove([key]) # type: ignore[reportUnknownMemberType]
163
-
164
- return True
165
-
166
- except Exception as e:
167
- logger.error(f"Unexpected error deleting {key} from Supabase: {e}")
168
- raise StorageException(f"Delete failed: {e}") from e
169
-
170
- async def exists(self, key: str) -> bool:
171
- """Check if file exists."""
172
- try:
173
- client = await self._get_client()
174
- # Try to get file info - if it doesn't exist, this will error
175
- await client.storage.from_(self.bucket).get_public_url(key)
176
- # If we get here without error, the file exists
177
- return True
178
- except Exception:
179
- # Any error means the file doesn't exist or we can't access it
180
- return False
181
-
182
- async def get_metadata(self, key: str) -> dict[str, Any]:
183
- """Get file metadata (size, modified date, etc.)."""
184
- try:
185
- client = await self._get_client()
186
- # Supabase doesn't have a direct metadata endpoint
187
- # We'll need to use the list method with a prefix
188
- response = await client.storage.from_(self.bucket).list(
189
- path="/".join(key.split("/")[:-1]) or "/"
190
- )
191
-
192
- # Find our file in the results
193
- file_info = None
194
- filename = key.split("/")[-1]
195
- for item in response:
196
- if item.get("name") == filename:
197
- file_info = item
198
- break
199
-
200
- if not file_info:
201
- raise StorageException(f"File not found: {key}")
202
-
203
- metadata = file_info.get("metadata", {})
204
- result = {
205
- "size": file_info.get("size", 0),
206
- "last_modified": file_info.get("updated_at"),
207
- "content_type": file_info.get("mimetype"),
208
- "etag": file_info.get("id"),
209
- }
210
- if isinstance(metadata, dict):
211
- result.update(metadata)
212
- return result
213
-
214
- except Exception as e:
215
- if isinstance(e, StorageException):
216
- raise
217
- logger.error(f"Failed to get metadata for {key} from Supabase: {e}")
218
- raise StorageException(f"Get metadata failed: {e}") from e
@@ -1,446 +0,0 @@
1
- """
2
- Tenant isolation validation and enforcement utilities.
3
-
4
- This module provides utilities to validate and enforce tenant isolation
5
- across the application to ensure data security in multi-tenant deployments.
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 sqlalchemy import select, text
15
- from sqlalchemy.ext.asyncio import AsyncSession
16
-
17
- from .config import settings
18
- from .dbmodels import Boards, Generations, Users
19
- from .logging import get_logger
20
-
21
- logger = get_logger(__name__)
22
-
23
-
24
- class TenantIsolationError(Exception):
25
- """Raised when tenant isolation validation fails."""
26
-
27
- pass
28
-
29
-
30
- class TenantIsolationValidator:
31
- """
32
- Utility class for validating tenant isolation in multi-tenant environments.
33
-
34
- This class provides methods to:
35
- 1. Validate tenant-scoped queries
36
- 2. Check for cross-tenant data access
37
- 3. Ensure proper tenant filtering
38
- 4. Audit tenant isolation compliance
39
- """
40
-
41
- def __init__(self, db: AsyncSession):
42
- self.db = db
43
-
44
- async def validate_user_tenant_isolation(self, user_id: UUID, tenant_id: UUID) -> bool:
45
- """
46
- Validate that a user belongs to the specified tenant.
47
-
48
- Args:
49
- user_id: UUID of the user
50
- tenant_id: UUID of the tenant
51
-
52
- Returns:
53
- True if user belongs to tenant, False otherwise
54
-
55
- Raises:
56
- TenantIsolationError: If validation fails
57
- """
58
- try:
59
- stmt = select(Users).where((Users.id == user_id) & (Users.tenant_id == tenant_id))
60
- result = await self.db.execute(stmt)
61
- user = result.scalar_one_or_none()
62
-
63
- if not user:
64
- logger.warning(
65
- "User tenant isolation violation",
66
- user_id=str(user_id),
67
- expected_tenant=str(tenant_id),
68
- )
69
- return False
70
-
71
- return True
72
-
73
- except Exception as e:
74
- logger.error(
75
- "User tenant isolation validation failed",
76
- user_id=str(user_id),
77
- tenant_id=str(tenant_id),
78
- error=str(e),
79
- )
80
- raise TenantIsolationError(f"User tenant validation failed: {e}") from e
81
-
82
- async def validate_board_tenant_isolation(self, board_id: UUID, tenant_id: UUID) -> bool:
83
- """
84
- Validate that a board belongs to the specified tenant.
85
-
86
- Args:
87
- board_id: UUID of the board
88
- tenant_id: UUID of the tenant
89
-
90
- Returns:
91
- True if board belongs to tenant, False otherwise
92
- """
93
- try:
94
- stmt = select(Boards).where((Boards.id == board_id) & (Boards.tenant_id == tenant_id))
95
- result = await self.db.execute(stmt)
96
- board = result.scalar_one_or_none()
97
-
98
- if not board:
99
- logger.warning(
100
- "Board tenant isolation violation",
101
- board_id=str(board_id),
102
- expected_tenant=str(tenant_id),
103
- )
104
- return False
105
-
106
- return True
107
-
108
- except Exception as e:
109
- logger.error(
110
- "Board tenant isolation validation failed",
111
- board_id=str(board_id),
112
- tenant_id=str(tenant_id),
113
- error=str(e),
114
- )
115
- raise TenantIsolationError(f"Board tenant validation failed: {e}") from e
116
-
117
- async def validate_generation_tenant_isolation(
118
- self, generation_id: UUID, tenant_id: UUID
119
- ) -> bool:
120
- """
121
- Validate that a generation belongs to the specified tenant.
122
- """
123
- try:
124
- stmt = select(Generations).where(
125
- (Generations.id == generation_id) & (Generations.tenant_id == tenant_id)
126
- )
127
- result = await self.db.execute(stmt)
128
- generation = result.scalar_one_or_none()
129
-
130
- if not generation:
131
- logger.warning(
132
- "Generation tenant isolation violation",
133
- generation_id=str(generation_id),
134
- expected_tenant=str(tenant_id),
135
- )
136
- return False
137
-
138
- return True
139
-
140
- except Exception as e:
141
- logger.error(
142
- "Generation tenant isolation validation failed",
143
- generation_id=str(generation_id),
144
- tenant_id=str(tenant_id),
145
- error=str(e),
146
- )
147
- raise TenantIsolationError(f"Generation tenant validation failed: {e}") from e
148
-
149
- async def audit_tenant_isolation(self, tenant_id: UUID) -> dict[str, Any]:
150
- """
151
- Perform comprehensive tenant isolation audit.
152
-
153
- Args:
154
- tenant_id: UUID of the tenant to audit
155
-
156
- Returns:
157
- Dictionary with audit results and statistics
158
- """
159
- logger.info("Starting tenant isolation audit", tenant_id=str(tenant_id))
160
-
161
- audit_results = {
162
- "tenant_id": str(tenant_id),
163
- "audit_timestamp": None,
164
- "isolation_violations": [],
165
- "statistics": {},
166
- "recommendations": [],
167
- }
168
-
169
- try:
170
- from datetime import datetime
171
-
172
- audit_results["audit_timestamp"] = datetime.now(UTC).isoformat()
173
-
174
- # 1. Check for orphaned records
175
- orphaned_records = await self._check_orphaned_records(tenant_id)
176
- if orphaned_records:
177
- audit_results["isolation_violations"].extend(orphaned_records)
178
-
179
- # 2. Check cross-tenant board memberships
180
- cross_tenant_memberships = await self._check_cross_tenant_memberships(tenant_id)
181
- if cross_tenant_memberships:
182
- audit_results["isolation_violations"].extend(cross_tenant_memberships)
183
-
184
- # 3. Gather tenant statistics
185
- audit_results["statistics"] = await self._gather_tenant_statistics(tenant_id)
186
-
187
- # 4. Generate recommendations
188
- audit_results["recommendations"] = self._generate_isolation_recommendations(
189
- audit_results["isolation_violations"]
190
- )
191
-
192
- logger.info(
193
- "Tenant isolation audit completed",
194
- tenant_id=str(tenant_id),
195
- violations_count=len(audit_results["isolation_violations"]),
196
- )
197
-
198
- return audit_results
199
-
200
- except Exception as e:
201
- logger.error(
202
- "Tenant isolation audit failed",
203
- tenant_id=str(tenant_id),
204
- error=str(e),
205
- )
206
- raise TenantIsolationError(f"Tenant isolation audit failed: {e}") from e
207
-
208
- async def _check_orphaned_records(self, tenant_id: UUID) -> list[dict[str, Any]]:
209
- """Check for records that should belong to tenant but don't."""
210
- violations = []
211
-
212
- try:
213
- # Check for users with boards in different tenants
214
- stmt = text(
215
- """
216
- SELECT u.id as user_id, b.id as board_id, b.tenant_id as board_tenant_id
217
- FROM boards.users u
218
- JOIN boards.boards b ON u.id = b.owner_id
219
- WHERE u.tenant_id = :tenant_id AND b.tenant_id != :tenant_id
220
- """
221
- )
222
- result = await self.db.execute(stmt, {"tenant_id": tenant_id})
223
- orphaned_boards = result.fetchall()
224
-
225
- for row in orphaned_boards:
226
- violations.append(
227
- {
228
- "type": "orphaned_board",
229
- "description": f"User {row.user_id} owns board {row.board_id} in different tenant", # noqa: E501
230
- "user_id": str(row.user_id),
231
- "board_id": str(row.board_id),
232
- "board_tenant_id": str(row.board_tenant_id),
233
- }
234
- )
235
-
236
- # Check for generations with boards in different tenants
237
- stmt = text(
238
- """
239
- SELECT g.id as generation_id,
240
- g.tenant_id,
241
- g.board_id,
242
- b.tenant_id as board_tenant_id
243
- FROM boards.generations g
244
- JOIN boards.boards b ON g.board_id = b.id
245
- WHERE g.tenant_id = :tenant_id AND b.tenant_id != :tenant_id
246
- """
247
- )
248
- result = await self.db.execute(stmt, {"tenant_id": tenant_id})
249
- orphaned_generations = result.fetchall()
250
-
251
- for row in orphaned_generations:
252
- violations.append(
253
- {
254
- "type": "orphaned_generation",
255
- "description": f"Generation {row.generation_id} belongs to different tenant than its board", # noqa: E501
256
- "generation_id": str(row.generation_id),
257
- "board_id": str(row.board_id),
258
- "board_tenant_id": str(row.board_tenant_id),
259
- }
260
- )
261
-
262
- except Exception as e:
263
- logger.error("Failed to check orphaned records", error=str(e))
264
-
265
- return violations
266
-
267
- async def _check_cross_tenant_memberships(self, tenant_id: UUID) -> list[dict[str, Any]]:
268
- """Check for cross-tenant board memberships."""
269
- violations = []
270
-
271
- try:
272
- stmt = text(
273
- """
274
- SELECT bm.board_id,
275
- bm.user_id,
276
- b.tenant_id as board_tenant_id,
277
- u.tenant_id as user_tenant_id
278
- FROM boards.board_members bm
279
- JOIN boards.boards b ON bm.board_id = b.id
280
- JOIN boards.users u ON bm.user_id = u.id
281
- WHERE b.tenant_id = :tenant_id AND u.tenant_id != :tenant_id
282
- """
283
- )
284
- result = await self.db.execute(stmt, {"tenant_id": tenant_id})
285
- cross_tenant_members = result.fetchall()
286
-
287
- for row in cross_tenant_members:
288
- violations.append(
289
- {
290
- "type": "cross_tenant_membership",
291
- "description": "User from different tenant has board membership",
292
- "board_id": str(row.board_id),
293
- "user_id": str(row.user_id),
294
- "board_tenant_id": str(row.board_tenant_id),
295
- "user_tenant_id": str(row.user_tenant_id),
296
- }
297
- )
298
-
299
- except Exception as e:
300
- logger.error("Failed to check cross-tenant memberships", error=str(e))
301
-
302
- return violations
303
-
304
- async def _gather_tenant_statistics(self, tenant_id: UUID) -> dict[str, int]:
305
- """Gather statistics for the tenant."""
306
- stats = {}
307
-
308
- try:
309
- # Count users
310
- stmt = select(Users).where(Users.tenant_id == tenant_id)
311
- result = await self.db.execute(stmt)
312
- stats["users_count"] = len(result.scalars().all())
313
-
314
- # Count boards
315
- stmt = select(Boards).where(Boards.tenant_id == tenant_id)
316
- result = await self.db.execute(stmt)
317
- stats["boards_count"] = len(result.scalars().all())
318
-
319
- # Count generations
320
- stmt = select(Generations).where(Generations.tenant_id == tenant_id)
321
- result = await self.db.execute(stmt)
322
- stats["generations_count"] = len(result.scalars().all())
323
-
324
- # Count board memberships
325
- stmt = text(
326
- """
327
- SELECT COUNT(*) as count
328
- FROM boards.board_members bm
329
- JOIN boards.boards b ON bm.board_id = b.id
330
- WHERE b.tenant_id = :tenant_id
331
- """
332
- )
333
- result = await self.db.execute(stmt, {"tenant_id": tenant_id})
334
- stats["board_memberships_count"] = result.scalar()
335
-
336
- except Exception as e:
337
- logger.error("Failed to gather tenant statistics", error=str(e))
338
-
339
- return stats
340
-
341
- def _generate_isolation_recommendations(self, violations: list[dict[str, Any]]) -> list[str]:
342
- """Generate recommendations based on isolation violations."""
343
- recommendations = []
344
-
345
- if not violations:
346
- recommendations.append("Tenant isolation is properly maintained - no violations found")
347
- return recommendations
348
-
349
- violation_types = {v["type"] for v in violations}
350
-
351
- if "orphaned_board" in violation_types:
352
- recommendations.append(
353
- "Fix orphaned boards by ensuring board tenant_id matches owner's tenant_id"
354
- )
355
-
356
- if "orphaned_generation" in violation_types:
357
- recommendations.append(
358
- "Fix orphaned generations by ensuring generation tenant_id matches board tenant_id"
359
- )
360
-
361
- if "cross_tenant_membership" in violation_types:
362
- recommendations.append(
363
- "Remove cross-tenant board memberships or migrate users to appropriate tenants"
364
- )
365
-
366
- recommendations.append("Run isolation audit regularly to detect future violations")
367
- recommendations.append(
368
- "Consider adding database constraints to prevent isolation violations"
369
- )
370
-
371
- return recommendations
372
-
373
-
374
- async def ensure_tenant_isolation(
375
- db: AsyncSession,
376
- user_id: UUID | None,
377
- tenant_id: UUID,
378
- resource_type: str,
379
- resource_id: UUID | None = None,
380
- ) -> None:
381
- """
382
- Ensure tenant isolation for a specific operation.
383
-
384
- Args:
385
- db: Database session
386
- user_id: ID of the user performing the operation
387
- tenant_id: ID of the tenant context
388
- resource_type: Type of resource being accessed (user, board, generation)
389
- resource_id: ID of the specific resource (if applicable)
390
-
391
- Raises:
392
- TenantIsolationError: If isolation validation fails
393
- """
394
- if not settings.multi_tenant_mode:
395
- # Skip validation in single-tenant mode
396
- return
397
-
398
- validator = TenantIsolationValidator(db)
399
-
400
- try:
401
- # Validate user belongs to tenant
402
- if user_id:
403
- user_valid = await validator.validate_user_tenant_isolation(user_id, tenant_id)
404
- if not user_valid:
405
- raise TenantIsolationError(f"User {user_id} does not belong to tenant {tenant_id}")
406
-
407
- # Validate resource belongs to tenant (if resource_id provided)
408
- if resource_id:
409
- if resource_type == "board":
410
- board_valid = await validator.validate_board_tenant_isolation(
411
- resource_id, tenant_id
412
- )
413
- if not board_valid:
414
- raise TenantIsolationError(
415
- f"Board {resource_id} does not belong to tenant {tenant_id}"
416
- )
417
-
418
- elif resource_type == "generation":
419
- generation_valid = await validator.validate_generation_tenant_isolation(
420
- resource_id, tenant_id
421
- )
422
- if not generation_valid:
423
- raise TenantIsolationError(
424
- f"Generation {resource_id} does not belong to tenant {tenant_id}"
425
- )
426
-
427
- logger.debug(
428
- "Tenant isolation validated successfully",
429
- user_id=str(user_id) if user_id else None,
430
- tenant_id=str(tenant_id),
431
- resource_type=resource_type,
432
- resource_id=str(resource_id) if resource_id else None,
433
- )
434
-
435
- except TenantIsolationError:
436
- # Re-raise isolation errors
437
- raise
438
- except Exception as e:
439
- logger.error(
440
- "Tenant isolation validation error",
441
- user_id=str(user_id) if user_id else None,
442
- tenant_id=str(tenant_id),
443
- resource_type=resource_type,
444
- error=str(e),
445
- )
446
- raise TenantIsolationError(f"Tenant isolation validation failed: {e}") from e