@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,221 +0,0 @@
1
- """Authentication middleware for FastAPI."""
2
-
3
- from __future__ import annotations
4
-
5
- from uuid import UUID
6
-
7
- from fastapi import Header, HTTPException
8
-
9
- from ..database.connection import get_async_session
10
- from ..database.seed_data import ensure_tenant
11
- from ..logging import get_logger
12
- from .adapters.base import AuthenticationError
13
- from .context import DEFAULT_TENANT_UUID, AuthContext
14
- from .factory import get_auth_adapter_cached
15
- from .provisioning import ensure_local_user
16
- from .tenant_extraction import extract_tenant_from_claims
17
-
18
- logger = get_logger(__name__)
19
-
20
-
21
- async def get_auth_context(
22
- authorization: str | None = Header(None),
23
- x_tenant: str | None = Header(None, alias="X-Tenant"),
24
- ) -> AuthContext:
25
- """
26
- Extract authentication context from request headers.
27
-
28
- This function:
29
- 1. Extracts Bearer token from Authorization header
30
- 2. Verifies token using the configured auth adapter
31
- 3. Resolves tenant (defaults to 'default' for single-tenant)
32
- 4. Performs JIT user provisioning
33
- 5. Returns AuthContext for the request
34
-
35
- For no-auth mode, any token (or "dev-token") will work.
36
-
37
- Args:
38
- authorization: Authorization header (Bearer token)
39
- x_tenant: Tenant identifier header
40
-
41
- Returns:
42
- AuthContext with user, tenant, and token info
43
- """
44
- adapter = get_auth_adapter_cached()
45
-
46
- # Check if we're in no-auth mode
47
- is_no_auth_mode = hasattr(adapter, "default_user_id") # NoAuthAdapter has this attribute
48
-
49
- # Handle unauthenticated requests
50
- if not authorization:
51
- if is_no_auth_mode:
52
- # In no-auth mode, create a default token
53
- authorization = "Bearer dev-token"
54
- else:
55
- # Use header tenant or default for unauthenticated requests
56
- tenant_slug = x_tenant or "default"
57
- tenant_uuid = await _resolve_tenant_uuid(tenant_slug)
58
- return AuthContext(
59
- user_id=None,
60
- tenant_id=tenant_uuid,
61
- principal=None,
62
- token=None,
63
- )
64
-
65
- # Extract Bearer token
66
- if not authorization.startswith("Bearer "):
67
- logger.warning("Invalid authorization format received")
68
- raise HTTPException(
69
- status_code=401,
70
- detail="Invalid authorization format. Expected: Bearer <token>",
71
- headers={"WWW-Authenticate": "Bearer"},
72
- )
73
-
74
- token = authorization[7:] # Remove "Bearer " prefix
75
-
76
- if not token:
77
- logger.warning("Empty token provided")
78
- raise HTTPException(
79
- status_code=401,
80
- detail="Empty token",
81
- headers={"WWW-Authenticate": "Bearer"},
82
- )
83
-
84
- try:
85
- # Verify token with auth adapter
86
- principal = await adapter.verify_token(token)
87
-
88
- # Extract tenant from JWT/OIDC claims with fallback to header
89
- tenant_slug = extract_tenant_from_claims(principal, fallback_tenant=x_tenant)
90
-
91
- logger.debug(
92
- "Tenant resolved for authenticated request",
93
- tenant_slug=tenant_slug,
94
- header_tenant=x_tenant,
95
- provider=principal.get("provider"),
96
- subject=principal.get("subject"),
97
- )
98
-
99
- # Resolve tenant slug to UUID and perform JIT user provisioning
100
- try:
101
- async with get_async_session() as db:
102
- # Ensure tenant exists and get its UUID
103
- tenant_uuid = await ensure_tenant(db, slug=tenant_slug)
104
- # Now provision the user with the tenant UUID
105
- user_id = await ensure_local_user(db, tenant_uuid, principal)
106
-
107
- logger.debug(
108
- "User provisioned and tenant resolved",
109
- user_id=str(user_id),
110
- tenant_uuid=str(tenant_uuid),
111
- tenant_slug=tenant_slug,
112
- )
113
- except Exception as db_error:
114
- # Database connection failed, use the same deterministic fallback
115
- logger.error(
116
- "Database connection failed, using fallback user ID generation",
117
- error=str(db_error),
118
- principal_provider=principal.get("provider"),
119
- principal_subject=principal.get("subject"),
120
- )
121
-
122
- import hashlib
123
-
124
- provider = principal.get("provider", "unknown")
125
- subject = principal.get("subject", "anonymous")
126
- stable_input = f"{provider}:{subject}:{tenant_slug}"
127
- user_id_hash = hashlib.sha256(stable_input.encode()).hexdigest()[:32]
128
- # Format hash as UUID: 8-4-4-4-12 pattern
129
- formatted_uuid = (
130
- f"{user_id_hash[:8]}-{user_id_hash[8:12]}-"
131
- f"{user_id_hash[12:16]}-{user_id_hash[16:20]}-"
132
- f"{user_id_hash[20:32]}"
133
- )
134
- from uuid import UUID
135
-
136
- user_id = UUID(formatted_uuid)
137
-
138
- # Also resolve tenant_uuid in this fallback path
139
- tenant_uuid = await _resolve_tenant_uuid(tenant_slug)
140
-
141
- return AuthContext(
142
- user_id=user_id,
143
- tenant_id=tenant_uuid,
144
- principal=principal,
145
- token=token,
146
- )
147
-
148
- except AuthenticationError as e:
149
- logger.warning("Authentication failed", error=str(e))
150
- raise HTTPException(
151
- status_code=401,
152
- detail=str(e),
153
- headers={"WWW-Authenticate": "Bearer"},
154
- ) from e
155
- except Exception as e:
156
- logger.error("Unexpected authentication error", error=str(e))
157
- raise HTTPException(
158
- status_code=401,
159
- detail="Authentication failed",
160
- headers={"WWW-Authenticate": "Bearer"},
161
- ) from e
162
-
163
-
164
- async def get_auth_context_optional(
165
- authorization: str | None = Header(None),
166
- x_tenant: str | None = Header(None, alias="X-Tenant"),
167
- ) -> AuthContext:
168
- """
169
- Optional authentication - returns unauthenticated context if no token.
170
-
171
- Use this for endpoints that work both authenticated and unauthenticated.
172
- """
173
- try:
174
- return await get_auth_context(authorization, x_tenant)
175
- except HTTPException:
176
- # Return unauthenticated context
177
- tenant_slug = x_tenant or "default"
178
- tenant_uuid = await _resolve_tenant_uuid(tenant_slug)
179
- return AuthContext(
180
- user_id=None,
181
- tenant_id=tenant_uuid,
182
- principal=None,
183
- token=None,
184
- )
185
-
186
-
187
- async def _resolve_tenant_uuid(tenant_slug: str) -> UUID:
188
- """
189
- Resolve a tenant slug to its UUID.
190
-
191
- Falls back to DEFAULT_TENANT_UUID if:
192
- - Database lookup fails
193
- - Tenant doesn't exist
194
- - Running in single-tenant mode
195
-
196
- Args:
197
- tenant_slug: The tenant slug to resolve (e.g., "default", "acme-corp")
198
-
199
- Returns:
200
- UUID of the tenant, or DEFAULT_TENANT_UUID if resolution fails
201
- """
202
- try:
203
- from ..database.connection import get_async_session
204
- from ..database.seed_data import ensure_tenant
205
-
206
- async with get_async_session() as db:
207
- tenant_uuid = await ensure_tenant(db, slug=tenant_slug)
208
- logger.debug(
209
- "Resolved tenant slug to UUID",
210
- tenant_slug=tenant_slug,
211
- tenant_uuid=str(tenant_uuid),
212
- )
213
- return tenant_uuid
214
- except Exception as e:
215
- logger.warning(
216
- "Failed to resolve tenant UUID, using default",
217
- tenant_slug=tenant_slug,
218
- error=str(e),
219
- default_uuid=str(DEFAULT_TENANT_UUID),
220
- )
221
- return DEFAULT_TENANT_UUID
@@ -1,129 +0,0 @@
1
- """User provisioning and management."""
2
-
3
- from __future__ import annotations
4
-
5
- from uuid import UUID
6
-
7
- from sqlalchemy import and_, select
8
- from sqlalchemy.ext.asyncio import AsyncSession
9
-
10
- from ..dbmodels import Users
11
- from ..logging import get_logger
12
- from .adapters.base import Principal
13
-
14
- logger = get_logger(__name__)
15
-
16
-
17
- async def ensure_local_user(db: AsyncSession, tenant_id: UUID, principal: Principal) -> UUID:
18
- """
19
- Ensure a local user exists for the given principal (JIT provisioning).
20
-
21
- This function creates a local user record if one doesn't exist for the
22
- given (tenant_id, auth_provider, auth_subject) combination.
23
-
24
- Args:
25
- db: Database session
26
- tenant_id: Tenant UUID
27
- principal: Authenticated principal from auth provider
28
-
29
- Returns:
30
- UUID of the local user
31
- """
32
- provider = principal["provider"]
33
- subject = principal["subject"]
34
-
35
- # Ensure tenant_id is a UUID
36
- if not isinstance(tenant_id, UUID):
37
- raise ValueError(f"tenant_id must be a UUID, got {type(tenant_id)}")
38
-
39
- # Try to find existing user
40
- stmt = select(Users).where(
41
- and_(
42
- Users.tenant_id == tenant_id,
43
- Users.auth_provider == provider,
44
- Users.auth_subject == subject,
45
- )
46
- )
47
-
48
- result = await db.execute(stmt)
49
- user = result.scalar_one_or_none()
50
-
51
- if user:
52
- # Update user info if provided in principal, but preserve existing non-empty values
53
- updated = False
54
-
55
- email = principal.get("email")
56
- if email and not user.email: # Only update if current email is empty/None
57
- user.email = email
58
- updated = True
59
-
60
- display_name = principal.get("display_name")
61
- # Only update if current display_name is empty/None
62
- if display_name and not user.display_name:
63
- user.display_name = display_name
64
- updated = True
65
-
66
- avatar_url = principal.get("avatar_url")
67
- if avatar_url and not user.avatar_url: # Only update if current avatar_url is empty/None
68
- user.avatar_url = avatar_url
69
- updated = True
70
-
71
- if updated:
72
- await db.commit()
73
- await db.refresh(user)
74
- logger.info("Updated user info (preserving existing values)", user_id=str(user.id))
75
-
76
- return user.id
77
-
78
- # Create new user
79
- user = Users(
80
- tenant_id=tenant_id,
81
- auth_provider=provider,
82
- auth_subject=subject,
83
- email=principal.get("email"),
84
- display_name=principal.get("display_name"),
85
- avatar_url=principal.get("avatar_url"),
86
- metadata_={
87
- "created_via": "jit_provisioning",
88
- "provider_claims": principal.get("claims", {}),
89
- },
90
- )
91
-
92
- db.add(user)
93
- await db.commit()
94
- await db.refresh(user)
95
-
96
- logger.info(
97
- "Created new user via JIT provisioning",
98
- user_id=str(user.id),
99
- tenant_id=tenant_id,
100
- provider=provider,
101
- )
102
-
103
- return user.id
104
-
105
-
106
- async def get_user_by_id(db: AsyncSession, user_id: UUID) -> Users | None:
107
- """Get a user by ID."""
108
- stmt = select(Users).where(Users.id == user_id)
109
- result = await db.execute(stmt)
110
- return result.scalar_one_or_none()
111
-
112
-
113
- async def get_user_by_auth_info(
114
- db: AsyncSession, tenant_id: UUID, auth_provider: str, auth_subject: str
115
- ) -> Users | None:
116
- """Get a user by auth provider information."""
117
- # Ensure tenant_id is a UUID
118
- if not isinstance(tenant_id, UUID):
119
- raise ValueError(f"tenant_id must be a UUID, got {type(tenant_id)}")
120
-
121
- stmt = select(Users).where(
122
- and_(
123
- Users.tenant_id == tenant_id,
124
- Users.auth_provider == auth_provider,
125
- Users.auth_subject == auth_subject,
126
- )
127
- )
128
- result = await db.execute(stmt)
129
- return result.scalar_one_or_none()
@@ -1,278 +0,0 @@
1
- """
2
- Tenant extraction utilities for multi-tenant authentication.
3
-
4
- This module provides utilities to extract tenant information from JWT/OIDC claims
5
- and other authentication contexts.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from typing import Any
11
-
12
- from ..config import settings
13
- from ..logging import get_logger
14
- from .context import Principal
15
-
16
- logger = get_logger(__name__)
17
-
18
-
19
- def extract_tenant_from_claims(
20
- principal: Principal,
21
- fallback_tenant: str | None = None,
22
- ) -> str:
23
- """
24
- Extract tenant slug from JWT/OIDC claims.
25
-
26
- This function supports multiple tenant extraction strategies:
27
- 1. Direct 'tenant' claim in JWT
28
- 2. Organization-based claims (org, organization, org_slug)
29
- 3. Custom claims (configurable via settings)
30
- 4. Domain-based extraction from email
31
- 5. Fallback to header or default
32
-
33
- Args:
34
- principal: Principal with claims from JWT/OIDC
35
- fallback_tenant: Fallback tenant slug if no tenant found in claims
36
-
37
- Returns:
38
- Tenant slug extracted from claims or fallback
39
- """
40
- claims = principal.get("claims", {})
41
-
42
- if not claims:
43
- logger.debug("No claims available in principal, using fallback")
44
- return fallback_tenant or settings.default_tenant_slug
45
-
46
- # Strategy 1: Direct tenant claim
47
- if tenant_slug := claims.get("tenant"):
48
- logger.info("Extracted tenant from 'tenant' claim", tenant_slug=tenant_slug)
49
- return _validate_tenant_slug(tenant_slug)
50
-
51
- # Strategy 2: Organization-based claims
52
- org_claims = ["org", "organization", "org_slug", "org_name"]
53
- for claim_name in org_claims:
54
- if org_value := claims.get(claim_name):
55
- tenant_slug = _normalize_tenant_slug(org_value)
56
- logger.info(
57
- "Extracted tenant from organization claim",
58
- claim_name=claim_name,
59
- org_value=org_value,
60
- tenant_slug=tenant_slug,
61
- )
62
- return tenant_slug
63
-
64
- # Strategy 3: Custom claims (configurable)
65
- custom_tenant_claim = getattr(settings, "jwt_tenant_claim", None)
66
- if custom_tenant_claim and (custom_value := claims.get(custom_tenant_claim)):
67
- tenant_slug = _normalize_tenant_slug(custom_value)
68
- logger.info(
69
- "Extracted tenant from custom claim",
70
- claim_name=custom_tenant_claim,
71
- custom_value=custom_value,
72
- tenant_slug=tenant_slug,
73
- )
74
- return tenant_slug
75
-
76
- # Strategy 4: Domain-based extraction from email
77
- if settings.multi_tenant_mode and (email := claims.get("email")):
78
- tenant_slug = _extract_tenant_from_email_domain(email)
79
- if tenant_slug:
80
- logger.info(
81
- "Extracted tenant from email domain",
82
- email=email,
83
- tenant_slug=tenant_slug,
84
- )
85
- return tenant_slug
86
-
87
- # Strategy 5: Namespace/sub-organization claims
88
- namespace_claims = ["namespace", "group", "team", "workspace"]
89
- for claim_name in namespace_claims:
90
- if namespace_value := claims.get(claim_name):
91
- tenant_slug = _normalize_tenant_slug(namespace_value)
92
- logger.info(
93
- "Extracted tenant from namespace claim",
94
- claim_name=claim_name,
95
- namespace_value=namespace_value,
96
- tenant_slug=tenant_slug,
97
- )
98
- return tenant_slug
99
-
100
- # No tenant found in claims, use fallback
101
- logger.debug(
102
- "No tenant information found in JWT/OIDC claims",
103
- available_claims=list(claims.keys()),
104
- using_fallback=fallback_tenant or settings.default_tenant_slug,
105
- )
106
- return fallback_tenant or settings.default_tenant_slug
107
-
108
-
109
- def extract_tenant_from_oidc_userinfo(
110
- userinfo: dict[str, Any],
111
- fallback_tenant: str | None = None,
112
- ) -> str:
113
- """
114
- Extract tenant slug from OIDC userinfo endpoint response.
115
-
116
- Args:
117
- userinfo: Response from OIDC userinfo endpoint
118
- fallback_tenant: Fallback tenant slug if no tenant found
119
-
120
- Returns:
121
- Tenant slug extracted from userinfo or fallback
122
- """
123
- if not userinfo:
124
- return fallback_tenant or settings.default_tenant_slug
125
-
126
- # Similar extraction strategies as claims
127
- if tenant_slug := userinfo.get("tenant"):
128
- return _validate_tenant_slug(tenant_slug)
129
-
130
- # Organization-based extraction
131
- for org_field in ["organization", "org", "company"]:
132
- if org_value := userinfo.get(org_field):
133
- return _normalize_tenant_slug(org_value)
134
-
135
- # Groups/roles extraction
136
- if groups := userinfo.get("groups", []):
137
- if isinstance(groups, list) and groups:
138
- # Use first group as tenant
139
- return _normalize_tenant_slug(groups[0])
140
-
141
- return fallback_tenant or settings.default_tenant_slug
142
-
143
-
144
- def _normalize_tenant_slug(value: Any) -> str:
145
- """
146
- Normalize a value to a valid tenant slug.
147
-
148
- Args:
149
- value: Value to normalize (string, dict, etc.)
150
-
151
- Returns:
152
- Normalized tenant slug
153
- """
154
- if isinstance(value, dict):
155
- # Extract slug if it's an object with slug/id fields
156
- return _normalize_tenant_slug(value.get("slug") or value.get("id") or value.get("name", ""))
157
-
158
- # Convert to string and normalize
159
- slug = str(value).lower().strip()
160
-
161
- # Replace spaces and invalid characters with hyphens
162
- import re
163
-
164
- slug = re.sub(r"[^a-z0-9-]", "-", slug)
165
-
166
- # Remove multiple consecutive hyphens
167
- slug = re.sub(r"-+", "-", slug)
168
-
169
- # Remove leading/trailing hyphens
170
- slug = slug.strip("-")
171
-
172
- # Ensure it's not empty and not too long
173
- if not slug:
174
- slug = "unknown"
175
- elif len(slug) > 50: # Reasonable limit for tenant slugs
176
- slug = slug[:50].rstrip("-")
177
-
178
- return slug
179
-
180
-
181
- def _validate_tenant_slug(slug: str) -> str:
182
- """
183
- Validate a tenant slug format.
184
-
185
- Args:
186
- slug: Tenant slug to validate
187
-
188
- Returns:
189
- Validated slug
190
-
191
- Raises:
192
- ValueError: If slug is invalid
193
- """
194
- if not slug:
195
- raise ValueError("Tenant slug cannot be empty")
196
-
197
- if len(slug) > 255:
198
- raise ValueError("Tenant slug too long (max 255 characters)")
199
-
200
- import re
201
-
202
- if not re.match(r"^[a-z0-9-]+$", slug):
203
- raise ValueError("Tenant slug must contain only lowercase letters, numbers, and hyphens")
204
-
205
- if slug.startswith("-") or slug.endswith("-"):
206
- raise ValueError("Tenant slug cannot start or end with hyphen")
207
-
208
- return slug
209
-
210
-
211
- def _extract_tenant_from_email_domain(email: str) -> str | None:
212
- """
213
- Extract tenant slug from email domain.
214
-
215
- This is useful for organizations that want to automatically
216
- assign tenants based on email domains.
217
-
218
- Args:
219
- email: Email address
220
-
221
- Returns:
222
- Tenant slug derived from domain, or None if not applicable
223
- """
224
- try:
225
- domain = email.split("@")[1].lower()
226
-
227
- # Skip common public email domains
228
- public_domains = {
229
- "gmail.com",
230
- "yahoo.com",
231
- "outlook.com",
232
- "hotmail.com",
233
- "icloud.com",
234
- "protonmail.com",
235
- "aol.com",
236
- }
237
-
238
- if domain in public_domains:
239
- logger.debug(
240
- "Skipping tenant extraction from public email domain",
241
- domain=domain,
242
- )
243
- return None
244
-
245
- # Extract organization name from domain
246
- # e.g., "user@acme-corp.com" -> "acme-corp"
247
- org_name = domain.split(".")[0]
248
- return _normalize_tenant_slug(org_name)
249
-
250
- except (IndexError, AttributeError):
251
- logger.warning("Invalid email format for domain extraction", email=email)
252
- return None
253
-
254
-
255
- def get_tenant_extraction_config() -> dict[str, Any]:
256
- """
257
- Get current tenant extraction configuration.
258
-
259
- Returns:
260
- Dictionary with tenant extraction settings
261
- """
262
- return {
263
- "multi_tenant_mode": settings.multi_tenant_mode,
264
- "default_tenant_slug": settings.default_tenant_slug,
265
- "custom_tenant_claim": getattr(settings, "jwt_tenant_claim", None),
266
- "domain_based_extraction": settings.multi_tenant_mode,
267
- "supported_claim_names": [
268
- "tenant",
269
- "org",
270
- "organization",
271
- "org_slug",
272
- "org_name",
273
- "namespace",
274
- "group",
275
- "team",
276
- "workspace",
277
- ],
278
- }