@nextsparkjs/core 0.1.0-beta.184 → 0.1.0-beta.186
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/app/guards/DeveloperGuard.d.ts.map +1 -1
- package/dist/components/app/guards/SuperAdminGuard.d.ts.map +1 -1
- package/dist/components/app/layouts/PublicFooter.d.ts.map +1 -1
- package/dist/components/app/layouts/PublicFooter.js +2 -2
- package/dist/components/app/layouts/PublicNavbar.d.ts.map +1 -1
- package/dist/components/app/layouts/PublicNavbar.js +2 -2
- package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
- package/dist/components/app/misc/ThemeToggle.js +4 -1
- package/dist/components/auth/DevKeyring.d.ts.map +1 -1
- package/dist/components/auth/DevKeyring.js +10 -2
- package/dist/components/auth/SessionCookieRefresher.d.ts +21 -0
- package/dist/components/auth/SessionCookieRefresher.d.ts.map +1 -0
- package/dist/components/auth/SessionCookieRefresher.js +11 -0
- package/dist/components/auth/forms/LoginForm.d.ts.map +1 -1
- package/dist/components/auth/forms/LoginForm.js +300 -6
- package/dist/components/auth/index.d.ts +1 -0
- package/dist/components/auth/index.d.ts.map +1 -1
- package/dist/components/auth/index.js +2 -0
- package/dist/components/billing/InvoicesTable.d.ts.map +1 -1
- package/dist/components/billing/InvoicesTable.js +4 -3
- package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/AuthGuard.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/AuthGuard.js +2 -2
- package/dist/components/dashboard/layouts/DashboardAuthLayout.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/DashboardAuthLayout.js +2 -2
- package/dist/components/dashboard/layouts/DashboardAuthSkeleton.d.ts +17 -0
- package/dist/components/dashboard/layouts/DashboardAuthSkeleton.d.ts.map +1 -0
- package/dist/components/dashboard/layouts/DashboardAuthSkeleton.js +52 -0
- package/dist/components/dashboard/layouts/Sidebar.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/Sidebar.js +2 -3
- package/dist/components/dashboard/layouts/TopNavbar.js +2 -2
- package/dist/components/dashboard/layouts/index.d.ts +1 -0
- package/dist/components/dashboard/layouts/index.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/index.js +2 -0
- package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
- package/dist/components/entities/EntityFieldRenderer.js +3 -0
- package/dist/components/entities/EntityTable.d.ts.map +1 -1
- package/dist/components/entities/EntityTable.js +2 -1
- package/dist/components/entities/wrappers/EntityFormWrapper.d.ts.map +1 -1
- package/dist/components/entities/wrappers/EntityFormWrapper.js +7 -2
- package/dist/components/ui/combobox.d.ts +8 -1
- package/dist/components/ui/combobox.d.ts.map +1 -1
- package/dist/components/ui/combobox.js +37 -44
- package/dist/components/ui/country-select.d.ts +3 -1
- package/dist/components/ui/country-select.d.ts.map +1 -1
- package/dist/components/ui/country-select.js +3 -1
- package/dist/components/ui/currency-select.d.ts +3 -1
- package/dist/components/ui/currency-select.d.ts.map +1 -1
- package/dist/components/ui/currency-select.js +3 -1
- package/dist/components/ui/sheet.d.ts +5 -0
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +46 -26
- package/dist/components/ui/timezone-select.d.ts +3 -1
- package/dist/components/ui/timezone-select.d.ts.map +1 -1
- package/dist/components/ui/timezone-select.js +3 -1
- package/dist/contexts/TeamContext.d.ts +6 -0
- package/dist/contexts/TeamContext.d.ts.map +1 -1
- package/dist/contexts/TeamContext.js +8 -1
- package/dist/emails/otp-verification.d.ts.map +1 -1
- package/dist/emails/reset-password.d.ts.map +1 -1
- package/dist/emails/team-invitation.d.ts.map +1 -1
- package/dist/emails/team-invitation.js +3 -2
- package/dist/emails/verify-email.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useAuth.d.ts +103 -25
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +24 -0
- package/dist/hooks/useEnsureUserMetadata.d.ts +22 -0
- package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
- package/dist/hooks/useEnsureUserMetadata.js +18 -5
- package/dist/hooks/useSessionCookieRefresh.d.ts +35 -0
- package/dist/hooks/useSessionCookieRefresh.d.ts.map +1 -0
- package/dist/hooks/useSessionCookieRefresh.js +49 -0
- package/dist/hooks/useValidationSchemas.d.ts +7 -7
- package/dist/lib/api/api-error.d.ts +3 -1
- package/dist/lib/api/api-error.d.ts.map +1 -1
- package/dist/lib/api/api-error.js +4 -2
- package/dist/lib/api/auth/dual-auth.d.ts +45 -3
- package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
- package/dist/lib/api/auth/dual-auth.js +42 -7
- package/dist/lib/api/auth/permissions.d.ts +7 -2
- package/dist/lib/api/auth/permissions.d.ts.map +1 -1
- package/dist/lib/api/auth/permissions.js +2 -2
- package/dist/lib/api/auth/scope-policy.d.ts +71 -0
- package/dist/lib/api/auth/scope-policy.d.ts.map +1 -0
- package/dist/lib/api/auth/scope-policy.js +53 -0
- package/dist/lib/api/auth.d.ts +35 -3
- package/dist/lib/api/auth.d.ts.map +1 -1
- package/dist/lib/api/auth.js +54 -23
- package/dist/lib/api/entity/audit-log.d.ts +35 -0
- package/dist/lib/api/entity/audit-log.d.ts.map +1 -0
- package/dist/lib/api/entity/audit-log.js +35 -0
- package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
- package/dist/lib/api/entity/generic-handler.js +333 -155
- package/dist/lib/api/helpers.d.ts +29 -4
- package/dist/lib/api/helpers.d.ts.map +1 -1
- package/dist/lib/api/helpers.js +30 -6
- package/dist/lib/api/keys.d.ts +16 -0
- package/dist/lib/api/keys.d.ts.map +1 -1
- package/dist/lib/api/keys.js +19 -1
- package/dist/lib/api/routes/patterns-usages.d.ts.map +1 -1
- package/dist/lib/api/routes/patterns-usages.js +3 -11
- package/dist/lib/auth/auth-methods.d.ts +52 -0
- package/dist/lib/auth/auth-methods.d.ts.map +1 -0
- package/dist/lib/auth/auth-methods.js +63 -0
- package/dist/lib/auth/registration-guard-plugin.d.ts.map +1 -1
- package/dist/lib/auth/registration-helpers.d.ts +9 -0
- package/dist/lib/auth/registration-helpers.d.ts.map +1 -1
- package/dist/lib/auth/registration-helpers.js +8 -2
- package/dist/lib/auth/security-notifications/device-fingerprint.d.ts +52 -0
- package/dist/lib/auth/security-notifications/device-fingerprint.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/device-fingerprint.js +42 -0
- package/dist/lib/auth/security-notifications/dispatcher.d.ts +37 -0
- package/dist/lib/auth/security-notifications/dispatcher.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/dispatcher.js +157 -0
- package/dist/lib/auth/security-notifications/index.d.ts +20 -0
- package/dist/lib/auth/security-notifications/index.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/index.js +22 -0
- package/dist/lib/auth/security-notifications/registry.d.ts +36 -0
- package/dist/lib/auth/security-notifications/registry.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/registry.js +26 -0
- package/dist/lib/auth/security-notifications/templates/email-changed.d.ts +12 -0
- package/dist/lib/auth/security-notifications/templates/email-changed.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/templates/email-changed.js +43 -0
- package/dist/lib/auth/security-notifications/templates/new-device-login.d.ts +7 -0
- package/dist/lib/auth/security-notifications/templates/new-device-login.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/templates/new-device-login.js +46 -0
- package/dist/lib/auth/security-notifications/templates/password-changed.d.ts +7 -0
- package/dist/lib/auth/security-notifications/templates/password-changed.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/templates/password-changed.js +35 -0
- package/dist/lib/auth/security-notifications/templates/shared.d.ts +36 -0
- package/dist/lib/auth/security-notifications/templates/shared.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/templates/shared.js +68 -0
- package/dist/lib/auth/security-notifications/types.d.ts +74 -0
- package/dist/lib/auth/security-notifications/types.d.ts.map +1 -0
- package/dist/lib/auth/security-notifications/types.js +0 -0
- package/dist/lib/auth/session-config.d.ts +37 -0
- package/dist/lib/auth/session-config.d.ts.map +1 -0
- package/dist/lib/auth/session-config.js +59 -0
- package/dist/lib/auth/session-refresh.d.ts +46 -0
- package/dist/lib/auth/session-refresh.d.ts.map +1 -0
- package/dist/lib/auth/session-refresh.js +11 -0
- package/dist/lib/auth-client.d.ts +1489 -3445
- package/dist/lib/auth-client.d.ts.map +1 -1
- package/dist/lib/auth-client.js +4 -0
- package/dist/lib/auth.d.ts +1 -3057
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +21 -8
- package/dist/lib/billing/gateways/factory.d.ts +8 -3
- package/dist/lib/billing/gateways/factory.d.ts.map +1 -1
- package/dist/lib/billing/gateways/factory.js +37 -23
- package/dist/lib/billing/schema.d.ts +1 -1
- package/dist/lib/billing/stripe-webhook.d.ts.map +1 -1
- package/dist/lib/billing/stripe-webhook.js +2 -1
- package/dist/lib/config/app.config.d.ts.map +1 -1
- package/dist/lib/config/app.config.js +71 -0
- package/dist/lib/config/config-sync.d.ts +1 -4
- package/dist/lib/config/config-sync.d.ts.map +1 -1
- package/dist/lib/config/config-sync.js +6 -2
- package/dist/lib/config/types.d.ts +130 -0
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/entities/entity-hooks.d.ts +9 -1
- package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
- package/dist/lib/entities/entity-hooks.js +77 -4
- package/dist/lib/entities/schema-generator.d.ts.map +1 -1
- package/dist/lib/entities/schema-generator.js +20 -4
- package/dist/lib/entities/serialization.d.ts +2 -2
- package/dist/lib/entities/serialization.d.ts.map +1 -1
- package/dist/lib/entities/serialization.js +3 -2
- package/dist/lib/entities/types.d.ts +8 -0
- package/dist/lib/entities/types.d.ts.map +1 -1
- package/dist/lib/locale.d.ts.map +1 -1
- package/dist/lib/locale.js +4 -1
- package/dist/lib/mcp/audit.d.ts +21 -0
- package/dist/lib/mcp/audit.d.ts.map +1 -0
- package/dist/lib/mcp/audit.js +48 -0
- package/dist/lib/mcp/engine.d.ts +28 -0
- package/dist/lib/mcp/engine.d.ts.map +1 -0
- package/dist/lib/mcp/engine.js +158 -0
- package/dist/lib/mcp/errors.d.ts +16 -0
- package/dist/lib/mcp/errors.d.ts.map +1 -0
- package/dist/lib/mcp/errors.js +66 -0
- package/dist/lib/mcp/executor.d.ts +15 -0
- package/dist/lib/mcp/executor.d.ts.map +1 -0
- package/dist/lib/mcp/executor.js +63 -0
- package/dist/lib/mcp/index.d.ts +9 -0
- package/dist/lib/mcp/index.d.ts.map +1 -0
- package/dist/lib/mcp/index.js +32 -0
- package/dist/lib/mcp/schema-builder.d.ts +45 -0
- package/dist/lib/mcp/schema-builder.d.ts.map +1 -0
- package/dist/lib/mcp/schema-builder.js +212 -0
- package/dist/lib/mcp/tool-generator.d.ts +31 -0
- package/dist/lib/mcp/tool-generator.d.ts.map +1 -0
- package/dist/lib/mcp/tool-generator.js +271 -0
- package/dist/lib/mcp/transport.d.ts +31 -0
- package/dist/lib/mcp/transport.d.ts.map +1 -0
- package/dist/lib/mcp/transport.js +58 -0
- package/dist/lib/mcp/types.d.ts +184 -0
- package/dist/lib/mcp/types.d.ts.map +1 -0
- package/dist/lib/mcp/types.js +11 -0
- package/dist/lib/plugins/hook-system.d.ts +13 -0
- package/dist/lib/plugins/hook-system.d.ts.map +1 -1
- package/dist/lib/plugins/hook-system.js +32 -2
- package/dist/lib/scheduled-actions/handlers/index.d.ts +1 -0
- package/dist/lib/scheduled-actions/handlers/index.d.ts.map +1 -1
- package/dist/lib/scheduled-actions/handlers/index.js +3 -1
- package/dist/lib/scheduled-actions/handlers/security-notification.d.ts +26 -0
- package/dist/lib/scheduled-actions/handlers/security-notification.d.ts.map +1 -0
- package/dist/lib/scheduled-actions/handlers/security-notification.js +54 -0
- package/dist/lib/selectors/core-selectors.d.ts +12 -0
- package/dist/lib/selectors/core-selectors.d.ts.map +1 -1
- package/dist/lib/selectors/domains/auth.selectors.d.ts +12 -0
- package/dist/lib/selectors/domains/auth.selectors.d.ts.map +1 -1
- package/dist/lib/selectors/domains/auth.selectors.js +14 -1
- package/dist/lib/selectors/selectors.d.ts +24 -0
- package/dist/lib/selectors/selectors.d.ts.map +1 -1
- package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
- package/dist/lib/services/generic-entity.service.js +16 -32
- package/dist/lib/services/membership.service.d.ts +10 -2
- package/dist/lib/services/membership.service.d.ts.map +1 -1
- package/dist/lib/services/membership.service.js +37 -16
- package/dist/lib/services/subscription.service.d.ts.map +1 -1
- package/dist/lib/services/subscription.service.js +2 -1
- package/dist/lib/services/team-member.service.d.ts +15 -3
- package/dist/lib/services/team-member.service.d.ts.map +1 -1
- package/dist/lib/services/team-member.service.js +27 -6
- package/dist/lib/services/team.service.d.ts.map +1 -1
- package/dist/lib/teams/permissions.d.ts +3 -1
- package/dist/lib/teams/permissions.d.ts.map +1 -1
- package/dist/lib/teams/permissions.js +6 -0
- package/dist/lib/teams/schema.d.ts.map +1 -1
- package/dist/lib/teams/schema.js +3 -0
- package/dist/lib/theme/forced-theme.d.ts +34 -0
- package/dist/lib/theme/forced-theme.d.ts.map +1 -0
- package/dist/lib/theme/forced-theme.js +27 -0
- package/dist/lib/theme/get-default-theme-mode.d.ts +3 -0
- package/dist/lib/theme/get-default-theme-mode.d.ts.map +1 -1
- package/dist/lib/theme/get-default-theme-mode.js +5 -3
- package/dist/messages/de/auth.json +25 -2
- package/dist/messages/de/index.d.ts +81 -0
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/de/index.js +2 -0
- package/dist/messages/de/index.ts +2 -0
- package/dist/messages/en/auth.json +25 -2
- package/dist/messages/en/index.d.ts +81 -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/es/auth.json +25 -2
- package/dist/messages/es/index.d.ts +81 -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/fr/auth.json +25 -2
- package/dist/messages/fr/index.d.ts +81 -0
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/fr/index.js +2 -0
- package/dist/messages/fr/index.ts +2 -0
- package/dist/messages/it/auth.json +25 -2
- package/dist/messages/it/index.d.ts +81 -0
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/it/index.js +2 -0
- package/dist/messages/it/index.ts +2 -0
- package/dist/messages/pt/auth.json +25 -2
- package/dist/messages/pt/index.d.ts +81 -0
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/messages/pt/index.js +2 -0
- package/dist/messages/pt/index.ts +2 -0
- package/dist/migrations/005_api_audit_log.sql +2 -1
- package/dist/migrations/025_api_audit_log_nullable_api_key.sql +12 -0
- package/dist/migrations/026_login_events.sql +91 -0
- package/dist/migrations/027_rls_bypass_developer_and_system_admin_team.sql +197 -0
- package/dist/providers/DashboardProviders.d.ts +12 -3
- package/dist/providers/DashboardProviders.d.ts.map +1 -1
- package/dist/providers/DashboardProviders.js +6 -2
- package/dist/providers/theme-provider.d.ts +19 -2
- package/dist/providers/theme-provider.d.ts.map +1 -1
- package/dist/providers/theme-provider.js +6 -2
- package/dist/templates/app/(auth)/signup/page.tsx +8 -0
- package/dist/templates/app/(public)/[...slug]/page.tsx +9 -14
- package/dist/templates/app/api/auth/[...all]/route.ts +23 -1
- package/dist/templates/app/api/health/route.ts +4 -2
- package/dist/templates/app/api/internal/user-metadata/route.ts +5 -3
- package/dist/templates/app/api/mcp/route.ts +176 -0
- package/dist/templates/app/api/superadmin/subscriptions/route.ts +2 -1
- package/dist/templates/app/api/superadmin/teams/[teamId]/route.ts +4 -3
- package/dist/templates/app/api/superadmin/users/[userId]/route.ts +10 -3
- package/dist/templates/app/api/v1/api-keys/[id]/route.ts +50 -71
- package/dist/templates/app/api/v1/api-keys/route.ts +35 -45
- package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +14 -14
- package/dist/templates/app/api/v1/billing/cancel/route.ts +7 -6
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +5 -4
- package/dist/templates/app/api/v1/billing/check-action/route.ts +6 -4
- package/dist/templates/app/api/v1/billing/checkout/route.ts +7 -5
- package/dist/templates/app/api/v1/billing/plans/route.ts +8 -12
- package/dist/templates/app/api/v1/billing/portal/route.ts +7 -5
- package/dist/templates/app/api/v1/billing/webhooks/polar/route.ts +1 -1
- package/dist/templates/app/api/v1/devtools/blocks/route.ts +5 -4
- package/dist/templates/app/api/v1/devtools/features/route.ts +5 -4
- package/dist/templates/app/api/v1/devtools/flows/route.ts +5 -4
- package/dist/templates/app/api/v1/devtools/scheduled-actions/route.ts +9 -7
- package/dist/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +5 -4
- package/dist/templates/app/api/v1/devtools/testing/route.ts +5 -4
- package/dist/templates/app/api/v1/media/[id]/route.ts +13 -25
- package/dist/templates/app/api/v1/media/[id]/tags/route.ts +17 -25
- package/dist/templates/app/api/v1/media/check-duplicates/route.ts +5 -7
- package/dist/templates/app/api/v1/media/route.ts +5 -9
- package/dist/templates/app/api/v1/media/upload/route.ts +9 -21
- package/dist/templates/app/api/v1/media-tags/route.ts +9 -13
- package/dist/templates/app/api/v1/patterns/[id]/usages/route.ts +11 -19
- package/dist/templates/app/api/v1/post-categories/[id]/route.ts +9 -7
- package/dist/templates/app/api/v1/post-categories/route.ts +5 -4
- package/dist/templates/app/api/v1/team-invitations/[token]/accept/route.ts +16 -18
- package/dist/templates/app/api/v1/team-invitations/[token]/decline/route.ts +15 -17
- package/dist/templates/app/api/v1/team-invitations/[token]/route.ts +8 -8
- package/dist/templates/app/api/v1/team-invitations/route.ts +9 -11
- package/dist/templates/app/api/v1/teams/[teamId]/invitations/route.ts +41 -32
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +15 -16
- package/dist/templates/app/api/v1/teams/[teamId]/invoices/route.ts +14 -15
- package/dist/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +32 -36
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +33 -33
- package/dist/templates/app/api/v1/teams/[teamId]/route.ts +35 -41
- package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +5 -3
- package/dist/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +10 -6
- package/dist/templates/app/api/v1/teams/route.ts +33 -27
- package/dist/templates/app/api/v1/teams/switch/route.ts +11 -13
- package/dist/templates/app/api/v1/theme/[...path]/route.ts +23 -2
- package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +32 -77
- package/dist/templates/app/api/v1/users/[id]/route.ts +35 -68
- package/dist/templates/app/api/v1/users/route.ts +21 -25
- package/dist/templates/app/dashboard/layout.tsx +2 -6
- package/dist/templates/app/layout.ppr.tsx +9 -14
- package/dist/templates/app/layout.tsx +39 -19
- package/dist/templates/contents/themes/starter/config/app.config.ts +19 -0
- package/dist/templates/contents/themes/starter/config/theme.config.ts +6 -0
- package/dist/templates/contents/themes/starter/styles/globals.css +6 -2
- package/dist/templates/proxy.ts +46 -10
- package/dist/templates/tsconfig.json +4 -0
- package/dist/types/theme.d.ts +9 -0
- package/dist/types/theme.d.ts.map +1 -1
- package/migrations/005_api_audit_log.sql +2 -1
- package/migrations/025_api_audit_log_nullable_api_key.sql +12 -0
- package/migrations/026_login_events.sql +91 -0
- package/migrations/027_rls_bypass_developer_and_system_admin_team.sql +197 -0
- package/package.json +20 -12
- package/scripts/build/registry/__tests__/mcp-registry.test.mjs +64 -0
- package/scripts/build/registry/__tests__/scope-registry.test.mjs +106 -0
- package/scripts/build/registry/discovery/mcp-overrides.mjs +72 -0
- package/scripts/build/registry/discovery/mcp-overrides.test.mjs +86 -0
- package/scripts/build/registry/generators/mcp-registry.mjs +61 -0
- package/scripts/build/registry/generators/scope-registry.mjs +140 -88
- package/scripts/build/registry.mjs +8 -4
- package/templates/app/(auth)/signup/page.tsx +8 -0
- package/templates/app/(public)/[...slug]/page.tsx +9 -14
- package/templates/app/api/auth/[...all]/route.ts +23 -1
- package/templates/app/api/health/route.ts +4 -2
- package/templates/app/api/internal/user-metadata/route.ts +5 -3
- package/templates/app/api/mcp/route.ts +176 -0
- package/templates/app/api/superadmin/subscriptions/route.ts +2 -1
- package/templates/app/api/superadmin/teams/[teamId]/route.ts +4 -3
- package/templates/app/api/superadmin/users/[userId]/route.ts +10 -3
- package/templates/app/api/v1/api-keys/[id]/route.ts +50 -71
- package/templates/app/api/v1/api-keys/route.ts +35 -45
- package/templates/app/api/v1/auth/signup-with-invite/route.ts +14 -14
- package/templates/app/api/v1/billing/cancel/route.ts +7 -6
- package/templates/app/api/v1/billing/change-plan/route.ts +5 -4
- package/templates/app/api/v1/billing/check-action/route.ts +6 -4
- package/templates/app/api/v1/billing/checkout/route.ts +7 -5
- package/templates/app/api/v1/billing/plans/route.ts +8 -12
- package/templates/app/api/v1/billing/portal/route.ts +7 -5
- package/templates/app/api/v1/billing/webhooks/polar/route.ts +1 -1
- package/templates/app/api/v1/devtools/blocks/route.ts +5 -4
- package/templates/app/api/v1/devtools/features/route.ts +5 -4
- package/templates/app/api/v1/devtools/flows/route.ts +5 -4
- package/templates/app/api/v1/devtools/scheduled-actions/route.ts +9 -7
- package/templates/app/api/v1/devtools/scheduled-actions/run/route.ts +5 -4
- package/templates/app/api/v1/devtools/testing/route.ts +5 -4
- package/templates/app/api/v1/media/[id]/route.ts +13 -25
- package/templates/app/api/v1/media/[id]/tags/route.ts +17 -25
- package/templates/app/api/v1/media/check-duplicates/route.ts +5 -7
- package/templates/app/api/v1/media/route.ts +5 -9
- package/templates/app/api/v1/media/upload/route.ts +9 -21
- package/templates/app/api/v1/media-tags/route.ts +9 -13
- package/templates/app/api/v1/patterns/[id]/usages/route.ts +11 -19
- package/templates/app/api/v1/post-categories/[id]/route.ts +9 -7
- package/templates/app/api/v1/post-categories/route.ts +5 -4
- package/templates/app/api/v1/team-invitations/[token]/accept/route.ts +16 -18
- package/templates/app/api/v1/team-invitations/[token]/decline/route.ts +15 -17
- package/templates/app/api/v1/team-invitations/[token]/route.ts +8 -8
- package/templates/app/api/v1/team-invitations/route.ts +9 -11
- package/templates/app/api/v1/teams/[teamId]/invitations/route.ts +41 -32
- package/templates/app/api/v1/teams/[teamId]/invoices/[invoiceNumber]/route.ts +15 -16
- package/templates/app/api/v1/teams/[teamId]/invoices/route.ts +14 -15
- package/templates/app/api/v1/teams/[teamId]/members/[memberId]/route.ts +32 -36
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +33 -33
- package/templates/app/api/v1/teams/[teamId]/route.ts +35 -41
- package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +5 -3
- package/templates/app/api/v1/teams/[teamId]/usage/[limitSlug]/route.ts +10 -6
- package/templates/app/api/v1/teams/route.ts +33 -27
- package/templates/app/api/v1/teams/switch/route.ts +11 -13
- package/templates/app/api/v1/theme/[...path]/route.ts +23 -2
- package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +32 -77
- package/templates/app/api/v1/users/[id]/route.ts +35 -68
- package/templates/app/api/v1/users/route.ts +21 -25
- package/templates/app/dashboard/layout.tsx +2 -6
- package/templates/app/layout.ppr.tsx +9 -14
- package/templates/app/layout.tsx +39 -19
- package/templates/contents/themes/starter/config/app.config.ts +19 -0
- package/templates/contents/themes/starter/config/theme.config.ts +6 -0
- package/templates/contents/themes/starter/styles/globals.css +6 -2
- package/templates/proxy.ts +46 -10
- package/templates/tsconfig.json +4 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +23 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/scope-registry.ts +56 -6
- package/tests/jest/__mocks__/next-server.js +24 -0
- package/dist/styles/classes.json +0 -1093
- package/dist/styles/ui.css +0 -2
- package/scripts/build/build-ui-css.mjs +0 -351
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import { authenticateRequest,
|
|
2
|
+
import { authenticateRequest, createAuthFailureResponse, canBypassTeamContext } from "../auth/dual-auth.js";
|
|
3
3
|
import { resolveEntityFromUrl, validateEntityOperation } from "./resolver.js";
|
|
4
4
|
import { generateEntitySchemas } from "../../entities/schema-generator.js";
|
|
5
5
|
import { queryWithRLS, mutateWithRLS, queryOneWithRLS } from "../../db.js";
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
addCorsHeaders,
|
|
18
18
|
handleCorsPreflightRequest
|
|
19
19
|
} from "../helpers.js";
|
|
20
|
-
import { afterEntityCreate, afterEntityUpdate, afterEntityDelete } from "../../entities/entity-hooks.js";
|
|
20
|
+
import { beforeEntityCreate, afterEntityCreate, beforeEntityUpdate, afterEntityUpdate, beforeEntityDelete, afterEntityDelete, beforeEntityRead, afterEntityRead } from "../../entities/entity-hooks.js";
|
|
21
|
+
import { logGenericHandlerUsage } from "./audit-log.js";
|
|
21
22
|
import { checkPermission } from "../../permissions/check.js";
|
|
22
23
|
import { extractPatternIds } from "../../blocks/pattern-resolver.js";
|
|
23
24
|
import { isPatternReference } from "../../../types/pattern-reference.js";
|
|
@@ -169,6 +170,102 @@ function parseTaxonomyFilterParams(url, entityConfig) {
|
|
|
169
170
|
function getTableName(entityConfig) {
|
|
170
171
|
return entityConfig.tableName || entityConfig.slug;
|
|
171
172
|
}
|
|
173
|
+
function isEntityField(entityConfig, fieldName) {
|
|
174
|
+
return entityConfig.fields.some((field) => field.name === fieldName);
|
|
175
|
+
}
|
|
176
|
+
const LIST_RESERVED_PARAMS = /* @__PURE__ */ new Set([
|
|
177
|
+
"page",
|
|
178
|
+
"limit",
|
|
179
|
+
"fields",
|
|
180
|
+
"distinct",
|
|
181
|
+
"ids",
|
|
182
|
+
"parentId",
|
|
183
|
+
"search",
|
|
184
|
+
"sortBy",
|
|
185
|
+
"sortOrder",
|
|
186
|
+
"sort",
|
|
187
|
+
"order",
|
|
188
|
+
"child",
|
|
189
|
+
"meta",
|
|
190
|
+
"metas",
|
|
191
|
+
"includeMeta",
|
|
192
|
+
"userId",
|
|
193
|
+
"from",
|
|
194
|
+
"to",
|
|
195
|
+
"dateField",
|
|
196
|
+
"userFiltered"
|
|
197
|
+
]);
|
|
198
|
+
const TAXONOMY_FILTER_PARAMS = ["categoryId", "taxonomyId", "taxonomyType"];
|
|
199
|
+
const SEARCHABLE_FIELDS = ["name", "title", "slug", "content"];
|
|
200
|
+
const SORTABLE_BASE_FIELDS = ["id", "createdAt", "updatedAt", "teamId"];
|
|
201
|
+
const BARE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
202
|
+
function omitHandlerManagedKeys(entityConfig, data) {
|
|
203
|
+
var _a;
|
|
204
|
+
const managed = /* @__PURE__ */ new Set(["userId", "teamId"]);
|
|
205
|
+
if ((_a = entityConfig.taxonomies) == null ? void 0 : _a.enabled) {
|
|
206
|
+
for (const taxonomyType of entityConfig.taxonomies.types ?? []) {
|
|
207
|
+
managed.add(taxonomyType.field);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return Object.fromEntries(Object.entries(data).filter(([key]) => !managed.has(key)));
|
|
211
|
+
}
|
|
212
|
+
function validationErrorMessage(issues) {
|
|
213
|
+
const unknownKeys = issues.filter((issue) => issue.code === "unrecognized_keys").flatMap((issue) => issue.keys ?? []);
|
|
214
|
+
return unknownKeys.length > 0 ? `Unknown field(s): ${unknownKeys.join(", ")}` : "Validation error";
|
|
215
|
+
}
|
|
216
|
+
function asPgError(error) {
|
|
217
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
218
|
+
return error;
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function mapConstraintViolation(error, operation) {
|
|
223
|
+
const pgError = asPgError(error);
|
|
224
|
+
if (!pgError) return null;
|
|
225
|
+
const { constraint, detail, table } = pgError;
|
|
226
|
+
switch (pgError.code) {
|
|
227
|
+
case "23505":
|
|
228
|
+
return createApiError(
|
|
229
|
+
"A record with this value already exists",
|
|
230
|
+
409,
|
|
231
|
+
{ constraint, detail },
|
|
232
|
+
"UNIQUE_CONSTRAINT_VIOLATION"
|
|
233
|
+
);
|
|
234
|
+
case "23514":
|
|
235
|
+
return createApiError(
|
|
236
|
+
constraint ? `Value rejected by check constraint "${constraint}"` : "Value rejected by a database check constraint",
|
|
237
|
+
422,
|
|
238
|
+
{ constraint, table },
|
|
239
|
+
"CHECK_CONSTRAINT_VIOLATION"
|
|
240
|
+
);
|
|
241
|
+
case "23503":
|
|
242
|
+
if (operation === "delete") {
|
|
243
|
+
return createApiError(
|
|
244
|
+
detail || "Cannot delete: this record is referenced by other records",
|
|
245
|
+
409,
|
|
246
|
+
{ constraint },
|
|
247
|
+
"FOREIGN_KEY_VIOLATION"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return createApiError(
|
|
251
|
+
detail ? `Referenced record does not exist: ${detail}` : "Referenced record does not exist",
|
|
252
|
+
422,
|
|
253
|
+
{ constraint, detail },
|
|
254
|
+
"FOREIGN_KEY_VIOLATION"
|
|
255
|
+
);
|
|
256
|
+
default:
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function hookRejectionStatus(error) {
|
|
261
|
+
if (error && typeof error === "object") {
|
|
262
|
+
const candidate = error.status ?? error.statusCode;
|
|
263
|
+
if (typeof candidate === "number" && candidate >= 400 && candidate < 500) {
|
|
264
|
+
return candidate;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return 400;
|
|
268
|
+
}
|
|
172
269
|
async function filterOrphanedPatternReferences(blocks, userId) {
|
|
173
270
|
if (!Array.isArray(blocks) || blocks.length === 0) return blocks;
|
|
174
271
|
const patternIds = extractPatternIds(blocks);
|
|
@@ -193,11 +290,11 @@ function getTeamIdFromRequest(request) {
|
|
|
193
290
|
function isBuilderRequest(request) {
|
|
194
291
|
return request.headers.get("x-builder-source") === "true";
|
|
195
292
|
}
|
|
196
|
-
async function resolveOwnershipFilter(entityConfig, userId, teamId, isBypass
|
|
293
|
+
async function resolveOwnershipFilter(entityConfig, userId, teamId, isBypass) {
|
|
197
294
|
var _a;
|
|
198
295
|
const ownershipFilter = (_a = entityConfig.access) == null ? void 0 : _a.ownershipFilter;
|
|
199
296
|
if (!ownershipFilter) return { applies: false };
|
|
200
|
-
if (isBypass
|
|
297
|
+
if (isBypass) return { applies: false };
|
|
201
298
|
const memberRow = await queryOneWithRLS(
|
|
202
299
|
'SELECT role FROM "team_members" WHERE "teamId" = $1 AND "userId" = $2',
|
|
203
300
|
[teamId, userId],
|
|
@@ -221,9 +318,9 @@ async function resolveOwnershipFilter(entityConfig, userId, teamId, isBypass, au
|
|
|
221
318
|
const filterValue = linkedRow ? linkedRow[valueField] ?? null : null;
|
|
222
319
|
return { applies: true, field: ownershipFilter.field, value: filterValue };
|
|
223
320
|
}
|
|
224
|
-
async function
|
|
321
|
+
async function checkAuthPermission(authResult, entitySlug, action, teamId, request) {
|
|
225
322
|
var _a;
|
|
226
|
-
if (
|
|
323
|
+
if (!((_a = authResult.user) == null ? void 0 : _a.id)) return null;
|
|
227
324
|
const permission = `${entitySlug}.${action}`;
|
|
228
325
|
try {
|
|
229
326
|
const allowed = await checkPermission(authResult.user.id, teamId, permission);
|
|
@@ -291,7 +388,24 @@ async function validateTeamContextWithBypass(request, authResult, userId) {
|
|
|
291
388
|
}
|
|
292
389
|
return { valid: true, teamId, isBypass: false };
|
|
293
390
|
}
|
|
391
|
+
async function runWithAuditLog(request, run) {
|
|
392
|
+
const startedAt = Date.now();
|
|
393
|
+
const audit = { auth: null };
|
|
394
|
+
let statusCode = 500;
|
|
395
|
+
try {
|
|
396
|
+
const response = await run(audit);
|
|
397
|
+
statusCode = response.status;
|
|
398
|
+
return response;
|
|
399
|
+
} finally {
|
|
400
|
+
logGenericHandlerUsage(audit.auth, request, statusCode, Date.now() - startedAt).catch((error) => {
|
|
401
|
+
console.error("[generic-handler:audit] unexpected audit failure:", error);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
294
405
|
async function handleGenericList(request) {
|
|
406
|
+
return runWithAuditLog(request, (audit) => handleGenericListImpl(request, audit));
|
|
407
|
+
}
|
|
408
|
+
async function handleGenericListImpl(request, audit) {
|
|
295
409
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
296
410
|
try {
|
|
297
411
|
const resolution = await resolveEntityFromUrl(request.nextUrl.pathname);
|
|
@@ -303,25 +417,22 @@ async function handleGenericList(request) {
|
|
|
303
417
|
const response2 = createApiError("List operation not supported for this entity", 405);
|
|
304
418
|
return addCorsHeaders(response2, request);
|
|
305
419
|
}
|
|
306
|
-
const authResult = await authenticateRequest(request);
|
|
420
|
+
const authResult = await authenticateRequest(request, { requiredScope: `${resolution.entityConfig.slug}:read` });
|
|
421
|
+
audit.auth = authResult;
|
|
307
422
|
let userId = null;
|
|
308
423
|
let teamId = null;
|
|
309
424
|
let isBypass = false;
|
|
425
|
+
if (!authResult.success && authResult.type === "api-key") {
|
|
426
|
+
return addCorsHeaders(createAuthFailureResponse(authResult), request);
|
|
427
|
+
}
|
|
310
428
|
if (!authResult.success && ((_a = resolution.entityConfig.access) == null ? void 0 : _a.public)) {
|
|
311
429
|
teamId = request.headers.get("x-team-id") ?? null;
|
|
312
430
|
} else if (!authResult.success) {
|
|
313
|
-
return
|
|
314
|
-
{ success: false, error: "Authentication required", code: "AUTHENTICATION_FAILED" },
|
|
315
|
-
{ status: 401 }
|
|
316
|
-
);
|
|
431
|
+
return createAuthFailureResponse(authResult);
|
|
317
432
|
} else {
|
|
318
433
|
if (authResult.rateLimitResponse) {
|
|
319
434
|
return authResult.rateLimitResponse;
|
|
320
435
|
}
|
|
321
|
-
if (!hasRequiredScope(authResult, `${resolution.entityConfig.slug}:read`)) {
|
|
322
|
-
const response2 = createApiError("Insufficient permissions", 403);
|
|
323
|
-
return addCorsHeaders(response2, request);
|
|
324
|
-
}
|
|
325
436
|
userId = authResult.user.id;
|
|
326
437
|
const teamValidation = await validateTeamContextWithBypass(request, authResult, userId);
|
|
327
438
|
if (!teamValidation.valid) {
|
|
@@ -330,7 +441,7 @@ async function handleGenericList(request) {
|
|
|
330
441
|
teamId = teamValidation.teamId;
|
|
331
442
|
isBypass = teamValidation.isBypass;
|
|
332
443
|
if (!isBypass && teamId) {
|
|
333
|
-
const permDenied = await
|
|
444
|
+
const permDenied = await checkAuthPermission(authResult, resolution.entityConfig.slug, "list", teamId, request);
|
|
334
445
|
if (permDenied) return permDenied;
|
|
335
446
|
}
|
|
336
447
|
}
|
|
@@ -341,10 +452,9 @@ async function handleGenericList(request) {
|
|
|
341
452
|
const distinctParam = url.searchParams.get("distinct") === "true";
|
|
342
453
|
const idsFromParams = url.searchParams.getAll("ids");
|
|
343
454
|
const specificIds = idsFromParams.length > 0 ? idsFromParams.flatMap((param) => param.split(",").map((id) => id.trim())).filter(Boolean) : null;
|
|
344
|
-
const knownParams = /* @__PURE__ */ new Set(["page", "limit", "fields", "distinct", "ids", "parentId", "search", "sortBy", "sortOrder", "child", "meta", "from", "to", "dateField"]);
|
|
345
455
|
const customFilters = {};
|
|
346
456
|
url.searchParams.forEach((value, key) => {
|
|
347
|
-
if (!
|
|
457
|
+
if (!LIST_RESERVED_PARAMS.has(key) && value) {
|
|
348
458
|
if (!customFilters[key]) {
|
|
349
459
|
customFilters[key] = [];
|
|
350
460
|
}
|
|
@@ -353,9 +463,59 @@ async function handleGenericList(request) {
|
|
|
353
463
|
}
|
|
354
464
|
});
|
|
355
465
|
const entityConfig = resolution.entityConfig;
|
|
466
|
+
const shapedQuery = await beforeEntityRead(entityConfig.slug, { ...customFilters }, userId ?? void 0);
|
|
467
|
+
for (const [key, value] of Object.entries(shapedQuery)) {
|
|
468
|
+
customFilters[key] = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
469
|
+
}
|
|
470
|
+
const taxonomyFilterParams = ((_b = entityConfig.taxonomies) == null ? void 0 : _b.enabled) ? TAXONOMY_FILTER_PARAMS : [];
|
|
471
|
+
const invalidFilterKeys = Object.keys(customFilters).filter(
|
|
472
|
+
(key) => !isEntityField(entityConfig, key) && !taxonomyFilterParams.includes(key)
|
|
473
|
+
);
|
|
474
|
+
if (invalidFilterKeys.length > 0) {
|
|
475
|
+
const response2 = createApiError(
|
|
476
|
+
`Unknown filter parameter(s) for ${entityConfig.slug}: ${invalidFilterKeys.join(", ")}`,
|
|
477
|
+
400,
|
|
478
|
+
{
|
|
479
|
+
invalidKeys: invalidFilterKeys,
|
|
480
|
+
allowedKeys: [...entityConfig.fields.map((f) => f.name), ...taxonomyFilterParams]
|
|
481
|
+
},
|
|
482
|
+
"INVALID_FILTER"
|
|
483
|
+
);
|
|
484
|
+
return addCorsHeaders(response2, request);
|
|
485
|
+
}
|
|
486
|
+
const searchParam = url.searchParams.get("search");
|
|
487
|
+
const searchableFields = SEARCHABLE_FIELDS.filter((name) => isEntityField(entityConfig, name));
|
|
488
|
+
if (searchParam && searchParam.trim() !== "" && searchableFields.length === 0) {
|
|
489
|
+
const response2 = createApiError(
|
|
490
|
+
`Entity ${entityConfig.slug} has no searchable field (${SEARCHABLE_FIELDS.join(", ")}); the search parameter is not supported`,
|
|
491
|
+
400,
|
|
492
|
+
{ searchableFields: SEARCHABLE_FIELDS },
|
|
493
|
+
"SEARCH_NOT_SUPPORTED"
|
|
494
|
+
);
|
|
495
|
+
return addCorsHeaders(response2, request);
|
|
496
|
+
}
|
|
497
|
+
const sortByParam = url.searchParams.get("sortBy") || url.searchParams.get("sort");
|
|
498
|
+
if (sortByParam && !isEntityField(entityConfig, sortByParam) && !SORTABLE_BASE_FIELDS.includes(sortByParam)) {
|
|
499
|
+
const response2 = createApiError(
|
|
500
|
+
`Invalid sortBy field for ${entityConfig.slug}: ${sortByParam}`,
|
|
501
|
+
400,
|
|
502
|
+
{ allowedFields: [...SORTABLE_BASE_FIELDS, ...entityConfig.fields.map((f) => f.name)] },
|
|
503
|
+
"INVALID_SORT_FIELD"
|
|
504
|
+
);
|
|
505
|
+
return addCorsHeaders(response2, request);
|
|
506
|
+
}
|
|
356
507
|
let fields;
|
|
357
508
|
if (fieldsParam && distinctParam) {
|
|
358
509
|
const fieldName = fieldsParam;
|
|
510
|
+
if (!isEntityField(entityConfig, fieldName)) {
|
|
511
|
+
const response2 = createApiError(
|
|
512
|
+
`Invalid field for distinct query: ${fieldName}`,
|
|
513
|
+
400,
|
|
514
|
+
void 0,
|
|
515
|
+
"INVALID_FIELD"
|
|
516
|
+
);
|
|
517
|
+
return addCorsHeaders(response2, request);
|
|
518
|
+
}
|
|
359
519
|
const isJsonbField = fieldName === "contentLanguages" || fieldName === "brandValues" || fieldName === "hashtags";
|
|
360
520
|
if (isJsonbField) {
|
|
361
521
|
return handleDistinctJsonbField(entityConfig, fieldName, url, userId);
|
|
@@ -364,19 +524,17 @@ async function handleGenericList(request) {
|
|
|
364
524
|
}
|
|
365
525
|
} else if (fieldsParam) {
|
|
366
526
|
const requestedFields = fieldsParam.split(",").map((f) => f.trim());
|
|
367
|
-
const validFields = requestedFields.filter(
|
|
368
|
-
(fieldName) => entityConfig.fields.some((field) => field.name === fieldName)
|
|
369
|
-
);
|
|
527
|
+
const validFields = requestedFields.filter((fieldName) => isEntityField(entityConfig, fieldName));
|
|
370
528
|
fields = ["id", ...validFields].map((fieldName) => {
|
|
371
529
|
const columnName = /[A-Z]/.test(fieldName) ? `"${fieldName}"` : fieldName;
|
|
372
530
|
return `t.${columnName}`;
|
|
373
531
|
}).join(", ");
|
|
374
532
|
} else {
|
|
375
533
|
const systemFields = ["id", "userId", "teamId", "createdAt", "updatedAt"];
|
|
376
|
-
if ((
|
|
534
|
+
if ((_c = entityConfig.builder) == null ? void 0 : _c.enabled) {
|
|
377
535
|
systemFields.push("blocks");
|
|
378
536
|
}
|
|
379
|
-
if ((
|
|
537
|
+
if ((_d = entityConfig.table) == null ? void 0 : _d.softDelete) {
|
|
380
538
|
systemFields.push("deletedAt", "deletedBy");
|
|
381
539
|
}
|
|
382
540
|
const configFields = entityConfig.fields.map((field) => {
|
|
@@ -395,12 +553,11 @@ async function handleGenericList(request) {
|
|
|
395
553
|
let query;
|
|
396
554
|
let queryParams;
|
|
397
555
|
let paramIndex = 1;
|
|
398
|
-
const searchParam = url.searchParams.get("search");
|
|
399
556
|
const fromDate = url.searchParams.get("from");
|
|
400
557
|
const toDate = url.searchParams.get("to");
|
|
401
558
|
const dateFieldName = url.searchParams.get("dateField");
|
|
402
|
-
const isPublicEntity = ((
|
|
403
|
-
const requestingPublishedOnly = (
|
|
559
|
+
const isPublicEntity = ((_e = entityConfig.access) == null ? void 0 : _e.public) === true;
|
|
560
|
+
const requestingPublishedOnly = (_f = customFilters["status"]) == null ? void 0 : _f.includes("published");
|
|
404
561
|
const skipUserFilter = isPublicEntity && requestingPublishedOnly;
|
|
405
562
|
if (skipUserFilter && process.env.NODE_ENV === "development") {
|
|
406
563
|
console.log(`[GenericHandler] Public access mode: skipping userId filter for ${entityConfig.slug} (status=published)`);
|
|
@@ -413,8 +570,7 @@ async function handleGenericList(request) {
|
|
|
413
570
|
entityConfig,
|
|
414
571
|
userId,
|
|
415
572
|
teamId,
|
|
416
|
-
isBypass
|
|
417
|
-
authResult.type
|
|
573
|
+
isBypass
|
|
418
574
|
);
|
|
419
575
|
}
|
|
420
576
|
if (ownershipFilter.applies && ownershipFilter.value === null) {
|
|
@@ -430,7 +586,7 @@ async function handleGenericList(request) {
|
|
|
430
586
|
query += ` AND t."teamId" = $${paramIndex++}`;
|
|
431
587
|
queryParams.push(teamId);
|
|
432
588
|
}
|
|
433
|
-
if (userId && !((
|
|
589
|
+
if (userId && !((_g = entityConfig.access) == null ? void 0 : _g.shared) && !skipUserFilter && !isBypass) {
|
|
434
590
|
query += ` AND t."userId" = $${paramIndex++}`;
|
|
435
591
|
queryParams.push(userId);
|
|
436
592
|
}
|
|
@@ -438,7 +594,7 @@ async function handleGenericList(request) {
|
|
|
438
594
|
query += ` AND t."${ownershipFilter.field}" = $${paramIndex++}`;
|
|
439
595
|
queryParams.push(ownershipFilter.value);
|
|
440
596
|
}
|
|
441
|
-
if (((
|
|
597
|
+
if (((_h = entityConfig.table) == null ? void 0 : _h.softDelete) && !isBypass) {
|
|
442
598
|
query += ` AND t."deletedAt" IS NULL`;
|
|
443
599
|
}
|
|
444
600
|
} else if (fieldsParam && distinctParam) {
|
|
@@ -454,7 +610,7 @@ async function handleGenericList(request) {
|
|
|
454
610
|
query += ` AND t."parentId" = $${paramIndex++}`;
|
|
455
611
|
queryParams.push(parentId);
|
|
456
612
|
}
|
|
457
|
-
if (userId && !((
|
|
613
|
+
if (userId && !((_i = entityConfig.access) == null ? void 0 : _i.shared) && !skipUserFilter && !isBypass) {
|
|
458
614
|
query += ` AND t."userId" = $${paramIndex++}`;
|
|
459
615
|
queryParams.push(userId);
|
|
460
616
|
}
|
|
@@ -462,7 +618,7 @@ async function handleGenericList(request) {
|
|
|
462
618
|
query += ` AND t."${ownershipFilter.field}" = $${paramIndex++}`;
|
|
463
619
|
queryParams.push(ownershipFilter.value);
|
|
464
620
|
}
|
|
465
|
-
if (((
|
|
621
|
+
if (((_j = entityConfig.table) == null ? void 0 : _j.softDelete) && !isBypass) {
|
|
466
622
|
query += ` AND t."deletedAt" IS NULL`;
|
|
467
623
|
}
|
|
468
624
|
query += ` ORDER BY "${fieldName}" ASC LIMIT $${paramIndex++}`;
|
|
@@ -475,7 +631,7 @@ async function handleGenericList(request) {
|
|
|
475
631
|
whereConditions.push(`t."teamId" = $${paramIndex++}`);
|
|
476
632
|
queryParams.push(teamId);
|
|
477
633
|
}
|
|
478
|
-
if (userId && !((
|
|
634
|
+
if (userId && !((_k = entityConfig.access) == null ? void 0 : _k.shared) && !skipUserFilter && !isBypass) {
|
|
479
635
|
whereConditions.push(`t."userId" = $${paramIndex++}`);
|
|
480
636
|
queryParams.push(userId);
|
|
481
637
|
}
|
|
@@ -483,33 +639,15 @@ async function handleGenericList(request) {
|
|
|
483
639
|
whereConditions.push(`t."${ownershipFilter.field}" = $${paramIndex++}`);
|
|
484
640
|
queryParams.push(ownershipFilter.value);
|
|
485
641
|
}
|
|
486
|
-
if (((
|
|
642
|
+
if (((_l = entityConfig.table) == null ? void 0 : _l.softDelete) && !isBypass) {
|
|
487
643
|
whereConditions.push(`t."deletedAt" IS NULL`);
|
|
488
644
|
}
|
|
489
|
-
if (searchParam && searchParam.trim() !== "") {
|
|
645
|
+
if (searchParam && searchParam.trim() !== "" && searchableFields.length > 0) {
|
|
490
646
|
const searchTerm = searchParam.trim();
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (hasName || hasTitle || hasSlug || hasContent) {
|
|
496
|
-
const searchConditions = [];
|
|
497
|
-
if (hasName) {
|
|
498
|
-
searchConditions.push(`t.name ILIKE $${paramIndex}`);
|
|
499
|
-
}
|
|
500
|
-
if (hasTitle) {
|
|
501
|
-
searchConditions.push(`t.title ILIKE $${paramIndex}`);
|
|
502
|
-
}
|
|
503
|
-
if (hasSlug) {
|
|
504
|
-
searchConditions.push(`t.slug ILIKE $${paramIndex}`);
|
|
505
|
-
}
|
|
506
|
-
if (hasContent) {
|
|
507
|
-
searchConditions.push(`t.content ILIKE $${paramIndex}`);
|
|
508
|
-
}
|
|
509
|
-
whereConditions.push(`(${searchConditions.join(" OR ")})`);
|
|
510
|
-
queryParams.push(`%${searchTerm}%`);
|
|
511
|
-
paramIndex++;
|
|
512
|
-
}
|
|
647
|
+
const searchConditions = searchableFields.map((name) => `t.${name} ILIKE $${paramIndex}`);
|
|
648
|
+
whereConditions.push(`(${searchConditions.join(" OR ")})`);
|
|
649
|
+
queryParams.push(`%${searchTerm}%`);
|
|
650
|
+
paramIndex++;
|
|
513
651
|
}
|
|
514
652
|
if (dateFieldName && (fromDate || toDate)) {
|
|
515
653
|
const field = entityConfig.fields.find((f) => f.name === dateFieldName);
|
|
@@ -537,6 +675,18 @@ async function handleGenericList(request) {
|
|
|
537
675
|
queryParams.push(JSON.stringify([value]));
|
|
538
676
|
});
|
|
539
677
|
whereConditions.push(`(${orConditions.join(" OR ")})`);
|
|
678
|
+
} else if (field.type === "date" || field.type === "datetime") {
|
|
679
|
+
const orConditions = values.map((value) => {
|
|
680
|
+
if (BARE_DATE_RE.test(value)) {
|
|
681
|
+
const lower = paramIndex++;
|
|
682
|
+
const upper = paramIndex++;
|
|
683
|
+
queryParams.push(value, value);
|
|
684
|
+
return `(t.${columnName} >= $${lower}::date AND t.${columnName} < $${upper}::date + 1)`;
|
|
685
|
+
}
|
|
686
|
+
queryParams.push(value);
|
|
687
|
+
return `t.${columnName} = $${paramIndex++}`;
|
|
688
|
+
});
|
|
689
|
+
whereConditions.push(`(${orConditions.join(" OR ")})`);
|
|
540
690
|
} else {
|
|
541
691
|
const orConditions = values.map(() => {
|
|
542
692
|
const condition = `t.${columnName} = $${paramIndex++}`;
|
|
@@ -569,16 +719,9 @@ async function handleGenericList(request) {
|
|
|
569
719
|
) pu ON t.id = pu."patternId"
|
|
570
720
|
`;
|
|
571
721
|
}
|
|
572
|
-
const
|
|
573
|
-
const sortOrderParam = (
|
|
574
|
-
|
|
575
|
-
if (sortByParam) {
|
|
576
|
-
const baseFields = ["id", "createdAt", "updatedAt", "teamId"];
|
|
577
|
-
const isValidField = entityConfig.fields.some((f) => f.name === sortByParam) || baseFields.includes(sortByParam);
|
|
578
|
-
if (isValidField) {
|
|
579
|
-
orderByClause = `t."${sortByParam}" ${sortOrderParam}`;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
722
|
+
const sortOrderRaw = url.searchParams.get("sortOrder") || url.searchParams.get("order");
|
|
723
|
+
const sortOrderParam = (sortOrderRaw == null ? void 0 : sortOrderRaw.toUpperCase()) === "ASC" ? "ASC" : "DESC";
|
|
724
|
+
const orderByClause = sortByParam ? `t."${sortByParam}" ${sortOrderParam}` : 't."createdAt" DESC';
|
|
582
725
|
query = `
|
|
583
726
|
SELECT ${fields}, COUNT(*) OVER() as total_count
|
|
584
727
|
FROM "${tableName}" t
|
|
@@ -625,7 +768,8 @@ async function handleGenericList(request) {
|
|
|
625
768
|
);
|
|
626
769
|
}
|
|
627
770
|
const finalData = filterPublicFields(dataWithTaxonomies, entityConfig, userId);
|
|
628
|
-
const
|
|
771
|
+
const shapedData = await afterEntityRead(entityConfig.slug, finalData, { ...customFilters }, userId ?? void 0);
|
|
772
|
+
const response = createApiResponse(shapedData, paginationMeta);
|
|
629
773
|
return addCorsHeaders(response, request);
|
|
630
774
|
} catch (error) {
|
|
631
775
|
console.error("Error in generic list handler:", error);
|
|
@@ -634,6 +778,9 @@ async function handleGenericList(request) {
|
|
|
634
778
|
}
|
|
635
779
|
}
|
|
636
780
|
async function handleGenericCreate(request) {
|
|
781
|
+
return runWithAuditLog(request, (audit) => handleGenericCreateImpl(request, audit));
|
|
782
|
+
}
|
|
783
|
+
async function handleGenericCreateImpl(request, audit) {
|
|
637
784
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
638
785
|
try {
|
|
639
786
|
const resolution = await resolveEntityFromUrl(request.nextUrl.pathname);
|
|
@@ -645,20 +792,14 @@ async function handleGenericCreate(request) {
|
|
|
645
792
|
const response2 = createApiError("Create operation not supported for this entity", 405);
|
|
646
793
|
return addCorsHeaders(response2, request);
|
|
647
794
|
}
|
|
648
|
-
const authResult = await authenticateRequest(request);
|
|
795
|
+
const authResult = await authenticateRequest(request, { requiredScope: `${resolution.entityConfig.slug}:write` });
|
|
796
|
+
audit.auth = authResult;
|
|
649
797
|
if (!authResult.success) {
|
|
650
|
-
return
|
|
651
|
-
{ success: false, error: "Authentication required", code: "AUTHENTICATION_FAILED" },
|
|
652
|
-
{ status: 401 }
|
|
653
|
-
);
|
|
798
|
+
return createAuthFailureResponse(authResult);
|
|
654
799
|
}
|
|
655
800
|
if (authResult.rateLimitResponse) {
|
|
656
801
|
return authResult.rateLimitResponse;
|
|
657
802
|
}
|
|
658
|
-
if (!hasRequiredScope(authResult, `${resolution.entityConfig.slug}:write`)) {
|
|
659
|
-
const response2 = createApiError("Insufficient permissions", 403);
|
|
660
|
-
return addCorsHeaders(response2, request);
|
|
661
|
-
}
|
|
662
803
|
let body;
|
|
663
804
|
try {
|
|
664
805
|
body = await request.json();
|
|
@@ -671,16 +812,21 @@ async function handleGenericCreate(request) {
|
|
|
671
812
|
const entityConfig = resolution.entityConfig;
|
|
672
813
|
const tableName = getTableName(entityConfig);
|
|
673
814
|
const schemas = generateEntitySchemas(entityConfig);
|
|
674
|
-
const validation = schemas.create.safeParse(entityData);
|
|
815
|
+
const validation = schemas.create.safeParse(omitHandlerManagedKeys(entityConfig, entityData));
|
|
675
816
|
if (!validation.success) {
|
|
676
817
|
console.error(`[${entityConfig.slug}] Validation failed:`, {
|
|
677
818
|
entityData,
|
|
678
819
|
errors: validation.error.issues
|
|
679
820
|
});
|
|
680
|
-
const response2 = createApiError(
|
|
821
|
+
const response2 = createApiError(
|
|
822
|
+
validationErrorMessage(validation.error.issues),
|
|
823
|
+
400,
|
|
824
|
+
validation.error.issues,
|
|
825
|
+
"VALIDATION_ERROR"
|
|
826
|
+
);
|
|
681
827
|
return addCorsHeaders(response2, request);
|
|
682
828
|
}
|
|
683
|
-
|
|
829
|
+
let validatedData = validation.data;
|
|
684
830
|
const xssField = checkNameFieldXss(entityConfig, validatedData);
|
|
685
831
|
if (xssField) {
|
|
686
832
|
const xssResponse = createApiError(
|
|
@@ -714,8 +860,24 @@ async function handleGenericCreate(request) {
|
|
|
714
860
|
const response2 = createApiError("Team context required for create operations. Include x-team-id header.", 400, void 0, "TEAM_CONTEXT_REQUIRED");
|
|
715
861
|
return addCorsHeaders(response2, request);
|
|
716
862
|
}
|
|
717
|
-
const permDenied = await
|
|
863
|
+
const permDenied = await checkAuthPermission(authResult, entityConfig.slug, "create", teamId, request);
|
|
718
864
|
if (permDenied) return permDenied;
|
|
865
|
+
try {
|
|
866
|
+
validatedData = await beforeEntityCreate(
|
|
867
|
+
entityConfig.slug,
|
|
868
|
+
{ ...validatedData, userId: userIdToUse, teamId },
|
|
869
|
+
authResult.user.id
|
|
870
|
+
);
|
|
871
|
+
} catch (hookError) {
|
|
872
|
+
console.error(`[generic-handler] beforeEntityCreate rejected ${entityConfig.slug} create:`, hookError);
|
|
873
|
+
const response2 = createApiError(
|
|
874
|
+
hookError instanceof Error && hookError.message ? hookError.message : `Create rejected by ${entityConfig.slug} before_create hook`,
|
|
875
|
+
hookRejectionStatus(hookError),
|
|
876
|
+
void 0,
|
|
877
|
+
"BEFORE_CREATE_REJECTED"
|
|
878
|
+
);
|
|
879
|
+
return addCorsHeaders(response2, request);
|
|
880
|
+
}
|
|
719
881
|
if (idStrategy === "serial") {
|
|
720
882
|
insertFields = ['"userId"', '"teamId"'];
|
|
721
883
|
placeholders = ["$1", "$2"];
|
|
@@ -801,6 +963,7 @@ async function handleGenericCreate(request) {
|
|
|
801
963
|
if (!actionResult.allowed) {
|
|
802
964
|
const statusCode = actionResult.reason === "quota_exceeded" ? 429 : 403;
|
|
803
965
|
const response2 = createApiError(
|
|
966
|
+
// @ts-expect-error — pre-existing type error, tracked in https://github.com/NextSpark-js/nextspark/issues/131
|
|
804
967
|
actionResult.message || `Quota exceeded for ${entityConfig.slug}`,
|
|
805
968
|
statusCode,
|
|
806
969
|
void 0,
|
|
@@ -883,22 +1046,18 @@ async function handleGenericCreate(request) {
|
|
|
883
1046
|
return addCorsHeaders(response, request);
|
|
884
1047
|
} catch (error) {
|
|
885
1048
|
console.error("Error in generic create handler:", error);
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const response2 = createApiError(
|
|
890
|
-
"A record with this value already exists",
|
|
891
|
-
409,
|
|
892
|
-
{ constraint, detail },
|
|
893
|
-
"UNIQUE_CONSTRAINT_VIOLATION"
|
|
894
|
-
);
|
|
895
|
-
return addCorsHeaders(response2, request);
|
|
1049
|
+
const constraintResponse = mapConstraintViolation(error, "create");
|
|
1050
|
+
if (constraintResponse) {
|
|
1051
|
+
return addCorsHeaders(constraintResponse, request);
|
|
896
1052
|
}
|
|
897
1053
|
const response = createApiError("Internal server error", 500);
|
|
898
1054
|
return addCorsHeaders(response, request);
|
|
899
1055
|
}
|
|
900
1056
|
}
|
|
901
1057
|
async function handleGenericRead(request, { params }) {
|
|
1058
|
+
return runWithAuditLog(request, (audit) => handleGenericReadImpl(request, audit, { params }));
|
|
1059
|
+
}
|
|
1060
|
+
async function handleGenericReadImpl(request, audit, { params }) {
|
|
902
1061
|
var _a, _b, _c, _d, _e, _f;
|
|
903
1062
|
try {
|
|
904
1063
|
const { id } = await params;
|
|
@@ -911,25 +1070,22 @@ async function handleGenericRead(request, { params }) {
|
|
|
911
1070
|
const response2 = createApiError("Read operation not supported for this entity", 405);
|
|
912
1071
|
return addCorsHeaders(response2, request);
|
|
913
1072
|
}
|
|
914
|
-
const authResult = await authenticateRequest(request);
|
|
1073
|
+
const authResult = await authenticateRequest(request, { requiredScope: `${resolution.entityConfig.slug}:read` });
|
|
1074
|
+
audit.auth = authResult;
|
|
915
1075
|
let userId = null;
|
|
916
1076
|
let teamId = null;
|
|
917
1077
|
let isBypass = false;
|
|
1078
|
+
if (!authResult.success && authResult.type === "api-key") {
|
|
1079
|
+
return addCorsHeaders(createAuthFailureResponse(authResult), request);
|
|
1080
|
+
}
|
|
918
1081
|
if (!authResult.success && ((_a = resolution.entityConfig.access) == null ? void 0 : _a.public)) {
|
|
919
1082
|
console.log(`[GenericHandler] Public access allowed for ${resolution.entityConfig.slug} read`);
|
|
920
1083
|
} else if (!authResult.success) {
|
|
921
|
-
return
|
|
922
|
-
{ success: false, error: "Authentication required", code: "AUTHENTICATION_FAILED" },
|
|
923
|
-
{ status: 401 }
|
|
924
|
-
);
|
|
1084
|
+
return createAuthFailureResponse(authResult);
|
|
925
1085
|
} else {
|
|
926
1086
|
if (authResult.rateLimitResponse) {
|
|
927
1087
|
return authResult.rateLimitResponse;
|
|
928
1088
|
}
|
|
929
|
-
if (!hasRequiredScope(authResult, `${resolution.entityConfig.slug}:read`)) {
|
|
930
|
-
const response2 = createApiError("Insufficient permissions", 403);
|
|
931
|
-
return addCorsHeaders(response2, request);
|
|
932
|
-
}
|
|
933
1089
|
userId = authResult.user.id;
|
|
934
1090
|
const teamValidation = await validateTeamContextWithBypass(request, authResult, userId);
|
|
935
1091
|
if (!teamValidation.valid) {
|
|
@@ -938,7 +1094,7 @@ async function handleGenericRead(request, { params }) {
|
|
|
938
1094
|
teamId = teamValidation.teamId;
|
|
939
1095
|
isBypass = teamValidation.isBypass;
|
|
940
1096
|
if (!isBypass && teamId) {
|
|
941
|
-
const permDenied = await
|
|
1097
|
+
const permDenied = await checkAuthPermission(authResult, resolution.entityConfig.slug, "read", teamId, request);
|
|
942
1098
|
if (permDenied) return permDenied;
|
|
943
1099
|
}
|
|
944
1100
|
}
|
|
@@ -982,8 +1138,7 @@ async function handleGenericRead(request, { params }) {
|
|
|
982
1138
|
entityConfig,
|
|
983
1139
|
userId,
|
|
984
1140
|
teamId,
|
|
985
|
-
isBypass
|
|
986
|
-
authResult.type
|
|
1141
|
+
isBypass
|
|
987
1142
|
);
|
|
988
1143
|
if (readOwnershipFilter.applies) {
|
|
989
1144
|
if (readOwnershipFilter.value === null) {
|
|
@@ -1036,7 +1191,8 @@ async function handleGenericRead(request, { params }) {
|
|
|
1036
1191
|
itemWithTaxonomies = itemsWithTaxonomies[0] || itemWithChild;
|
|
1037
1192
|
}
|
|
1038
1193
|
const finalItem = filterPublicFields(itemWithTaxonomies, entityConfig, userId);
|
|
1039
|
-
const
|
|
1194
|
+
const [shapedItem] = await afterEntityRead(entityConfig.slug, [finalItem], { id }, userId ?? void 0);
|
|
1195
|
+
const response = createApiResponse(shapedItem ?? finalItem);
|
|
1040
1196
|
return addCorsHeaders(response, request);
|
|
1041
1197
|
} catch (error) {
|
|
1042
1198
|
console.error("Error in generic read handler:", error);
|
|
@@ -1045,7 +1201,10 @@ async function handleGenericRead(request, { params }) {
|
|
|
1045
1201
|
}
|
|
1046
1202
|
}
|
|
1047
1203
|
async function handleGenericUpdate(request, { params }) {
|
|
1048
|
-
|
|
1204
|
+
return runWithAuditLog(request, (audit) => handleGenericUpdateImpl(request, audit, { params }));
|
|
1205
|
+
}
|
|
1206
|
+
async function handleGenericUpdateImpl(request, audit, { params }) {
|
|
1207
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1049
1208
|
try {
|
|
1050
1209
|
const { id } = await params;
|
|
1051
1210
|
const resolution = await resolveEntityFromUrl(request.nextUrl.pathname);
|
|
@@ -1057,20 +1216,14 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1057
1216
|
const response2 = createApiError("Update operation not supported for this entity", 405);
|
|
1058
1217
|
return addCorsHeaders(response2, request);
|
|
1059
1218
|
}
|
|
1060
|
-
const authResult = await authenticateRequest(request);
|
|
1219
|
+
const authResult = await authenticateRequest(request, { requiredScope: `${resolution.entityConfig.slug}:write` });
|
|
1220
|
+
audit.auth = authResult;
|
|
1061
1221
|
if (!authResult.success) {
|
|
1062
|
-
return
|
|
1063
|
-
{ success: false, error: "Authentication required", code: "AUTHENTICATION_FAILED" },
|
|
1064
|
-
{ status: 401 }
|
|
1065
|
-
);
|
|
1222
|
+
return createAuthFailureResponse(authResult);
|
|
1066
1223
|
}
|
|
1067
1224
|
if (authResult.rateLimitResponse) {
|
|
1068
1225
|
return authResult.rateLimitResponse;
|
|
1069
1226
|
}
|
|
1070
|
-
if (!hasRequiredScope(authResult, `${resolution.entityConfig.slug}:write`)) {
|
|
1071
|
-
const response2 = createApiError("Insufficient permissions", 403);
|
|
1072
|
-
return addCorsHeaders(response2, request);
|
|
1073
|
-
}
|
|
1074
1227
|
const teamValidation = await validateTeamContextWithBypass(request, authResult, authResult.user.id);
|
|
1075
1228
|
if (!teamValidation.valid) {
|
|
1076
1229
|
return teamValidation.error;
|
|
@@ -1080,7 +1233,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1080
1233
|
const response2 = createApiError("Team context required for update operations. Include x-team-id header.", 400, void 0, "TEAM_CONTEXT_REQUIRED");
|
|
1081
1234
|
return addCorsHeaders(response2, request);
|
|
1082
1235
|
}
|
|
1083
|
-
const permDenied = await
|
|
1236
|
+
const permDenied = await checkAuthPermission(authResult, resolution.entityConfig.slug, "update", teamId, request);
|
|
1084
1237
|
if (permDenied) return permDenied;
|
|
1085
1238
|
let body;
|
|
1086
1239
|
try {
|
|
@@ -1092,7 +1245,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1092
1245
|
const { metas, ...entityData } = body;
|
|
1093
1246
|
const entityConfig = resolution.entityConfig;
|
|
1094
1247
|
const fieldGuards = (_b = (_a = entityConfig.access) == null ? void 0 : _a.ownershipFilter) == null ? void 0 : _b.fieldGuards;
|
|
1095
|
-
if (fieldGuards && fieldGuards.length > 0 && authResult.
|
|
1248
|
+
if (fieldGuards && fieldGuards.length > 0 && ((_c = authResult.user) == null ? void 0 : _c.id)) {
|
|
1096
1249
|
const guardMemberRow = await queryOneWithRLS(
|
|
1097
1250
|
'SELECT role FROM "team_members" WHERE "teamId" = $1 AND "userId" = $2',
|
|
1098
1251
|
[teamId, authResult.user.id],
|
|
@@ -1117,16 +1270,21 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1117
1270
|
}
|
|
1118
1271
|
const tableName = getTableName(entityConfig);
|
|
1119
1272
|
const schemas = generateEntitySchemas(entityConfig);
|
|
1120
|
-
const validation = schemas.update.safeParse(entityData);
|
|
1273
|
+
const validation = schemas.update.safeParse(omitHandlerManagedKeys(entityConfig, entityData));
|
|
1121
1274
|
if (!validation.success) {
|
|
1122
1275
|
console.log(`[${entityConfig.slug}] Validation failed:`, {
|
|
1123
1276
|
entityData,
|
|
1124
1277
|
errors: validation.error.issues
|
|
1125
1278
|
});
|
|
1126
|
-
const response2 = createApiError(
|
|
1279
|
+
const response2 = createApiError(
|
|
1280
|
+
validationErrorMessage(validation.error.issues),
|
|
1281
|
+
400,
|
|
1282
|
+
validation.error.issues,
|
|
1283
|
+
"VALIDATION_ERROR"
|
|
1284
|
+
);
|
|
1127
1285
|
return addCorsHeaders(response2, request);
|
|
1128
1286
|
}
|
|
1129
|
-
|
|
1287
|
+
let validatedData = validation.data;
|
|
1130
1288
|
const updateXssField = checkNameFieldXss(entityConfig, validatedData);
|
|
1131
1289
|
if (updateXssField) {
|
|
1132
1290
|
const updateXssResponse = createApiError(
|
|
@@ -1137,11 +1295,28 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1137
1295
|
);
|
|
1138
1296
|
return addCorsHeaders(updateXssResponse, request);
|
|
1139
1297
|
}
|
|
1298
|
+
try {
|
|
1299
|
+
validatedData = await beforeEntityUpdate(
|
|
1300
|
+
entityConfig.slug,
|
|
1301
|
+
id,
|
|
1302
|
+
validatedData,
|
|
1303
|
+
authResult.user.id
|
|
1304
|
+
);
|
|
1305
|
+
} catch (hookError) {
|
|
1306
|
+
console.error(`[generic-handler] beforeEntityUpdate rejected ${entityConfig.slug} update:`, hookError);
|
|
1307
|
+
const response2 = createApiError(
|
|
1308
|
+
hookError instanceof Error && hookError.message ? hookError.message : `Update rejected by ${entityConfig.slug} before_update hook`,
|
|
1309
|
+
hookRejectionStatus(hookError),
|
|
1310
|
+
void 0,
|
|
1311
|
+
"BEFORE_UPDATE_REJECTED"
|
|
1312
|
+
);
|
|
1313
|
+
return addCorsHeaders(response2, request);
|
|
1314
|
+
}
|
|
1140
1315
|
const updates = [];
|
|
1141
1316
|
const values = [];
|
|
1142
1317
|
let paramCount = 1;
|
|
1143
1318
|
const builderRequest = isBuilderRequest(request);
|
|
1144
|
-
const entityHasBuilder = ((
|
|
1319
|
+
const entityHasBuilder = ((_d = entityConfig.builder) == null ? void 0 : _d.enabled) === true;
|
|
1145
1320
|
if (builderRequest && entityHasBuilder && "blocks" in entityData) {
|
|
1146
1321
|
let blocksData = entityData.blocks;
|
|
1147
1322
|
blocksData = await filterOrphanedPatternReferences(blocksData, authResult.user.id);
|
|
@@ -1217,7 +1392,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1217
1392
|
whereConditions.push(`"teamId" = $${paramCount++}`);
|
|
1218
1393
|
values.push(teamId);
|
|
1219
1394
|
}
|
|
1220
|
-
if (!((
|
|
1395
|
+
if (!((_e = entityConfig.access) == null ? void 0 : _e.shared)) {
|
|
1221
1396
|
whereConditions.push(`"userId" = $${paramCount++}`);
|
|
1222
1397
|
values.push(authResult.user.id);
|
|
1223
1398
|
}
|
|
@@ -1225,8 +1400,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1225
1400
|
entityConfig,
|
|
1226
1401
|
authResult.user.id,
|
|
1227
1402
|
teamId,
|
|
1228
|
-
teamValidation.isBypass
|
|
1229
|
-
authResult.type
|
|
1403
|
+
teamValidation.isBypass
|
|
1230
1404
|
);
|
|
1231
1405
|
if (updateOwnershipFilter.applies) {
|
|
1232
1406
|
if (updateOwnershipFilter.value === null) {
|
|
@@ -1262,7 +1436,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1262
1436
|
}
|
|
1263
1437
|
updatedItem = result[0];
|
|
1264
1438
|
}
|
|
1265
|
-
if ((
|
|
1439
|
+
if ((_f = entityConfig.taxonomies) == null ? void 0 : _f.enabled) {
|
|
1266
1440
|
await processTaxonomyRelations(entityConfig, id, body, authResult.user.id, true);
|
|
1267
1441
|
}
|
|
1268
1442
|
if (metadataWasProvided) {
|
|
@@ -1274,7 +1448,7 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1274
1448
|
);
|
|
1275
1449
|
}
|
|
1276
1450
|
let responseItem = updatedItem;
|
|
1277
|
-
if ((
|
|
1451
|
+
if ((_g = entityConfig.taxonomies) == null ? void 0 : _g.enabled) {
|
|
1278
1452
|
const itemsWithTaxonomies = await includeTaxonomiesInData(
|
|
1279
1453
|
entityConfig,
|
|
1280
1454
|
[updatedItem],
|
|
@@ -1297,22 +1471,18 @@ async function handleGenericUpdate(request, { params }) {
|
|
|
1297
1471
|
return addCorsHeaders(response, request);
|
|
1298
1472
|
} catch (error) {
|
|
1299
1473
|
console.error("Error in generic update handler:", error);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const response2 = createApiError(
|
|
1304
|
-
"A record with this value already exists",
|
|
1305
|
-
409,
|
|
1306
|
-
{ constraint, detail },
|
|
1307
|
-
"UNIQUE_CONSTRAINT_VIOLATION"
|
|
1308
|
-
);
|
|
1309
|
-
return addCorsHeaders(response2, request);
|
|
1474
|
+
const constraintResponse = mapConstraintViolation(error, "update");
|
|
1475
|
+
if (constraintResponse) {
|
|
1476
|
+
return addCorsHeaders(constraintResponse, request);
|
|
1310
1477
|
}
|
|
1311
1478
|
const response = createApiError("Internal server error", 500);
|
|
1312
1479
|
return addCorsHeaders(response, request);
|
|
1313
1480
|
}
|
|
1314
1481
|
}
|
|
1315
1482
|
async function handleGenericDelete(request, { params }) {
|
|
1483
|
+
return runWithAuditLog(request, (audit) => handleGenericDeleteImpl(request, audit, { params }));
|
|
1484
|
+
}
|
|
1485
|
+
async function handleGenericDeleteImpl(request, audit, { params }) {
|
|
1316
1486
|
var _a, _b, _c, _d, _e;
|
|
1317
1487
|
try {
|
|
1318
1488
|
const { id } = await params;
|
|
@@ -1325,20 +1495,14 @@ async function handleGenericDelete(request, { params }) {
|
|
|
1325
1495
|
const response2 = createApiError("Delete operation not supported for this entity", 405);
|
|
1326
1496
|
return addCorsHeaders(response2, request);
|
|
1327
1497
|
}
|
|
1328
|
-
const authResult = await authenticateRequest(request);
|
|
1498
|
+
const authResult = await authenticateRequest(request, { requiredScope: `${resolution.entityConfig.slug}:delete` });
|
|
1499
|
+
audit.auth = authResult;
|
|
1329
1500
|
if (!authResult.success) {
|
|
1330
|
-
return
|
|
1331
|
-
{ success: false, error: "Authentication required", code: "AUTHENTICATION_FAILED" },
|
|
1332
|
-
{ status: 401 }
|
|
1333
|
-
);
|
|
1501
|
+
return createAuthFailureResponse(authResult);
|
|
1334
1502
|
}
|
|
1335
1503
|
if (authResult.rateLimitResponse) {
|
|
1336
1504
|
return authResult.rateLimitResponse;
|
|
1337
1505
|
}
|
|
1338
|
-
if (!hasRequiredScope(authResult, `${resolution.entityConfig.slug}:write`)) {
|
|
1339
|
-
const response2 = createApiError("Insufficient permissions", 403);
|
|
1340
|
-
return addCorsHeaders(response2, request);
|
|
1341
|
-
}
|
|
1342
1506
|
const teamValidation = await validateTeamContextWithBypass(request, authResult, authResult.user.id);
|
|
1343
1507
|
if (!teamValidation.valid) {
|
|
1344
1508
|
return teamValidation.error;
|
|
@@ -1348,10 +1512,31 @@ async function handleGenericDelete(request, { params }) {
|
|
|
1348
1512
|
const response2 = createApiError("Team context required for delete operations. Include x-team-id header.", 400, void 0, "TEAM_CONTEXT_REQUIRED");
|
|
1349
1513
|
return addCorsHeaders(response2, request);
|
|
1350
1514
|
}
|
|
1351
|
-
const permDenied = await
|
|
1515
|
+
const permDenied = await checkAuthPermission(authResult, resolution.entityConfig.slug, "delete", teamId, request);
|
|
1352
1516
|
if (permDenied) return permDenied;
|
|
1353
1517
|
const entityConfig = resolution.entityConfig;
|
|
1354
1518
|
const tableName = getTableName(entityConfig);
|
|
1519
|
+
try {
|
|
1520
|
+
const allowDelete = await beforeEntityDelete(entityConfig.slug, id, authResult.user.id);
|
|
1521
|
+
if (!allowDelete) {
|
|
1522
|
+
const response2 = createApiError(
|
|
1523
|
+
`Delete rejected by ${entityConfig.slug} before_delete hook`,
|
|
1524
|
+
403,
|
|
1525
|
+
void 0,
|
|
1526
|
+
"BEFORE_DELETE_REJECTED"
|
|
1527
|
+
);
|
|
1528
|
+
return addCorsHeaders(response2, request);
|
|
1529
|
+
}
|
|
1530
|
+
} catch (hookError) {
|
|
1531
|
+
console.error(`[generic-handler] beforeEntityDelete rejected ${entityConfig.slug} delete:`, hookError);
|
|
1532
|
+
const response2 = createApiError(
|
|
1533
|
+
hookError instanceof Error && hookError.message ? hookError.message : `Delete rejected by ${entityConfig.slug} before_delete hook`,
|
|
1534
|
+
hookRejectionStatus(hookError),
|
|
1535
|
+
void 0,
|
|
1536
|
+
"BEFORE_DELETE_REJECTED"
|
|
1537
|
+
);
|
|
1538
|
+
return addCorsHeaders(response2, request);
|
|
1539
|
+
}
|
|
1355
1540
|
const whereConditions = ["id = $1"];
|
|
1356
1541
|
const deleteParams = [id];
|
|
1357
1542
|
let paramIndex = 2;
|
|
@@ -1408,16 +1593,9 @@ async function handleGenericDelete(request, { params }) {
|
|
|
1408
1593
|
return addCorsHeaders(response, request);
|
|
1409
1594
|
} catch (error) {
|
|
1410
1595
|
console.error("Error in generic delete handler:", error);
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const response2 = createApiError(
|
|
1415
|
-
detail || "Cannot delete: this record is referenced by other records",
|
|
1416
|
-
409,
|
|
1417
|
-
{ constraint },
|
|
1418
|
-
"FOREIGN_KEY_VIOLATION"
|
|
1419
|
-
);
|
|
1420
|
-
return addCorsHeaders(response2, request);
|
|
1596
|
+
const constraintResponse = mapConstraintViolation(error, "delete");
|
|
1597
|
+
if (constraintResponse) {
|
|
1598
|
+
return addCorsHeaders(constraintResponse, request);
|
|
1421
1599
|
}
|
|
1422
1600
|
const response = createApiError("Internal server error", 500);
|
|
1423
1601
|
return addCorsHeaders(response, request);
|