@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.
- package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/array-field.js +55 -3
- package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/dynamic-form.js +82 -2
- package/dist/components/dashboard/navigation/DynamicNavigation.d.ts.map +1 -1
- package/dist/components/dashboard/navigation/DynamicNavigation.js +7 -1
- package/dist/components/devtools/scheduled-actions/actions-table.d.ts +1 -0
- package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
- package/dist/components/devtools/scheduled-actions/actions-table.js +182 -46
- package/dist/components/devtools/scheduled-actions/types.d.ts +1 -0
- package/dist/components/devtools/scheduled-actions/types.d.ts.map +1 -1
- package/dist/components/media/MediaCard.d.ts +23 -0
- package/dist/components/media/MediaCard.d.ts.map +1 -0
- package/dist/components/media/MediaCard.js +154 -0
- package/dist/components/media/MediaDetailPanel.d.ts +17 -0
- package/dist/components/media/MediaDetailPanel.d.ts.map +1 -0
- package/dist/components/media/MediaDetailPanel.js +331 -0
- package/dist/components/media/MediaGrid.d.ts +26 -0
- package/dist/components/media/MediaGrid.d.ts.map +1 -0
- package/dist/components/media/MediaGrid.js +77 -0
- package/dist/components/media/MediaLibrary.d.ts +20 -0
- package/dist/components/media/MediaLibrary.d.ts.map +1 -0
- package/dist/components/media/MediaLibrary.js +229 -0
- package/dist/components/media/MediaList.d.ts +24 -0
- package/dist/components/media/MediaList.d.ts.map +1 -0
- package/dist/components/media/MediaList.js +181 -0
- package/dist/components/media/MediaSelector.d.ts +19 -0
- package/dist/components/media/MediaSelector.d.ts.map +1 -0
- package/dist/components/media/MediaSelector.js +145 -0
- package/dist/components/media/MediaTagFilter.d.ts +16 -0
- package/dist/components/media/MediaTagFilter.d.ts.map +1 -0
- package/dist/components/media/MediaTagFilter.js +122 -0
- package/dist/components/media/MediaToolbar.d.ts +25 -0
- package/dist/components/media/MediaToolbar.d.ts.map +1 -0
- package/dist/components/media/MediaToolbar.js +136 -0
- package/dist/components/media/MediaUploadZone.d.ts +19 -0
- package/dist/components/media/MediaUploadZone.d.ts.map +1 -0
- package/dist/components/media/MediaUploadZone.js +248 -0
- package/dist/components/media/index.d.ts +15 -0
- package/dist/components/media/index.d.ts.map +1 -0
- package/dist/components/media/index.js +20 -0
- package/dist/contexts/TeamContext.js +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/useEnsureUserMetadata.d.ts +4 -0
- package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
- package/dist/hooks/useEnsureUserMetadata.js +85 -60
- package/dist/hooks/useEntityMutations.d.ts.map +1 -1
- package/dist/hooks/useEntityMutations.js +5 -9
- package/dist/hooks/useMedia.d.ts +56 -0
- package/dist/hooks/useMedia.d.ts.map +1 -0
- package/dist/hooks/useMedia.js +181 -0
- package/dist/hooks/useMediaUpload.d.ts +27 -0
- package/dist/hooks/useMediaUpload.d.ts.map +1 -0
- package/dist/hooks/useMediaUpload.js +36 -0
- package/dist/hooks/useUserSettings.d.ts +5 -4
- package/dist/hooks/useUserSettings.d.ts.map +1 -1
- package/dist/hooks/useUserSettings.js +42 -40
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/lib/api/auth/dual-auth.d.ts +6 -2
- package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
- package/dist/lib/api/auth/dual-auth.js +5 -9
- package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
- package/dist/lib/api/entity/generic-handler.js +3 -3
- package/dist/lib/config/app.config.d.ts.map +1 -1
- package/dist/lib/config/app.config.js +37 -0
- package/dist/lib/config/config-sync.d.ts +1 -0
- package/dist/lib/config/config-sync.d.ts.map +1 -1
- package/dist/lib/config/config-sync.js +2 -0
- package/dist/lib/config/types.d.ts +29 -0
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/media/schemas.d.ts +39 -0
- package/dist/lib/media/schemas.d.ts.map +1 -0
- package/dist/lib/media/schemas.js +32 -0
- package/dist/lib/media/types.d.ts +69 -0
- package/dist/lib/media/types.d.ts.map +1 -0
- package/dist/lib/media/types.js +0 -0
- package/dist/lib/media/utils.d.ts +26 -0
- package/dist/lib/media/utils.d.ts.map +1 -0
- package/dist/lib/media/utils.js +33 -0
- package/dist/lib/rate-limit-redis.d.ts.map +1 -1
- package/dist/lib/rate-limit-redis.js +13 -4
- package/dist/lib/scheduled-actions/initializer.d.ts +6 -3
- package/dist/lib/scheduled-actions/initializer.d.ts.map +1 -1
- package/dist/lib/scheduled-actions/initializer.js +11 -6
- package/dist/lib/scheduled-actions/processor.d.ts +20 -4
- package/dist/lib/scheduled-actions/processor.d.ts.map +1 -1
- package/dist/lib/scheduled-actions/processor.js +128 -34
- package/dist/lib/scheduled-actions/registry.d.ts +3 -0
- package/dist/lib/scheduled-actions/registry.d.ts.map +1 -1
- package/dist/lib/scheduled-actions/registry.js +2 -1
- package/dist/lib/scheduled-actions/scheduler.d.ts +1 -1
- package/dist/lib/scheduled-actions/scheduler.d.ts.map +1 -1
- package/dist/lib/scheduled-actions/scheduler.js +76 -38
- package/dist/lib/scheduled-actions/types.d.ts +73 -0
- package/dist/lib/scheduled-actions/types.d.ts.map +1 -1
- package/dist/lib/selectors/core-selectors.d.ts +102 -0
- package/dist/lib/selectors/core-selectors.d.ts.map +1 -1
- package/dist/lib/selectors/core-selectors.js +3 -1
- package/dist/lib/selectors/domains/block-editor.selectors.d.ts +8 -0
- package/dist/lib/selectors/domains/block-editor.selectors.d.ts.map +1 -1
- package/dist/lib/selectors/domains/block-editor.selectors.js +9 -0
- package/dist/lib/selectors/domains/devtools.selectors.d.ts +6 -0
- package/dist/lib/selectors/domains/devtools.selectors.d.ts.map +1 -1
- package/dist/lib/selectors/domains/devtools.selectors.js +6 -0
- package/dist/lib/selectors/domains/index.d.ts +1 -0
- package/dist/lib/selectors/domains/index.d.ts.map +1 -1
- package/dist/lib/selectors/domains/index.js +2 -0
- package/dist/lib/selectors/domains/media.selectors.d.ts +96 -0
- package/dist/lib/selectors/domains/media.selectors.d.ts.map +1 -0
- package/dist/lib/selectors/domains/media.selectors.js +103 -0
- package/dist/lib/selectors/selectors.d.ts +204 -0
- package/dist/lib/selectors/selectors.d.ts.map +1 -1
- package/dist/lib/services/index.d.ts +2 -0
- package/dist/lib/services/index.d.ts.map +1 -1
- package/dist/lib/services/index.js +2 -0
- package/dist/lib/services/media.service.d.ts +158 -0
- package/dist/lib/services/media.service.d.ts.map +1 -0
- package/dist/lib/services/media.service.js +410 -0
- package/dist/messages/de/devtools.json +16 -0
- package/dist/messages/de/index.d.ts +16 -0
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/en/admin.json +4 -1
- package/dist/messages/en/devtools.json +16 -0
- package/dist/messages/en/index.d.ts +167 -0
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/en/index.js +2 -0
- package/dist/messages/en/index.ts +2 -0
- package/dist/messages/en/media.json +147 -0
- package/dist/messages/en/navigation.json +1 -0
- package/dist/messages/es/admin.json +4 -1
- package/dist/messages/es/devtools.json +16 -0
- package/dist/messages/es/index.d.ts +167 -0
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/es/index.js +2 -0
- package/dist/messages/es/index.ts +2 -0
- package/dist/messages/es/media.json +147 -0
- package/dist/messages/es/navigation.json +1 -0
- package/dist/messages/fr/devtools.json +16 -0
- package/dist/messages/fr/index.d.ts +16 -0
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/it/devtools.json +16 -0
- package/dist/messages/it/index.d.ts +16 -0
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/pt/devtools.json +16 -0
- package/dist/messages/pt/index.d.ts +16 -0
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/migrations/017_scheduled_actions_table.sql +21 -0
- package/dist/migrations/021_media.sql +154 -0
- package/dist/migrations/090_sample_data.sql +53 -0
- package/dist/styles/classes.json +36 -3
- package/dist/styles/ui.css +1 -1
- package/dist/templates/app/api/devtools/config/entities/route.ts +18 -11
- package/dist/templates/app/api/devtools/config/theme/route.ts +5 -4
- package/dist/templates/app/api/devtools/tests/[...path]/route.ts +6 -5
- package/dist/templates/app/api/devtools/tests/route.ts +5 -4
- package/dist/templates/app/api/health/route.ts +6 -4
- package/dist/templates/app/api/internal/user-metadata/route.ts +3 -2
- package/dist/templates/app/api/superadmin/subscriptions/route.ts +5 -6
- package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -7
- package/dist/templates/app/api/superadmin/teams/route.ts +5 -6
- package/dist/templates/app/api/superadmin/users/[userId]/route.ts +11 -16
- package/dist/templates/app/api/superadmin/users/route.ts +9 -10
- package/dist/templates/app/api/user/delete-account/route.ts +3 -2
- package/dist/templates/app/api/user/plan-flags/route.ts +11 -24
- package/dist/templates/app/api/user/profile/route.ts +7 -6
- package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +16 -18
- package/dist/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +17 -19
- package/dist/templates/app/api/v1/[entity]/[id]/route.ts +10 -12
- package/dist/templates/app/api/v1/[entity]/route.ts +9 -11
- package/dist/templates/app/api/v1/api-keys/[id]/route.ts +9 -8
- package/dist/templates/app/api/v1/api-keys/route.ts +7 -6
- package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +3 -2
- package/dist/templates/app/api/v1/billing/cancel/route.ts +15 -14
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +10 -9
- package/dist/templates/app/api/v1/billing/check-action/route.ts +8 -7
- package/dist/templates/app/api/v1/billing/checkout/route.ts +10 -9
- package/dist/templates/app/api/v1/billing/plans/route.ts +5 -4
- package/dist/templates/app/api/v1/billing/portal/route.ts +9 -8
- package/dist/templates/app/api/v1/blocks/[slug]/route.ts +4 -3
- package/dist/templates/app/api/v1/blocks/route.ts +3 -2
- package/dist/templates/app/api/v1/blocks/validate/route.ts +5 -3
- package/dist/templates/app/api/v1/cron/process/route.ts +4 -6
- package/dist/templates/app/api/v1/devtools/blocks/route.ts +3 -2
- package/dist/templates/app/api/v1/devtools/docs/route.ts +3 -2
- package/dist/templates/app/api/v1/devtools/features/route.ts +3 -2
- package/dist/templates/app/api/v1/devtools/flows/route.ts +3 -2
- package/dist/templates/app/api/v1/devtools/scheduled-actions/route.ts +125 -3
- package/dist/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +110 -0
- package/dist/templates/app/api/v1/devtools/testing/route.ts +3 -2
- package/dist/templates/app/api/v1/media/[id]/route.ts +144 -0
- package/dist/templates/app/api/v1/media/[id]/tags/route.ts +154 -0
- package/dist/templates/app/api/v1/media/check-duplicates/route.ts +56 -0
- package/dist/templates/app/api/v1/media/route.ts +56 -0
- package/dist/templates/app/api/v1/media/upload/route.ts +157 -33
- package/dist/templates/app/api/v1/media-tags/route.ts +65 -0
- package/dist/templates/app/api/v1/plugin/[...path]/route.ts +16 -15
- package/dist/templates/app/api/v1/plugin/route.ts +3 -2
- package/dist/templates/app/api/v1/post-categories/[id]/route.ts +10 -9
- package/dist/templates/app/api/v1/post-categories/route.ts +5 -4
- package/dist/templates/app/api/v1/team-invitations/[token]/accept/route.ts +3 -3
- package/dist/templates/app/api/v1/team-invitations/[token]/decline/route.ts +3 -3
- package/dist/templates/app/api/v1/team-invitations/[token]/route.ts +3 -2
- package/dist/templates/app/api/v1/team-invitations/route.ts +3 -2
- package/dist/templates/app/api/v1/teams/[teamId]/invitations/route.ts +5 -4
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +3 -2
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/route.ts +3 -2
- package/dist/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +5 -4
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +5 -5
- package/dist/templates/app/api/v1/teams/[teamId]/route.ts +31 -58
- package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -2
- package/dist/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +5 -4
- package/dist/templates/app/api/v1/teams/route.ts +18 -17
- package/dist/templates/app/api/v1/teams/switch/route.ts +3 -2
- package/dist/templates/app/api/v1/theme/[...path]/route.ts +16 -15
- package/dist/templates/app/api/v1/theme/route.ts +3 -2
- package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +7 -6
- package/dist/templates/app/api/v1/users/[id]/route.ts +9 -8
- package/dist/templates/app/api/v1/users/route.ts +7 -6
- package/dist/templates/app/dashboard/(main)/media/page.tsx +607 -0
- package/dist/templates/contents/themes/starter/messages/de/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/de/index.ts +2 -0
- package/dist/templates/contents/themes/starter/messages/en/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/en/index.ts +2 -0
- package/dist/templates/contents/themes/starter/messages/es/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/es/index.ts +2 -0
- package/dist/templates/contents/themes/starter/messages/fr/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/fr/index.ts +2 -0
- package/dist/templates/contents/themes/starter/messages/it/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/it/index.ts +2 -0
- package/dist/templates/contents/themes/starter/messages/pt/dev.json +106 -0
- package/dist/templates/contents/themes/starter/messages/pt/index.ts +2 -0
- package/dist/templates/contents/themes/starter/styles/globals.css +14 -0
- package/dist/templates/instrumentation.ts +33 -0
- package/dist/types/blocks.d.ts +1 -1
- package/dist/types/blocks.d.ts.map +1 -1
- package/migrations/017_scheduled_actions_table.sql +21 -0
- package/migrations/021_media.sql +154 -0
- package/migrations/090_sample_data.sql +53 -0
- package/package.json +3 -2
- package/scripts/build/registry/config.mjs +41 -0
- package/scripts/build/registry/discovery/templates.mjs +0 -1
- package/scripts/build/registry/generators/entity-registry.mjs +16 -6
- package/scripts/build/registry/generators/route-handlers.mjs +8 -2
- package/scripts/build/registry/generators/template-registry.mjs +16 -4
- package/scripts/build/registry/post-build/route-cleanup.mjs +0 -1
- package/scripts/build/registry/validate-env.test.mjs +92 -0
- package/scripts/build/registry.mjs +18 -1
- package/scripts/deploy/vercel-deploy.mjs +1 -1
- package/templates/app/api/devtools/config/entities/route.ts +18 -11
- package/templates/app/api/devtools/config/theme/route.ts +5 -4
- package/templates/app/api/devtools/tests/[...path]/route.ts +6 -5
- package/templates/app/api/devtools/tests/route.ts +5 -4
- package/templates/app/api/health/route.ts +6 -4
- package/templates/app/api/internal/user-metadata/route.ts +3 -2
- package/templates/app/api/superadmin/subscriptions/route.ts +5 -6
- package/templates/app/api/superadmin/teams/[teamId]/route.ts +6 -7
- package/templates/app/api/superadmin/teams/route.ts +5 -6
- package/templates/app/api/superadmin/users/[userId]/route.ts +11 -16
- package/templates/app/api/superadmin/users/route.ts +9 -10
- package/templates/app/api/user/delete-account/route.ts +3 -2
- package/templates/app/api/user/plan-flags/route.ts +11 -24
- package/templates/app/api/user/profile/route.ts +7 -6
- package/templates/app/api/v1/[entity]/[id]/child/[childType]/[childId]/route.ts +16 -18
- package/templates/app/api/v1/[entity]/[id]/child/[childType]/route.ts +17 -19
- package/templates/app/api/v1/[entity]/[id]/route.ts +10 -12
- package/templates/app/api/v1/[entity]/route.ts +9 -11
- package/templates/app/api/v1/api-keys/[id]/route.ts +9 -8
- package/templates/app/api/v1/api-keys/route.ts +7 -6
- package/templates/app/api/v1/auth/signup-with-invite/route.ts +3 -2
- package/templates/app/api/v1/billing/cancel/route.ts +15 -14
- package/templates/app/api/v1/billing/change-plan/route.ts +10 -9
- package/templates/app/api/v1/billing/check-action/route.ts +8 -7
- package/templates/app/api/v1/billing/checkout/route.ts +10 -9
- package/templates/app/api/v1/billing/plans/route.ts +5 -4
- package/templates/app/api/v1/billing/portal/route.ts +9 -8
- package/templates/app/api/v1/blocks/[slug]/route.ts +4 -3
- package/templates/app/api/v1/blocks/route.ts +3 -2
- package/templates/app/api/v1/blocks/validate/route.ts +5 -3
- package/templates/app/api/v1/cron/process/route.ts +4 -6
- package/templates/app/api/v1/devtools/blocks/route.ts +3 -2
- package/templates/app/api/v1/devtools/docs/route.ts +3 -2
- package/templates/app/api/v1/devtools/features/route.ts +3 -2
- package/templates/app/api/v1/devtools/flows/route.ts +3 -2
- package/templates/app/api/v1/devtools/scheduled-actions/route.ts +125 -3
- package/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +110 -0
- package/templates/app/api/v1/devtools/testing/route.ts +3 -2
- package/templates/app/api/v1/media/[id]/route.ts +144 -0
- package/templates/app/api/v1/media/[id]/tags/route.ts +154 -0
- package/templates/app/api/v1/media/check-duplicates/route.ts +56 -0
- package/templates/app/api/v1/media/route.ts +56 -0
- package/templates/app/api/v1/media/upload/route.ts +157 -33
- package/templates/app/api/v1/media-tags/route.ts +65 -0
- package/templates/app/api/v1/plugin/[...path]/route.ts +16 -15
- package/templates/app/api/v1/plugin/route.ts +3 -2
- package/templates/app/api/v1/post-categories/[id]/route.ts +10 -9
- package/templates/app/api/v1/post-categories/route.ts +5 -4
- package/templates/app/api/v1/team-invitations/[token]/accept/route.ts +3 -3
- package/templates/app/api/v1/team-invitations/[token]/decline/route.ts +3 -3
- package/templates/app/api/v1/team-invitations/[token]/route.ts +3 -2
- package/templates/app/api/v1/team-invitations/route.ts +3 -2
- package/templates/app/api/v1/teams/[teamId]/invitations/route.ts +5 -4
- package/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +3 -2
- package/templates/app/api/v1/teams/[teamId]/invoices/route.ts +3 -2
- package/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +5 -4
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +5 -5
- package/templates/app/api/v1/teams/[teamId]/route.ts +31 -58
- package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -2
- package/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +5 -4
- package/templates/app/api/v1/teams/route.ts +18 -17
- package/templates/app/api/v1/teams/switch/route.ts +3 -2
- package/templates/app/api/v1/theme/[...path]/route.ts +16 -15
- package/templates/app/api/v1/theme/route.ts +3 -2
- package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +7 -6
- package/templates/app/api/v1/users/[id]/route.ts +9 -8
- package/templates/app/api/v1/users/route.ts +7 -6
- package/templates/app/dashboard/(main)/media/page.tsx +607 -0
- package/templates/contents/themes/starter/messages/de/dev.json +106 -0
- package/templates/contents/themes/starter/messages/de/index.ts +2 -0
- package/templates/contents/themes/starter/messages/en/dev.json +106 -0
- package/templates/contents/themes/starter/messages/en/index.ts +2 -0
- package/templates/contents/themes/starter/messages/es/dev.json +106 -0
- package/templates/contents/themes/starter/messages/es/index.ts +2 -0
- package/templates/contents/themes/starter/messages/fr/dev.json +106 -0
- package/templates/contents/themes/starter/messages/fr/index.ts +2 -0
- package/templates/contents/themes/starter/messages/it/dev.json +106 -0
- package/templates/contents/themes/starter/messages/it/index.ts +2 -0
- package/templates/contents/themes/starter/messages/pt/dev.json +106 -0
- package/templates/contents/themes/starter/messages/pt/index.ts +2 -0
- package/templates/contents/themes/starter/styles/globals.css +14 -0
- package/templates/instrumentation.ts +33 -0
- package/dist/presets/plugin/.env.example.template +0 -19
- package/dist/presets/plugin/entities/.gitkeep +0 -18
- package/dist/presets/theme/blocks/.gitkeep +0 -17
- package/dist/presets/theme/public/brand/.gitkeep +0 -8
- package/dist/presets/theme/tests/cypress/.gitkeep +0 -10
- package/dist/templates/contents/plugins/starter/plugin/.env.example.template +0 -19
- 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
|
-
|
|
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
|
|
37
|
-
const allowedTypes = [
|
|
38
|
-
'image/jpeg',
|
|
39
|
-
'
|
|
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
|
-
//
|
|
58
|
-
const
|
|
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
|
|
124
|
+
`File ${file.name} is too large. Maximum size is ${maxSizeMB}MB.`,
|
|
62
125
|
400,
|
|
63
|
-
{ maxSize:
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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:
|
|
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');
|