@nextsparkjs/core 0.1.0-beta.92 → 0.1.0-beta.94

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 (341) hide show
  1. package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
  2. package/dist/components/dashboard/block-editor/array-field.js +55 -3
  3. package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
  4. package/dist/components/dashboard/block-editor/dynamic-form.js +82 -2
  5. package/dist/components/dashboard/navigation/DynamicNavigation.d.ts.map +1 -1
  6. package/dist/components/dashboard/navigation/DynamicNavigation.js +7 -1
  7. package/dist/components/devtools/scheduled-actions/actions-table.d.ts +1 -0
  8. package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
  9. package/dist/components/devtools/scheduled-actions/actions-table.js +182 -46
  10. package/dist/components/devtools/scheduled-actions/types.d.ts +1 -0
  11. package/dist/components/devtools/scheduled-actions/types.d.ts.map +1 -1
  12. package/dist/components/media/MediaCard.d.ts +23 -0
  13. package/dist/components/media/MediaCard.d.ts.map +1 -0
  14. package/dist/components/media/MediaCard.js +154 -0
  15. package/dist/components/media/MediaDetailPanel.d.ts +17 -0
  16. package/dist/components/media/MediaDetailPanel.d.ts.map +1 -0
  17. package/dist/components/media/MediaDetailPanel.js +331 -0
  18. package/dist/components/media/MediaGrid.d.ts +26 -0
  19. package/dist/components/media/MediaGrid.d.ts.map +1 -0
  20. package/dist/components/media/MediaGrid.js +77 -0
  21. package/dist/components/media/MediaLibrary.d.ts +20 -0
  22. package/dist/components/media/MediaLibrary.d.ts.map +1 -0
  23. package/dist/components/media/MediaLibrary.js +229 -0
  24. package/dist/components/media/MediaList.d.ts +24 -0
  25. package/dist/components/media/MediaList.d.ts.map +1 -0
  26. package/dist/components/media/MediaList.js +181 -0
  27. package/dist/components/media/MediaSelector.d.ts +19 -0
  28. package/dist/components/media/MediaSelector.d.ts.map +1 -0
  29. package/dist/components/media/MediaSelector.js +145 -0
  30. package/dist/components/media/MediaTagFilter.d.ts +16 -0
  31. package/dist/components/media/MediaTagFilter.d.ts.map +1 -0
  32. package/dist/components/media/MediaTagFilter.js +122 -0
  33. package/dist/components/media/MediaToolbar.d.ts +25 -0
  34. package/dist/components/media/MediaToolbar.d.ts.map +1 -0
  35. package/dist/components/media/MediaToolbar.js +136 -0
  36. package/dist/components/media/MediaUploadZone.d.ts +19 -0
  37. package/dist/components/media/MediaUploadZone.d.ts.map +1 -0
  38. package/dist/components/media/MediaUploadZone.js +248 -0
  39. package/dist/components/media/index.d.ts +15 -0
  40. package/dist/components/media/index.d.ts.map +1 -0
  41. package/dist/components/media/index.js +20 -0
  42. package/dist/contexts/TeamContext.js +1 -1
  43. package/dist/hooks/index.d.ts +2 -0
  44. package/dist/hooks/index.d.ts.map +1 -1
  45. package/dist/hooks/index.js +2 -0
  46. package/dist/hooks/useEnsureUserMetadata.d.ts +4 -0
  47. package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
  48. package/dist/hooks/useEnsureUserMetadata.js +85 -60
  49. package/dist/hooks/useEntityMutations.d.ts.map +1 -1
  50. package/dist/hooks/useEntityMutations.js +5 -9
  51. package/dist/hooks/useMedia.d.ts +56 -0
  52. package/dist/hooks/useMedia.d.ts.map +1 -0
  53. package/dist/hooks/useMedia.js +181 -0
  54. package/dist/hooks/useMediaUpload.d.ts +27 -0
  55. package/dist/hooks/useMediaUpload.d.ts.map +1 -0
  56. package/dist/hooks/useMediaUpload.js +36 -0
  57. package/dist/hooks/useUserSettings.d.ts +5 -4
  58. package/dist/hooks/useUserSettings.d.ts.map +1 -1
  59. package/dist/hooks/useUserSettings.js +42 -40
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +2 -3
  63. package/dist/lib/api/auth/dual-auth.d.ts +6 -2
  64. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  65. package/dist/lib/api/auth/dual-auth.js +5 -9
  66. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  67. package/dist/lib/api/entity/generic-handler.js +3 -3
  68. package/dist/lib/config/app.config.d.ts.map +1 -1
  69. package/dist/lib/config/app.config.js +37 -0
  70. package/dist/lib/config/config-sync.d.ts +1 -0
  71. package/dist/lib/config/config-sync.d.ts.map +1 -1
  72. package/dist/lib/config/config-sync.js +2 -0
  73. package/dist/lib/config/types.d.ts +29 -0
  74. package/dist/lib/config/types.d.ts.map +1 -1
  75. package/dist/lib/media/schemas.d.ts +39 -0
  76. package/dist/lib/media/schemas.d.ts.map +1 -0
  77. package/dist/lib/media/schemas.js +32 -0
  78. package/dist/lib/media/types.d.ts +69 -0
  79. package/dist/lib/media/types.d.ts.map +1 -0
  80. package/dist/lib/media/types.js +0 -0
  81. package/dist/lib/media/utils.d.ts +26 -0
  82. package/dist/lib/media/utils.d.ts.map +1 -0
  83. package/dist/lib/media/utils.js +33 -0
  84. package/dist/lib/rate-limit-redis.d.ts.map +1 -1
  85. package/dist/lib/rate-limit-redis.js +13 -4
  86. package/dist/lib/scheduled-actions/initializer.d.ts +6 -3
  87. package/dist/lib/scheduled-actions/initializer.d.ts.map +1 -1
  88. package/dist/lib/scheduled-actions/initializer.js +11 -6
  89. package/dist/lib/scheduled-actions/processor.d.ts +20 -4
  90. package/dist/lib/scheduled-actions/processor.d.ts.map +1 -1
  91. package/dist/lib/scheduled-actions/processor.js +128 -34
  92. package/dist/lib/scheduled-actions/registry.d.ts +3 -0
  93. package/dist/lib/scheduled-actions/registry.d.ts.map +1 -1
  94. package/dist/lib/scheduled-actions/registry.js +2 -1
  95. package/dist/lib/scheduled-actions/scheduler.d.ts +1 -1
  96. package/dist/lib/scheduled-actions/scheduler.d.ts.map +1 -1
  97. package/dist/lib/scheduled-actions/scheduler.js +76 -38
  98. package/dist/lib/scheduled-actions/types.d.ts +73 -0
  99. package/dist/lib/scheduled-actions/types.d.ts.map +1 -1
  100. package/dist/lib/selectors/core-selectors.d.ts +102 -0
  101. package/dist/lib/selectors/core-selectors.d.ts.map +1 -1
  102. package/dist/lib/selectors/core-selectors.js +3 -1
  103. package/dist/lib/selectors/domains/block-editor.selectors.d.ts +8 -0
  104. package/dist/lib/selectors/domains/block-editor.selectors.d.ts.map +1 -1
  105. package/dist/lib/selectors/domains/block-editor.selectors.js +9 -0
  106. package/dist/lib/selectors/domains/devtools.selectors.d.ts +6 -0
  107. package/dist/lib/selectors/domains/devtools.selectors.d.ts.map +1 -1
  108. package/dist/lib/selectors/domains/devtools.selectors.js +6 -0
  109. package/dist/lib/selectors/domains/index.d.ts +1 -0
  110. package/dist/lib/selectors/domains/index.d.ts.map +1 -1
  111. package/dist/lib/selectors/domains/index.js +2 -0
  112. package/dist/lib/selectors/domains/media.selectors.d.ts +96 -0
  113. package/dist/lib/selectors/domains/media.selectors.d.ts.map +1 -0
  114. package/dist/lib/selectors/domains/media.selectors.js +103 -0
  115. package/dist/lib/selectors/selectors.d.ts +204 -0
  116. package/dist/lib/selectors/selectors.d.ts.map +1 -1
  117. package/dist/lib/services/index.d.ts +2 -0
  118. package/dist/lib/services/index.d.ts.map +1 -1
  119. package/dist/lib/services/index.js +2 -0
  120. package/dist/lib/services/media.service.d.ts +158 -0
  121. package/dist/lib/services/media.service.d.ts.map +1 -0
  122. package/dist/lib/services/media.service.js +410 -0
  123. package/dist/messages/de/devtools.json +16 -0
  124. package/dist/messages/de/index.d.ts +16 -0
  125. package/dist/messages/de/index.d.ts.map +1 -1
  126. package/dist/messages/en/admin.json +4 -1
  127. package/dist/messages/en/devtools.json +16 -0
  128. package/dist/messages/en/index.d.ts +167 -0
  129. package/dist/messages/en/index.d.ts.map +1 -1
  130. package/dist/messages/en/index.js +2 -0
  131. package/dist/messages/en/index.ts +2 -0
  132. package/dist/messages/en/media.json +147 -0
  133. package/dist/messages/en/navigation.json +1 -0
  134. package/dist/messages/es/admin.json +4 -1
  135. package/dist/messages/es/devtools.json +16 -0
  136. package/dist/messages/es/index.d.ts +167 -0
  137. package/dist/messages/es/index.d.ts.map +1 -1
  138. package/dist/messages/es/index.js +2 -0
  139. package/dist/messages/es/index.ts +2 -0
  140. package/dist/messages/es/media.json +147 -0
  141. package/dist/messages/es/navigation.json +1 -0
  142. package/dist/messages/fr/devtools.json +16 -0
  143. package/dist/messages/fr/index.d.ts +16 -0
  144. package/dist/messages/fr/index.d.ts.map +1 -1
  145. package/dist/messages/it/devtools.json +16 -0
  146. package/dist/messages/it/index.d.ts +16 -0
  147. package/dist/messages/it/index.d.ts.map +1 -1
  148. package/dist/messages/pt/devtools.json +16 -0
  149. package/dist/messages/pt/index.d.ts +16 -0
  150. package/dist/messages/pt/index.d.ts.map +1 -1
  151. package/dist/migrations/017_scheduled_actions_table.sql +21 -0
  152. package/dist/migrations/021_media.sql +154 -0
  153. package/dist/migrations/090_sample_data.sql +53 -0
  154. package/dist/styles/classes.json +36 -3
  155. package/dist/styles/ui.css +1 -1
  156. package/dist/templates/app/api/devtools/config/entities/route.ts +18 -11
  157. package/dist/templates/app/api/devtools/config/theme/route.ts +5 -4
  158. package/dist/templates/app/api/devtools/tests/[...path]/route.ts +6 -5
  159. package/dist/templates/app/api/devtools/tests/route.ts +5 -4
  160. package/dist/templates/app/api/health/route.ts +6 -4
  161. package/dist/templates/app/api/internal/user-metadata/route.ts +3 -2
  162. package/dist/templates/app/api/superadmin/subscriptions/route.ts +5 -6
  163. package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -7
  164. package/dist/templates/app/api/superadmin/teams/route.ts +5 -6
  165. package/dist/templates/app/api/superadmin/users/[userId]/route.ts +11 -16
  166. package/dist/templates/app/api/superadmin/users/route.ts +9 -10
  167. package/dist/templates/app/api/user/delete-account/route.ts +3 -2
  168. package/dist/templates/app/api/user/plan-flags/route.ts +11 -24
  169. package/dist/templates/app/api/user/profile/route.ts +7 -6
  170. package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +16 -18
  171. package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +17 -19
  172. package/dist/templates/app/api/v1/[entity]/[id]/route.ts +10 -12
  173. package/dist/templates/app/api/v1/[entity]/route.ts +9 -11
  174. package/dist/templates/app/api/v1/api-keys/[id]/route.ts +9 -8
  175. package/dist/templates/app/api/v1/api-keys/route.ts +7 -6
  176. package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +3 -2
  177. package/dist/templates/app/api/v1/billing/cancel/route.ts +15 -14
  178. package/dist/templates/app/api/v1/billing/change-plan/route.ts +10 -9
  179. package/dist/templates/app/api/v1/billing/check-action/route.ts +8 -7
  180. package/dist/templates/app/api/v1/billing/checkout/route.ts +10 -9
  181. package/dist/templates/app/api/v1/billing/plans/route.ts +5 -4
  182. package/dist/templates/app/api/v1/billing/portal/route.ts +9 -8
  183. package/dist/templates/app/api/v1/blocks/[slug]/route.ts +4 -3
  184. package/dist/templates/app/api/v1/blocks/route.ts +3 -2
  185. package/dist/templates/app/api/v1/blocks/validate/route.ts +5 -3
  186. package/dist/templates/app/api/v1/cron/process/route.ts +4 -6
  187. package/dist/templates/app/api/v1/devtools/blocks/route.ts +3 -2
  188. package/dist/templates/app/api/v1/devtools/docs/route.ts +3 -2
  189. package/dist/templates/app/api/v1/devtools/features/route.ts +3 -2
  190. package/dist/templates/app/api/v1/devtools/flows/route.ts +3 -2
  191. package/dist/templates/app/api/v1/devtools/scheduled-actions/route.ts +125 -3
  192. package/dist/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +110 -0
  193. package/dist/templates/app/api/v1/devtools/testing/route.ts +3 -2
  194. package/dist/templates/app/api/v1/media/[id]/route.ts +144 -0
  195. package/dist/templates/app/api/v1/media/[id]/tags/route.ts +154 -0
  196. package/dist/templates/app/api/v1/media/check-duplicates/route.ts +56 -0
  197. package/dist/templates/app/api/v1/media/route.ts +56 -0
  198. package/dist/templates/app/api/v1/media/upload/route.ts +157 -33
  199. package/dist/templates/app/api/v1/media-tags/route.ts +65 -0
  200. package/dist/templates/app/api/v1/plugin/[...path]/route.ts +16 -15
  201. package/dist/templates/app/api/v1/plugin/route.ts +3 -2
  202. package/dist/templates/app/api/v1/post-categories/[id]/route.ts +10 -9
  203. package/dist/templates/app/api/v1/post-categories/route.ts +5 -4
  204. package/dist/templates/app/api/v1/team-invitations/[token]/accept/route.ts +3 -3
  205. package/dist/templates/app/api/v1/team-invitations/[token]/decline/route.ts +3 -3
  206. package/dist/templates/app/api/v1/team-invitations/[token]/route.ts +3 -2
  207. package/dist/templates/app/api/v1/team-invitations/route.ts +3 -2
  208. package/dist/templates/app/api/v1/teams/[teamId]/invitations/route.ts +5 -4
  209. package/dist/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +3 -2
  210. package/dist/templates/app/api/v1/teams/[teamId]/invoices/route.ts +3 -2
  211. package/dist/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +5 -4
  212. package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +5 -5
  213. package/dist/templates/app/api/v1/teams/[teamId]/route.ts +31 -58
  214. package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -2
  215. package/dist/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +5 -4
  216. package/dist/templates/app/api/v1/teams/route.ts +18 -17
  217. package/dist/templates/app/api/v1/teams/switch/route.ts +3 -2
  218. package/dist/templates/app/api/v1/theme/[...path]/route.ts +16 -15
  219. package/dist/templates/app/api/v1/theme/route.ts +3 -2
  220. package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +7 -6
  221. package/dist/templates/app/api/v1/users/[id]/route.ts +9 -8
  222. package/dist/templates/app/api/v1/users/route.ts +7 -6
  223. package/dist/templates/app/dashboard/(main)/media/page.tsx +607 -0
  224. package/dist/templates/contents/themes/starter/messages/de/dev.json +106 -0
  225. package/dist/templates/contents/themes/starter/messages/de/index.ts +2 -0
  226. package/dist/templates/contents/themes/starter/messages/en/dev.json +106 -0
  227. package/dist/templates/contents/themes/starter/messages/en/index.ts +2 -0
  228. package/dist/templates/contents/themes/starter/messages/es/dev.json +106 -0
  229. package/dist/templates/contents/themes/starter/messages/es/index.ts +2 -0
  230. package/dist/templates/contents/themes/starter/messages/fr/dev.json +106 -0
  231. package/dist/templates/contents/themes/starter/messages/fr/index.ts +2 -0
  232. package/dist/templates/contents/themes/starter/messages/it/dev.json +106 -0
  233. package/dist/templates/contents/themes/starter/messages/it/index.ts +2 -0
  234. package/dist/templates/contents/themes/starter/messages/pt/dev.json +106 -0
  235. package/dist/templates/contents/themes/starter/messages/pt/index.ts +2 -0
  236. package/dist/templates/contents/themes/starter/styles/globals.css +14 -0
  237. package/dist/templates/instrumentation.ts +33 -0
  238. package/dist/types/blocks.d.ts +1 -1
  239. package/dist/types/blocks.d.ts.map +1 -1
  240. package/migrations/017_scheduled_actions_table.sql +21 -0
  241. package/migrations/021_media.sql +154 -0
  242. package/migrations/090_sample_data.sql +53 -0
  243. package/package.json +3 -2
  244. package/scripts/build/registry/config.mjs +41 -0
  245. package/scripts/build/registry/discovery/templates.mjs +0 -1
  246. package/scripts/build/registry/generators/entity-registry.mjs +16 -6
  247. package/scripts/build/registry/generators/route-handlers.mjs +8 -2
  248. package/scripts/build/registry/generators/template-registry.mjs +16 -4
  249. package/scripts/build/registry/post-build/route-cleanup.mjs +0 -1
  250. package/scripts/build/registry/validate-env.test.mjs +92 -0
  251. package/scripts/build/registry.mjs +18 -1
  252. package/scripts/deploy/vercel-deploy.mjs +1 -1
  253. package/templates/app/api/devtools/config/entities/route.ts +18 -11
  254. package/templates/app/api/devtools/config/theme/route.ts +5 -4
  255. package/templates/app/api/devtools/tests/[...path]/route.ts +6 -5
  256. package/templates/app/api/devtools/tests/route.ts +5 -4
  257. package/templates/app/api/health/route.ts +6 -4
  258. package/templates/app/api/internal/user-metadata/route.ts +3 -2
  259. package/templates/app/api/superadmin/subscriptions/route.ts +5 -6
  260. package/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -7
  261. package/templates/app/api/superadmin/teams/route.ts +5 -6
  262. package/templates/app/api/superadmin/users/[userId]/route.ts +11 -16
  263. package/templates/app/api/superadmin/users/route.ts +9 -10
  264. package/templates/app/api/user/delete-account/route.ts +3 -2
  265. package/templates/app/api/user/plan-flags/route.ts +11 -24
  266. package/templates/app/api/user/profile/route.ts +7 -6
  267. package/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +16 -18
  268. package/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +17 -19
  269. package/templates/app/api/v1/[entity]/[id]/route.ts +10 -12
  270. package/templates/app/api/v1/[entity]/route.ts +9 -11
  271. package/templates/app/api/v1/api-keys/[id]/route.ts +9 -8
  272. package/templates/app/api/v1/api-keys/route.ts +7 -6
  273. package/templates/app/api/v1/auth/signup-with-invite/route.ts +3 -2
  274. package/templates/app/api/v1/billing/cancel/route.ts +15 -14
  275. package/templates/app/api/v1/billing/change-plan/route.ts +10 -9
  276. package/templates/app/api/v1/billing/check-action/route.ts +8 -7
  277. package/templates/app/api/v1/billing/checkout/route.ts +10 -9
  278. package/templates/app/api/v1/billing/plans/route.ts +5 -4
  279. package/templates/app/api/v1/billing/portal/route.ts +9 -8
  280. package/templates/app/api/v1/blocks/[slug]/route.ts +4 -3
  281. package/templates/app/api/v1/blocks/route.ts +3 -2
  282. package/templates/app/api/v1/blocks/validate/route.ts +5 -3
  283. package/templates/app/api/v1/cron/process/route.ts +4 -6
  284. package/templates/app/api/v1/devtools/blocks/route.ts +3 -2
  285. package/templates/app/api/v1/devtools/docs/route.ts +3 -2
  286. package/templates/app/api/v1/devtools/features/route.ts +3 -2
  287. package/templates/app/api/v1/devtools/flows/route.ts +3 -2
  288. package/templates/app/api/v1/devtools/scheduled-actions/route.ts +125 -3
  289. package/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +110 -0
  290. package/templates/app/api/v1/devtools/testing/route.ts +3 -2
  291. package/templates/app/api/v1/media/[id]/route.ts +144 -0
  292. package/templates/app/api/v1/media/[id]/tags/route.ts +154 -0
  293. package/templates/app/api/v1/media/check-duplicates/route.ts +56 -0
  294. package/templates/app/api/v1/media/route.ts +56 -0
  295. package/templates/app/api/v1/media/upload/route.ts +157 -33
  296. package/templates/app/api/v1/media-tags/route.ts +65 -0
  297. package/templates/app/api/v1/plugin/[...path]/route.ts +16 -15
  298. package/templates/app/api/v1/plugin/route.ts +3 -2
  299. package/templates/app/api/v1/post-categories/[id]/route.ts +10 -9
  300. package/templates/app/api/v1/post-categories/route.ts +5 -4
  301. package/templates/app/api/v1/team-invitations/[token]/accept/route.ts +3 -3
  302. package/templates/app/api/v1/team-invitations/[token]/decline/route.ts +3 -3
  303. package/templates/app/api/v1/team-invitations/[token]/route.ts +3 -2
  304. package/templates/app/api/v1/team-invitations/route.ts +3 -2
  305. package/templates/app/api/v1/teams/[teamId]/invitations/route.ts +5 -4
  306. package/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +3 -2
  307. package/templates/app/api/v1/teams/[teamId]/invoices/route.ts +3 -2
  308. package/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +5 -4
  309. package/templates/app/api/v1/teams/[teamId]/members/route.ts +5 -5
  310. package/templates/app/api/v1/teams/[teamId]/route.ts +31 -58
  311. package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -2
  312. package/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +5 -4
  313. package/templates/app/api/v1/teams/route.ts +18 -17
  314. package/templates/app/api/v1/teams/switch/route.ts +3 -2
  315. package/templates/app/api/v1/theme/[...path]/route.ts +16 -15
  316. package/templates/app/api/v1/theme/route.ts +3 -2
  317. package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +7 -6
  318. package/templates/app/api/v1/users/[id]/route.ts +9 -8
  319. package/templates/app/api/v1/users/route.ts +7 -6
  320. package/templates/app/dashboard/(main)/media/page.tsx +607 -0
  321. package/templates/contents/themes/starter/messages/de/dev.json +106 -0
  322. package/templates/contents/themes/starter/messages/de/index.ts +2 -0
  323. package/templates/contents/themes/starter/messages/en/dev.json +106 -0
  324. package/templates/contents/themes/starter/messages/en/index.ts +2 -0
  325. package/templates/contents/themes/starter/messages/es/dev.json +106 -0
  326. package/templates/contents/themes/starter/messages/es/index.ts +2 -0
  327. package/templates/contents/themes/starter/messages/fr/dev.json +106 -0
  328. package/templates/contents/themes/starter/messages/fr/index.ts +2 -0
  329. package/templates/contents/themes/starter/messages/it/dev.json +106 -0
  330. package/templates/contents/themes/starter/messages/it/index.ts +2 -0
  331. package/templates/contents/themes/starter/messages/pt/dev.json +106 -0
  332. package/templates/contents/themes/starter/messages/pt/index.ts +2 -0
  333. package/templates/contents/themes/starter/styles/globals.css +14 -0
  334. package/templates/instrumentation.ts +33 -0
  335. package/dist/presets/plugin/.env.example.template +0 -19
  336. package/dist/presets/plugin/entities/.gitkeep +0 -18
  337. package/dist/presets/theme/blocks/.gitkeep +0 -17
  338. package/dist/presets/theme/public/brand/.gitkeep +0 -8
  339. package/dist/presets/theme/tests/cypress/.gitkeep +0 -10
  340. package/dist/templates/contents/plugins/starter/plugin/.env.example.template +0 -19
  341. package/templates/contents/plugins/starter/plugin/.env.example.template +0 -19
@@ -0,0 +1,144 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
3
+ import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
4
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
5
+ import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
6
+ import { updateMediaSchema } from '@nextsparkjs/core/lib/media/schemas'
7
+
8
+ /**
9
+ * GET /api/v1/media/:id
10
+ *
11
+ * Get a single media item by ID.
12
+ *
13
+ * Authentication: Requires valid session or API key with media:read scope
14
+ * RLS: Returns only media from teams the user is a member of
15
+ */
16
+ export const GET = withRateLimitTier(async (
17
+ request: NextRequest,
18
+ { params }: { params: Promise<{ id: string }> }
19
+ ) => {
20
+ try {
21
+ // 1. Authenticate
22
+ const authResult = await authenticateRequest(request)
23
+ if (!authResult.success) {
24
+ return createApiError('Unauthorized', 401)
25
+ }
26
+
27
+ // 2. Check permissions
28
+ if (!hasRequiredScope(authResult, 'media:read')) {
29
+ return createApiError('Insufficient permissions', 403)
30
+ }
31
+
32
+ // 3. Get media ID from params
33
+ const { id } = await params
34
+
35
+ // 4. Fetch media with RLS
36
+ const media = await MediaService.getById(id, authResult.user!.id)
37
+
38
+ if (!media) {
39
+ return createApiError('Media not found', 404)
40
+ }
41
+
42
+ return createApiResponse(media)
43
+ } catch (error) {
44
+ console.error('[Media API] Error fetching media:', error)
45
+ return createApiError('Failed to fetch media', 500)
46
+ }
47
+ }, 'read')
48
+
49
+ /**
50
+ * PATCH /api/v1/media/:id
51
+ *
52
+ * Update media metadata (alt text and caption).
53
+ * File properties (url, filename, size, dimensions) are immutable.
54
+ *
55
+ * Request Body:
56
+ * - alt: Alt text for accessibility (max 500 characters, optional)
57
+ * - caption: Caption or description (max 1000 characters, optional)
58
+ *
59
+ * Authentication: Requires valid session or API key with media:write scope
60
+ * RLS: Can only update media from teams the user is a member of
61
+ */
62
+ export const PATCH = withRateLimitTier(async (
63
+ request: NextRequest,
64
+ { params }: { params: Promise<{ id: string }> }
65
+ ) => {
66
+ try {
67
+ // 1. Authenticate
68
+ const authResult = await authenticateRequest(request)
69
+ if (!authResult.success) {
70
+ return createApiError('Unauthorized', 401)
71
+ }
72
+
73
+ // 2. Check permissions
74
+ if (!hasRequiredScope(authResult, 'media:write')) {
75
+ return createApiError('Insufficient permissions', 403)
76
+ }
77
+
78
+ // 3. Get media ID from params
79
+ const { id } = await params
80
+
81
+ // 4. Parse and validate request body
82
+ const body = await request.json()
83
+ const parsed = updateMediaSchema.safeParse(body)
84
+
85
+ if (!parsed.success) {
86
+ return createApiError('Validation failed', 400, {
87
+ errors: parsed.error.issues,
88
+ })
89
+ }
90
+
91
+ // 5. Update media with RLS
92
+ const media = await MediaService.update(id, authResult.user!.id, parsed.data)
93
+
94
+ return createApiResponse(media)
95
+ } catch (error) {
96
+ console.error('[Media API] Error updating media:', error)
97
+ return createApiError(
98
+ error instanceof Error ? error.message : 'Failed to update media',
99
+ 500
100
+ )
101
+ }
102
+ }, 'write')
103
+
104
+ /**
105
+ * DELETE /api/v1/media/:id
106
+ *
107
+ * Soft delete a media item (sets status to 'deleted').
108
+ * The file remains in storage but is hidden from queries.
109
+ *
110
+ * Authentication: Requires valid session or API key with media:delete scope
111
+ * RLS: Can only delete media from teams the user is a member of
112
+ */
113
+ export const DELETE = withRateLimitTier(async (
114
+ request: NextRequest,
115
+ { params }: { params: Promise<{ id: string }> }
116
+ ) => {
117
+ try {
118
+ // 1. Authenticate
119
+ const authResult = await authenticateRequest(request)
120
+ if (!authResult.success) {
121
+ return createApiError('Unauthorized', 401)
122
+ }
123
+
124
+ // 2. Check permissions
125
+ if (!hasRequiredScope(authResult, 'media:delete')) {
126
+ return createApiError('Insufficient permissions', 403)
127
+ }
128
+
129
+ // 3. Get media ID from params
130
+ const { id } = await params
131
+
132
+ // 4. Soft delete media with RLS
133
+ const deleted = await MediaService.softDelete(id, authResult.user!.id)
134
+
135
+ if (!deleted) {
136
+ return createApiError('Media not found', 404)
137
+ }
138
+
139
+ return createApiResponse({ message: 'Media deleted successfully' })
140
+ } catch (error) {
141
+ console.error('[Media API] Error deleting media:', error)
142
+ return createApiError('Failed to delete media', 500)
143
+ }
144
+ }, 'write')
@@ -0,0 +1,154 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
3
+ import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
4
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
5
+ import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
6
+ import { z } from 'zod'
7
+
8
+ const addTagSchema = z.object({
9
+ tagId: z.string().min(1),
10
+ })
11
+
12
+ const setTagsSchema = z.object({
13
+ tagIds: z.array(z.string().min(1)),
14
+ })
15
+
16
+ /**
17
+ * GET /api/v1/media/{id}/tags
18
+ *
19
+ * Get all tags assigned to a media item.
20
+ */
21
+ export const GET = withRateLimitTier(async (
22
+ request: NextRequest,
23
+ { params }: { params: Promise<{ id: string }> }
24
+ ) => {
25
+ try {
26
+ const authResult = await authenticateRequest(request)
27
+ if (!authResult.success) {
28
+ return createApiError('Unauthorized', 401)
29
+ }
30
+
31
+ if (!hasRequiredScope(authResult, 'media:read')) {
32
+ return createApiError('Insufficient permissions', 403)
33
+ }
34
+
35
+ const { id } = await params
36
+ const tags = await MediaService.getMediaTags(id, authResult.user!.id)
37
+ return createApiResponse(tags)
38
+ } catch (error) {
39
+ console.error('[Media Tags API] Error getting tags:', error)
40
+ return createApiError('Failed to get media tags', 500)
41
+ }
42
+ }, 'read')
43
+
44
+ /**
45
+ * POST /api/v1/media/{id}/tags
46
+ *
47
+ * Add a tag to a media item.
48
+ * Body: { tagId: string }
49
+ */
50
+ export const POST = withRateLimitTier(async (
51
+ request: NextRequest,
52
+ { params }: { params: Promise<{ id: string }> }
53
+ ) => {
54
+ try {
55
+ const authResult = await authenticateRequest(request)
56
+ if (!authResult.success) {
57
+ return createApiError('Unauthorized', 401)
58
+ }
59
+
60
+ if (!hasRequiredScope(authResult, 'media:write')) {
61
+ return createApiError('Insufficient permissions', 403)
62
+ }
63
+
64
+ const { id } = await params
65
+ const body = await request.json()
66
+ const parsed = addTagSchema.safeParse(body)
67
+
68
+ if (!parsed.success) {
69
+ return createApiError('Invalid request body', 400, { errors: parsed.error.issues })
70
+ }
71
+
72
+ await MediaService.addTag(id, parsed.data.tagId, authResult.user!.id)
73
+ const tags = await MediaService.getMediaTags(id, authResult.user!.id)
74
+
75
+ return createApiResponse(tags, undefined, 201)
76
+ } catch (error) {
77
+ console.error('[Media Tags API] Error adding tag:', error)
78
+ return createApiError('Failed to add tag', 500)
79
+ }
80
+ }, 'write')
81
+
82
+ /**
83
+ * PUT /api/v1/media/{id}/tags
84
+ *
85
+ * Replace all tags for a media item.
86
+ * Body: { tagIds: string[] }
87
+ */
88
+ export const PUT = withRateLimitTier(async (
89
+ request: NextRequest,
90
+ { params }: { params: Promise<{ id: string }> }
91
+ ) => {
92
+ try {
93
+ const authResult = await authenticateRequest(request)
94
+ if (!authResult.success) {
95
+ return createApiError('Unauthorized', 401)
96
+ }
97
+
98
+ if (!hasRequiredScope(authResult, 'media:write')) {
99
+ return createApiError('Insufficient permissions', 403)
100
+ }
101
+
102
+ const { id } = await params
103
+ const body = await request.json()
104
+ const parsed = setTagsSchema.safeParse(body)
105
+
106
+ if (!parsed.success) {
107
+ return createApiError('Invalid request body', 400, { errors: parsed.error.issues })
108
+ }
109
+
110
+ await MediaService.setTags(id, parsed.data.tagIds, authResult.user!.id)
111
+ const tags = await MediaService.getMediaTags(id, authResult.user!.id)
112
+
113
+ return createApiResponse(tags)
114
+ } catch (error) {
115
+ console.error('[Media Tags API] Error setting tags:', error)
116
+ return createApiError('Failed to set tags', 500)
117
+ }
118
+ }, 'write')
119
+
120
+ /**
121
+ * DELETE /api/v1/media/{id}/tags
122
+ *
123
+ * Remove a tag from a media item.
124
+ * Query parameter: tagId
125
+ */
126
+ export const DELETE = withRateLimitTier(async (
127
+ request: NextRequest,
128
+ { params }: { params: Promise<{ id: string }> }
129
+ ) => {
130
+ try {
131
+ const authResult = await authenticateRequest(request)
132
+ if (!authResult.success) {
133
+ return createApiError('Unauthorized', 401)
134
+ }
135
+
136
+ if (!hasRequiredScope(authResult, 'media:delete')) {
137
+ return createApiError('Insufficient permissions', 403)
138
+ }
139
+
140
+ const { id } = await params
141
+ const { searchParams } = new URL(request.url)
142
+ const tagId = searchParams.get('tagId')
143
+
144
+ if (!tagId) {
145
+ return createApiError('tagId query parameter is required', 400)
146
+ }
147
+
148
+ await MediaService.removeTag(id, tagId, authResult.user!.id)
149
+ return createApiResponse({ success: true })
150
+ } catch (error) {
151
+ console.error('[Media Tags API] Error removing tag:', error)
152
+ return createApiError('Failed to remove tag', 500)
153
+ }
154
+ }, 'write')
@@ -0,0 +1,56 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
3
+ import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
4
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
5
+ import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
6
+
7
+ /**
8
+ * POST /api/v1/media/check-duplicates
9
+ *
10
+ * Check if files with the same name+size already exist in the media library.
11
+ * Used by the upload zone to warn users before uploading duplicates.
12
+ *
13
+ * Body: { files: [{ filename: string, fileSize: number }] }
14
+ * Returns: { duplicates: [{ filename, fileSize, existing: Media[] }] }
15
+ */
16
+ export const POST = withRateLimitTier(async (request: NextRequest) => {
17
+ try {
18
+ const authResult = await authenticateRequest(request)
19
+ if (!authResult.success) {
20
+ return createApiError('Unauthorized', 401)
21
+ }
22
+
23
+ if (!hasRequiredScope(authResult, 'media:read')) {
24
+ return createApiError('Insufficient permissions - media:read scope required', 403)
25
+ }
26
+
27
+ const body = await request.json()
28
+ const files = body.files as { filename: string; fileSize: number }[]
29
+
30
+ if (!files || !Array.isArray(files) || files.length === 0) {
31
+ return createApiError('files array is required', 400)
32
+ }
33
+
34
+ const duplicates: { filename: string; fileSize: number; existing: { id: string; url: string; createdAt: string }[] }[] = []
35
+
36
+ for (const file of files) {
37
+ const existing = await MediaService.findDuplicates(
38
+ authResult.user!.id,
39
+ file.filename,
40
+ file.fileSize
41
+ )
42
+ if (existing.length > 0) {
43
+ duplicates.push({
44
+ filename: file.filename,
45
+ fileSize: file.fileSize,
46
+ existing: existing.map(m => ({ id: m.id, url: m.url, createdAt: m.createdAt })),
47
+ })
48
+ }
49
+ }
50
+
51
+ return createApiResponse({ duplicates })
52
+ } catch (error) {
53
+ console.error('Error checking duplicates:', error)
54
+ return createApiError('Failed to check duplicates', 500)
55
+ }
56
+ }, 'read')
@@ -0,0 +1,56 @@
1
+ import { NextRequest } from 'next/server'
2
+ import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
3
+ import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
4
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
5
+ import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
6
+ import { mediaListQuerySchema } from '@nextsparkjs/core/lib/media/schemas'
7
+
8
+ /**
9
+ * GET /api/v1/media
10
+ *
11
+ * List media files with pagination, filtering, and search.
12
+ * Supports filtering by type (image/video), searching by filename, and sorting.
13
+ *
14
+ * Query Parameters:
15
+ * - limit: Number of items per page (default: 20, max: 100)
16
+ * - offset: Number of items to skip (default: 0)
17
+ * - orderBy: Sort field (createdAt|filename|fileSize, default: createdAt)
18
+ * - orderDir: Sort direction (asc|desc, default: desc)
19
+ * - type: Filter by type (image|video|all, default: all)
20
+ * - search: Search by filename (case-insensitive)
21
+ *
22
+ * Authentication: Requires valid session or API key with media:read scope
23
+ * RLS: Returns only media from teams the user is a member of
24
+ */
25
+ export const GET = withRateLimitTier(async (request: NextRequest) => {
26
+ try {
27
+ // 1. Authenticate
28
+ const authResult = await authenticateRequest(request)
29
+ if (!authResult.success) {
30
+ return createApiError('Unauthorized', 401)
31
+ }
32
+
33
+ // 2. Check permissions
34
+ if (!hasRequiredScope(authResult, 'media:read')) {
35
+ return createApiError('Insufficient permissions - media:read scope required', 403)
36
+ }
37
+
38
+ // 3. Parse and validate query parameters
39
+ const { searchParams } = new URL(request.url)
40
+ const parsed = mediaListQuerySchema.safeParse(Object.fromEntries(searchParams))
41
+
42
+ if (!parsed.success) {
43
+ return createApiError('Invalid query parameters', 400, {
44
+ errors: parsed.error.issues,
45
+ })
46
+ }
47
+
48
+ // 4. Query media list with RLS
49
+ const result = await MediaService.list(authResult.user!.id, parsed.data)
50
+
51
+ return createApiResponse(result)
52
+ } catch (error) {
53
+ console.error('[Media API] Error listing media:', error)
54
+ return createApiError('Failed to list media', 500)
55
+ }
56
+ }, 'read')
@@ -2,8 +2,56 @@ import { NextRequest } from 'next/server'
2
2
  import { put } from '@vercel/blob'
3
3
  import { authenticateRequest, hasRequiredScope } from '@nextsparkjs/core/lib/api/auth/dual-auth'
4
4
  import { createApiResponse, createApiError } from '@nextsparkjs/core/lib/api/helpers'
5
+ import { withRateLimitTier } from '@nextsparkjs/core/lib/api/rate-limit'
6
+ import { MEDIA_CONFIG } from '@nextsparkjs/core/lib/config/config-sync'
7
+ import { MediaService } from '@nextsparkjs/core/lib/services/media.service'
8
+ import { extractImageDimensions } from '@nextsparkjs/core/lib/media/utils'
9
+ import type { Media } from '@nextsparkjs/core/lib/media/types'
10
+ import { writeFile, mkdir } from 'fs/promises'
11
+ import { join } from 'path'
12
+ import { existsSync } from 'fs'
5
13
 
6
- export async function POST(request: NextRequest) {
14
+ // Check if Vercel Blob is configured
15
+ // NOTE: We use Vercel Blob even in development when available because some external APIs
16
+ // (like social media platforms) cannot access localhost URLs - they need publicly accessible URLs
17
+ const isVercelBlobConfigured = () => {
18
+ const token = process.env.BLOB_READ_WRITE_TOKEN
19
+ return !!token && token.startsWith('vercel_blob_')
20
+ }
21
+
22
+ // Local storage fallback - accepts buffer directly
23
+ // Used when Vercel Blob is not configured or fails
24
+ async function uploadToLocalStorageBuffer(buffer: Buffer, fileName: string): Promise<string> {
25
+ const uploadDir = join(process.cwd(), 'public', 'uploads', 'temp')
26
+
27
+ // Create directory if it doesn't exist
28
+ if (!existsSync(uploadDir)) {
29
+ await mkdir(uploadDir, { recursive: true })
30
+ }
31
+
32
+ const filePath = join(uploadDir, fileName)
33
+ await writeFile(filePath, buffer)
34
+
35
+ // Return relative URL that can be served from public folder
36
+ return `/uploads/temp/${fileName}`
37
+ }
38
+
39
+ /**
40
+ * POST /api/v1/media/upload
41
+ *
42
+ * Upload media files (images and videos).
43
+ * Enhanced version that creates database records for uploaded files.
44
+ *
45
+ * Features:
46
+ * - Uploads files to Vercel Blob (production) or local storage (development)
47
+ * - Creates media records in database for tracking
48
+ * - Extracts image dimensions automatically
49
+ * - Returns both legacy URLs array and new media records array
50
+ *
51
+ * Authentication: Requires valid session or API key with media:write scope
52
+ * RLS: Media records are associated with user's active team
53
+ */
54
+ export const POST = withRateLimitTier(async (request: NextRequest) => {
7
55
  try {
8
56
  // 1. Dual Authentication (API Key OR Session)
9
57
  const authResult = await authenticateRequest(request)
@@ -19,6 +67,16 @@ export async function POST(request: NextRequest) {
19
67
  return createApiError('Insufficient permissions - media:write scope required', 403)
20
68
  }
21
69
 
70
+ // 3. Get team context (x-team-id header or default team)
71
+ const teamId = request.headers.get('x-team-id') || authResult.user!.defaultTeamId
72
+
73
+ if (!teamId) {
74
+ return createApiError(
75
+ 'No team context available. Please provide x-team-id header or have a default team.',
76
+ 400
77
+ )
78
+ }
79
+
22
80
  const formData = await request.formData()
23
81
  const files = formData.getAll('files') as File[]
24
82
 
@@ -27,23 +85,21 @@ export async function POST(request: NextRequest) {
27
85
  }
28
86
 
29
87
  const uploadedUrls: string[] = []
88
+ const uploadedMedia: Media[] = []
89
+ const useVercelBlob = isVercelBlobConfigured()
90
+
91
+ console.log(`📤 [Media Upload] Storage mode: ${useVercelBlob ? 'Vercel Blob' : 'Local Storage'}`)
92
+ console.log(`📤 [Media Upload] Team context: ${teamId}`)
30
93
 
31
94
  for (const file of files) {
32
95
  if (!file.size) {
33
96
  continue // Skip empty files
34
97
  }
35
98
 
36
- // Validate file type (images and videos)
37
- const allowedTypes = [
38
- 'image/jpeg',
39
- 'image/jpg',
40
- 'image/png',
41
- 'image/gif',
42
- 'image/webp',
43
- 'video/mp4',
44
- 'video/mpeg',
45
- 'video/quicktime',
46
- 'video/webm'
99
+ // Validate file type using config (theme-overridable)
100
+ const allowedTypes = MEDIA_CONFIG?.allowedMimeTypes ?? [
101
+ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp',
102
+ 'video/mp4', 'video/mpeg', 'video/quicktime', 'video/webm',
47
103
  ]
48
104
 
49
105
  if (!allowedTypes.includes(file.type)) {
@@ -54,32 +110,88 @@ export async function POST(request: NextRequest) {
54
110
  )
55
111
  }
56
112
 
57
- // Validate file size (max 10MB)
58
- const maxSize = 10 * 1024 * 1024 // 10MB
113
+ // Determine max size based on file type category (theme-overridable)
114
+ const defaultMaxSizeMB = MEDIA_CONFIG?.maxSizeMB ?? 10
115
+ let maxSizeMB = defaultMaxSizeMB
116
+ if (file.type.startsWith('image/') && MEDIA_CONFIG?.maxSizeImageMB != null) {
117
+ maxSizeMB = MEDIA_CONFIG.maxSizeImageMB
118
+ } else if (file.type.startsWith('video/') && MEDIA_CONFIG?.maxSizeVideoMB != null) {
119
+ maxSizeMB = MEDIA_CONFIG.maxSizeVideoMB
120
+ }
121
+ const maxSize = maxSizeMB * 1024 * 1024
59
122
  if (file.size > maxSize) {
60
123
  return createApiError(
61
- `File ${file.name} is too large. Maximum size is 10MB.`,
124
+ `File ${file.name} is too large. Maximum size is ${maxSizeMB}MB.`,
62
125
  400,
63
- { maxSize: '10MB', fileSize: `${(file.size / 1024 / 1024).toFixed(2)}MB` }
126
+ { maxSize: `${maxSizeMB}MB`, fileSize: `${(file.size / 1024 / 1024).toFixed(2)}MB` }
64
127
  )
65
128
  }
66
129
 
67
130
  // Generate unique filename
68
131
  const timestamp = Date.now()
69
132
  const randomString = Math.random().toString(36).substring(2, 15)
70
- const extension = file.name.split('.').pop()
133
+ const extension = file.name.split('.').pop() || 'bin'
71
134
  const fileName = `${timestamp}_${randomString}.${extension}`
72
135
 
73
136
  try {
74
- // Upload to Vercel Blob
75
- const blob = await put(`uploads/temp/${fileName}`, file, {
76
- access: 'public',
77
- addRandomSuffix: false
78
- })
137
+ let uploadedUrl: string
138
+
139
+ // Read file buffer once - needed for both Vercel Blob and local storage
140
+ // Important: File stream can only be read once, so we buffer it first
141
+ const fileBuffer = Buffer.from(await file.arrayBuffer())
142
+
143
+ if (useVercelBlob) {
144
+ // Try Vercel Blob first - use buffer with content type
145
+ try {
146
+ const blob = await put(`uploads/temp/${fileName}`, fileBuffer, {
147
+ access: 'public',
148
+ addRandomSuffix: false,
149
+ contentType: file.type
150
+ })
151
+ uploadedUrl = blob.url
152
+ console.log(`✅ [Media Upload] Uploaded to Vercel Blob: ${uploadedUrl}`)
153
+ } catch (blobError) {
154
+ // Fallback to local storage if Vercel Blob fails
155
+ console.warn(`⚠️ [Media Upload] Vercel Blob failed, falling back to local storage:`, blobError)
156
+ uploadedUrl = await uploadToLocalStorageBuffer(fileBuffer, fileName)
157
+ console.log(`✅ [Media Upload] Uploaded to local storage (fallback): ${uploadedUrl}`)
158
+ }
159
+ } else {
160
+ // Use local storage directly
161
+ uploadedUrl = await uploadToLocalStorageBuffer(fileBuffer, fileName)
162
+ console.log(`✅ [Media Upload] Uploaded to local storage: ${uploadedUrl}`)
163
+ }
164
+
165
+ uploadedUrls.push(uploadedUrl)
166
+
167
+ // NEW: Create media record in database
168
+ try {
169
+ // Extract image dimensions if it's an image
170
+ const dimensions = await extractImageDimensions(fileBuffer, file.type)
171
+
172
+ const mediaRecord = await MediaService.create(
173
+ authResult.user!.id,
174
+ teamId,
175
+ {
176
+ url: uploadedUrl,
177
+ filename: file.name,
178
+ fileSize: file.size,
179
+ mimeType: file.type,
180
+ width: dimensions?.width ?? null,
181
+ height: dimensions?.height ?? null,
182
+ }
183
+ )
184
+
185
+ uploadedMedia.push(mediaRecord)
186
+ console.log(`✅ [Media Upload] Created media record: ${mediaRecord.id}`)
187
+ } catch (dbError) {
188
+ // Graceful degradation: If DB insert fails, still return the URL
189
+ console.warn(`⚠️ [Media Upload] Failed to create media record:`, dbError)
190
+ // Continue - upload succeeded even if DB insert failed
191
+ }
79
192
 
80
- uploadedUrls.push(blob.url)
81
193
  } catch (fileError) {
82
- console.error(`❌ Failed to upload ${file.name} to Vercel Blob:`)
194
+ console.error(`❌ Failed to upload ${file.name}:`)
83
195
  console.error(`❌ Error details:`, fileError)
84
196
 
85
197
  const errorMessage = fileError instanceof Error ? fileError.message : String(fileError)
@@ -97,10 +209,13 @@ export async function POST(request: NextRequest) {
97
209
  }
98
210
  }
99
211
 
212
+ // Return both legacy URLs array AND new media records array (backward compatible)
100
213
  return createApiResponse({
101
214
  message: 'Files uploaded successfully',
102
- urls: uploadedUrls,
103
- count: uploadedUrls.length
215
+ urls: uploadedUrls, // LEGACY: backward compatible
216
+ media: uploadedMedia, // NEW: full media records
217
+ count: uploadedUrls.length,
218
+ storage: useVercelBlob ? 'vercel-blob' : 'local'
104
219
  })
105
220
 
106
221
  } catch (error) {
@@ -111,10 +226,14 @@ export async function POST(request: NextRequest) {
111
226
  { error: error instanceof Error ? error.message : String(error) }
112
227
  )
113
228
  }
114
- }
229
+ }, 'write');
115
230
 
116
- // Optional: Add a GET endpoint to get upload info
117
- export async function GET(request: NextRequest) {
231
+ /**
232
+ * GET /api/v1/media/upload
233
+ *
234
+ * Get upload endpoint information.
235
+ */
236
+ export const GET = withRateLimitTier(async (request: NextRequest) => {
118
237
  try {
119
238
  // 1. Dual Authentication (API Key OR Session)
120
239
  const authResult = await authenticateRequest(request)
@@ -130,13 +249,18 @@ export async function GET(request: NextRequest) {
130
249
  return createApiError('Insufficient permissions - media:read scope required', 403)
131
250
  }
132
251
 
252
+ const useVercelBlob = isVercelBlobConfigured()
253
+
133
254
  // This could be used for cleanup or management
255
+ const maxSizeMB = MEDIA_CONFIG?.maxSizeMB ?? 10
134
256
  return createApiResponse({
135
257
  message: 'Media upload endpoint is active',
136
- storage: 'Vercel Blob',
258
+ storage: useVercelBlob ? 'Vercel Blob' : 'Local Storage',
137
259
  uploadPath: 'uploads/temp/',
138
- supportedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm'],
139
- maxFileSize: '10MB'
260
+ supportedTypes: MEDIA_CONFIG?.allowedMimeTypes ?? ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm'],
261
+ maxFileSize: `${maxSizeMB}MB`,
262
+ ...(MEDIA_CONFIG?.maxSizeImageMB != null && { maxImageSize: `${MEDIA_CONFIG.maxSizeImageMB}MB` }),
263
+ ...(MEDIA_CONFIG?.maxSizeVideoMB != null && { maxVideoSize: `${MEDIA_CONFIG.maxSizeVideoMB}MB` }),
140
264
  })
141
265
 
142
266
  } catch (error) {
@@ -147,4 +271,4 @@ export async function GET(request: NextRequest) {
147
271
  { error: error instanceof Error ? error.message : String(error) }
148
272
  )
149
273
  }
150
- }
274
+ }, 'read');