@weirdfingers/baseboards 0.9.6 → 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 +560 -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,463 +0,0 @@
1
- """Resolvers for artifact upload operations."""
2
-
3
- from __future__ import annotations
4
-
5
- import ipaddress
6
- from datetime import UTC, datetime
7
- from decimal import Decimal
8
- from typing import TYPE_CHECKING
9
- from urllib.parse import urlparse
10
- from uuid import UUID
11
-
12
- import aiohttp
13
- import strawberry
14
- from sqlalchemy import select
15
- from sqlalchemy.orm import selectinload
16
-
17
- from ...auth.context import AuthContext
18
- from ...database.connection import get_async_session
19
- from ...dbmodels import Boards, Generations
20
- from ...logging import get_logger
21
- from ...storage.factory import create_storage_manager
22
- from ..access_control import get_auth_context_from_info
23
- from ..types.generation import ArtifactType
24
-
25
- if TYPE_CHECKING:
26
- from ..types.generation import Generation as GenerationType
27
- from ..types.generation import UploadArtifactInput
28
-
29
- logger = get_logger(__name__)
30
-
31
-
32
- def _validate_mime_type(
33
- content_type: str, artifact_type: ArtifactType, filename: str | None
34
- ) -> tuple[bool, str | None]:
35
- """
36
- Validate that MIME type matches the expected artifact type.
37
-
38
- Args:
39
- content_type: The MIME type to validate (e.g., "image/jpeg")
40
- artifact_type: The expected artifact type enum
41
- filename: Optional filename for additional context
42
-
43
- Returns:
44
- Tuple of (is_valid, error_message)
45
- """
46
- # Define allowed MIME types for each artifact type
47
- allowed_mime_types = {
48
- ArtifactType.IMAGE: [
49
- "image/jpeg",
50
- "image/jpg",
51
- "image/png",
52
- "image/gif",
53
- "image/webp",
54
- "image/bmp",
55
- "image/svg+xml",
56
- ],
57
- ArtifactType.VIDEO: [
58
- "video/mp4",
59
- "video/quicktime",
60
- "video/x-msvideo",
61
- "video/webm",
62
- "video/mpeg",
63
- "video/x-matroska",
64
- ],
65
- ArtifactType.AUDIO: [
66
- "audio/mpeg",
67
- "audio/mp3",
68
- "audio/wav",
69
- "audio/ogg",
70
- "audio/webm",
71
- "audio/x-m4a",
72
- "audio/mp4",
73
- ],
74
- ArtifactType.TEXT: [
75
- "text/plain",
76
- "text/markdown",
77
- "application/json",
78
- "text/html",
79
- "text/csv",
80
- ],
81
- }
82
-
83
- # Normalize MIME type (remove charset, etc.)
84
- mime_type = content_type.split(";")[0].strip().lower()
85
-
86
- # Check if artifact type is supported
87
- if artifact_type not in allowed_mime_types:
88
- return False, f"Unsupported artifact type: {artifact_type.value}"
89
-
90
- # Check if MIME type is allowed for this artifact type
91
- if mime_type not in allowed_mime_types[artifact_type]:
92
- # Also check for generic types
93
- mime_category = mime_type.split("/")[0]
94
- if mime_category != artifact_type.value:
95
- return (
96
- False,
97
- f"MIME type '{mime_type}' does not match artifact type '{artifact_type.value}'",
98
- )
99
-
100
- return True, None
101
-
102
-
103
- def _is_safe_url(url: str) -> tuple[bool, str | None]:
104
- """
105
- Validate URL to prevent SSRF attacks.
106
-
107
- Returns:
108
- Tuple of (is_safe, error_message)
109
- """
110
- try:
111
- parsed = urlparse(url)
112
-
113
- # Only allow http and https
114
- if parsed.scheme not in ("http", "https"):
115
- return (
116
- False,
117
- f"URL scheme '{parsed.scheme}' not allowed. Only http and https are supported.",
118
- )
119
-
120
- hostname = parsed.hostname
121
- if not hostname:
122
- return False, "Invalid URL: no hostname found"
123
-
124
- # Block localhost
125
- if hostname.lower() in ("localhost", "127.0.0.1", "::1"):
126
- return False, "Access to localhost is not allowed"
127
-
128
- # Try to resolve hostname to IP
129
- try:
130
- # Check if it's already an IP address
131
- ip = ipaddress.ip_address(hostname)
132
-
133
- # Block private IP ranges
134
- if ip.is_private:
135
- return False, f"Access to private IP address {ip} is not allowed"
136
-
137
- # Block link-local addresses (including AWS metadata endpoint)
138
- if ip.is_link_local:
139
- return False, f"Access to link-local address {ip} is not allowed"
140
-
141
- # Block loopback
142
- if ip.is_loopback:
143
- return False, f"Access to loopback address {ip} is not allowed"
144
-
145
- except ValueError:
146
- # Not an IP address, it's a hostname - this is OK
147
- # In production, you might want to resolve the hostname and check the IP
148
- # but that adds complexity and potential DNS rebinding issues
149
- pass
150
-
151
- return True, None
152
-
153
- except Exception as e:
154
- return False, f"Invalid URL: {e}"
155
-
156
-
157
- async def upload_artifact_from_url(
158
- info: strawberry.Info,
159
- input: UploadArtifactInput,
160
- ) -> GenerationType:
161
- """Upload artifact from URL (synchronous)."""
162
- from ...config import settings
163
-
164
- auth_context = await get_auth_context_from_info(info)
165
- if not auth_context or not auth_context.is_authenticated:
166
- raise RuntimeError("Authentication required")
167
-
168
- if not input.file_url:
169
- raise RuntimeError("file_url is required")
170
-
171
- # Validate URL to prevent SSRF attacks
172
- is_safe, error_msg = _is_safe_url(input.file_url)
173
- if not is_safe:
174
- logger.warning("Unsafe URL blocked", url=input.file_url, reason=error_msg)
175
- raise RuntimeError(f"URL not allowed: {error_msg}")
176
-
177
- # Download file from URL
178
- async with aiohttp.ClientSession() as http_session:
179
- try:
180
- async with http_session.get(
181
- input.file_url, timeout=aiohttp.ClientTimeout(total=60)
182
- ) as resp:
183
- if resp.status != 200:
184
- raise RuntimeError(f"Failed to download from URL: HTTP {resp.status}")
185
-
186
- # Check Content-Length before downloading to prevent memory exhaustion
187
- content_length = resp.headers.get("Content-Length")
188
- if content_length:
189
- file_size = int(content_length)
190
- if file_size > settings.max_upload_size:
191
- raise RuntimeError(
192
- f"File size ({file_size} bytes) exceeds maximum allowed "
193
- f"size ({settings.max_upload_size} bytes)"
194
- )
195
-
196
- content = await resp.read()
197
- content_type = resp.headers.get("Content-Type", "application/octet-stream")
198
-
199
- # Extract filename from URL if not provided
200
- filename = input.original_filename
201
- if not filename:
202
- path = urlparse(input.file_url).path
203
- filename = path.split("/")[-1] if path else "uploaded_file"
204
-
205
- except aiohttp.ClientError as e:
206
- logger.error("URL download failed", url=input.file_url, error=str(e))
207
- raise RuntimeError("Failed to download file from URL") from e
208
-
209
- # Process upload
210
- return await _process_upload(
211
- auth_context=auth_context,
212
- board_id=input.board_id,
213
- artifact_type=input.artifact_type,
214
- file_content=content,
215
- filename=filename,
216
- content_type=content_type,
217
- user_description=input.user_description,
218
- parent_generation_id=input.parent_generation_id,
219
- upload_source="url",
220
- source_url=input.file_url,
221
- )
222
-
223
-
224
- async def upload_artifact_from_file(
225
- auth_context: AuthContext,
226
- board_id: UUID,
227
- artifact_type: str,
228
- file_content: bytes,
229
- filename: str | None,
230
- content_type: str | None,
231
- user_description: str | None,
232
- parent_generation_id: UUID | None,
233
- ) -> GenerationType:
234
- """Upload artifact from file (synchronous)."""
235
- return await _process_upload(
236
- auth_context=auth_context,
237
- board_id=board_id,
238
- artifact_type=ArtifactType(artifact_type),
239
- file_content=file_content,
240
- filename=filename or "uploaded_file",
241
- content_type=content_type or "application/octet-stream",
242
- user_description=user_description,
243
- parent_generation_id=parent_generation_id,
244
- upload_source="file",
245
- source_url=None,
246
- )
247
-
248
-
249
- def _sanitize_filename(filename: str) -> str:
250
- """
251
- Sanitize filename to prevent path traversal and other security issues.
252
-
253
- Returns:
254
- Sanitized filename (basename only, no path components)
255
- """
256
- import os
257
- import re
258
-
259
- # Get basename only (remove any path components)
260
- filename = os.path.basename(filename)
261
-
262
- # Remove any null bytes
263
- filename = filename.replace("\x00", "")
264
-
265
- # Replace potentially dangerous characters (including backslash for Windows paths)
266
- filename = re.sub(r'[<>:"|?*\\]', "_", filename)
267
-
268
- # Remove leading/trailing whitespace and dots
269
- filename = filename.strip(". ")
270
-
271
- # If filename is empty after sanitization, use a default
272
- if not filename:
273
- filename = "uploaded_file"
274
-
275
- return filename
276
-
277
-
278
- async def _process_upload(
279
- auth_context: AuthContext,
280
- board_id: UUID,
281
- artifact_type: ArtifactType,
282
- file_content: bytes,
283
- filename: str,
284
- content_type: str,
285
- user_description: str | None,
286
- parent_generation_id: UUID | None,
287
- upload_source: str,
288
- source_url: str | None,
289
- ) -> GenerationType:
290
- """Common upload processing logic.
291
-
292
- Args:
293
- auth_context: Authentication context for the request
294
- board_id: UUID of the board to upload to
295
- artifact_type: Type of artifact being uploaded (enum)
296
- file_content: Binary content of the file
297
- filename: Original filename
298
- content_type: MIME type of the file
299
- user_description: Optional user-provided description
300
- parent_generation_id: Optional parent generation UUID
301
- upload_source: Source of upload ("file" or "url")
302
- source_url: URL if uploaded from URL, None otherwise
303
-
304
- Returns:
305
- GenerationType object representing the uploaded artifact
306
- """
307
- from ...config import settings
308
- from ..types.generation import Generation as GenerationType
309
- from ..types.generation import GenerationStatus
310
-
311
- # Sanitize filename to prevent path traversal
312
- filename = _sanitize_filename(filename)
313
-
314
- # Validate MIME type matches artifact type
315
- is_valid, error_msg = _validate_mime_type(content_type, artifact_type, filename)
316
- if not is_valid:
317
- logger.warning(
318
- "Invalid MIME type for artifact",
319
- mime_type=content_type,
320
- artifact_type=artifact_type.value,
321
- reason=error_msg,
322
- )
323
- raise RuntimeError(f"Invalid file type: {error_msg}")
324
-
325
- # Validate file size (double-check even after Content-Length check)
326
- if len(file_content) > settings.max_upload_size:
327
- raise RuntimeError(
328
- f"File size ({len(file_content)} bytes) exceeds maximum allowed "
329
- f"size ({settings.max_upload_size} bytes)"
330
- )
331
-
332
- async with get_async_session() as session:
333
- # Validate board access
334
- board_stmt = (
335
- select(Boards).where(Boards.id == board_id).options(selectinload(Boards.board_members))
336
- )
337
- board = (await session.execute(board_stmt)).scalar_one_or_none()
338
-
339
- if not board:
340
- raise RuntimeError("Board not found")
341
-
342
- # Check permissions (same as create_generation)
343
- if not auth_context.user_id:
344
- raise RuntimeError("User ID is required")
345
-
346
- is_owner = board.owner_id == auth_context.user_id
347
- is_editor = any(
348
- m.user_id == auth_context.user_id and m.role in {"editor", "admin"}
349
- for m in board.board_members
350
- )
351
-
352
- if not is_owner and not is_editor:
353
- raise RuntimeError(
354
- "Permission denied: You don't have permission to upload to this board"
355
- )
356
-
357
- # Create generation record (status=pending temporarily)
358
- gen = Generations()
359
- gen.tenant_id = auth_context.tenant_id
360
- gen.board_id = board_id
361
- gen.user_id = auth_context.user_id
362
- gen.generator_name = f"user-upload-{artifact_type.value}"
363
- gen.artifact_type = artifact_type.value
364
- gen.status = "pending"
365
- gen.progress = Decimal(0.0)
366
- gen.input_params = {
367
- "upload_source": upload_source,
368
- "original_filename": filename,
369
- "source_url": source_url,
370
- "user_description": user_description,
371
- }
372
- gen.output_metadata = {
373
- "file_size": len(file_content),
374
- "mime_type": content_type,
375
- "upload_timestamp": datetime.now(UTC).isoformat(),
376
- }
377
- # If parent_generation_id is provided, add it to input_artifacts
378
- if parent_generation_id:
379
- gen.input_artifacts = [
380
- {
381
- "generation_id": str(parent_generation_id),
382
- "role": "parent",
383
- "artifact_type": artifact_type.value,
384
- }
385
- ]
386
- else:
387
- gen.input_artifacts = []
388
- gen.started_at = datetime.now(UTC)
389
-
390
- session.add(gen)
391
- await session.flush() # Get ID
392
-
393
- try:
394
- # Upload to storage
395
- storage_manager = create_storage_manager()
396
- artifact_ref = await storage_manager.store_artifact(
397
- artifact_id=str(gen.id),
398
- content=file_content,
399
- artifact_type=artifact_type.value,
400
- content_type=content_type,
401
- tenant_id=str(auth_context.tenant_id),
402
- board_id=str(board_id),
403
- )
404
-
405
- # Update generation with storage info
406
- gen.storage_url = artifact_ref.storage_url
407
- gen.status = "completed"
408
- gen.progress = Decimal(100.0)
409
- gen.completed_at = datetime.now(UTC)
410
-
411
- # Update metadata with storage details
412
- if gen.output_metadata is None:
413
- gen.output_metadata = {}
414
- gen.output_metadata["storage_key"] = artifact_ref.storage_key
415
- gen.output_metadata["storage_provider"] = artifact_ref.storage_provider
416
-
417
- await session.commit()
418
- await session.refresh(gen)
419
-
420
- logger.info(
421
- "Artifact uploaded",
422
- generation_id=str(gen.id),
423
- artifact_type=artifact_type,
424
- file_size=len(file_content),
425
- upload_source=upload_source,
426
- )
427
-
428
- # Convert to GraphQL type
429
- return GenerationType(
430
- id=gen.id,
431
- tenant_id=gen.tenant_id,
432
- board_id=gen.board_id,
433
- user_id=gen.user_id,
434
- generator_name=gen.generator_name,
435
- artifact_type=ArtifactType(gen.artifact_type),
436
- storage_url=gen.storage_url,
437
- thumbnail_url=gen.thumbnail_url,
438
- additional_files=gen.additional_files or [],
439
- input_params=gen.input_params or {},
440
- output_metadata=gen.output_metadata or {},
441
- external_job_id=gen.external_job_id,
442
- status=GenerationStatus(gen.status),
443
- progress=float(gen.progress),
444
- error_message=gen.error_message,
445
- started_at=gen.started_at,
446
- completed_at=gen.completed_at,
447
- created_at=gen.created_at,
448
- updated_at=gen.updated_at,
449
- )
450
-
451
- except Exception as e:
452
- # Mark as failed
453
- gen.status = "failed"
454
- gen.error_message = str(e)
455
- gen.completed_at = datetime.now(UTC)
456
- await session.commit()
457
-
458
- logger.error(
459
- "Upload failed",
460
- generation_id=str(gen.id),
461
- error=str(e),
462
- )
463
- raise RuntimeError(f"Upload failed: {e}") from e
@@ -1,25 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- import strawberry
6
-
7
- if TYPE_CHECKING:
8
- from ..types.board import Board
9
- from ..types.user import User
10
-
11
-
12
- async def resolve_current_user(info: strawberry.Info) -> User | None:
13
- raise NotImplementedError
14
-
15
-
16
- async def resolve_user_by_id(info: strawberry.Info, id: str) -> User | None:
17
- raise NotImplementedError
18
-
19
-
20
- async def resolve_user_boards(user: User, info: strawberry.Info) -> list[Board]:
21
- raise NotImplementedError
22
-
23
-
24
- async def resolve_user_member_boards(user: User, info: strawberry.Info) -> list[Board]:
25
- raise NotImplementedError
@@ -1,81 +0,0 @@
1
- """
2
- Main GraphQL schema definition using Strawberry
3
- """
4
-
5
- from typing import Any
6
-
7
- import strawberry
8
- from fastapi import Request
9
- from graphql import validate_schema as gql_validate_schema
10
- from strawberry.fastapi import GraphQLRouter
11
-
12
- from ..logging import get_logger
13
- from .mutations.root import Mutation
14
- from .queries.root import Query
15
-
16
- # Import types to ensure they're registered with Strawberry
17
-
18
- logger = get_logger(__name__)
19
-
20
- # Create the GraphQL schema
21
- schema = strawberry.Schema(
22
- query=Query,
23
- mutation=Mutation,
24
- # Note: Introspection is enabled by default in strawberry
25
- # TODO: Disable in production for security by using extensions
26
- )
27
-
28
-
29
- def validate_schema() -> None:
30
- """Validate the GraphQL schema at startup.
31
-
32
- This ensures that all type references can be resolved and catches
33
- circular reference errors early, causing the server to fail fast
34
- rather than returning 404s at runtime.
35
-
36
- Raises:
37
- Exception: If the schema is invalid or has unresolved types
38
- """
39
- try:
40
- # Convert to GraphQL core schema to trigger full validation
41
- graphql_schema = schema._schema
42
-
43
- # Validate the schema structure
44
- errors = gql_validate_schema(graphql_schema)
45
- if errors:
46
- error_messages = [str(e) for e in errors]
47
- raise Exception(f"GraphQL schema validation failed: {'; '.join(error_messages)}")
48
-
49
- # Check that introspection query works (catches most resolution issues)
50
- from graphql import get_introspection_query, graphql_sync
51
-
52
- introspection_query = get_introspection_query()
53
- result = graphql_sync(graphql_schema, introspection_query)
54
-
55
- if result.errors:
56
- error_messages = [str(e) for e in result.errors]
57
- raise Exception(f"GraphQL introspection failed: {'; '.join(error_messages)}")
58
-
59
- logger.info("GraphQL schema validation successful")
60
-
61
- except Exception as e:
62
- logger.error("GraphQL schema validation failed", error=str(e))
63
- raise
64
-
65
-
66
- # Create the GraphQL router for FastAPI integration
67
- def create_graphql_router() -> GraphQLRouter[dict[str, Any], None]:
68
- """Create a GraphQL router for FastAPI."""
69
-
70
- async def get_context(request: Request) -> dict[str, Any]:
71
- """Get the context for GraphQL resolvers."""
72
- return {
73
- "request": request,
74
- }
75
-
76
- return GraphQLRouter(
77
- schema,
78
- path="/graphql",
79
- graphiql=True, # Enable GraphiQL IDE in development
80
- context_getter=get_context,
81
- )
@@ -1,102 +0,0 @@
1
- """
2
- Board GraphQL type definitions
3
- """
4
-
5
- from datetime import datetime
6
- from enum import Enum
7
- from typing import TYPE_CHECKING, Annotated
8
- from uuid import UUID
9
-
10
- import strawberry
11
-
12
- if TYPE_CHECKING:
13
- from .generation import Generation
14
- from .user import User
15
-
16
-
17
- @strawberry.enum
18
- class BoardRole(Enum):
19
- """Board member role enumeration."""
20
-
21
- VIEWER = "viewer"
22
- EDITOR = "editor"
23
- ADMIN = "admin"
24
-
25
-
26
- @strawberry.type
27
- class BoardMember:
28
- """Board member type for GraphQL API."""
29
-
30
- id: UUID
31
- board_id: UUID
32
- user_id: UUID
33
- role: BoardRole
34
- invited_by: UUID | None
35
- joined_at: datetime
36
-
37
- @strawberry.field
38
- async def user(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
39
- """Get the user for this board member."""
40
- from ..resolvers.board import resolve_board_member_user
41
-
42
- return await resolve_board_member_user(self, info)
43
-
44
- @strawberry.field
45
- async def inviter(
46
- self, info: strawberry.Info
47
- ) -> Annotated["User", strawberry.lazy(".user")] | None: # noqa: E501
48
- """Get the user who invited this member."""
49
- if not self.invited_by:
50
- return None
51
- from ..resolvers.board import resolve_board_member_inviter
52
-
53
- return await resolve_board_member_inviter(self, info)
54
-
55
-
56
- @strawberry.type
57
- class Board:
58
- """Board type for GraphQL API."""
59
-
60
- id: UUID
61
- tenant_id: UUID
62
- owner_id: UUID
63
- title: str
64
- description: str | None
65
- is_public: bool
66
- settings: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
67
- metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
68
- created_at: datetime
69
- updated_at: datetime
70
-
71
- @strawberry.field
72
- async def owner(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
73
- """Get the owner of this board."""
74
- from ..resolvers.board import resolve_board_owner
75
-
76
- return await resolve_board_owner(self, info)
77
-
78
- @strawberry.field
79
- async def members(self, info: strawberry.Info) -> list[BoardMember]:
80
- """Get members of this board."""
81
- from ..resolvers.board import resolve_board_members
82
-
83
- return await resolve_board_members(self, info)
84
-
85
- @strawberry.field
86
- async def generations(
87
- self,
88
- info: strawberry.Info,
89
- limit: int | None = 50,
90
- offset: int | None = 0,
91
- ) -> list[Annotated["Generation", strawberry.lazy(".generation")]]:
92
- """Get generations in this board."""
93
- from ..resolvers.board import resolve_board_generations
94
-
95
- return await resolve_board_generations(self, info, limit or 50, offset or 0)
96
-
97
- @strawberry.field
98
- async def generation_count(self, info: strawberry.Info) -> int:
99
- """Get total number of generations in this board."""
100
- from ..resolvers.board import resolve_board_generation_count
101
-
102
- return await resolve_board_generation_count(self, info)