@open-mercato/core 0.6.8-develop.6924.1.a8d208fcdc → 0.6.8-develop.6930.1.1e5976efc3
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/eudr_due_diligence_statement/index.js +47 -0
- package/dist/generated/entities/eudr_due_diligence_statement/index.js.map +7 -0
- package/dist/generated/entities/eudr_evidence_submission/index.js +51 -0
- package/dist/generated/entities/eudr_evidence_submission/index.js.map +7 -0
- package/dist/generated/entities/eudr_mitigation_action/index.js +31 -0
- package/dist/generated/entities/eudr_mitigation_action/index.js.map +7 -0
- package/dist/generated/entities/eudr_plot/index.js +39 -0
- package/dist/generated/entities/eudr_plot/index.js.map +7 -0
- package/dist/generated/entities/eudr_product_mapping/index.js +31 -0
- package/dist/generated/entities/eudr_product_mapping/index.js.map +7 -0
- package/dist/generated/entities/eudr_risk_assessment/index.js +35 -0
- package/dist/generated/entities/eudr_risk_assessment/index.js.map +7 -0
- package/dist/generated/entities/role/index.js +2 -0
- package/dist/generated/entities/role/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +9 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +121 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +4 -0
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/modules/attachments/fields/attachment.js +5 -1
- package/dist/modules/attachments/fields/attachment.js.map +2 -2
- package/dist/modules/attachments/lib/crud.js +3 -1
- package/dist/modules/attachments/lib/crud.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +5 -0
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js +5 -0
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +4 -4
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/audit_logs/components/AuditLogsActions.js +9 -2
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
- package/dist/modules/auth/api/login.js +5 -2
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +8 -5
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +15 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +7 -1
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/auth/commands/users.js +175 -76
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +4 -0
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/sessionIntegrity.js +1 -1
- package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260728134212_auth.js +14 -0
- package/dist/modules/auth/migrations/Migration20260728134212_auth.js.map +7 -0
- package/dist/modules/eudr/acl.js +43 -0
- package/dist/modules/eudr/acl.js.map +7 -0
- package/dist/modules/eudr/ai-agents.js +205 -0
- package/dist/modules/eudr/ai-agents.js.map +7 -0
- package/dist/modules/eudr/ai-tools/compliance-pack.js +495 -0
- package/dist/modules/eudr/ai-tools/compliance-pack.js.map +7 -0
- package/dist/modules/eudr/ai-tools/types.js +10 -0
- package/dist/modules/eudr/ai-tools/types.js.map +7 -0
- package/dist/modules/eudr/ai-tools.js +10 -0
- package/dist/modules/eudr/ai-tools.js.map +7 -0
- package/dist/modules/eudr/api/dashboard/widgets/compliance-overview/route.js +386 -0
- package/dist/modules/eudr/api/dashboard/widgets/compliance-overview/route.js.map +7 -0
- package/dist/modules/eudr/api/evidence-submissions/route.js +318 -0
- package/dist/modules/eudr/api/evidence-submissions/route.js.map +7 -0
- package/dist/modules/eudr/api/mitigation-actions/route.js +249 -0
- package/dist/modules/eudr/api/mitigation-actions/route.js.map +7 -0
- package/dist/modules/eudr/api/openapi.js +27 -0
- package/dist/modules/eudr/api/openapi.js.map +7 -0
- package/dist/modules/eudr/api/plots/import/route.js +294 -0
- package/dist/modules/eudr/api/plots/import/route.js.map +7 -0
- package/dist/modules/eudr/api/plots/route.js +279 -0
- package/dist/modules/eudr/api/plots/route.js.map +7 -0
- package/dist/modules/eudr/api/product-mappings/route.js +257 -0
- package/dist/modules/eudr/api/product-mappings/route.js.map +7 -0
- package/dist/modules/eudr/api/product-mappings/suggestions/apply/route.js +298 -0
- package/dist/modules/eudr/api/product-mappings/suggestions/apply/route.js.map +7 -0
- package/dist/modules/eudr/api/product-mappings/suggestions/route.js +132 -0
- package/dist/modules/eudr/api/product-mappings/suggestions/route.js.map +7 -0
- package/dist/modules/eudr/api/reports/annual/route.js +266 -0
- package/dist/modules/eudr/api/reports/annual/route.js.map +7 -0
- package/dist/modules/eudr/api/risk-assessments/route.js +300 -0
- package/dist/modules/eudr/api/risk-assessments/route.js.map +7 -0
- package/dist/modules/eudr/api/statements/[id]/export/route.js +631 -0
- package/dist/modules/eudr/api/statements/[id]/export/route.js.map +7 -0
- package/dist/modules/eudr/api/statements/[id]/readiness/route.js +111 -0
- package/dist/modules/eudr/api/statements/[id]/readiness/route.js.map +7 -0
- package/dist/modules/eudr/api/statements/route.js +314 -0
- package/dist/modules/eudr/api/statements/route.js.map +7 -0
- package/dist/modules/eudr/api/suppliers/compliance/route.js +173 -0
- package/dist/modules/eudr/api/suppliers/compliance/route.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.js +468 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.js +351 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js +17 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js +313 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/page.js +360 -0
- package/dist/modules/eudr/backend/eudr/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.js +346 -0
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/create/page.js +253 -0
- package/dist/modules/eudr/backend/eudr/plots/create/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js +17 -0
- package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js +386 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/plots/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/plots/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.js +253 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.js +174 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js +17 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js +306 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.js +335 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.js +220 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js +17 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js +324 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.js +648 -0
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/create/page.js +432 -0
- package/dist/modules/eudr/backend/eudr/statements/create/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js +17 -0
- package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js +309 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +7 -0
- package/dist/modules/eudr/backend/eudr/statements/page.meta.js +16 -0
- package/dist/modules/eudr/backend/eudr/statements/page.meta.js.map +7 -0
- package/dist/modules/eudr/ce.js +56 -0
- package/dist/modules/eudr/ce.js.map +7 -0
- package/dist/modules/eudr/commands/evidence-submissions.js +571 -0
- package/dist/modules/eudr/commands/evidence-submissions.js.map +7 -0
- package/dist/modules/eudr/commands/index.js +7 -0
- package/dist/modules/eudr/commands/index.js.map +7 -0
- package/dist/modules/eudr/commands/lifecycle-events.js +19 -0
- package/dist/modules/eudr/commands/lifecycle-events.js.map +7 -0
- package/dist/modules/eudr/commands/mitigation-actions.js +488 -0
- package/dist/modules/eudr/commands/mitigation-actions.js.map +7 -0
- package/dist/modules/eudr/commands/plots.js +517 -0
- package/dist/modules/eudr/commands/plots.js.map +7 -0
- package/dist/modules/eudr/commands/product-mappings.js +468 -0
- package/dist/modules/eudr/commands/product-mappings.js.map +7 -0
- package/dist/modules/eudr/commands/risk-assessments.js +599 -0
- package/dist/modules/eudr/commands/risk-assessments.js.map +7 -0
- package/dist/modules/eudr/commands/statements.js +747 -0
- package/dist/modules/eudr/commands/statements.js.map +7 -0
- package/dist/modules/eudr/components/CountrySelectField.js +64 -0
- package/dist/modules/eudr/components/CountrySelectField.js.map +7 -0
- package/dist/modules/eudr/components/GeometryInput.js +133 -0
- package/dist/modules/eudr/components/GeometryInput.js.map +7 -0
- package/dist/modules/eudr/components/MappingSuggestionsDialog.js +307 -0
- package/dist/modules/eudr/components/MappingSuggestionsDialog.js.map +7 -0
- package/dist/modules/eudr/components/MitigationActionsSection.js +345 -0
- package/dist/modules/eudr/components/MitigationActionsSection.js.map +7 -0
- package/dist/modules/eudr/components/OrderSelectField.js +88 -0
- package/dist/modules/eudr/components/OrderSelectField.js.map +7 -0
- package/dist/modules/eudr/components/PlotImportDialog.js +229 -0
- package/dist/modules/eudr/components/PlotImportDialog.js.map +7 -0
- package/dist/modules/eudr/components/PlotMapPreview.js +35 -0
- package/dist/modules/eudr/components/PlotMapPreview.js.map +7 -0
- package/dist/modules/eudr/components/PlotMapPreviewImpl.js +97 -0
- package/dist/modules/eudr/components/PlotMapPreviewImpl.js.map +7 -0
- package/dist/modules/eudr/components/PlotMultiSelectField.js +198 -0
- package/dist/modules/eudr/components/PlotMultiSelectField.js.map +7 -0
- package/dist/modules/eudr/components/RiskCriteriaField.js +128 -0
- package/dist/modules/eudr/components/RiskCriteriaField.js.map +7 -0
- package/dist/modules/eudr/components/StatementLifecycleBar.js +274 -0
- package/dist/modules/eudr/components/StatementLifecycleBar.js.map +7 -0
- package/dist/modules/eudr/components/StatementReadinessChecklist.js +56 -0
- package/dist/modules/eudr/components/StatementReadinessChecklist.js.map +7 -0
- package/dist/modules/eudr/components/StatementRiskSection.js +179 -0
- package/dist/modules/eudr/components/StatementRiskSection.js.map +7 -0
- package/dist/modules/eudr/components/crudErrorI18n.js +31 -0
- package/dist/modules/eudr/components/crudErrorI18n.js.map +7 -0
- package/dist/modules/eudr/components/formConfig.js +593 -0
- package/dist/modules/eudr/components/formConfig.js.map +7 -0
- package/dist/modules/eudr/components/plotForm.js +68 -0
- package/dist/modules/eudr/components/plotForm.js.map +7 -0
- package/dist/modules/eudr/data/enrichers.js +62 -0
- package/dist/modules/eudr/data/enrichers.js.map +7 -0
- package/dist/modules/eudr/data/entities.js +437 -0
- package/dist/modules/eudr/data/entities.js.map +7 -0
- package/dist/modules/eudr/data/validators.js +271 -0
- package/dist/modules/eudr/data/validators.js.map +7 -0
- package/dist/modules/eudr/encryption.js +33 -0
- package/dist/modules/eudr/encryption.js.map +7 -0
- package/dist/modules/eudr/events.js +38 -0
- package/dist/modules/eudr/events.js.map +7 -0
- package/dist/modules/eudr/index.js +15 -0
- package/dist/modules/eudr/index.js.map +7 -0
- package/dist/modules/eudr/lib/annual-report.js +104 -0
- package/dist/modules/eudr/lib/annual-report.js.map +7 -0
- package/dist/modules/eudr/lib/completeness.js +64 -0
- package/dist/modules/eudr/lib/completeness.js.map +7 -0
- package/dist/modules/eudr/lib/detail-read-scope.js +16 -0
- package/dist/modules/eudr/lib/detail-read-scope.js.map +7 -0
- package/dist/modules/eudr/lib/geometry.js +142 -0
- package/dist/modules/eudr/lib/geometry.js.map +7 -0
- package/dist/modules/eudr/lib/notifications.js +56 -0
- package/dist/modules/eudr/lib/notifications.js.map +7 -0
- package/dist/modules/eudr/lib/plural.js +26 -0
- package/dist/modules/eudr/lib/plural.js.map +7 -0
- package/dist/modules/eudr/lib/reference-data.js +240 -0
- package/dist/modules/eudr/lib/reference-data.js.map +7 -0
- package/dist/modules/eudr/lib/species.js +10 -0
- package/dist/modules/eudr/lib/species.js.map +7 -0
- package/dist/modules/eudr/lib/statement-lifecycle.js +84 -0
- package/dist/modules/eudr/lib/statement-lifecycle.js.map +7 -0
- package/dist/modules/eudr/lib/statement-seeding.js +49 -0
- package/dist/modules/eudr/lib/statement-seeding.js.map +7 -0
- package/dist/modules/eudr/migrations/Migration20260706102002_eudr.js +15 -0
- package/dist/modules/eudr/migrations/Migration20260706102002_eudr.js.map +7 -0
- package/dist/modules/eudr/migrations/Migration20260706152612_eudr.js +24 -0
- package/dist/modules/eudr/migrations/Migration20260706152612_eudr.js.map +7 -0
- package/dist/modules/eudr/migrations/Migration20260722100000_eudr.js +15 -0
- package/dist/modules/eudr/migrations/Migration20260722100000_eudr.js.map +7 -0
- package/dist/modules/eudr/notifications.js +58 -0
- package/dist/modules/eudr/notifications.js.map +7 -0
- package/dist/modules/eudr/search.js +186 -0
- package/dist/modules/eudr/search.js.map +7 -0
- package/dist/modules/eudr/setup.js +12 -0
- package/dist/modules/eudr/setup.js.map +7 -0
- package/dist/modules/eudr/subscribers/notify-mitigation-completed.js +34 -0
- package/dist/modules/eudr/subscribers/notify-mitigation-completed.js.map +7 -0
- package/dist/modules/eudr/subscribers/notify-risk-concluded.js +35 -0
- package/dist/modules/eudr/subscribers/notify-risk-concluded.js.map +7 -0
- package/dist/modules/eudr/subscribers/notify-statement-reference-issued.js +37 -0
- package/dist/modules/eudr/subscribers/notify-statement-reference-issued.js.map +7 -0
- package/dist/modules/eudr/subscribers/notify-statement-submitted.js +34 -0
- package/dist/modules/eudr/subscribers/notify-statement-submitted.js.map +7 -0
- package/dist/modules/eudr/subscribers/notify-statement-withdrawn.js +34 -0
- package/dist/modules/eudr/subscribers/notify-statement-withdrawn.js.map +7 -0
- package/dist/modules/eudr/subscribers/recompute-completeness-on-attachment-created.js +124 -0
- package/dist/modules/eudr/subscribers/recompute-completeness-on-attachment-created.js.map +7 -0
- package/dist/modules/eudr/subscribers/recompute-completeness-on-attachment-deleted.js +22 -0
- package/dist/modules/eudr/subscribers/recompute-completeness-on-attachment-deleted.js.map +7 -0
- package/dist/modules/eudr/widgets/dashboard/compliance-overview/widget.client.js +203 -0
- package/dist/modules/eudr/widgets/dashboard/compliance-overview/widget.client.js.map +7 -0
- package/dist/modules/eudr/widgets/dashboard/compliance-overview/widget.js +23 -0
- package/dist/modules/eudr/widgets/dashboard/compliance-overview/widget.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/eudr-product-column/widget.client.js +19 -0
- package/dist/modules/eudr/widgets/injection/eudr-product-column/widget.client.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/eudr-product-column/widget.js +31 -0
- package/dist/modules/eudr/widgets/injection/eudr-product-column/widget.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/order-compliance/widget.client.js +100 -0
- package/dist/modules/eudr/widgets/injection/order-compliance/widget.client.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/order-compliance/widget.js +17 -0
- package/dist/modules/eudr/widgets/injection/order-compliance/widget.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/supplier-compliance/widget.client.js +116 -0
- package/dist/modules/eudr/widgets/injection/supplier-compliance/widget.client.js.map +7 -0
- package/dist/modules/eudr/widgets/injection/supplier-compliance/widget.js +17 -0
- package/dist/modules/eudr/widgets/injection/supplier-compliance/widget.js.map +7 -0
- package/dist/modules/eudr/widgets/injection-table.js +32 -0
- package/dist/modules/eudr/widgets/injection-table.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +67 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +121 -48
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +17 -0
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +32 -18
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/entities.js +1 -0
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260806120000_sales_tag_assignment_document_idx.js +17 -0
- package/dist/modules/sales/migrations/Migration20260806120000_sales_tag_assignment_document_idx.js.map +7 -0
- package/generated/entities/eudr_due_diligence_statement/index.ts +22 -0
- package/generated/entities/eudr_evidence_submission/index.ts +24 -0
- package/generated/entities/eudr_mitigation_action/index.ts +14 -0
- package/generated/entities/eudr_plot/index.ts +18 -0
- package/generated/entities/eudr_product_mapping/index.ts +14 -0
- package/generated/entities/eudr_risk_assessment/index.ts +16 -0
- package/generated/entities/role/index.ts +1 -0
- package/generated/entities.ids.generated.ts +9 -0
- package/generated/entity-fields-registry.ts +121 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +13 -0
- package/src/modules/attachments/fields/attachment.tsx +5 -0
- package/src/modules/attachments/lib/crud.ts +2 -0
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +7 -0
- package/src/modules/audit_logs/api/audit-logs/actions/undo/route.ts +9 -0
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +4 -4
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +20 -4
- package/src/modules/auth/api/login.ts +9 -2
- package/src/modules/auth/api/users/route.ts +8 -4
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -1
- package/src/modules/auth/backend/users/page.tsx +11 -0
- package/src/modules/auth/commands/users.ts +251 -85
- package/src/modules/auth/data/entities.ts +3 -0
- package/src/modules/auth/i18n/de.json +6 -0
- package/src/modules/auth/i18n/en.json +6 -0
- package/src/modules/auth/i18n/es.json +6 -0
- package/src/modules/auth/i18n/ko.json +6 -0
- package/src/modules/auth/i18n/pl.json +6 -0
- package/src/modules/auth/lib/sessionIntegrity.ts +1 -1
- package/src/modules/auth/lib/setup-app.ts +1 -1
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/auth/migrations/Migration20260728134212_auth.ts +14 -0
- package/src/modules/eudr/acl.ts +39 -0
- package/src/modules/eudr/ai-agents.ts +258 -0
- package/src/modules/eudr/ai-tools/compliance-pack.ts +618 -0
- package/src/modules/eudr/ai-tools/types.ts +44 -0
- package/src/modules/eudr/ai-tools.ts +15 -0
- package/src/modules/eudr/api/dashboard/widgets/compliance-overview/route.ts +481 -0
- package/src/modules/eudr/api/evidence-submissions/route.ts +335 -0
- package/src/modules/eudr/api/mitigation-actions/route.ts +263 -0
- package/src/modules/eudr/api/openapi.ts +28 -0
- package/src/modules/eudr/api/plots/import/route.ts +369 -0
- package/src/modules/eudr/api/plots/route.ts +296 -0
- package/src/modules/eudr/api/product-mappings/route.ts +269 -0
- package/src/modules/eudr/api/product-mappings/suggestions/apply/route.ts +382 -0
- package/src/modules/eudr/api/product-mappings/suggestions/route.ts +169 -0
- package/src/modules/eudr/api/reports/annual/route.ts +457 -0
- package/src/modules/eudr/api/risk-assessments/route.ts +323 -0
- package/src/modules/eudr/api/statements/[id]/export/route.ts +713 -0
- package/src/modules/eudr/api/statements/[id]/readiness/route.ts +129 -0
- package/src/modules/eudr/api/statements/route.ts +329 -0
- package/src/modules/eudr/api/suppliers/compliance/route.ts +241 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/[id]/page.tsx +545 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.ts +13 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/create/page.tsx +383 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.tsx +379 -0
- package/src/modules/eudr/backend/eudr/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/page.tsx +477 -0
- package/src/modules/eudr/backend/eudr/plots/[id]/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/plots/[id]/page.tsx +404 -0
- package/src/modules/eudr/backend/eudr/plots/create/page.meta.ts +13 -0
- package/src/modules/eudr/backend/eudr/plots/create/page.tsx +267 -0
- package/src/modules/eudr/backend/eudr/plots/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/plots/page.tsx +442 -0
- package/src/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/product-mappings/[id]/page.tsx +311 -0
- package/src/modules/eudr/backend/eudr/product-mappings/create/page.meta.ts +13 -0
- package/src/modules/eudr/backend/eudr/product-mappings/create/page.tsx +190 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.tsx +344 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/[id]/page.tsx +421 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/create/page.meta.ts +13 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/create/page.tsx +247 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.tsx +383 -0
- package/src/modules/eudr/backend/eudr/statements/[id]/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/statements/[id]/page.tsx +777 -0
- package/src/modules/eudr/backend/eudr/statements/create/page.meta.ts +13 -0
- package/src/modules/eudr/backend/eudr/statements/create/page.tsx +511 -0
- package/src/modules/eudr/backend/eudr/statements/page.meta.ts +12 -0
- package/src/modules/eudr/backend/eudr/statements/page.tsx +372 -0
- package/src/modules/eudr/ce.ts +52 -0
- package/src/modules/eudr/commands/evidence-submissions.ts +699 -0
- package/src/modules/eudr/commands/index.ts +6 -0
- package/src/modules/eudr/commands/lifecycle-events.ts +28 -0
- package/src/modules/eudr/commands/mitigation-actions.ts +574 -0
- package/src/modules/eudr/commands/plots.ts +627 -0
- package/src/modules/eudr/commands/product-mappings.ts +556 -0
- package/src/modules/eudr/commands/risk-assessments.ts +728 -0
- package/src/modules/eudr/commands/statements.ts +933 -0
- package/src/modules/eudr/components/CountrySelectField.tsx +77 -0
- package/src/modules/eudr/components/GeometryInput.tsx +170 -0
- package/src/modules/eudr/components/MappingSuggestionsDialog.tsx +385 -0
- package/src/modules/eudr/components/MitigationActionsSection.tsx +417 -0
- package/src/modules/eudr/components/OrderSelectField.tsx +116 -0
- package/src/modules/eudr/components/PlotImportDialog.tsx +277 -0
- package/src/modules/eudr/components/PlotMapPreview.tsx +46 -0
- package/src/modules/eudr/components/PlotMapPreviewImpl.tsx +111 -0
- package/src/modules/eudr/components/PlotMultiSelectField.tsx +238 -0
- package/src/modules/eudr/components/RiskCriteriaField.tsx +173 -0
- package/src/modules/eudr/components/StatementLifecycleBar.tsx +363 -0
- package/src/modules/eudr/components/StatementReadinessChecklist.tsx +82 -0
- package/src/modules/eudr/components/StatementRiskSection.tsx +242 -0
- package/src/modules/eudr/components/crudErrorI18n.ts +36 -0
- package/src/modules/eudr/components/formConfig.tsx +717 -0
- package/src/modules/eudr/components/plotForm.ts +75 -0
- package/src/modules/eudr/data/enrichers.ts +82 -0
- package/src/modules/eudr/data/entities.ts +369 -0
- package/src/modules/eudr/data/validators.ts +357 -0
- package/src/modules/eudr/encryption.ts +31 -0
- package/src/modules/eudr/events.ts +38 -0
- package/src/modules/eudr/i18n/de.json +747 -0
- package/src/modules/eudr/i18n/en.json +747 -0
- package/src/modules/eudr/i18n/es.json +747 -0
- package/src/modules/eudr/i18n/ko.json +747 -0
- package/src/modules/eudr/i18n/pl.json +747 -0
- package/src/modules/eudr/index.ts +13 -0
- package/src/modules/eudr/lib/annual-report.ts +120 -0
- package/src/modules/eudr/lib/completeness.ts +90 -0
- package/src/modules/eudr/lib/detail-read-scope.ts +44 -0
- package/src/modules/eudr/lib/geometry.ts +191 -0
- package/src/modules/eudr/lib/notifications.ts +85 -0
- package/src/modules/eudr/lib/plural.ts +36 -0
- package/src/modules/eudr/lib/reference-data.ts +128 -0
- package/src/modules/eudr/lib/species.ts +12 -0
- package/src/modules/eudr/lib/statement-lifecycle.ts +109 -0
- package/src/modules/eudr/lib/statement-seeding.ts +86 -0
- package/src/modules/eudr/migrations/.snapshot-open-mercato.json +2046 -0
- package/src/modules/eudr/migrations/Migration20260706102002_eudr.ts +16 -0
- package/src/modules/eudr/migrations/Migration20260706152612_eudr.ts +30 -0
- package/src/modules/eudr/migrations/Migration20260722100000_eudr.ts +17 -0
- package/src/modules/eudr/notifications.ts +56 -0
- package/src/modules/eudr/search.ts +212 -0
- package/src/modules/eudr/setup.ts +10 -0
- package/src/modules/eudr/subscribers/notify-mitigation-completed.ts +56 -0
- package/src/modules/eudr/subscribers/notify-risk-concluded.ts +58 -0
- package/src/modules/eudr/subscribers/notify-statement-reference-issued.ts +59 -0
- package/src/modules/eudr/subscribers/notify-statement-submitted.ts +49 -0
- package/src/modules/eudr/subscribers/notify-statement-withdrawn.ts +49 -0
- package/src/modules/eudr/subscribers/recompute-completeness-on-attachment-created.ts +189 -0
- package/src/modules/eudr/subscribers/recompute-completeness-on-attachment-deleted.ts +31 -0
- package/src/modules/eudr/widgets/dashboard/compliance-overview/widget.client.tsx +271 -0
- package/src/modules/eudr/widgets/dashboard/compliance-overview/widget.ts +24 -0
- package/src/modules/eudr/widgets/injection/eudr-product-column/widget.client.tsx +37 -0
- package/src/modules/eudr/widgets/injection/eudr-product-column/widget.ts +30 -0
- package/src/modules/eudr/widgets/injection/order-compliance/widget.client.tsx +131 -0
- package/src/modules/eudr/widgets/injection/order-compliance/widget.ts +16 -0
- package/src/modules/eudr/widgets/injection/supplier-compliance/widget.client.tsx +168 -0
- package/src/modules/eudr/widgets/injection/supplier-compliance/widget.ts +16 -0
- package/src/modules/eudr/widgets/injection-table.ts +31 -0
- package/src/modules/notifications/lib/notificationService.ts +83 -4
- package/src/modules/query_index/lib/engine.ts +139 -51
- package/src/modules/sales/api/documents/factory.ts +29 -0
- package/src/modules/sales/commands/documents.ts +65 -18
- package/src/modules/sales/data/entities.ts +1 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/sales/migrations/Migration20260806120000_sales_tag_assignment_document_idx.ts +59 -0
|
@@ -5,6 +5,9 @@ function resolveUrl(path) {
|
|
|
5
5
|
}
|
|
6
6
|
const tokenCache = /* @__PURE__ */ new Map();
|
|
7
7
|
const TOKEN_TTL_MS = 45 * 60 * 1e3;
|
|
8
|
+
function clearAuthTokenCache() {
|
|
9
|
+
tokenCache.clear();
|
|
10
|
+
}
|
|
8
11
|
async function getAuthToken(request, roleOrEmail = "admin", password) {
|
|
9
12
|
const role = roleOrEmail in DEFAULT_CREDENTIALS ? roleOrEmail : null;
|
|
10
13
|
const credentialAttempts = [];
|
|
@@ -86,6 +89,7 @@ async function postForm(request, path, data, options) {
|
|
|
86
89
|
}
|
|
87
90
|
export {
|
|
88
91
|
apiRequest,
|
|
92
|
+
clearAuthTokenCache,
|
|
89
93
|
getAuthToken,
|
|
90
94
|
postForm
|
|
91
95
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/helpers/integration/api.ts"],
|
|
4
|
-
"sourcesContent": ["import { type APIRequestContext } from '@playwright/test';\nimport { DEFAULT_CREDENTIALS, type Role } from './auth';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n// Cached tokens per credential to dodge the login rate limit\n// (5 attempts/60s per email). Tokens are reused across tests in the same\n// Playwright worker; each worker still mints its own.\nconst tokenCache = new Map<string, { token: string; mintedAt: number }>();\nconst TOKEN_TTL_MS = 45 * 60 * 1000; // 45 min; well under the default 2h session TTL.\n\nexport async function getAuthToken(\n request: APIRequestContext,\n roleOrEmail: Role | string = 'admin',\n password?: string,\n): Promise<string> {\n const role = roleOrEmail in DEFAULT_CREDENTIALS ? (roleOrEmail as Role) : null;\n const credentialAttempts: Array<{ email: string; password: string }> = [];\n\n if (role) {\n const configured = DEFAULT_CREDENTIALS[role];\n credentialAttempts.push({ email: configured.email, password: password ?? configured.password });\n if (!password) {\n credentialAttempts.push({ email: `${role}@acme.com`, password: 'secret' });\n }\n } else {\n credentialAttempts.push({ email: roleOrEmail, password: password ?? 'secret' });\n }\n\n const cacheKey = credentialAttempts\n .map((entry) => `${entry.email}:${entry.password}`)\n .join('|');\n const cached = tokenCache.get(cacheKey);\n if (cached && Date.now() - cached.mintedAt < TOKEN_TTL_MS) {\n return cached.token;\n }\n\n let lastStatus = 0;\n\n for (const attempt of credentialAttempts) {\n const form = new URLSearchParams();\n form.set('email', attempt.email);\n form.set('password', attempt.password);\n\n // Retry on 429 (auth rate limit kicks in after ~25-30 rapid attempts from\n // the same test run). Capped exponential backoff: 1s, 2s, 4s; 3 retries.\n for (let retry = 0; retry < 4; retry += 1) {\n const response = await request.post(resolveUrl('/api/auth/login'), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: form.toString(),\n });\n\n const raw = await response.text();\n let body: Record<string, unknown> | null = null;\n try {\n body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;\n } catch {\n body = null;\n }\n\n lastStatus = response.status();\n if (response.ok() && body && typeof body.token === 'string' && body.token) {\n tokenCache.set(cacheKey, { token: body.token, mintedAt: Date.now() });\n return body.token;\n }\n if (response.status() !== 429) break;\n const backoffMs = 1000 * 2 ** retry;\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n }\n\n throw new Error(`Failed to obtain auth token (status ${lastStatus})`);\n}\n\nexport async function apiRequest(\n request: APIRequestContext,\n method: string,\n path: string,\n options: { token: string; data?: unknown; timeout?: number },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n };\n const timeout = options.timeout ?? 30_000;\n let lastError: unknown = null;\n for (let attempt = 0; attempt < 2; attempt += 1) {\n try {\n return await request.fetch(resolveUrl(path), { method, headers, data: options.data, timeout });\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message : '';\n const retryable = /timeout|idle-session|socket|ECONNRESET|Target page, context or browser has been closed/i.test(message);\n if (!retryable || attempt === 1) throw error;\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n throw lastError;\n}\n\nexport async function postForm(\n request: APIRequestContext,\n path: string,\n data: Record<string, string>,\n options?: { headers?: Record<string, string> },\n) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(data)) form.set(key, value);\n return request.post(resolveUrl(path), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n ...(options?.headers ?? {}),\n },\n data: form.toString(),\n });\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,2BAAsC;AAE/C,MAAM,WAAW,QAAQ,IAAI,UAAU,KAAK,KAAK;AAEjD,SAAS,WAAW,MAAsB;AACxC,SAAO,WAAW,GAAG,QAAQ,GAAG,IAAI,KAAK;AAC3C;AAKA,MAAM,aAAa,oBAAI,IAAiD;AACxE,MAAM,eAAe,KAAK,KAAK;
|
|
4
|
+
"sourcesContent": ["import { type APIRequestContext } from '@playwright/test';\nimport { DEFAULT_CREDENTIALS, type Role } from './auth';\n\nconst BASE_URL = process.env.BASE_URL?.trim() || null;\n\nfunction resolveUrl(path: string): string {\n return BASE_URL ? `${BASE_URL}${path}` : path;\n}\n\n// Cached tokens per credential to dodge the login rate limit\n// (5 attempts/60s per email). Tokens are reused across tests in the same\n// Playwright worker; each worker still mints its own.\nconst tokenCache = new Map<string, { token: string; mintedAt: number }>();\nconst TOKEN_TTL_MS = 45 * 60 * 1000; // 45 min; well under the default 2h session TTL.\n\n/**\n * Drops cached tokens so the next getAuthToken() mints a fresh session.\n *\n * A cached token points at a `sessions` row. Anything that deletes those rows \u2014\n * deactivating the user, a password change, an explicit session purge \u2014 leaves the\n * cached token resolving to 401 for the rest of the TTL, and `apiRequest` neither\n * retries nor re-mints. A spec that revokes sessions for an account other specs also\n * use MUST call this afterwards.\n */\nexport function clearAuthTokenCache(): void {\n tokenCache.clear();\n}\n\nexport async function getAuthToken(\n request: APIRequestContext,\n roleOrEmail: Role | string = 'admin',\n password?: string,\n): Promise<string> {\n const role = roleOrEmail in DEFAULT_CREDENTIALS ? (roleOrEmail as Role) : null;\n const credentialAttempts: Array<{ email: string; password: string }> = [];\n\n if (role) {\n const configured = DEFAULT_CREDENTIALS[role];\n credentialAttempts.push({ email: configured.email, password: password ?? configured.password });\n if (!password) {\n credentialAttempts.push({ email: `${role}@acme.com`, password: 'secret' });\n }\n } else {\n credentialAttempts.push({ email: roleOrEmail, password: password ?? 'secret' });\n }\n\n const cacheKey = credentialAttempts\n .map((entry) => `${entry.email}:${entry.password}`)\n .join('|');\n const cached = tokenCache.get(cacheKey);\n if (cached && Date.now() - cached.mintedAt < TOKEN_TTL_MS) {\n return cached.token;\n }\n\n let lastStatus = 0;\n\n for (const attempt of credentialAttempts) {\n const form = new URLSearchParams();\n form.set('email', attempt.email);\n form.set('password', attempt.password);\n\n // Retry on 429 (auth rate limit kicks in after ~25-30 rapid attempts from\n // the same test run). Capped exponential backoff: 1s, 2s, 4s; 3 retries.\n for (let retry = 0; retry < 4; retry += 1) {\n const response = await request.post(resolveUrl('/api/auth/login'), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n },\n data: form.toString(),\n });\n\n const raw = await response.text();\n let body: Record<string, unknown> | null = null;\n try {\n body = raw ? (JSON.parse(raw) as Record<string, unknown>) : null;\n } catch {\n body = null;\n }\n\n lastStatus = response.status();\n if (response.ok() && body && typeof body.token === 'string' && body.token) {\n tokenCache.set(cacheKey, { token: body.token, mintedAt: Date.now() });\n return body.token;\n }\n if (response.status() !== 429) break;\n const backoffMs = 1000 * 2 ** retry;\n await new Promise((resolve) => setTimeout(resolve, backoffMs));\n }\n }\n\n throw new Error(`Failed to obtain auth token (status ${lastStatus})`);\n}\n\nexport async function apiRequest(\n request: APIRequestContext,\n method: string,\n path: string,\n options: { token: string; data?: unknown; timeout?: number },\n) {\n const headers = {\n Authorization: `Bearer ${options.token}`,\n 'Content-Type': 'application/json',\n };\n const timeout = options.timeout ?? 30_000;\n let lastError: unknown = null;\n for (let attempt = 0; attempt < 2; attempt += 1) {\n try {\n return await request.fetch(resolveUrl(path), { method, headers, data: options.data, timeout });\n } catch (error) {\n lastError = error;\n const message = error instanceof Error ? error.message : '';\n const retryable = /timeout|idle-session|socket|ECONNRESET|Target page, context or browser has been closed/i.test(message);\n if (!retryable || attempt === 1) throw error;\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n throw lastError;\n}\n\nexport async function postForm(\n request: APIRequestContext,\n path: string,\n data: Record<string, string>,\n options?: { headers?: Record<string, string> },\n) {\n const form = new URLSearchParams();\n for (const [key, value] of Object.entries(data)) form.set(key, value);\n return request.post(resolveUrl(path), {\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n ...(options?.headers ?? {}),\n },\n data: form.toString(),\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -28,7 +28,8 @@ const AttachmentInput = ({
|
|
|
28
28
|
entityId,
|
|
29
29
|
recordId,
|
|
30
30
|
def,
|
|
31
|
-
disabled
|
|
31
|
+
disabled,
|
|
32
|
+
onUploaded
|
|
32
33
|
}) => {
|
|
33
34
|
const t = useT();
|
|
34
35
|
const [items, setItems] = React.useState([]);
|
|
@@ -65,6 +66,7 @@ const AttachmentInput = ({
|
|
|
65
66
|
if (!files || !entityId || !recordId) return;
|
|
66
67
|
setError(null);
|
|
67
68
|
setUploading(true);
|
|
69
|
+
let uploadedCount = 0;
|
|
68
70
|
try {
|
|
69
71
|
for (const file of Array.from(files)) {
|
|
70
72
|
const ext = (file.name || "").split(".").pop()?.toLowerCase() || "";
|
|
@@ -98,8 +100,10 @@ const AttachmentInput = ({
|
|
|
98
100
|
setError(call.result?.error || t("attachments.library.upload.failed", "Upload failed."));
|
|
99
101
|
break;
|
|
100
102
|
}
|
|
103
|
+
uploadedCount += 1;
|
|
101
104
|
}
|
|
102
105
|
await load();
|
|
106
|
+
if (uploadedCount > 0) onUploaded?.();
|
|
103
107
|
} finally {
|
|
104
108
|
setUploading(false);
|
|
105
109
|
if (fileInputRef.current) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/fields/attachment.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Upload } from 'lucide-react'\n\nfunction humanSize(n: number): string {\n if (!Number.isFinite(n)) return String(n)\n const units = ['B','KB','MB','GB']\n let i = 0\n let x = n\n while (x >= 1024 && i < units.length - 1) { x /= 1024; i++ }\n return `${x.toFixed(i === 0 ? 0 : 1)} ${units[i]}`\n}\n\ntype AttachmentsResponse = {\n items?: Array<{ id: string; url: string; fileName: string; fileSize: number }>\n error?: string\n}\n\ntype AttachmentFieldDef = CustomFieldDefDto & {\n configJson?: {\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n partitionCode?: string\n }\n}\n\ntype AttachmentDefEditorPatch = {\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n partitionCode?: string\n}\n\nfunction buildAcceptAttribute(def?: AttachmentFieldDef): string | undefined {\n if (!Array.isArray(def?.acceptExtensions) || def.acceptExtensions.length === 0) return undefined\n const values = def.acceptExtensions\n .map((entry) => String(entry ?? '').trim().replace(/^\\./, ''))\n .filter((entry) => entry.length > 0)\n if (values.length === 0) return undefined\n return values.map((entry) => `.${entry}`).join(',')\n}\n\nexport const AttachmentInput = ({\n entityId,\n recordId,\n def,\n disabled,\n}: {\n entityId?: string\n recordId?: string\n def?: AttachmentFieldDef\n disabled?: boolean\n}) => {\n const t = useT()\n const [items, setItems] = React.useState<Array<{ id: string; url: string; fileName: string; fileSize: number }>>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [uploading, setUploading] = React.useState(false)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const accept = React.useMemo(() => buildAcceptAttribute(def), [def])\n\n const load = React.useCallback(async () => {\n if (!entityId || !recordId) return\n try {\n setLoading(true)\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const j = call.result ?? { items: [] }\n setItems(Array.isArray(j.items) ? j.items : [])\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => { load() }, [load])\n\n const onUpload = async (files: FileList | null) => {\n if (!files || !entityId || !recordId) return\n setError(null)\n setUploading(true)\n try {\n for (const file of Array.from(files)) {\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n const acceptExtensions = Array.isArray(def?.acceptExtensions) ? def.acceptExtensions : []\n if (acceptExtensions.length > 0) {\n const allowed = new Set(acceptExtensions.map((entry) => String(entry).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) { setError('File type not allowed'); continue }\n }\n const maxAttachmentSizeMb = typeof def?.maxAttachmentSizeMb === 'number' ? def.maxAttachmentSizeMb : undefined\n if (typeof maxAttachmentSizeMb === 'number' && maxAttachmentSizeMb > 0) {\n const maxBytes = Math.floor(maxAttachmentSizeMb * 1024 * 1024)\n if (file.size > maxBytes) { setError(`File exceeds ${maxAttachmentSizeMb} MB limit`); continue }\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n if (def?.key) fd.set('fieldKey', String(def.key))\n fd.set('file', file)\n const call = await apiCall<{ error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error || t('attachments.library.upload.failed', 'Upload failed.'))\n break\n }\n }\n await load()\n } finally {\n setUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n }\n\n return (\n <div className=\"space-y-3\">\n {!entityId || !recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\n <div className=\"rounded-md border border-dashed border-border/70 bg-muted/30 px-4 py-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => fileInputRef.current?.click()}\n disabled={disabled || uploading}\n >\n <Upload className=\"h-4 w-4\" />\n {uploading\n ? t('attachments.library.upload.submitting', 'Uploading\u2026')\n : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n accept={accept}\n disabled={disabled || uploading}\n onChange={(event) => { void onUpload(event.target.files) }}\n />\n </div>\n )}\n {error ? <div className=\"text-xs text-status-error-text\">{error}</div> : null}\n <div className=\"space-y-1\">\n {loading ? <div className=\"text-xs text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div> : null}\n {items.map(it => (\n <div key={it.id} className=\"text-sm\">\n <a className=\"underline\" href={it.url} target=\"_blank\" rel=\"noreferrer\">{it.fileName}</a>\n <span className=\"text-xs text-muted-foreground\"> \u2022 {humanSize(it.fileSize)}</span>\n </div>\n ))}\n {!loading && items.length === 0 ? <div className=\"text-xs text-muted-foreground\">{t('attachments.library.table.empty', 'No attachments found.')}</div> : null}\n </div>\n </div>\n )\n}\n\n// Register with field registry under kind 'attachment'\nfunction AttachmentDefEditor({ def, onChange }: { def: AttachmentFieldDef; onChange: (patch: AttachmentDefEditorPatch) => void }) {\n const cfg = def?.configJson || {}\n const [maxMb, setMaxMb] = React.useState<number | ''>(typeof cfg.maxAttachmentSizeMb === 'number' ? cfg.maxAttachmentSizeMb : '')\n const [exts, setExts] = React.useState<string>((Array.isArray(cfg.acceptExtensions) ? cfg.acceptExtensions : []).join(', '))\n const [partition, setPartition] = React.useState<string>(typeof cfg.partitionCode === 'string' ? cfg.partitionCode : '')\n return (\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Max file size (MB)</label>\n <Input\n type=\"number\"\n min={0}\n placeholder=\"e.g., 10\"\n value={maxMb}\n onChange={(e) => setMaxMb(e.target.value === '' ? '' : Number(e.target.value))}\n onBlur={() => onChange({ maxAttachmentSizeMb: maxMb === '' ? undefined : Number(maxMb) })}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Accepted extensions</label>\n <Input\n placeholder=\"e.g., pdf, jpg, png\"\n value={exts}\n onChange={(e) => setExts(e.target.value)}\n onBlur={() => onChange({ acceptExtensions: exts.split(',').map((s) => s.trim()).filter(Boolean) })}\n />\n <div className=\"text-xs text-muted-foreground\">Leave blank to allow any.</div>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Partition code</label>\n <Input\n placeholder=\"e.g., productsMedia\"\n value={partition}\n onChange={(e) => setPartition(e.target.value)}\n onBlur={() => onChange({ partitionCode: partition.trim() || undefined })}\n />\n <div className=\"text-xs text-muted-foreground\">\n Configure partitions under Settings \u2192 Attachments. Leave blank for default.\n </div>\n </div>\n </div>\n )\n}\n\nFieldRegistry.register('attachment', {\n input: (props) => <AttachmentInput entityId={props.entityId} recordId={props.recordId} def={props.def} disabled={props.disabled} />,\n defEditor: (p) => <AttachmentDefEditor {...p} />,\n})\n\nexport {}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Upload } from 'lucide-react'\n\nfunction humanSize(n: number): string {\n if (!Number.isFinite(n)) return String(n)\n const units = ['B','KB','MB','GB']\n let i = 0\n let x = n\n while (x >= 1024 && i < units.length - 1) { x /= 1024; i++ }\n return `${x.toFixed(i === 0 ? 0 : 1)} ${units[i]}`\n}\n\ntype AttachmentsResponse = {\n items?: Array<{ id: string; url: string; fileName: string; fileSize: number }>\n error?: string\n}\n\ntype AttachmentFieldDef = CustomFieldDefDto & {\n configJson?: {\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n partitionCode?: string\n }\n}\n\ntype AttachmentDefEditorPatch = {\n maxAttachmentSizeMb?: number\n acceptExtensions?: string[]\n partitionCode?: string\n}\n\nfunction buildAcceptAttribute(def?: AttachmentFieldDef): string | undefined {\n if (!Array.isArray(def?.acceptExtensions) || def.acceptExtensions.length === 0) return undefined\n const values = def.acceptExtensions\n .map((entry) => String(entry ?? '').trim().replace(/^\\./, ''))\n .filter((entry) => entry.length > 0)\n if (values.length === 0) return undefined\n return values.map((entry) => `.${entry}`).join(',')\n}\n\nexport const AttachmentInput = ({\n entityId,\n recordId,\n def,\n disabled,\n onUploaded,\n}: {\n entityId?: string\n recordId?: string\n def?: AttachmentFieldDef\n disabled?: boolean\n onUploaded?: () => void\n}) => {\n const t = useT()\n const [items, setItems] = React.useState<Array<{ id: string; url: string; fileName: string; fileSize: number }>>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [uploading, setUploading] = React.useState(false)\n const fileInputRef = React.useRef<HTMLInputElement | null>(null)\n const accept = React.useMemo(() => buildAcceptAttribute(def), [def])\n\n const load = React.useCallback(async () => {\n if (!entityId || !recordId) return\n try {\n setLoading(true)\n const call = await apiCall<AttachmentsResponse>(\n `/api/attachments?entityId=${encodeURIComponent(entityId)}&recordId=${encodeURIComponent(recordId)}`,\n undefined,\n { fallback: { items: [] } },\n )\n if (!call.ok) {\n const message = call.result?.error || t('attachments.library.errors.load', 'Failed to load attachments.')\n throw new Error(message)\n }\n const j = call.result ?? { items: [] }\n setItems(Array.isArray(j.items) ? j.items : [])\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : t('attachments.library.errors.load', 'Failed to load attachments.'))\n } finally {\n setLoading(false)\n }\n }, [entityId, recordId, t])\n\n React.useEffect(() => { load() }, [load])\n\n const onUpload = async (files: FileList | null) => {\n if (!files || !entityId || !recordId) return\n setError(null)\n setUploading(true)\n let uploadedCount = 0\n try {\n for (const file of Array.from(files)) {\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n const acceptExtensions = Array.isArray(def?.acceptExtensions) ? def.acceptExtensions : []\n if (acceptExtensions.length > 0) {\n const allowed = new Set(acceptExtensions.map((entry) => String(entry).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) { setError('File type not allowed'); continue }\n }\n const maxAttachmentSizeMb = typeof def?.maxAttachmentSizeMb === 'number' ? def.maxAttachmentSizeMb : undefined\n if (typeof maxAttachmentSizeMb === 'number' && maxAttachmentSizeMb > 0) {\n const maxBytes = Math.floor(maxAttachmentSizeMb * 1024 * 1024)\n if (file.size > maxBytes) { setError(`File exceeds ${maxAttachmentSizeMb} MB limit`); continue }\n }\n const fd = new FormData()\n fd.set('entityId', entityId)\n fd.set('recordId', recordId)\n if (def?.key) fd.set('fieldKey', String(def.key))\n fd.set('file', file)\n const call = await apiCall<{ error?: string }>(\n '/api/attachments',\n { method: 'POST', body: fd },\n { fallback: null },\n )\n if (!call.ok) {\n setError(call.result?.error || t('attachments.library.upload.failed', 'Upload failed.'))\n break\n }\n uploadedCount += 1\n }\n await load()\n if (uploadedCount > 0) onUploaded?.()\n } finally {\n setUploading(false)\n if (fileInputRef.current) {\n fileInputRef.current.value = ''\n }\n }\n }\n\n return (\n <div className=\"space-y-3\">\n {!entityId || !recordId ? (\n <div className=\"rounded-md border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground\">\n {t('attachments.library.upload.saveFirst', 'Save the record before uploading files.')}\n </div>\n ) : (\n <div className=\"rounded-md border border-dashed border-border/70 bg-muted/30 px-4 py-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() => fileInputRef.current?.click()}\n disabled={disabled || uploading}\n >\n <Upload className=\"h-4 w-4\" />\n {uploading\n ? t('attachments.library.upload.submitting', 'Uploading\u2026')\n : t('attachments.library.upload.choose', 'Choose files')}\n </Button>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n className=\"hidden\"\n accept={accept}\n disabled={disabled || uploading}\n onChange={(event) => { void onUpload(event.target.files) }}\n />\n </div>\n )}\n {error ? <div className=\"text-xs text-status-error-text\">{error}</div> : null}\n <div className=\"space-y-1\">\n {loading ? <div className=\"text-xs text-muted-foreground\">{t('attachments.library.loading', 'Loading attachments\u2026')}</div> : null}\n {items.map(it => (\n <div key={it.id} className=\"text-sm\">\n <a className=\"underline\" href={it.url} target=\"_blank\" rel=\"noreferrer\">{it.fileName}</a>\n <span className=\"text-xs text-muted-foreground\"> \u2022 {humanSize(it.fileSize)}</span>\n </div>\n ))}\n {!loading && items.length === 0 ? <div className=\"text-xs text-muted-foreground\">{t('attachments.library.table.empty', 'No attachments found.')}</div> : null}\n </div>\n </div>\n )\n}\n\n// Register with field registry under kind 'attachment'\nfunction AttachmentDefEditor({ def, onChange }: { def: AttachmentFieldDef; onChange: (patch: AttachmentDefEditorPatch) => void }) {\n const cfg = def?.configJson || {}\n const [maxMb, setMaxMb] = React.useState<number | ''>(typeof cfg.maxAttachmentSizeMb === 'number' ? cfg.maxAttachmentSizeMb : '')\n const [exts, setExts] = React.useState<string>((Array.isArray(cfg.acceptExtensions) ? cfg.acceptExtensions : []).join(', '))\n const [partition, setPartition] = React.useState<string>(typeof cfg.partitionCode === 'string' ? cfg.partitionCode : '')\n return (\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Max file size (MB)</label>\n <Input\n type=\"number\"\n min={0}\n placeholder=\"e.g., 10\"\n value={maxMb}\n onChange={(e) => setMaxMb(e.target.value === '' ? '' : Number(e.target.value))}\n onBlur={() => onChange({ maxAttachmentSizeMb: maxMb === '' ? undefined : Number(maxMb) })}\n />\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Accepted extensions</label>\n <Input\n placeholder=\"e.g., pdf, jpg, png\"\n value={exts}\n onChange={(e) => setExts(e.target.value)}\n onBlur={() => onChange({ acceptExtensions: exts.split(',').map((s) => s.trim()).filter(Boolean) })}\n />\n <div className=\"text-xs text-muted-foreground\">Leave blank to allow any.</div>\n </div>\n <div className=\"space-y-2\">\n <label className=\"text-xs font-medium\">Partition code</label>\n <Input\n placeholder=\"e.g., productsMedia\"\n value={partition}\n onChange={(e) => setPartition(e.target.value)}\n onBlur={() => onChange({ partitionCode: partition.trim() || undefined })}\n />\n <div className=\"text-xs text-muted-foreground\">\n Configure partitions under Settings \u2192 Attachments. Leave blank for default.\n </div>\n </div>\n </div>\n )\n}\n\nFieldRegistry.register('attachment', {\n input: (props) => <AttachmentInput entityId={props.entityId} recordId={props.recordId} def={props.def} disabled={props.disabled} />,\n defEditor: (p) => <AttachmentDefEditor {...p} />,\n})\n\nexport {}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["error"]
|
|
7
7
|
}
|
|
@@ -6,7 +6,9 @@ const attachmentCrudEvents = {
|
|
|
6
6
|
buildPayload: (ctx) => ({
|
|
7
7
|
id: ctx.identifiers.id,
|
|
8
8
|
organizationId: ctx.identifiers.organizationId,
|
|
9
|
-
tenantId: ctx.identifiers.tenantId
|
|
9
|
+
tenantId: ctx.identifiers.tenantId,
|
|
10
|
+
entityId: ctx.entity?.entityId ?? null,
|
|
11
|
+
recordId: ctx.entity?.recordId ?? null
|
|
10
12
|
})
|
|
11
13
|
};
|
|
12
14
|
const attachmentCrudIndexer = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/crud.ts"],
|
|
4
|
-
"sourcesContent": ["import { E } from '#generated/entities.ids.generated'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { Attachment } from '../data/entities'\n\nexport const attachmentCrudEvents: CrudEventsConfig<Attachment> = {\n module: 'attachments',\n entity: 'attachment',\n persistent: false,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\nexport const attachmentCrudIndexer: CrudIndexerConfig<Attachment> = {\n entityType: E.attachments.attachment,\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAIX,MAAM,uBAAqD;AAAA,EAChE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc,CAAC,SAAS;AAAA,IACtB,IAAI,IAAI,YAAY;AAAA,IACpB,gBAAgB,IAAI,YAAY;AAAA,IAChC,UAAU,IAAI,YAAY;AAAA,
|
|
4
|
+
"sourcesContent": ["import { E } from '#generated/entities.ids.generated'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport type { Attachment } from '../data/entities'\n\nexport const attachmentCrudEvents: CrudEventsConfig<Attachment> = {\n module: 'attachments',\n entity: 'attachment',\n persistent: false,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n entityId: ctx.entity?.entityId ?? null,\n recordId: ctx.entity?.recordId ?? null,\n }),\n}\n\nexport const attachmentCrudIndexer: CrudIndexerConfig<Attachment> = {\n entityType: E.attachments.attachment,\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAIX,MAAM,uBAAqD;AAAA,EAChE,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc,CAAC,SAAS;AAAA,IACtB,IAAI,IAAI,YAAY;AAAA,IACpB,gBAAgB,IAAI,YAAY;AAAA,IAChC,UAAU,IAAI,YAAY;AAAA,IAC1B,UAAU,IAAI,QAAQ,YAAY;AAAA,IAClC,UAAU,IAAI,QAAQ,YAAY;AAAA,EACpC;AACF;AAEO,MAAM,wBAAuD;AAAA,EAClE,YAAY,EAAE,YAAY;AAC5B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
5
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
6
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
@@ -116,6 +117,10 @@ async function POST(req) {
|
|
|
116
117
|
}
|
|
117
118
|
return response;
|
|
118
119
|
} catch (err) {
|
|
120
|
+
if (isCrudHttpError(err)) {
|
|
121
|
+
logger.warn("Redo rejected", { err });
|
|
122
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
123
|
+
}
|
|
119
124
|
logger.error("Redo failed", { err });
|
|
120
125
|
return NextResponse.json({ error: "Redo failed" }, { status: 400 });
|
|
121
126
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/redo/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : organizationId ?? auth.orgId ?? null\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && log.actorUserId !== auth.sub && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant ? (log.actorUserId ?? auth.sub) : auth.sub\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA4C;AACrD,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0CAA0C;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext, CommandLogMetadata } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { AwilixContainer } from 'awilix'\nimport type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'redo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.redo_self'] },\n}\n\ntype RedoRequestBody = {\n logId?: string\n}\n\nconst redoRequestSchema = z.object({\n logId: z.string().min(1).describe('Identifier of the previously undone action log'),\n})\n\nconst redoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().nullable().describe('Identifier of the new redo log entry, if available'),\n undoToken: z.string().nullable().describe('New undo token associated with the redone action'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as RedoRequestBody | null\n const logId = typeof body?.logId === 'string' ? body.logId.trim() : ''\n if (!logId) return NextResponse.json({ error: 'Invalid log id' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const canRedoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.redo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const scopedOrgId = canRedoTenant ? organizationId ?? null : organizationId ?? auth.orgId ?? null\n const log = await logs.findById(logId)\n\n if (!log || log.executionState !== 'undone') {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n if (log.actorUserId && log.actorUserId !== auth.sub && !canRedoTenant) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the\n // hardened undo route (issue #2685, ported in #2931).\n if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n // Tenant-level redoers may redo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685, ported in #2931).\n const orgScopeMismatch = canRedoTenant\n ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)\n : Boolean(log.organizationId && log.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n const lookupActorId = canRedoTenant ? (log.actorUserId ?? auth.sub) : auth.sub\n const latestUndoneOrganizationId = log.organizationId ?? null\n const latestUndone = await logs.latestUndoneForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: latestUndoneOrganizationId,\n })\n if (!latestUndone || latestUndone.id !== log.id) {\n return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })\n }\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n const contextRecord = log.contextJson && typeof log.contextJson === 'object' ? (log.contextJson as Record<string, unknown>) : null\n const cacheAliasesRaw = Array.isArray(contextRecord?.cacheAliases as unknown[])\n ? (contextRecord!.cacheAliases as unknown[])\n : []\n const cacheAliases = cacheAliasesRaw\n .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)\n .map((value) => value.trim())\n const metadataContext: Record<string, unknown> = {\n historyAction: 'redo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n if (cacheAliases.length) metadataContext.cacheAliases = cacheAliases\n const metadata: CommandLogMetadata = {\n tenantId: log.tenantId,\n organizationId: log.organizationId,\n actorUserId: auth.sub,\n actionLabel: log.actionLabel,\n resourceKind: log.resourceKind,\n resourceId: log.resourceId,\n context: metadataContext,\n }\n const resolvedInput = resolveRedoInput(log.commandPayload, log)\n if (!resolvedInput) {\n return NextResponse.json({ error: 'Redo data unavailable for this action' }, { status: 400 })\n }\n const commandInput = resolvedInput\n const { logEntry } = await commandBus.execute(log.commandId, {\n input: commandInput,\n ctx,\n metadata,\n redoLogEntry: log,\n })\n await logs.markRedone(log.id)\n const actionLog = asActionLog(logEntry)\n const response = NextResponse.json({\n ok: true,\n logId: actionLog?.id ?? null,\n undoToken: actionLog?.undoToken ?? null,\n })\n if (actionLog?.undoToken && actionLog.id) {\n const createdAt = actionLog.createdAt instanceof Date\n ? actionLog.createdAt.toISOString()\n : (typeof actionLog.createdAt === 'string' ? actionLog.createdAt : new Date().toISOString())\n response.headers.set('x-om-operation', serializeOperationMetadata({\n id: actionLog.id,\n undoToken: actionLog.undoToken,\n commandId: actionLog.commandId ?? log.commandId,\n actionLabel: actionLog.actionLabel ?? log.actionLabel ?? null,\n resourceKind: typeof actionLog.resourceKind === 'string' ? actionLog.resourceKind : log.resourceKind ?? null,\n resourceId: typeof actionLog.resourceId === 'string' ? actionLog.resourceId : log.resourceId ?? null,\n executedAt: createdAt,\n }))\n }\n return response\n } catch (err) {\n // Same contract as undo: a domain refusal carries an already-localized body the\n // operator can act on, so surface it instead of a generic message.\n if (isCrudHttpError(err)) {\n logger.warn('Redo rejected', { err })\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Redo failed', { err })\n return NextResponse.json({ error: 'Redo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nfunction asActionLog(entry: unknown): ActionLog | null {\n if (!entry || typeof entry !== 'object') return null\n if (typeof (entry as { id?: unknown }).id !== 'string') return null\n return entry as ActionLog\n}\n\nfunction resolveRedoInput(payload: unknown, log: ActionLog): unknown | null {\n if (payload && typeof payload === 'object' && !Array.isArray(payload) && '__redoInput' in payload) {\n const envelope = payload as { __redoInput?: unknown }\n return envelope.__redoInput ?? {}\n }\n const updateFallback = deriveUpdateInput(log)\n if (updateFallback) return updateFallback\n return null\n}\n\nfunction deriveUpdateInput(log: ActionLog): Record<string, unknown> | null {\n if (!log.commandId.endsWith('.update')) return null\n if (!log.resourceId) return null\n const changes = log.changesJson\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) return { id: log.resourceId }\n const payload: Record<string, unknown> = { id: log.resourceId }\n for (const [key, value] of Object.entries(changes)) {\n if (value && typeof value === 'object' && !Array.isArray(value) && 'to' in value) {\n payload[key] = (value as Record<string, unknown>).to\n } else {\n payload[key] = value\n }\n }\n return payload\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Redo a previously undone action',\n description: 'Replays the command associated with a recently undone action, reapplying the change and issuing a fresh undo token.',\n methods: {\n POST: {\n summary: 'Redo by action log id',\n description:\n 'Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: redoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Redo executed successfully', schema: redoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Log not eligible for redo', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Redo blocked by scope checks', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["metadata"]
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
5
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
6
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
@@ -80,6 +81,10 @@ async function POST(req) {
|
|
|
80
81
|
await commandBus.undo(undoToken, ctx);
|
|
81
82
|
return NextResponse.json({ ok: true, logId: target.id });
|
|
82
83
|
} catch (err) {
|
|
84
|
+
if (isCrudHttpError(err)) {
|
|
85
|
+
logger.warn("Undo rejected", { err, logId: target.id });
|
|
86
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
87
|
+
}
|
|
83
88
|
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
84
89
|
if (interceptorRejection) {
|
|
85
90
|
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/audit_logs/api/audit-logs/actions/undo/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'undo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.undo_self'] },\n}\n\ntype UndoRequestBody = {\n undoToken?: string\n}\n\nconst undoRequestSchema = z.object({\n undoToken: z.string().min(1).describe('Undo token issued by the action log entry'),\n})\n\nconst undoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().describe('Identifier of the action log that was undone'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as UndoRequestBody | null\n const undoToken = body?.undoToken?.trim()\n if (!undoToken) return NextResponse.json({ error: 'Invalid undo token' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const canUndoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.undo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const target = await logs.findByUndoToken(undoToken)\n if (!target || target.executionState !== 'done') {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (target.actorUserId && target.actorUserId !== auth.sub && !canUndoTenant) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.undo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never undo a tenant-scoped row (issue #2685).\n if (target.tenantId && target.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const scopedOrgId = canUndoTenant ? organizationId ?? null : organizationId ?? auth.orgId ?? null\n // Tenant-level undoers may undo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685).\n const orgScopeMismatch = canUndoTenant\n ? Boolean(target.organizationId && scopedOrgId && target.organizationId !== scopedOrgId)\n : Boolean(target.organizationId && target.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n const lookupActorId = canUndoTenant ? (target.actorUserId ?? auth.sub) : auth.sub\n // Scope the latest-undoable re-lookup to the target row's own organization, not\n // the caller's currently-resolved org. The actor/tenant/org guards above already\n // authorized the caller for this row; reusing the caller's scope here breaks undo\n // for tenant-level rows (organization create/update/delete/reparent log with a\n // null organization_id) whenever the caller resolves to a concrete home org, so\n // the lookup never matches and returns \"Undo token not available\" (issue #2398).\n const lookupOrgId = target.organizationId ?? null\n let latest = null\n if (target.resourceKind || target.resourceId) {\n latest = await logs.latestUndoableForResource({\n actorUserId: lookupActorId,\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n resourceKind: target.resourceKind ?? undefined,\n resourceId: target.resourceId ?? undefined,\n })\n }\n if (!latest) {\n latest = await logs.latestUndoableForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n })\n }\n if (!latest || latest.id !== target.id) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n await commandBus.undo(undoToken, ctx)\n return NextResponse.json({ ok: true, logId: target.id })\n } catch (err) {\n // A beforeUndo interceptor that blocked with an explicit status is a deliberate business\n // rejection, not an undo failure \u2014 surface its status and message (issue #5045).\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Undo failed', { err })\n return NextResponse.json({ error: 'Undo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Undo a recent action',\n description: 'Executes the undo operation for the most recent undoable action belonging to the caller.',\n methods: {\n POST: {\n summary: 'Undo action by token',\n description:\n 'Replays the undo handler registered for a command. The provided undo token must match the latest undoable log entry accessible to the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: undoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Undo applied successfully', schema: undoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or unavailable undo token', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Undo blocked by organization or tenant scope', schema: errorSchema },\n {\n status: 422,\n description:\n 'Undo deliberately blocked by a beforeUndo command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA4C;AACrD,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0CAA0C;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveFeatureCheckContext, resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { AwilixContainer } from 'awilix'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'undo' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['audit_logs.undo_self'] },\n}\n\ntype UndoRequestBody = {\n undoToken?: string\n}\n\nconst undoRequestSchema = z.object({\n undoToken: z.string().min(1).describe('Undo token issued by the action log entry'),\n})\n\nconst undoResponseSchema = z.object({\n ok: z.literal(true),\n logId: z.string().describe('Identifier of the action log that was undone'),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const body = (await req.json().catch(() => null)) as UndoRequestBody | null\n const undoToken = body?.undoToken?.trim()\n if (!undoToken) return NextResponse.json({ error: 'Invalid undo token' }, { status: 400 })\n\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const logs = (container.resolve('actionLogService') as ActionLogService)\n let rbac: RbacService | null = null\n try {\n rbac = (container.resolve('rbacService') as RbacService)\n } catch {\n rbac = null\n }\n\n const { organizationId } = await resolveFeatureCheckContext({ container, auth, request: req })\n\n const canUndoTenant = rbac\n ? await rbac.userHasAllFeatures(auth.sub, ['audit_logs.undo_tenant'], {\n tenantId: auth.tenantId ?? null,\n organizationId,\n })\n : false\n\n const target = await logs.findByUndoToken(undoToken)\n if (!target || target.executionState !== 'done') {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n if (target.actorUserId && target.actorUserId !== auth.sub && !canUndoTenant) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n // Fail closed on tenant scope: `audit_logs.undo_tenant` only widens scope WITHIN a\n // tenant, never across tenants, so a tenant-scoped target always requires a caller\n // bound to that same tenant. A caller whose tenantId is null (tenant-less global\n // account or unscoped API key) must never undo a tenant-scoped row (issue #2685).\n if (target.tenantId && target.tenantId !== (auth.tenantId ?? null)) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n const scopedOrgId = canUndoTenant ? organizationId ?? null : organizationId ?? auth.orgId ?? null\n // Tenant-level undoers may undo across organizations within the tenant, so an\n // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.\n // Every other caller must resolve to the target's own organization \u2014 a null caller\n // org must not bypass an org-scoped target (issue #2685).\n const orgScopeMismatch = canUndoTenant\n ? Boolean(target.organizationId && scopedOrgId && target.organizationId !== scopedOrgId)\n : Boolean(target.organizationId && target.organizationId !== scopedOrgId)\n if (orgScopeMismatch) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n const lookupActorId = canUndoTenant ? (target.actorUserId ?? auth.sub) : auth.sub\n // Scope the latest-undoable re-lookup to the target row's own organization, not\n // the caller's currently-resolved org. The actor/tenant/org guards above already\n // authorized the caller for this row; reusing the caller's scope here breaks undo\n // for tenant-level rows (organization create/update/delete/reparent log with a\n // null organization_id) whenever the caller resolves to a concrete home org, so\n // the lookup never matches and returns \"Undo token not available\" (issue #2398).\n const lookupOrgId = target.organizationId ?? null\n let latest = null\n if (target.resourceKind || target.resourceId) {\n latest = await logs.latestUndoableForResource({\n actorUserId: lookupActorId,\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n resourceKind: target.resourceKind ?? undefined,\n resourceId: target.resourceId ?? undefined,\n })\n }\n if (!latest) {\n latest = await logs.latestUndoableForActor(lookupActorId, {\n tenantId: auth.tenantId ?? null,\n organizationId: lookupOrgId,\n })\n }\n if (!latest || latest.id !== target.id) {\n return NextResponse.json({ error: 'Undo token not available' }, { status: 400 })\n }\n\n try {\n const ctx = await createRuntimeContext(container, auth, req)\n await commandBus.undo(undoToken, ctx)\n return NextResponse.json({ ok: true, logId: target.id })\n } catch (err) {\n // A command can refuse an undo for a domain reason the operator can act on \u2014 e.g.\n // reverting a role grant would drop a tenant below a protected role's active-holder\n // floor. Those arrive as CrudHttpError with an already-localized body, so pass them\n // through verbatim instead of flattening every failure to a generic message.\n if (isCrudHttpError(err)) {\n logger.warn('Undo rejected', { err, logId: target.id })\n return NextResponse.json(err.body, { status: err.status })\n }\n // A beforeUndo interceptor that blocked with an explicit status is a deliberate business\n // rejection, not an undo failure \u2014 surface its status and message (issue #5045).\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n logger.error('Undo failed', { err })\n return NextResponse.json({ error: 'Undo failed' }, { status: 400 })\n }\n}\n\nasync function createRuntimeContext(container: AwilixContainer, auth: AuthContext, request: Request): Promise<CommandRuntimeContext> {\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n return {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n request,\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Undo a recent action',\n description: 'Executes the undo operation for the most recent undoable action belonging to the caller.',\n methods: {\n POST: {\n summary: 'Undo action by token',\n description:\n 'Replays the undo handler registered for a command. The provided undo token must match the latest undoable log entry accessible to the caller.',\n requestBody: {\n contentType: 'application/json',\n schema: undoRequestSchema,\n },\n responses: [\n { status: 200, description: 'Undo applied successfully', schema: undoResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or unavailable undo token', schema: errorSchema },\n { status: 401, description: 'Authentication required', schema: errorSchema },\n { status: 403, description: 'Undo blocked by organization or tenant scope', schema: errorSchema },\n {\n status: 422,\n description:\n 'Undo deliberately blocked by a beforeUndo command interceptor. The interceptor chooses the status (any 4xx/5xx) and may replace the body.',\n schema: errorSchema,\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -108,11 +108,11 @@ function AuditLogsPage() {
|
|
|
108
108
|
children: loading ? t("audit_logs.common.refreshing") : t("audit_logs.common.refresh")
|
|
109
109
|
}
|
|
110
110
|
), [loadWithState, actionsPage, actionsPageSize, accessPage, accessPageSize, loading, t]);
|
|
111
|
-
const handleUndoError = React.useCallback(() => {
|
|
112
|
-
setError(t("audit_logs.error.undo"));
|
|
111
|
+
const handleUndoError = React.useCallback((reason) => {
|
|
112
|
+
setError(reason || t("audit_logs.error.undo"));
|
|
113
113
|
}, [t]);
|
|
114
|
-
const handleRedoError = React.useCallback(() => {
|
|
115
|
-
setError(t("audit_logs.error.redo"));
|
|
114
|
+
const handleRedoError = React.useCallback((reason) => {
|
|
115
|
+
setError(reason || t("audit_logs.error.redo"));
|
|
116
116
|
}, [t]);
|
|
117
117
|
const handleActionsPageChange = React.useCallback((nextPage) => {
|
|
118
118
|
const totalPages = actionsTotalPages || 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/audit_logs/backend/audit-logs/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport React from 'react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { ActionLogItem } from '../../components/AuditLogsActions'\nimport { AuditLogsActions } from '../../components/AuditLogsActions'\nimport type { AccessLogItem } from '../../components/AccessLogsTable'\nimport { AccessLogsTable } from '../../components/AccessLogsTable'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'audit-logs-page' })\n\ntype ActionLogResponse = {\n items: ActionLogItem[]\n canViewTenant: boolean\n page: number\n pageSize: number\n total: number\n totalPages: number\n}\n\ntype AccessLogResponse = {\n items: AccessLogItem[]\n canViewTenant: boolean\n page: number\n pageSize: number\n total: number\n totalPages: number\n}\n\ntype TabOption = 'actions' | 'access'\n\nconst DEFAULT_PAGE_SIZE = 50\n\nexport default function AuditLogsPage() {\n const t = useT()\n const [tab, setTab] = React.useState<TabOption>('actions')\n const [actions, setActions] = React.useState<ActionLogItem[]>([])\n const [accessLogs, setAccessLogs] = React.useState<AccessLogItem[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [undoableOnly, setUndoableOnly] = React.useState(false)\n const [actionsPage, setActionsPage] = React.useState(1)\n const [actionsPageSize, setActionsPageSize] = React.useState(DEFAULT_PAGE_SIZE)\n const [actionsTotal, setActionsTotal] = React.useState(0)\n const [actionsTotalPages, setActionsTotalPages] = React.useState(1)\n const actionsPageSizeRef = React.useRef(DEFAULT_PAGE_SIZE)\n const [accessPage, setAccessPage] = React.useState(1)\n const [accessPageSize, setAccessPageSize] = React.useState(DEFAULT_PAGE_SIZE)\n const [accessTotal, setAccessTotal] = React.useState(0)\n const [accessTotalPages, setAccessTotalPages] = React.useState(1)\n const accessPageSizeRef = React.useRef(DEFAULT_PAGE_SIZE)\n\n const fetchActions = React.useCallback(async (page: number, pageSize: number) => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (undoableOnly) params.set('undoableOnly', 'true')\n return readApiResultOrThrow<ActionLogResponse>(\n `/api/audit_logs/audit-logs/actions?${params.toString()}`,\n undefined,\n { errorMessage: t('audit_logs.error.load') },\n )\n }, [undoableOnly, t])\n\n const fetchAccess = React.useCallback(async (page: number, pageSize: number) => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n return readApiResultOrThrow<AccessLogResponse>(\n `/api/audit_logs/audit-logs/access?${params.toString()}`,\n undefined,\n { errorMessage: t('audit_logs.error.load') },\n )\n }, [t])\n\n const loadAll = React.useCallback(async (\n actionPage: number, actionPageSize: number,\n accessPageNum: number, accessPageSizeNum: number,\n ) => {\n const [actionsRes, accessRes] = await Promise.all([\n fetchActions(actionPage, actionPageSize),\n fetchAccess(accessPageNum, accessPageSizeNum),\n ])\n setActions(actionsRes.items ?? [])\n setActionsPage(actionsRes.page ?? actionPage)\n setActionsPageSize((prev) => {\n const resolved = actionsRes.pageSize ?? actionPageSize\n actionsPageSizeRef.current = resolved\n return resolved === prev ? prev : resolved\n })\n setActionsTotal(actionsRes.total ?? (actionsRes.items?.length ?? 0))\n setActionsTotalPages(actionsRes.totalPages ?? 1)\n setAccessLogs(accessRes.items ?? [])\n const resolvedPage = accessRes.page ?? accessPageNum\n const resolvedPageSize = accessRes.pageSize ?? accessPageSizeNum\n const resolvedTotal = accessRes.total ?? (accessRes.items?.length ?? 0)\n const resolvedTotalPages = accessRes.totalPages ?? Math.max(1, Math.ceil((resolvedTotal || 0) / (resolvedPageSize || 1)))\n setAccessPage(resolvedPage)\n setAccessPageSize((prev) => {\n if (resolvedPageSize === prev) {\n accessPageSizeRef.current = prev\n return prev\n }\n accessPageSizeRef.current = resolvedPageSize\n return resolvedPageSize\n })\n setAccessTotal(resolvedTotal)\n setAccessTotalPages(resolvedTotalPages)\n }, [fetchActions, fetchAccess])\n\n const loadWithState = React.useCallback(async (\n actionPage: number, actionPageSize: number,\n accessPageNum: number, accessPageSizeNum: number,\n ) => {\n setLoading(true)\n setError(null)\n try {\n await loadAll(actionPage, actionPageSize, accessPageNum, accessPageSizeNum)\n } catch (err) {\n logger.error('Failed to load audit logs', { err })\n setError(t('audit_logs.error.load'))\n } finally {\n setLoading(false)\n }\n }, [loadAll, t])\n\n React.useEffect(() => {\n setActionsPage(1)\n setAccessPage(1)\n void loadWithState(1, actionsPageSizeRef.current, 1, accessPageSizeRef.current)\n }, [loadWithState, undoableOnly])\n\n const renderRefreshButton = React.useCallback(() => (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void loadWithState(actionsPage, actionsPageSize, accessPage, accessPageSize)}\n disabled={loading}\n >\n {loading ? t('audit_logs.common.refreshing') : t('audit_logs.common.refresh')}\n </Button>\n ), [loadWithState, actionsPage, actionsPageSize, accessPage, accessPageSize, loading, t])\n\n const handleUndoError = React.useCallback(() => {\n setError(t('audit_logs.error.undo'))\n }, [t])\n const handleRedoError = React.useCallback(() => {\n setError(t('audit_logs.error.redo'))\n }, [t])\n\n const handleActionsPageChange = React.useCallback((nextPage: number) => {\n const totalPages = actionsTotalPages || 1\n const normalized = Math.max(1, Math.min(nextPage, totalPages))\n void loadWithState(normalized, actionsPageSizeRef.current, accessPage, accessPageSizeRef.current)\n }, [actionsTotalPages, loadWithState, accessPage])\n\n const handleAccessPageChange = React.useCallback((nextPage: number) => {\n const totalPages = accessTotalPages || 1\n const normalized = Math.max(1, Math.min(nextPage, totalPages))\n void loadWithState(actionsPage, actionsPageSizeRef.current, normalized, accessPageSizeRef.current)\n }, [accessTotalPages, loadWithState, actionsPage])\n\n const headerExtras = (\n <>\n {renderRefreshButton()}\n <label className=\"flex items-center gap-2 rounded border border-transparent px-2 py-1 text-sm text-muted-foreground\">\n <span>{t('audit_logs.filters.undoable_only')}</span>\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border border-input\"\n checked={undoableOnly}\n onChange={(e) => setUndoableOnly(e.target.checked)}\n />\n </label>\n </>\n )\n\n return (\n <Page>\n <PageBody>\n <Tabs\n value={tab}\n onValueChange={(value) => setTab(value as TabOption)}\n variant=\"underline\"\n className=\"mb-6\"\n >\n <TabsList className=\"w-full flex-wrap\" aria-label={t('audit_logs.tabs.label')}>\n <TabsTrigger value=\"actions\">{t('audit_logs.actions.title')}</TabsTrigger>\n <TabsTrigger value=\"access\">{t('audit_logs.access.title')}</TabsTrigger>\n </TabsList>\n </Tabs>\n\n {error && <div className=\"mb-4 rounded-md border border-status-error-border bg-status-error-bg p-3 text-sm text-status-error-text\">{error}</div>}\n\n {tab === 'actions' && (\n <AuditLogsActions\n items={actions}\n onRefresh={() => loadWithState(actionsPage, actionsPageSize, accessPage, accessPageSize)}\n isLoading={loading}\n headerExtras={headerExtras}\n onUndoError={handleUndoError}\n onRedoError={handleRedoError}\n pagination={{\n page: actionsPage,\n pageSize: actionsPageSize,\n total: actionsTotal,\n totalPages: actionsTotalPages,\n onPageChange: handleActionsPageChange,\n }}\n />\n )}\n\n {tab === 'access' && (\n <AccessLogsTable\n items={accessLogs}\n isLoading={loading}\n actions={renderRefreshButton()}\n pagination={{\n page: accessPage,\n pageSize: accessPageSize,\n total: accessTotal,\n totalPages: accessTotalPages,\n onPageChange: handleAccessPageChange,\n }}\n />\n )}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AA0II,SA8BA,UA9BA,KAgCE,YAhCF;AAxIJ,OAAO,WAAW;AAClB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,cAAc;AACvB,SAAS,MAAM,UAAU,mBAAmB;AAC5C,SAAS,YAAY;AAErB,SAAS,wBAAwB;AAEjC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AAsBhF,MAAM,oBAAoB;AAEX,SAAR,gBAAiC;AACtC,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,KAAK,MAAM,IAAI,MAAM,SAAoB,SAAS;AACzD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA0B,CAAC,CAAC;AAChE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA0B,CAAC,CAAC;AACtE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,KAAK;AAC5D,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAS,iBAAiB;AAC9E,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAS,CAAC;AACxD,QAAM,CAAC,mBAAmB,oBAAoB,IAAI,MAAM,SAAS,CAAC;AAClE,QAAM,qBAAqB,MAAM,OAAO,iBAAiB;AACzD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,CAAC;AACpD,QAAM,CAAC,gBAAgB,iBAAiB,IAAI,MAAM,SAAS,iBAAiB;AAC5E,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,CAAC;AACtD,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,MAAM,SAAS,CAAC;AAChE,QAAM,oBAAoB,MAAM,OAAO,iBAAiB;AAExD,QAAM,eAAe,MAAM,YAAY,OAAO,MAAc,aAAqB;AAC/E,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;AAC/B,WAAO,IAAI,YAAY,OAAO,QAAQ,CAAC;AACvC,QAAI,aAAc,QAAO,IAAI,gBAAgB,MAAM;AACnD,WAAO;AAAA,MACL,sCAAsC,OAAO,SAAS,CAAC;AAAA,MACvD;AAAA,MACA,EAAE,cAAc,EAAE,uBAAuB,EAAE;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,cAAc,CAAC,CAAC;AAEpB,QAAM,cAAc,MAAM,YAAY,OAAO,MAAc,aAAqB;AAC9E,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,QAAQ,OAAO,IAAI,CAAC;AAC/B,WAAO,IAAI,YAAY,OAAO,QAAQ,CAAC;AACvC,WAAO;AAAA,MACL,qCAAqC,OAAO,SAAS,CAAC;AAAA,MACtD;AAAA,MACA,EAAE,cAAc,EAAE,uBAAuB,EAAE;AAAA,IAC7C;AAAA,EACF,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,UAAU,MAAM,YAAY,OAChC,YAAoB,gBACpB,eAAuB,sBACpB;AACH,UAAM,CAAC,YAAY,SAAS,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,aAAa,YAAY,cAAc;AAAA,MACvC,YAAY,eAAe,iBAAiB;AAAA,IAC9C,CAAC;AACD,eAAW,WAAW,SAAS,CAAC,CAAC;AACjC,mBAAe,WAAW,QAAQ,UAAU;AAC5C,uBAAmB,CAAC,SAAS;AAC3B,YAAM,WAAW,WAAW,YAAY;AACxC,yBAAmB,UAAU;AAC7B,aAAO,aAAa,OAAO,OAAO;AAAA,IACpC,CAAC;AACD,oBAAgB,WAAW,UAAU,WAAW,OAAO,UAAU,EAAE;AACnE,yBAAqB,WAAW,cAAc,CAAC;AAC/C,kBAAc,UAAU,SAAS,CAAC,CAAC;AACnC,UAAM,eAAe,UAAU,QAAQ;AACvC,UAAM,mBAAmB,UAAU,YAAY;AAC/C,UAAM,gBAAgB,UAAU,UAAU,UAAU,OAAO,UAAU;AACrE,UAAM,qBAAqB,UAAU,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,iBAAiB,MAAM,oBAAoB,EAAE,CAAC;AACxH,kBAAc,YAAY;AAC1B,sBAAkB,CAAC,SAAS;AAC1B,UAAI,qBAAqB,MAAM;AAC7B,0BAAkB,UAAU;AAC5B,eAAO;AAAA,MACT;AACA,wBAAkB,UAAU;AAC5B,aAAO;AAAA,IACT,CAAC;AACD,mBAAe,aAAa;AAC5B,wBAAoB,kBAAkB;AAAA,EACxC,GAAG,CAAC,cAAc,WAAW,CAAC;AAE9B,QAAM,gBAAgB,MAAM,YAAY,OACtC,YAAoB,gBACpB,eAAuB,sBACpB;AACH,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,QAAQ,YAAY,gBAAgB,eAAe,iBAAiB;AAAA,IAC5E,SAAS,KAAK;AACZ,aAAO,MAAM,6BAA6B,EAAE,IAAI,CAAC;AACjD,eAAS,EAAE,uBAAuB,CAAC;AAAA,IACrC,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,SAAS,CAAC,CAAC;AAEf,QAAM,UAAU,MAAM;AACpB,mBAAe,CAAC;AAChB,kBAAc,CAAC;AACf,SAAK,cAAc,GAAG,mBAAmB,SAAS,GAAG,kBAAkB,OAAO;AAAA,EAChF,GAAG,CAAC,eAAe,YAAY,CAAC;AAEhC,QAAM,sBAAsB,MAAM,YAAY,MAC5C;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,MAAM,KAAK,cAAc,aAAa,iBAAiB,YAAY,cAAc;AAAA,MAC1F,UAAU;AAAA,MAET,oBAAU,EAAE,8BAA8B,IAAI,EAAE,2BAA2B;AAAA;AAAA,EAC9E,GACC,CAAC,eAAe,aAAa,iBAAiB,YAAY,gBAAgB,SAAS,CAAC,CAAC;AAExF,QAAM,kBAAkB,MAAM,YAAY,
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport React from 'react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { ActionLogItem } from '../../components/AuditLogsActions'\nimport { AuditLogsActions } from '../../components/AuditLogsActions'\nimport type { AccessLogItem } from '../../components/AccessLogsTable'\nimport { AccessLogsTable } from '../../components/AccessLogsTable'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('audit_logs').child({ component: 'audit-logs-page' })\n\ntype ActionLogResponse = {\n items: ActionLogItem[]\n canViewTenant: boolean\n page: number\n pageSize: number\n total: number\n totalPages: number\n}\n\ntype AccessLogResponse = {\n items: AccessLogItem[]\n canViewTenant: boolean\n page: number\n pageSize: number\n total: number\n totalPages: number\n}\n\ntype TabOption = 'actions' | 'access'\n\nconst DEFAULT_PAGE_SIZE = 50\n\nexport default function AuditLogsPage() {\n const t = useT()\n const [tab, setTab] = React.useState<TabOption>('actions')\n const [actions, setActions] = React.useState<ActionLogItem[]>([])\n const [accessLogs, setAccessLogs] = React.useState<AccessLogItem[]>([])\n const [loading, setLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [undoableOnly, setUndoableOnly] = React.useState(false)\n const [actionsPage, setActionsPage] = React.useState(1)\n const [actionsPageSize, setActionsPageSize] = React.useState(DEFAULT_PAGE_SIZE)\n const [actionsTotal, setActionsTotal] = React.useState(0)\n const [actionsTotalPages, setActionsTotalPages] = React.useState(1)\n const actionsPageSizeRef = React.useRef(DEFAULT_PAGE_SIZE)\n const [accessPage, setAccessPage] = React.useState(1)\n const [accessPageSize, setAccessPageSize] = React.useState(DEFAULT_PAGE_SIZE)\n const [accessTotal, setAccessTotal] = React.useState(0)\n const [accessTotalPages, setAccessTotalPages] = React.useState(1)\n const accessPageSizeRef = React.useRef(DEFAULT_PAGE_SIZE)\n\n const fetchActions = React.useCallback(async (page: number, pageSize: number) => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (undoableOnly) params.set('undoableOnly', 'true')\n return readApiResultOrThrow<ActionLogResponse>(\n `/api/audit_logs/audit-logs/actions?${params.toString()}`,\n undefined,\n { errorMessage: t('audit_logs.error.load') },\n )\n }, [undoableOnly, t])\n\n const fetchAccess = React.useCallback(async (page: number, pageSize: number) => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n return readApiResultOrThrow<AccessLogResponse>(\n `/api/audit_logs/audit-logs/access?${params.toString()}`,\n undefined,\n { errorMessage: t('audit_logs.error.load') },\n )\n }, [t])\n\n const loadAll = React.useCallback(async (\n actionPage: number, actionPageSize: number,\n accessPageNum: number, accessPageSizeNum: number,\n ) => {\n const [actionsRes, accessRes] = await Promise.all([\n fetchActions(actionPage, actionPageSize),\n fetchAccess(accessPageNum, accessPageSizeNum),\n ])\n setActions(actionsRes.items ?? [])\n setActionsPage(actionsRes.page ?? actionPage)\n setActionsPageSize((prev) => {\n const resolved = actionsRes.pageSize ?? actionPageSize\n actionsPageSizeRef.current = resolved\n return resolved === prev ? prev : resolved\n })\n setActionsTotal(actionsRes.total ?? (actionsRes.items?.length ?? 0))\n setActionsTotalPages(actionsRes.totalPages ?? 1)\n setAccessLogs(accessRes.items ?? [])\n const resolvedPage = accessRes.page ?? accessPageNum\n const resolvedPageSize = accessRes.pageSize ?? accessPageSizeNum\n const resolvedTotal = accessRes.total ?? (accessRes.items?.length ?? 0)\n const resolvedTotalPages = accessRes.totalPages ?? Math.max(1, Math.ceil((resolvedTotal || 0) / (resolvedPageSize || 1)))\n setAccessPage(resolvedPage)\n setAccessPageSize((prev) => {\n if (resolvedPageSize === prev) {\n accessPageSizeRef.current = prev\n return prev\n }\n accessPageSizeRef.current = resolvedPageSize\n return resolvedPageSize\n })\n setAccessTotal(resolvedTotal)\n setAccessTotalPages(resolvedTotalPages)\n }, [fetchActions, fetchAccess])\n\n const loadWithState = React.useCallback(async (\n actionPage: number, actionPageSize: number,\n accessPageNum: number, accessPageSizeNum: number,\n ) => {\n setLoading(true)\n setError(null)\n try {\n await loadAll(actionPage, actionPageSize, accessPageNum, accessPageSizeNum)\n } catch (err) {\n logger.error('Failed to load audit logs', { err })\n setError(t('audit_logs.error.load'))\n } finally {\n setLoading(false)\n }\n }, [loadAll, t])\n\n React.useEffect(() => {\n setActionsPage(1)\n setAccessPage(1)\n void loadWithState(1, actionsPageSizeRef.current, 1, accessPageSizeRef.current)\n }, [loadWithState, undoableOnly])\n\n const renderRefreshButton = React.useCallback(() => (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => void loadWithState(actionsPage, actionsPageSize, accessPage, accessPageSize)}\n disabled={loading}\n >\n {loading ? t('audit_logs.common.refreshing') : t('audit_logs.common.refresh')}\n </Button>\n ), [loadWithState, actionsPage, actionsPageSize, accessPage, accessPageSize, loading, t])\n\n const handleUndoError = React.useCallback((reason?: string) => {\n setError(reason || t('audit_logs.error.undo'))\n }, [t])\n const handleRedoError = React.useCallback((reason?: string) => {\n setError(reason || t('audit_logs.error.redo'))\n }, [t])\n\n const handleActionsPageChange = React.useCallback((nextPage: number) => {\n const totalPages = actionsTotalPages || 1\n const normalized = Math.max(1, Math.min(nextPage, totalPages))\n void loadWithState(normalized, actionsPageSizeRef.current, accessPage, accessPageSizeRef.current)\n }, [actionsTotalPages, loadWithState, accessPage])\n\n const handleAccessPageChange = React.useCallback((nextPage: number) => {\n const totalPages = accessTotalPages || 1\n const normalized = Math.max(1, Math.min(nextPage, totalPages))\n void loadWithState(actionsPage, actionsPageSizeRef.current, normalized, accessPageSizeRef.current)\n }, [accessTotalPages, loadWithState, actionsPage])\n\n const headerExtras = (\n <>\n {renderRefreshButton()}\n <label className=\"flex items-center gap-2 rounded border border-transparent px-2 py-1 text-sm text-muted-foreground\">\n <span>{t('audit_logs.filters.undoable_only')}</span>\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded border border-input\"\n checked={undoableOnly}\n onChange={(e) => setUndoableOnly(e.target.checked)}\n />\n </label>\n </>\n )\n\n return (\n <Page>\n <PageBody>\n <Tabs\n value={tab}\n onValueChange={(value) => setTab(value as TabOption)}\n variant=\"underline\"\n className=\"mb-6\"\n >\n <TabsList className=\"w-full flex-wrap\" aria-label={t('audit_logs.tabs.label')}>\n <TabsTrigger value=\"actions\">{t('audit_logs.actions.title')}</TabsTrigger>\n <TabsTrigger value=\"access\">{t('audit_logs.access.title')}</TabsTrigger>\n </TabsList>\n </Tabs>\n\n {error && <div className=\"mb-4 rounded-md border border-status-error-border bg-status-error-bg p-3 text-sm text-status-error-text\">{error}</div>}\n\n {tab === 'actions' && (\n <AuditLogsActions\n items={actions}\n onRefresh={() => loadWithState(actionsPage, actionsPageSize, accessPage, accessPageSize)}\n isLoading={loading}\n headerExtras={headerExtras}\n onUndoError={handleUndoError}\n onRedoError={handleRedoError}\n pagination={{\n page: actionsPage,\n pageSize: actionsPageSize,\n total: actionsTotal,\n totalPages: actionsTotalPages,\n onPageChange: handleActionsPageChange,\n }}\n />\n )}\n\n {tab === 'access' && (\n <AccessLogsTable\n items={accessLogs}\n isLoading={loading}\n actions={renderRefreshButton()}\n pagination={{\n page: accessPage,\n pageSize: accessPageSize,\n total: accessTotal,\n totalPages: accessTotalPages,\n onPageChange: handleAccessPageChange,\n }}\n />\n )}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,13 @@ import { useAuditPermissions, canUndoEntry, canRedoEntry } from "@open-mercato/u
|
|
|
13
13
|
import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
|
|
14
14
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
15
|
const logger = createLogger("audit_logs").child({ component: "AuditLogsActions" });
|
|
16
|
+
function resolveServerReason(err) {
|
|
17
|
+
if (!(err instanceof Error)) return void 0;
|
|
18
|
+
const status = err.status;
|
|
19
|
+
if (typeof status !== "number") return void 0;
|
|
20
|
+
const message = err.message?.trim();
|
|
21
|
+
return message ? message : void 0;
|
|
22
|
+
}
|
|
16
23
|
function AuditLogsActions({
|
|
17
24
|
items,
|
|
18
25
|
onRefresh,
|
|
@@ -73,7 +80,7 @@ function AuditLogsActions({
|
|
|
73
80
|
await onRefresh();
|
|
74
81
|
} catch (err) {
|
|
75
82
|
logger.error("Undo action failed", { err });
|
|
76
|
-
onUndoError?.();
|
|
83
|
+
onUndoError?.(resolveServerReason(err));
|
|
77
84
|
} finally {
|
|
78
85
|
setUndoingToken(null);
|
|
79
86
|
}
|
|
@@ -91,7 +98,7 @@ function AuditLogsActions({
|
|
|
91
98
|
await onRefresh();
|
|
92
99
|
} catch (err) {
|
|
93
100
|
logger.error("Redo action failed", { err });
|
|
94
|
-
onRedoError?.();
|
|
101
|
+
onRedoError?.(resolveServerReason(err));
|
|
95
102
|
} finally {
|
|
96
103
|
setRedoingId(null);
|
|
97
104
|
}
|