From bcb937dd266da0fbe4b35f57da6f74e80ae8992f Mon Sep 17 00:00:00 2001 From: liyaqath Date: Tue, 30 Jun 2026 11:40:07 +0530 Subject: [PATCH] Initial commit --- .env.example | 12 + .gitignore | 11 + app.js | 40 + index.js | 37 + package-lock.json | 3624 +++++++++++++++++ package.json | 41 + .../attributes/attribute.controller.js | 50 + .../attributes/attributes/attribute.model.js | 38 + .../attributes/attribute.repository.js | 30 + .../attributes/attributes/attribute.routes.js | 126 + .../attributes/attribute.service.js | 75 + .../attributes/attribute.validation.js | 32 + src/features/attributes/attributes/index.js | 5 + src/features/attributes/index.js | 8 + .../auditLogs/auditLog.controller.js | 50 + .../auditLogs/auditLogs/auditLog.model.js | 38 + .../auditLogs/auditLog.repository.js | 30 + .../auditLogs/auditLogs/auditLog.routes.js | 126 + .../auditLogs/auditLogs/auditLog.service.js | 75 + .../auditLogs/auditLog.validation.js | 32 + src/features/auditLogs/auditLogs/index.js | 5 + src/features/auditLogs/index.js | 8 + .../access/permissionNode.model.js | 79 + .../authentication/access/role.model.js | 58 + .../access/rolePermission.model.js | 60 + .../authentication/access/userRole.model.js | 36 + .../authentication/auth/auth.controller.js | 33 + .../authentication/auth/auth.model.js | 38 + .../authentication/auth/auth.repository.js | 30 + .../authentication/auth/auth.routes.js | 67 + .../authentication/auth/auth.service.js | 134 + .../authentication/auth/auth.validation.js | 10 + src/features/authentication/auth/index.js | 5 + src/features/authentication/index.js | 8 + .../authentication/users/user.model.js | 88 + .../brands/brands/brand.controller.js | 50 + src/features/brands/brands/brand.model.js | 38 + .../brands/brands/brand.repository.js | 30 + src/features/brands/brands/brand.routes.js | 126 + src/features/brands/brands/brand.service.js | 75 + .../brands/brands/brand.validation.js | 32 + src/features/brands/brands/index.js | 5 + src/features/brands/index.js | 8 + .../catalogs/catalogs/catalog.controller.js | 50 + .../catalogs/catalogs/catalog.model.js | 38 + .../catalogs/catalogs/catalog.repository.js | 30 + .../catalogs/catalogs/catalog.routes.js | 126 + .../catalogs/catalogs/catalog.service.js | 75 + .../catalogs/catalogs/catalog.validation.js | 32 + src/features/catalogs/catalogs/index.js | 5 + src/features/catalogs/index.js | 8 + .../categories/categorie.controller.js | 50 + .../categories/categories/categorie.model.js | 38 + .../categories/categorie.repository.js | 30 + .../categories/categories/categorie.routes.js | 126 + .../categories/categorie.service.js | 75 + .../categories/categorie.validation.js | 32 + src/features/categories/categories/index.js | 5 + src/features/categories/index.js | 8 + .../channels/channels/channel.controller.js | 50 + .../channels/channels/channel.model.js | 38 + .../channels/channels/channel.repository.js | 30 + .../channels/channels/channel.routes.js | 126 + .../channels/channels/channel.service.js | 75 + .../channels/channels/channel.validation.js | 32 + src/features/channels/channels/index.js | 5 + src/features/channels/index.js | 8 + src/features/dashboard/index.js | 8 + src/features/dashboard/metrics/index.js | 5 + .../dashboard/metrics/metric.controller.js | 50 + .../dashboard/metrics/metric.model.js | 38 + .../dashboard/metrics/metric.repository.js | 30 + .../dashboard/metrics/metric.routes.js | 126 + .../dashboard/metrics/metric.service.js | 75 + .../dashboard/metrics/metric.validation.js | 32 + .../exports/exports/export.controller.js | 50 + src/features/exports/exports/export.model.js | 38 + .../exports/exports/export.repository.js | 30 + src/features/exports/exports/export.routes.js | 126 + .../exports/exports/export.service.js | 75 + .../exports/exports/export.validation.js | 32 + src/features/exports/exports/index.js | 5 + src/features/exports/index.js | 8 + .../imports/imports/import.controller.js | 50 + src/features/imports/imports/import.model.js | 38 + .../imports/imports/import.repository.js | 30 + src/features/imports/imports/import.routes.js | 126 + .../imports/imports/import.service.js | 75 + .../imports/imports/import.validation.js | 32 + src/features/imports/imports/index.js | 5 + src/features/imports/index.js | 8 + src/features/index.js | 47 + src/features/integrations/index.js | 8 + .../integrations/integrations/index.js | 5 + .../integrations/integration.controller.js | 50 + .../integrations/integration.model.js | 38 + .../integrations/integration.repository.js | 30 + .../integrations/integration.routes.js | 126 + .../integrations/integration.service.js | 75 + .../integrations/integration.validation.js | 32 + src/features/inventory/index.js | 8 + src/features/inventory/inventory/index.js | 5 + .../inventory/inventory.controller.js | 50 + .../inventory/inventory/inventory.model.js | 38 + .../inventory/inventory.repository.js | 30 + .../inventory/inventory/inventory.routes.js | 126 + .../inventory/inventory/inventory.service.js | 75 + .../inventory/inventory.validation.js | 32 + src/features/localization/index.js | 8 + .../localization/localization/index.js | 5 + .../localization/localization.controller.js | 50 + .../localization/localization.model.js | 38 + .../localization/localization.repository.js | 30 + .../localization/localization.routes.js | 126 + .../localization/localization.service.js | 75 + .../localization/localization.validation.js | 32 + src/features/media/index.js | 8 + src/features/media/media/index.js | 5 + src/features/media/media/media.controller.js | 50 + src/features/media/media/media.model.js | 38 + src/features/media/media/media.repository.js | 30 + src/features/media/media/media.routes.js | 126 + src/features/media/media/media.service.js | 75 + src/features/media/media/media.validation.js | 32 + src/features/notifications/index.js | 8 + .../notifications/notifications/index.js | 5 + .../notifications/notification.controller.js | 50 + .../notifications/notification.model.js | 38 + .../notifications/notification.repository.js | 30 + .../notifications/notification.routes.js | 126 + .../notifications/notification.service.js | 75 + .../notifications/notification.validation.js | 32 + src/features/organization/index.js | 8 + src/features/organization/org/index.js | 5 + .../organization/org/org.controller.js | 50 + src/features/organization/org/org.model.js | 38 + .../organization/org/org.repository.js | 30 + src/features/organization/org/org.routes.js | 126 + src/features/organization/org/org.service.js | 75 + .../organization/org/org.validation.js | 32 + src/features/organization/org/tenant.model.js | 51 + src/features/pricing/index.js | 8 + src/features/pricing/pricing/index.js | 5 + .../pricing/pricing/pricing.controller.js | 50 + src/features/pricing/pricing/pricing.model.js | 38 + .../pricing/pricing/pricing.repository.js | 30 + .../pricing/pricing/pricing.routes.js | 126 + .../pricing/pricing/pricing.service.js | 75 + .../pricing/pricing/pricing.validation.js | 32 + .../products/approvals/approval.controller.js | 50 + .../products/approvals/approval.model.js | 38 + .../products/approvals/approval.repository.js | 30 + .../products/approvals/approval.routes.js | 126 + .../products/approvals/approval.service.js | 75 + .../products/approvals/approval.validation.js | 32 + src/features/products/approvals/index.js | 5 + .../products/bundles/bundle.controller.js | 50 + src/features/products/bundles/bundle.model.js | 38 + .../products/bundles/bundle.repository.js | 30 + .../products/bundles/bundle.routes.js | 126 + .../products/bundles/bundle.service.js | 75 + .../products/bundles/bundle.validation.js | 32 + src/features/products/bundles/index.js | 5 + .../configurableProduct.controller.js | 50 + .../configurableProduct.model.js | 38 + .../configurableProduct.repository.js | 30 + .../configurableProduct.routes.js | 126 + .../configurableProduct.service.js | 75 + .../configurableProduct.validation.js | 32 + .../products/configurableProducts/index.js | 5 + .../digitalProduct.controller.js | 50 + .../digitalProducts/digitalProduct.model.js | 38 + .../digitalProduct.repository.js | 30 + .../digitalProducts/digitalProduct.routes.js | 126 + .../digitalProducts/digitalProduct.service.js | 75 + .../digitalProduct.validation.js | 32 + .../products/digitalProducts/index.js | 5 + src/features/products/index.js | 22 + src/features/products/lifecycle/index.js | 5 + .../lifecycle/lifecycle.controller.js | 50 + .../products/lifecycle/lifecycle.model.js | 38 + .../lifecycle/lifecycle.repository.js | 30 + .../products/lifecycle/lifecycle.routes.js | 126 + .../products/lifecycle/lifecycle.service.js | 75 + .../lifecycle/lifecycle.validation.js | 32 + src/features/products/products/index.js | 5 + .../products/products/product.controller.js | 50 + .../products/products/product.model.js | 38 + .../products/products/product.repository.js | 30 + .../products/products/product.routes.js | 126 + .../products/products/product.service.js | 75 + .../products/products/product.validation.js | 32 + src/features/products/publishing/index.js | 5 + .../publishing/publishing.controller.js | 50 + .../products/publishing/publishing.model.js | 38 + .../publishing/publishing.repository.js | 30 + .../products/publishing/publishing.routes.js | 126 + .../products/publishing/publishing.service.js | 75 + .../publishing/publishing.validation.js | 32 + src/features/products/variants/index.js | 5 + .../products/variants/variant.controller.js | 50 + .../products/variants/variant.model.js | 38 + .../products/variants/variant.repository.js | 30 + .../products/variants/variant.routes.js | 126 + .../products/variants/variant.service.js | 75 + .../products/variants/variant.validation.js | 32 + src/features/reports/index.js | 8 + src/features/reports/reports/index.js | 5 + .../reports/reports/report.controller.js | 50 + src/features/reports/reports/report.model.js | 38 + .../reports/reports/report.repository.js | 30 + src/features/reports/reports/report.routes.js | 126 + .../reports/reports/report.service.js | 75 + .../reports/reports/report.validation.js | 32 + src/features/settings/index.js | 8 + src/features/settings/settings/index.js | 5 + .../settings/settings/setting.controller.js | 50 + .../settings/settings/setting.model.js | 38 + .../settings/settings/setting.repository.js | 30 + .../settings/settings/setting.routes.js | 126 + .../settings/settings/setting.service.js | 75 + .../settings/settings/setting.validation.js | 32 + src/features/suppliers/index.js | 8 + src/features/suppliers/suppliers/index.js | 5 + .../suppliers/supplier.controller.js | 50 + .../suppliers/suppliers/supplier.model.js | 38 + .../suppliers/supplier.repository.js | 30 + .../suppliers/suppliers/supplier.routes.js | 126 + .../suppliers/suppliers/supplier.service.js | 75 + .../suppliers/supplier.validation.js | 32 + src/features/workflows/index.js | 8 + src/features/workflows/workflows/index.js | 5 + .../workflows/workflow.controller.js | 50 + .../workflows/workflows/workflow.model.js | 38 + .../workflows/workflow.repository.js | 30 + .../workflows/workflows/workflow.routes.js | 126 + .../workflows/workflows/workflow.service.js | 75 + .../workflows/workflow.validation.js | 32 + src/migrations/.gitkeep | 1 + .../20260629131001-create-tenants.cjs | 58 + .../20260629131002-create-users.cjs | 78 + ...20260629131003-create-permission-nodes.cjs | 89 + .../20260629131004-create-roles.cjs | 70 + ...20260629131005-create-role-permissions.cjs | 79 + .../20260629131006-create-user-roles.cjs | 62 + src/seeders/.gitkeep | 1 + .../20260629131000-initial-superadmin.cjs | 147 + src/shared/config/database.config.cjs | 38 + src/shared/config/database.config.js | 40 + src/shared/config/multer.config.js | 29 + src/shared/config/storage.config.js | 15 + src/shared/config/swagger.config.js | 35 + src/shared/database/connection.js | 36 + src/shared/database/models.js | 132 + src/shared/middleware/audit.middleware.js | 35 + src/shared/middleware/auth.middleware.js | 17 + src/shared/middleware/error.middleware.js | 12 + src/shared/middleware/logger.middleware.js | 3 + .../middleware/permission.middleware.js | 24 + src/shared/middleware/rateLimiter.js | 34 + src/shared/middleware/upload.middleware.js | 5 + .../middleware/validation.middleware.js | 12 + src/shared/services/audit.service.js | 10 + src/shared/services/cronJob.service.js | 6 + src/shared/services/notification.service.js | 18 + src/shared/services/search.service.js | 11 + src/shared/services/socket.service.js | 38 + src/shared/services/upload.service.js | 27 + src/utils/connectors/index.js | 1 + src/utils/connectors/redis.connector.js | 7 + src/utils/eventBroker/eventBroker.js | 12 + src/utils/eventBroker/index.js | 1 + src/utils/exporters/csv.exporter.js | 5 + src/utils/exporters/index.js | 1 + src/utils/helpers/ApiError.utils.js | 12 + src/utils/helpers/common.helper.js | 3 + src/utils/helpers/index.js | 1 + src/utils/helpers/jwt.utils.js | 29 + src/utils/imageProcessor/imageProcessor.js | 6 + src/utils/imageProcessor/index.js | 1 + src/utils/importers/csv.importer.js | 5 + src/utils/importers/index.js | 1 + src/utils/search/index.js | 1 + src/utils/search/searchHelper.js | 5 + src/utils/storage/index.js | 1 + src/utils/storage/storageHelper.js | 5 + 286 files changed, 15918 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app.js create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/features/attributes/attributes/attribute.controller.js create mode 100644 src/features/attributes/attributes/attribute.model.js create mode 100644 src/features/attributes/attributes/attribute.repository.js create mode 100644 src/features/attributes/attributes/attribute.routes.js create mode 100644 src/features/attributes/attributes/attribute.service.js create mode 100644 src/features/attributes/attributes/attribute.validation.js create mode 100644 src/features/attributes/attributes/index.js create mode 100644 src/features/attributes/index.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.controller.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.model.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.repository.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.routes.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.service.js create mode 100644 src/features/auditLogs/auditLogs/auditLog.validation.js create mode 100644 src/features/auditLogs/auditLogs/index.js create mode 100644 src/features/auditLogs/index.js create mode 100644 src/features/authentication/access/permissionNode.model.js create mode 100644 src/features/authentication/access/role.model.js create mode 100644 src/features/authentication/access/rolePermission.model.js create mode 100644 src/features/authentication/access/userRole.model.js create mode 100644 src/features/authentication/auth/auth.controller.js create mode 100644 src/features/authentication/auth/auth.model.js create mode 100644 src/features/authentication/auth/auth.repository.js create mode 100644 src/features/authentication/auth/auth.routes.js create mode 100644 src/features/authentication/auth/auth.service.js create mode 100644 src/features/authentication/auth/auth.validation.js create mode 100644 src/features/authentication/auth/index.js create mode 100644 src/features/authentication/index.js create mode 100644 src/features/authentication/users/user.model.js create mode 100644 src/features/brands/brands/brand.controller.js create mode 100644 src/features/brands/brands/brand.model.js create mode 100644 src/features/brands/brands/brand.repository.js create mode 100644 src/features/brands/brands/brand.routes.js create mode 100644 src/features/brands/brands/brand.service.js create mode 100644 src/features/brands/brands/brand.validation.js create mode 100644 src/features/brands/brands/index.js create mode 100644 src/features/brands/index.js create mode 100644 src/features/catalogs/catalogs/catalog.controller.js create mode 100644 src/features/catalogs/catalogs/catalog.model.js create mode 100644 src/features/catalogs/catalogs/catalog.repository.js create mode 100644 src/features/catalogs/catalogs/catalog.routes.js create mode 100644 src/features/catalogs/catalogs/catalog.service.js create mode 100644 src/features/catalogs/catalogs/catalog.validation.js create mode 100644 src/features/catalogs/catalogs/index.js create mode 100644 src/features/catalogs/index.js create mode 100644 src/features/categories/categories/categorie.controller.js create mode 100644 src/features/categories/categories/categorie.model.js create mode 100644 src/features/categories/categories/categorie.repository.js create mode 100644 src/features/categories/categories/categorie.routes.js create mode 100644 src/features/categories/categories/categorie.service.js create mode 100644 src/features/categories/categories/categorie.validation.js create mode 100644 src/features/categories/categories/index.js create mode 100644 src/features/categories/index.js create mode 100644 src/features/channels/channels/channel.controller.js create mode 100644 src/features/channels/channels/channel.model.js create mode 100644 src/features/channels/channels/channel.repository.js create mode 100644 src/features/channels/channels/channel.routes.js create mode 100644 src/features/channels/channels/channel.service.js create mode 100644 src/features/channels/channels/channel.validation.js create mode 100644 src/features/channels/channels/index.js create mode 100644 src/features/channels/index.js create mode 100644 src/features/dashboard/index.js create mode 100644 src/features/dashboard/metrics/index.js create mode 100644 src/features/dashboard/metrics/metric.controller.js create mode 100644 src/features/dashboard/metrics/metric.model.js create mode 100644 src/features/dashboard/metrics/metric.repository.js create mode 100644 src/features/dashboard/metrics/metric.routes.js create mode 100644 src/features/dashboard/metrics/metric.service.js create mode 100644 src/features/dashboard/metrics/metric.validation.js create mode 100644 src/features/exports/exports/export.controller.js create mode 100644 src/features/exports/exports/export.model.js create mode 100644 src/features/exports/exports/export.repository.js create mode 100644 src/features/exports/exports/export.routes.js create mode 100644 src/features/exports/exports/export.service.js create mode 100644 src/features/exports/exports/export.validation.js create mode 100644 src/features/exports/exports/index.js create mode 100644 src/features/exports/index.js create mode 100644 src/features/imports/imports/import.controller.js create mode 100644 src/features/imports/imports/import.model.js create mode 100644 src/features/imports/imports/import.repository.js create mode 100644 src/features/imports/imports/import.routes.js create mode 100644 src/features/imports/imports/import.service.js create mode 100644 src/features/imports/imports/import.validation.js create mode 100644 src/features/imports/imports/index.js create mode 100644 src/features/imports/index.js create mode 100644 src/features/index.js create mode 100644 src/features/integrations/index.js create mode 100644 src/features/integrations/integrations/index.js create mode 100644 src/features/integrations/integrations/integration.controller.js create mode 100644 src/features/integrations/integrations/integration.model.js create mode 100644 src/features/integrations/integrations/integration.repository.js create mode 100644 src/features/integrations/integrations/integration.routes.js create mode 100644 src/features/integrations/integrations/integration.service.js create mode 100644 src/features/integrations/integrations/integration.validation.js create mode 100644 src/features/inventory/index.js create mode 100644 src/features/inventory/inventory/index.js create mode 100644 src/features/inventory/inventory/inventory.controller.js create mode 100644 src/features/inventory/inventory/inventory.model.js create mode 100644 src/features/inventory/inventory/inventory.repository.js create mode 100644 src/features/inventory/inventory/inventory.routes.js create mode 100644 src/features/inventory/inventory/inventory.service.js create mode 100644 src/features/inventory/inventory/inventory.validation.js create mode 100644 src/features/localization/index.js create mode 100644 src/features/localization/localization/index.js create mode 100644 src/features/localization/localization/localization.controller.js create mode 100644 src/features/localization/localization/localization.model.js create mode 100644 src/features/localization/localization/localization.repository.js create mode 100644 src/features/localization/localization/localization.routes.js create mode 100644 src/features/localization/localization/localization.service.js create mode 100644 src/features/localization/localization/localization.validation.js create mode 100644 src/features/media/index.js create mode 100644 src/features/media/media/index.js create mode 100644 src/features/media/media/media.controller.js create mode 100644 src/features/media/media/media.model.js create mode 100644 src/features/media/media/media.repository.js create mode 100644 src/features/media/media/media.routes.js create mode 100644 src/features/media/media/media.service.js create mode 100644 src/features/media/media/media.validation.js create mode 100644 src/features/notifications/index.js create mode 100644 src/features/notifications/notifications/index.js create mode 100644 src/features/notifications/notifications/notification.controller.js create mode 100644 src/features/notifications/notifications/notification.model.js create mode 100644 src/features/notifications/notifications/notification.repository.js create mode 100644 src/features/notifications/notifications/notification.routes.js create mode 100644 src/features/notifications/notifications/notification.service.js create mode 100644 src/features/notifications/notifications/notification.validation.js create mode 100644 src/features/organization/index.js create mode 100644 src/features/organization/org/index.js create mode 100644 src/features/organization/org/org.controller.js create mode 100644 src/features/organization/org/org.model.js create mode 100644 src/features/organization/org/org.repository.js create mode 100644 src/features/organization/org/org.routes.js create mode 100644 src/features/organization/org/org.service.js create mode 100644 src/features/organization/org/org.validation.js create mode 100644 src/features/organization/org/tenant.model.js create mode 100644 src/features/pricing/index.js create mode 100644 src/features/pricing/pricing/index.js create mode 100644 src/features/pricing/pricing/pricing.controller.js create mode 100644 src/features/pricing/pricing/pricing.model.js create mode 100644 src/features/pricing/pricing/pricing.repository.js create mode 100644 src/features/pricing/pricing/pricing.routes.js create mode 100644 src/features/pricing/pricing/pricing.service.js create mode 100644 src/features/pricing/pricing/pricing.validation.js create mode 100644 src/features/products/approvals/approval.controller.js create mode 100644 src/features/products/approvals/approval.model.js create mode 100644 src/features/products/approvals/approval.repository.js create mode 100644 src/features/products/approvals/approval.routes.js create mode 100644 src/features/products/approvals/approval.service.js create mode 100644 src/features/products/approvals/approval.validation.js create mode 100644 src/features/products/approvals/index.js create mode 100644 src/features/products/bundles/bundle.controller.js create mode 100644 src/features/products/bundles/bundle.model.js create mode 100644 src/features/products/bundles/bundle.repository.js create mode 100644 src/features/products/bundles/bundle.routes.js create mode 100644 src/features/products/bundles/bundle.service.js create mode 100644 src/features/products/bundles/bundle.validation.js create mode 100644 src/features/products/bundles/index.js create mode 100644 src/features/products/configurableProducts/configurableProduct.controller.js create mode 100644 src/features/products/configurableProducts/configurableProduct.model.js create mode 100644 src/features/products/configurableProducts/configurableProduct.repository.js create mode 100644 src/features/products/configurableProducts/configurableProduct.routes.js create mode 100644 src/features/products/configurableProducts/configurableProduct.service.js create mode 100644 src/features/products/configurableProducts/configurableProduct.validation.js create mode 100644 src/features/products/configurableProducts/index.js create mode 100644 src/features/products/digitalProducts/digitalProduct.controller.js create mode 100644 src/features/products/digitalProducts/digitalProduct.model.js create mode 100644 src/features/products/digitalProducts/digitalProduct.repository.js create mode 100644 src/features/products/digitalProducts/digitalProduct.routes.js create mode 100644 src/features/products/digitalProducts/digitalProduct.service.js create mode 100644 src/features/products/digitalProducts/digitalProduct.validation.js create mode 100644 src/features/products/digitalProducts/index.js create mode 100644 src/features/products/index.js create mode 100644 src/features/products/lifecycle/index.js create mode 100644 src/features/products/lifecycle/lifecycle.controller.js create mode 100644 src/features/products/lifecycle/lifecycle.model.js create mode 100644 src/features/products/lifecycle/lifecycle.repository.js create mode 100644 src/features/products/lifecycle/lifecycle.routes.js create mode 100644 src/features/products/lifecycle/lifecycle.service.js create mode 100644 src/features/products/lifecycle/lifecycle.validation.js create mode 100644 src/features/products/products/index.js create mode 100644 src/features/products/products/product.controller.js create mode 100644 src/features/products/products/product.model.js create mode 100644 src/features/products/products/product.repository.js create mode 100644 src/features/products/products/product.routes.js create mode 100644 src/features/products/products/product.service.js create mode 100644 src/features/products/products/product.validation.js create mode 100644 src/features/products/publishing/index.js create mode 100644 src/features/products/publishing/publishing.controller.js create mode 100644 src/features/products/publishing/publishing.model.js create mode 100644 src/features/products/publishing/publishing.repository.js create mode 100644 src/features/products/publishing/publishing.routes.js create mode 100644 src/features/products/publishing/publishing.service.js create mode 100644 src/features/products/publishing/publishing.validation.js create mode 100644 src/features/products/variants/index.js create mode 100644 src/features/products/variants/variant.controller.js create mode 100644 src/features/products/variants/variant.model.js create mode 100644 src/features/products/variants/variant.repository.js create mode 100644 src/features/products/variants/variant.routes.js create mode 100644 src/features/products/variants/variant.service.js create mode 100644 src/features/products/variants/variant.validation.js create mode 100644 src/features/reports/index.js create mode 100644 src/features/reports/reports/index.js create mode 100644 src/features/reports/reports/report.controller.js create mode 100644 src/features/reports/reports/report.model.js create mode 100644 src/features/reports/reports/report.repository.js create mode 100644 src/features/reports/reports/report.routes.js create mode 100644 src/features/reports/reports/report.service.js create mode 100644 src/features/reports/reports/report.validation.js create mode 100644 src/features/settings/index.js create mode 100644 src/features/settings/settings/index.js create mode 100644 src/features/settings/settings/setting.controller.js create mode 100644 src/features/settings/settings/setting.model.js create mode 100644 src/features/settings/settings/setting.repository.js create mode 100644 src/features/settings/settings/setting.routes.js create mode 100644 src/features/settings/settings/setting.service.js create mode 100644 src/features/settings/settings/setting.validation.js create mode 100644 src/features/suppliers/index.js create mode 100644 src/features/suppliers/suppliers/index.js create mode 100644 src/features/suppliers/suppliers/supplier.controller.js create mode 100644 src/features/suppliers/suppliers/supplier.model.js create mode 100644 src/features/suppliers/suppliers/supplier.repository.js create mode 100644 src/features/suppliers/suppliers/supplier.routes.js create mode 100644 src/features/suppliers/suppliers/supplier.service.js create mode 100644 src/features/suppliers/suppliers/supplier.validation.js create mode 100644 src/features/workflows/index.js create mode 100644 src/features/workflows/workflows/index.js create mode 100644 src/features/workflows/workflows/workflow.controller.js create mode 100644 src/features/workflows/workflows/workflow.model.js create mode 100644 src/features/workflows/workflows/workflow.repository.js create mode 100644 src/features/workflows/workflows/workflow.routes.js create mode 100644 src/features/workflows/workflows/workflow.service.js create mode 100644 src/features/workflows/workflows/workflow.validation.js create mode 100644 src/migrations/.gitkeep create mode 100644 src/migrations/20260629131001-create-tenants.cjs create mode 100644 src/migrations/20260629131002-create-users.cjs create mode 100644 src/migrations/20260629131003-create-permission-nodes.cjs create mode 100644 src/migrations/20260629131004-create-roles.cjs create mode 100644 src/migrations/20260629131005-create-role-permissions.cjs create mode 100644 src/migrations/20260629131006-create-user-roles.cjs create mode 100644 src/seeders/.gitkeep create mode 100644 src/seeders/20260629131000-initial-superadmin.cjs create mode 100644 src/shared/config/database.config.cjs create mode 100644 src/shared/config/database.config.js create mode 100644 src/shared/config/multer.config.js create mode 100644 src/shared/config/storage.config.js create mode 100644 src/shared/config/swagger.config.js create mode 100644 src/shared/database/connection.js create mode 100644 src/shared/database/models.js create mode 100644 src/shared/middleware/audit.middleware.js create mode 100644 src/shared/middleware/auth.middleware.js create mode 100644 src/shared/middleware/error.middleware.js create mode 100644 src/shared/middleware/logger.middleware.js create mode 100644 src/shared/middleware/permission.middleware.js create mode 100644 src/shared/middleware/rateLimiter.js create mode 100644 src/shared/middleware/upload.middleware.js create mode 100644 src/shared/middleware/validation.middleware.js create mode 100644 src/shared/services/audit.service.js create mode 100644 src/shared/services/cronJob.service.js create mode 100644 src/shared/services/notification.service.js create mode 100644 src/shared/services/search.service.js create mode 100644 src/shared/services/socket.service.js create mode 100644 src/shared/services/upload.service.js create mode 100644 src/utils/connectors/index.js create mode 100644 src/utils/connectors/redis.connector.js create mode 100644 src/utils/eventBroker/eventBroker.js create mode 100644 src/utils/eventBroker/index.js create mode 100644 src/utils/exporters/csv.exporter.js create mode 100644 src/utils/exporters/index.js create mode 100644 src/utils/helpers/ApiError.utils.js create mode 100644 src/utils/helpers/common.helper.js create mode 100644 src/utils/helpers/index.js create mode 100644 src/utils/helpers/jwt.utils.js create mode 100644 src/utils/imageProcessor/imageProcessor.js create mode 100644 src/utils/imageProcessor/index.js create mode 100644 src/utils/importers/csv.importer.js create mode 100644 src/utils/importers/index.js create mode 100644 src/utils/search/index.js create mode 100644 src/utils/search/searchHelper.js create mode 100644 src/utils/storage/index.js create mode 100644 src/utils/storage/storageHelper.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a914dad --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +PORT=5000 +NODE_ENV=development +CORS_ORIGIN=http://localhost:3000 +JWT_SECRET=supersecretjwtkeythatislongandsecure +JWT_EXPIRES_IN=7d + +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_USER=postgres +DB_PASS=postgres +DB_NAME=maskan_pim +DB_DIALECT=postgres diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4167fe3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +.env +.DS_Store +dist/ +build/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.idea/ +.vscode/ +*.log diff --git a/app.js b/app.js new file mode 100644 index 0000000..5da4285 --- /dev/null +++ b/app.js @@ -0,0 +1,40 @@ +import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; +import compression from 'compression'; +import cookieParser from 'cookie-parser'; +import morgan from 'morgan'; +import swaggerUi from 'swagger-ui-express'; +import swaggerSpec from './src/shared/config/swagger.config.js'; +import { errorMiddleware } from './src/shared/middleware/error.middleware.js'; +import registerRoutes from './src/features/index.js'; + +const app = express(); + +// Middlewares +app.use(helmet()); +app.use(cors({ origin: process.env.CORS_ORIGIN || '*' })); +app.use(compression()); +app.use(cookieParser()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +if (process.env.NODE_ENV !== 'test') { + app.use(morgan('dev')); +} + +// Swagger setup +app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// Base Health Check +app.get('/health', (req, res) => { + res.status(200).json({ status: 'OK', timestamp: new Date() }); +}); + +// Centralized Feature Router Loader +registerRoutes(app); + +// Global Error Handler +app.use(errorMiddleware); + +export default app; diff --git a/index.js b/index.js new file mode 100644 index 0000000..786de1d --- /dev/null +++ b/index.js @@ -0,0 +1,37 @@ +import dotenv from 'dotenv'; +import http from 'http'; +import app from './app.js'; +import { connectDatabase } from './src/shared/database/connection.js'; +import { SocketService } from './src/shared/services/socket.service.js'; + +// Load environment variables +dotenv.config(); + +const PORT = process.env.PORT || 5000; + +async function bootstrap() { + try { + // Connect database + await connectDatabase(); + + // Create HTTP Server + const server = http.createServer(app); + + // Initialize Socket.io service + SocketService.init(server); + + server.listen(PORT, () => { + console.log(`==================================================`); + console.log(` Enterprise PIM Backend Server Started `); + console.log(` Port: ${PORT} `); + console.log(` Env: ${process.env.NODE_ENV || 'development'} `); + console.log(` Docs: http://localhost:${PORT}/api/docs `); + console.log(`==================================================`); + }); + } catch (error) { + console.error('Bootstrap error occurred:', error); + process.exit(1); + } +} + +bootstrap(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2997f85 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3624 @@ +{ + "name": "maskan-pim-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maskan-pim-backend", + "version": "1.0.0", + "dependencies": { + "bcrypt": "^6.0.0", + "compression": "^1.7.5", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-validator": "^7.1.0", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "pg": "^8.13.1", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.5", + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "sequelize-cli": "^6.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.4.1", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-cli": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.5.tgz", + "integrity": "sha512-DqyISCULOaEbTM+rRQH4YvcUWeOC1XDiSKcjsC6TfAnT7W837mNkChJhtB/Z4FdCFHRCojmiP7zsrA4pARmacA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "js-beautify": "1.15.4", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "11.1.0", + "lodash.mergewith": "^4.6.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/swagger-jsdoc/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.8", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.8.tgz", + "integrity": "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f0da432 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "maskan-pim-backend", + "version": "1.0.0", + "description": "Enterprise Product Information Management (PIM) Backend Service", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js", + "db:migrate": "sequelize-cli db:migrate --config src/shared/config/database.config.cjs --migrations-path src/migrations --models-path src/shared/database", + "db:seed": "sequelize-cli db:seed:all --config src/shared/config/database.config.cjs --seeders-path src/seeders --models-path src/shared/database", + "db:rollback": "sequelize-cli db:migrate:undo --config src/shared/config/database.config.cjs --migrations-path src/migrations --models-path src/shared/database" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "compression": "^1.7.5", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-validator": "^7.1.0", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "pg": "^8.13.1", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.5", + "socket.io": "^4.8.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1" + }, + "devDependencies": { + "nodemon": "^3.1.9", + "sequelize-cli": "^6.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "private": true +} diff --git a/src/features/attributes/attributes/attribute.controller.js b/src/features/attributes/attributes/attribute.controller.js new file mode 100644 index 0000000..68152ac --- /dev/null +++ b/src/features/attributes/attributes/attribute.controller.js @@ -0,0 +1,50 @@ +import service from './attribute.service.js'; + +export class AttributeController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Attribute deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new AttributeController(); diff --git a/src/features/attributes/attributes/attribute.model.js b/src/features/attributes/attributes/attribute.model.js new file mode 100644 index 0000000..0c76612 --- /dev/null +++ b/src/features/attributes/attributes/attribute.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Attribute extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Attribute.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Attribute', + tableName: 'attributes', + timestamps: true, + underscored: true + }); + + return Attribute; +}; diff --git a/src/features/attributes/attributes/attribute.repository.js b/src/features/attributes/attributes/attribute.repository.js new file mode 100644 index 0000000..4a385f8 --- /dev/null +++ b/src/features/attributes/attributes/attribute.repository.js @@ -0,0 +1,30 @@ +import { Attribute } from './attribute.model.js'; + +export class AttributeRepository { + async findAll(options = {}) { + return await Attribute.findAll(options); + } + + async findById(id, options = {}) { + return await Attribute.findByPk(id, options); + } + + async create(data, options = {}) { + return await Attribute.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new AttributeRepository(); diff --git a/src/features/attributes/attributes/attribute.routes.js b/src/features/attributes/attributes/attribute.routes.js new file mode 100644 index 0000000..7c276f5 --- /dev/null +++ b/src/features/attributes/attributes/attribute.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './attribute.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './attribute.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/attributes: + * get: + * summary: Retrieve all attributes + * tags: [Attributes] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:attributes']), + controller.getAll +); + +/** + * @swagger + * /api/v1/attributes/{id}: + * get: + * summary: Retrieve a single attribute + * tags: [Attributes] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:attributes']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/attributes: + * post: + * summary: Create a attribute + * tags: [Attributes] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:attributes']), + createValidation, + validate, + audit('CREATE_ATTRIBUTE'), + controller.create +); + +/** + * @swagger + * /api/v1/attributes/{id}: + * put: + * summary: Update a attribute + * tags: [Attributes] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:attributes']), + updateValidation, + validate, + audit('UPDATE_ATTRIBUTE'), + controller.update +); + +/** + * @swagger + * /api/v1/attributes/{id}: + * delete: + * summary: Delete a attribute + * tags: [Attributes] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:attributes']), + deleteValidation, + validate, + audit('DELETE_ATTRIBUTE'), + controller.delete +); + +export default router; diff --git a/src/features/attributes/attributes/attribute.service.js b/src/features/attributes/attributes/attribute.service.js new file mode 100644 index 0000000..13da8b2 --- /dev/null +++ b/src/features/attributes/attributes/attribute.service.js @@ -0,0 +1,75 @@ +import repository from './attribute.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class AttributeService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Attribute not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('attribute:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Attribute', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Attribute not found'); + } + + SocketService.broadcast('attribute:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Attribute', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Attribute not found'); + } + + SocketService.broadcast('attribute:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Attribute', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new AttributeService(); diff --git a/src/features/attributes/attributes/attribute.validation.js b/src/features/attributes/attributes/attribute.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/attributes/attributes/attribute.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/attributes/attributes/index.js b/src/features/attributes/attributes/index.js new file mode 100644 index 0000000..db33765 --- /dev/null +++ b/src/features/attributes/attributes/index.js @@ -0,0 +1,5 @@ +export { default as attributeRoutes } from './attribute.routes.js'; +export { default as attributeController } from './attribute.controller.js'; +export { default as attributeService } from './attribute.service.js'; +export { default as attributeRepository } from './attribute.repository.js'; +export { default as attributeModel } from './attribute.model.js'; diff --git a/src/features/attributes/index.js b/src/features/attributes/index.js new file mode 100644 index 0000000..af405c7 --- /dev/null +++ b/src/features/attributes/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import attributesRouter from './attributes/attribute.routes.js'; + +const router = Router(); + +router.use('/attributes', attributesRouter); + +export default router; diff --git a/src/features/auditLogs/auditLogs/auditLog.controller.js b/src/features/auditLogs/auditLogs/auditLog.controller.js new file mode 100644 index 0000000..3be3ad6 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.controller.js @@ -0,0 +1,50 @@ +import service from './auditLog.service.js'; + +export class AuditLogController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'AuditLog deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new AuditLogController(); diff --git a/src/features/auditLogs/auditLogs/auditLog.model.js b/src/features/auditLogs/auditLogs/auditLog.model.js new file mode 100644 index 0000000..af06d66 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class AuditLog extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + AuditLog.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'AuditLog', + tableName: 'auditlogs', + timestamps: true, + underscored: true + }); + + return AuditLog; +}; diff --git a/src/features/auditLogs/auditLogs/auditLog.repository.js b/src/features/auditLogs/auditLogs/auditLog.repository.js new file mode 100644 index 0000000..3b9c006 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.repository.js @@ -0,0 +1,30 @@ +import { AuditLog } from './auditLog.model.js'; + +export class AuditLogRepository { + async findAll(options = {}) { + return await AuditLog.findAll(options); + } + + async findById(id, options = {}) { + return await AuditLog.findByPk(id, options); + } + + async create(data, options = {}) { + return await AuditLog.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new AuditLogRepository(); diff --git a/src/features/auditLogs/auditLogs/auditLog.routes.js b/src/features/auditLogs/auditLogs/auditLog.routes.js new file mode 100644 index 0000000..36beb48 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './auditLog.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './auditLog.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/auditLogs: + * get: + * summary: Retrieve all auditLogs + * tags: [AuditLogs] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:auditLogs']), + controller.getAll +); + +/** + * @swagger + * /api/v1/auditLogs/{id}: + * get: + * summary: Retrieve a single auditLog + * tags: [AuditLogs] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:auditLogs']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/auditLogs: + * post: + * summary: Create a auditLog + * tags: [AuditLogs] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:auditLogs']), + createValidation, + validate, + audit('CREATE_AUDITLOG'), + controller.create +); + +/** + * @swagger + * /api/v1/auditLogs/{id}: + * put: + * summary: Update a auditLog + * tags: [AuditLogs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:auditLogs']), + updateValidation, + validate, + audit('UPDATE_AUDITLOG'), + controller.update +); + +/** + * @swagger + * /api/v1/auditLogs/{id}: + * delete: + * summary: Delete a auditLog + * tags: [AuditLogs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:auditLogs']), + deleteValidation, + validate, + audit('DELETE_AUDITLOG'), + controller.delete +); + +export default router; diff --git a/src/features/auditLogs/auditLogs/auditLog.service.js b/src/features/auditLogs/auditLogs/auditLog.service.js new file mode 100644 index 0000000..932a2f3 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.service.js @@ -0,0 +1,75 @@ +import repository from './auditLog.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class AuditLogService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('AuditLog not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('auditLog:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'AuditLog', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('AuditLog not found'); + } + + SocketService.broadcast('auditLog:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'AuditLog', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('AuditLog not found'); + } + + SocketService.broadcast('auditLog:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'AuditLog', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new AuditLogService(); diff --git a/src/features/auditLogs/auditLogs/auditLog.validation.js b/src/features/auditLogs/auditLogs/auditLog.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/auditLogs/auditLogs/auditLog.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/auditLogs/auditLogs/index.js b/src/features/auditLogs/auditLogs/index.js new file mode 100644 index 0000000..7bcdf30 --- /dev/null +++ b/src/features/auditLogs/auditLogs/index.js @@ -0,0 +1,5 @@ +export { default as auditLogRoutes } from './auditLog.routes.js'; +export { default as auditLogController } from './auditLog.controller.js'; +export { default as auditLogService } from './auditLog.service.js'; +export { default as auditLogRepository } from './auditLog.repository.js'; +export { default as auditLogModel } from './auditLog.model.js'; diff --git a/src/features/auditLogs/index.js b/src/features/auditLogs/index.js new file mode 100644 index 0000000..517b4d6 --- /dev/null +++ b/src/features/auditLogs/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import auditLogsRouter from './auditLogs/auditLog.routes.js'; + +const router = Router(); + +router.use('/auditlogs', auditLogsRouter); + +export default router; diff --git a/src/features/authentication/access/permissionNode.model.js b/src/features/authentication/access/permissionNode.model.js new file mode 100644 index 0000000..459fcd6 --- /dev/null +++ b/src/features/authentication/access/permissionNode.model.js @@ -0,0 +1,79 @@ +import { Model, DataTypes } from 'sequelize'; + +export class PermissionNode extends Model { + static associate(models) {} +} + +export default (sequelize) => { + PermissionNode.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + node_code: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true + }, + node_name: { + type: DataTypes.STRING(100), + allowNull: false + }, + node_type: { + type: DataTypes.STRING(20) + }, + parent_id: { + type: DataTypes.INTEGER, + allowNull: true + }, + node_level: { + type: DataTypes.INTEGER, + defaultValue: 1 + }, + display_order: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + can_view: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_create: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_edit: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_delete: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_alter: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_export: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_import: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + description: { + type: DataTypes.TEXT + } + }, { + sequelize, + modelName: 'PermissionNode', + tableName: 'permission_nodes', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' + }); + + return PermissionNode; +}; diff --git a/src/features/authentication/access/role.model.js b/src/features/authentication/access/role.model.js new file mode 100644 index 0000000..fc50d3c --- /dev/null +++ b/src/features/authentication/access/role.model.js @@ -0,0 +1,58 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Role extends Model { + static associate(models) {} +} + +export default (sequelize) => { + Role.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, + role_code: { + type: DataTypes.STRING(50), + allowNull: false + }, + role_name: { + type: DataTypes.STRING(100), + allowNull: false + }, + description: { + type: DataTypes.TEXT + }, + role_type: { + type: DataTypes.STRING(20), + defaultValue: 'tenant', + allowNull: false + }, + is_system_role: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + status: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + created_by: { + type: DataTypes.INTEGER + }, + updated_by: { + type: DataTypes.INTEGER + } + }, { + sequelize, + modelName: 'Role', + tableName: 'roles', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' + }); + + return Role; +}; diff --git a/src/features/authentication/access/rolePermission.model.js b/src/features/authentication/access/rolePermission.model.js new file mode 100644 index 0000000..2eb4916 --- /dev/null +++ b/src/features/authentication/access/rolePermission.model.js @@ -0,0 +1,60 @@ +import { Model, DataTypes } from 'sequelize'; + +export class RolePermission extends Model { + static associate(models) {} +} + +export default (sequelize) => { + RolePermission.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + role_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + node_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + can_view: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_create: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_edit: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_delete: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_alter: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_export: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + can_import: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }, { + sequelize, + modelName: 'RolePermission', + tableName: 'role_permissions', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' + }); + + return RolePermission; +}; diff --git a/src/features/authentication/access/userRole.model.js b/src/features/authentication/access/userRole.model.js new file mode 100644 index 0000000..c044d82 --- /dev/null +++ b/src/features/authentication/access/userRole.model.js @@ -0,0 +1,36 @@ +import { Model, DataTypes } from 'sequelize'; + +export class UserRole extends Model { + static associate(models) {} +} + +export default (sequelize) => { + UserRole.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + role_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + status: { + type: DataTypes.BOOLEAN, + defaultValue: true + } + }, { + sequelize, + modelName: 'UserRole', + tableName: 'user_roles', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' + }); + + return UserRole; +}; diff --git a/src/features/authentication/auth/auth.controller.js b/src/features/authentication/auth/auth.controller.js new file mode 100644 index 0000000..a34eac5 --- /dev/null +++ b/src/features/authentication/auth/auth.controller.js @@ -0,0 +1,33 @@ +import authService from './auth.service.js'; + +export class AuthController { + async login(req, res, next) { + try { + const { email, password } = req.body; + const result = await authService.login({ email, password }); + res.status(200).json({ + success: true, + message: 'Login successful', + data: result + }); + } catch (error) { + next(error); + } + } + + async refreshToken(req, res, next) { + try { + const { refreshToken } = req.body; + const result = await authService.refreshToken(refreshToken); + res.status(200).json({ + success: true, + message: 'Tokens refreshed successfully', + data: result + }); + } catch (error) { + next(error); + } + } +} + +export default new AuthController(); diff --git a/src/features/authentication/auth/auth.model.js b/src/features/authentication/auth/auth.model.js new file mode 100644 index 0000000..463f697 --- /dev/null +++ b/src/features/authentication/auth/auth.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Auth extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Auth.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Auth', + tableName: 'auths', + timestamps: true, + underscored: true + }); + + return Auth; +}; diff --git a/src/features/authentication/auth/auth.repository.js b/src/features/authentication/auth/auth.repository.js new file mode 100644 index 0000000..0f96905 --- /dev/null +++ b/src/features/authentication/auth/auth.repository.js @@ -0,0 +1,30 @@ +import { Auth } from './auth.model.js'; + +export class AuthRepository { + async findAll(options = {}) { + return await Auth.findAll(options); + } + + async findById(id, options = {}) { + return await Auth.findByPk(id, options); + } + + async create(data, options = {}) { + return await Auth.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new AuthRepository(); diff --git a/src/features/authentication/auth/auth.routes.js b/src/features/authentication/auth/auth.routes.js new file mode 100644 index 0000000..e250682 --- /dev/null +++ b/src/features/authentication/auth/auth.routes.js @@ -0,0 +1,67 @@ +import { Router } from 'express'; +import controller from './auth.controller.js'; +import { loginValidation, refreshTokenValidation } from './auth.validation.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/auth/login: + * post: + * summary: Login user + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Success + */ +router.post( + '/login', + loginValidation, + validate, + controller.login +); + +/** + * @swagger + * /api/v1/auth/refresh-token: + * post: + * summary: Refresh JWT token + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - refreshToken + * properties: + * refreshToken: + * type: string + * responses: + * 200: + * description: Success + */ +router.post( + '/refresh-token', + refreshTokenValidation, + validate, + controller.refreshToken +); + +export default router; diff --git a/src/features/authentication/auth/auth.service.js b/src/features/authentication/auth/auth.service.js new file mode 100644 index 0000000..832b25a --- /dev/null +++ b/src/features/authentication/auth/auth.service.js @@ -0,0 +1,134 @@ +import { models } from '../../../shared/database/models.js'; +const { User, Role, PermissionNode, Tenant } = models; +import { generateToken, generateRefreshToken, verifyRefreshToken } from '../../../utils/helpers/jwt.utils.js'; +import { ApiError } from '../../../utils/helpers/ApiError.utils.js'; + +const formatPermissions = (roles) => { + const permissionMap = {}; + + roles.forEach(role => { + if (!role.permissions) return; + role.permissions.forEach(p => { + const code = p.node_code; + if (!permissionMap[code]) { + permissionMap[code] = { + view: false, create: false, edit: false, delete: false, alter: false, import: false, export: false + }; + } + permissionMap[code].view = permissionMap[code].view || p.RolePermission.can_view; + permissionMap[code].create = permissionMap[code].create || p.RolePermission.can_create; + permissionMap[code].edit = permissionMap[code].edit || p.RolePermission.can_edit; + permissionMap[code].delete = permissionMap[code].delete || p.RolePermission.can_delete; + permissionMap[code].alter = permissionMap[code].alter || p.RolePermission.can_alter; + permissionMap[code].import = permissionMap[code].import || p.RolePermission.can_import; + permissionMap[code].export = permissionMap[code].export || p.RolePermission.can_export; + }); + }); + + return permissionMap; +}; + +export const login = async ({ email, password }) => { + const user = await User.findOne({ + where: { email }, + include: [ + { + model: Role, + as: 'roles', + include: [ + { + model: PermissionNode, + as: 'permissions', + through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] } + } + ], + through: { attributes: ['status'] } + }, + { + model: Tenant, + as: 'tenant' + } + ] + }); + + if (!user) { + throw new ApiError(401, 'Invalid email or password'); + } + + const isMatch = await user.validatePassword(password); + if (!isMatch) { + throw new ApiError(401, 'Invalid email or password'); + } + + const roleIds = user.roles.map(r => r.id); + + const payload = { + user_id: user.id, + tenant_id: user.tenant_id, + user_type: user.user_type, + role_ids: roleIds + }; + + const accessToken = generateToken(payload); + const refreshToken = generateRefreshToken(payload); + + user.last_login_at = new Date(); + await user.save(); + + const permissions = formatPermissions(user.roles); + + return { + user: { + id: user.id, + name: user.user_name, + email: user.email, + type: user.user_type, + tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.tenant_name } : null, + roles: user.roles.filter(r => r.UserRole && r.UserRole.status === true).map(r => ({ id: r.id, name: r.role_name, code: r.role_code })) + }, + accessToken, + refreshToken, + permissions + }; +}; + +export const refreshTokenAuth = async (oldRefreshToken) => { + if (!oldRefreshToken) { + throw new ApiError(401, 'Refresh token is required'); + } + + const decoded = verifyRefreshToken(oldRefreshToken); + + const user = await User.findByPk(decoded.user_id, { + include: [ + { + model: Role, + as: 'roles', + through: { attributes: ['status'] } + } + ] + }); + + if (!user) { + throw new ApiError(401, 'Invalid refresh token'); + } + + const roleIds = user.roles.filter(r => r.UserRole.status).map(r => r.id); + + const payload = { + user_id: user.id, + tenant_id: user.tenant_id, + user_type: user.user_type, + role_ids: roleIds + }; + + const accessToken = generateToken(payload); + const refreshToken = generateRefreshToken(payload); + + return { accessToken, refreshToken }; +}; + +export default { + login, + refreshToken: refreshTokenAuth +}; diff --git a/src/features/authentication/auth/auth.validation.js b/src/features/authentication/auth/auth.validation.js new file mode 100644 index 0000000..086997c --- /dev/null +++ b/src/features/authentication/auth/auth.validation.js @@ -0,0 +1,10 @@ +import { body } from 'express-validator'; + +export const loginValidation = [ + body('email').isEmail().withMessage('Valid email is required'), + body('password').notEmpty().withMessage('Password is required') +]; + +export const refreshTokenValidation = [ + body('refreshToken').notEmpty().withMessage('Refresh token is required') +]; diff --git a/src/features/authentication/auth/index.js b/src/features/authentication/auth/index.js new file mode 100644 index 0000000..3b608b1 --- /dev/null +++ b/src/features/authentication/auth/index.js @@ -0,0 +1,5 @@ +export { default as authRoutes } from './auth.routes.js'; +export { default as authController } from './auth.controller.js'; +export { default as authService } from './auth.service.js'; +export { default as authRepository } from './auth.repository.js'; +export { default as authModel } from './auth.model.js'; diff --git a/src/features/authentication/index.js b/src/features/authentication/index.js new file mode 100644 index 0000000..b5319ce --- /dev/null +++ b/src/features/authentication/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import authRouter from './auth/auth.routes.js'; + +const router = Router(); + +router.use('/auth', authRouter); + +export default router; diff --git a/src/features/authentication/users/user.model.js b/src/features/authentication/users/user.model.js new file mode 100644 index 0000000..0cead26 --- /dev/null +++ b/src/features/authentication/users/user.model.js @@ -0,0 +1,88 @@ +import { Model, DataTypes } from 'sequelize'; +import bcrypt from 'bcrypt'; + +export class User extends Model { + static associate(models) {} + async validatePassword(password) { + return await bcrypt.compare(password, this.password_hash); + } +} + +export default (sequelize) => { + User.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + tenant_id: { + type: DataTypes.INTEGER, + allowNull: true + }, + email: { + type: DataTypes.STRING(100), + allowNull: false, + validate: { + isEmail: true + } + }, + phone: { + type: DataTypes.STRING(20) + }, + password_hash: { + type: DataTypes.STRING(255), + allowNull: false + }, + user_code: { + type: DataTypes.STRING(50) + }, + user_name: { + type: DataTypes.STRING(100) + }, + profile_picture: { + type: DataTypes.STRING(255) + }, + last_login_at: { + type: DataTypes.DATE + }, + status: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + created_by: { + type: DataTypes.INTEGER + }, + updated_by: { + type: DataTypes.INTEGER + }, + user_type: { + type: DataTypes.STRING, + get() { + return this.tenant_id ? 'tenant' : 'platform'; + } + } + }, { + sequelize, + modelName: 'User', + tableName: 'users', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + hooks: { + beforeCreate: async (user) => { + if (user.password_hash) { + const salt = await bcrypt.genSalt(10); + user.password_hash = await bcrypt.hash(user.password_hash, salt); + } + }, + beforeUpdate: async (user) => { + if (user.changed('password_hash')) { + const salt = await bcrypt.genSalt(10); + user.password_hash = await bcrypt.hash(user.password_hash, salt); + } + } + } + }); + + return User; +}; diff --git a/src/features/brands/brands/brand.controller.js b/src/features/brands/brands/brand.controller.js new file mode 100644 index 0000000..752bf87 --- /dev/null +++ b/src/features/brands/brands/brand.controller.js @@ -0,0 +1,50 @@ +import service from './brand.service.js'; + +export class BrandController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Brand deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new BrandController(); diff --git a/src/features/brands/brands/brand.model.js b/src/features/brands/brands/brand.model.js new file mode 100644 index 0000000..c61c9f8 --- /dev/null +++ b/src/features/brands/brands/brand.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Brand extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Brand.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Brand', + tableName: 'brands', + timestamps: true, + underscored: true + }); + + return Brand; +}; diff --git a/src/features/brands/brands/brand.repository.js b/src/features/brands/brands/brand.repository.js new file mode 100644 index 0000000..1c6fea2 --- /dev/null +++ b/src/features/brands/brands/brand.repository.js @@ -0,0 +1,30 @@ +import { Brand } from './brand.model.js'; + +export class BrandRepository { + async findAll(options = {}) { + return await Brand.findAll(options); + } + + async findById(id, options = {}) { + return await Brand.findByPk(id, options); + } + + async create(data, options = {}) { + return await Brand.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new BrandRepository(); diff --git a/src/features/brands/brands/brand.routes.js b/src/features/brands/brands/brand.routes.js new file mode 100644 index 0000000..1cfa979 --- /dev/null +++ b/src/features/brands/brands/brand.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './brand.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './brand.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/brands: + * get: + * summary: Retrieve all brands + * tags: [Brands] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:brands']), + controller.getAll +); + +/** + * @swagger + * /api/v1/brands/{id}: + * get: + * summary: Retrieve a single brand + * tags: [Brands] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:brands']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/brands: + * post: + * summary: Create a brand + * tags: [Brands] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:brands']), + createValidation, + validate, + audit('CREATE_BRAND'), + controller.create +); + +/** + * @swagger + * /api/v1/brands/{id}: + * put: + * summary: Update a brand + * tags: [Brands] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:brands']), + updateValidation, + validate, + audit('UPDATE_BRAND'), + controller.update +); + +/** + * @swagger + * /api/v1/brands/{id}: + * delete: + * summary: Delete a brand + * tags: [Brands] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:brands']), + deleteValidation, + validate, + audit('DELETE_BRAND'), + controller.delete +); + +export default router; diff --git a/src/features/brands/brands/brand.service.js b/src/features/brands/brands/brand.service.js new file mode 100644 index 0000000..f27a1ed --- /dev/null +++ b/src/features/brands/brands/brand.service.js @@ -0,0 +1,75 @@ +import repository from './brand.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class BrandService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Brand not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('brand:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Brand', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Brand not found'); + } + + SocketService.broadcast('brand:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Brand', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Brand not found'); + } + + SocketService.broadcast('brand:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Brand', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new BrandService(); diff --git a/src/features/brands/brands/brand.validation.js b/src/features/brands/brands/brand.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/brands/brands/brand.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/brands/brands/index.js b/src/features/brands/brands/index.js new file mode 100644 index 0000000..2e33b0e --- /dev/null +++ b/src/features/brands/brands/index.js @@ -0,0 +1,5 @@ +export { default as brandRoutes } from './brand.routes.js'; +export { default as brandController } from './brand.controller.js'; +export { default as brandService } from './brand.service.js'; +export { default as brandRepository } from './brand.repository.js'; +export { default as brandModel } from './brand.model.js'; diff --git a/src/features/brands/index.js b/src/features/brands/index.js new file mode 100644 index 0000000..e7a68c1 --- /dev/null +++ b/src/features/brands/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import brandsRouter from './brands/brand.routes.js'; + +const router = Router(); + +router.use('/brands', brandsRouter); + +export default router; diff --git a/src/features/catalogs/catalogs/catalog.controller.js b/src/features/catalogs/catalogs/catalog.controller.js new file mode 100644 index 0000000..ecc0ab1 --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.controller.js @@ -0,0 +1,50 @@ +import service from './catalog.service.js'; + +export class CatalogController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Catalog deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new CatalogController(); diff --git a/src/features/catalogs/catalogs/catalog.model.js b/src/features/catalogs/catalogs/catalog.model.js new file mode 100644 index 0000000..4371aea --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Catalog extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Catalog.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Catalog', + tableName: 'catalogs', + timestamps: true, + underscored: true + }); + + return Catalog; +}; diff --git a/src/features/catalogs/catalogs/catalog.repository.js b/src/features/catalogs/catalogs/catalog.repository.js new file mode 100644 index 0000000..6b7624c --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.repository.js @@ -0,0 +1,30 @@ +import { Catalog } from './catalog.model.js'; + +export class CatalogRepository { + async findAll(options = {}) { + return await Catalog.findAll(options); + } + + async findById(id, options = {}) { + return await Catalog.findByPk(id, options); + } + + async create(data, options = {}) { + return await Catalog.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new CatalogRepository(); diff --git a/src/features/catalogs/catalogs/catalog.routes.js b/src/features/catalogs/catalogs/catalog.routes.js new file mode 100644 index 0000000..0466f44 --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './catalog.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './catalog.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/catalogs: + * get: + * summary: Retrieve all catalogs + * tags: [Catalogs] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:catalogs']), + controller.getAll +); + +/** + * @swagger + * /api/v1/catalogs/{id}: + * get: + * summary: Retrieve a single catalog + * tags: [Catalogs] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:catalogs']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/catalogs: + * post: + * summary: Create a catalog + * tags: [Catalogs] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:catalogs']), + createValidation, + validate, + audit('CREATE_CATALOG'), + controller.create +); + +/** + * @swagger + * /api/v1/catalogs/{id}: + * put: + * summary: Update a catalog + * tags: [Catalogs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:catalogs']), + updateValidation, + validate, + audit('UPDATE_CATALOG'), + controller.update +); + +/** + * @swagger + * /api/v1/catalogs/{id}: + * delete: + * summary: Delete a catalog + * tags: [Catalogs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:catalogs']), + deleteValidation, + validate, + audit('DELETE_CATALOG'), + controller.delete +); + +export default router; diff --git a/src/features/catalogs/catalogs/catalog.service.js b/src/features/catalogs/catalogs/catalog.service.js new file mode 100644 index 0000000..b174491 --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.service.js @@ -0,0 +1,75 @@ +import repository from './catalog.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class CatalogService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Catalog not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('catalog:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Catalog', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Catalog not found'); + } + + SocketService.broadcast('catalog:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Catalog', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Catalog not found'); + } + + SocketService.broadcast('catalog:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Catalog', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new CatalogService(); diff --git a/src/features/catalogs/catalogs/catalog.validation.js b/src/features/catalogs/catalogs/catalog.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/catalogs/catalogs/catalog.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/catalogs/catalogs/index.js b/src/features/catalogs/catalogs/index.js new file mode 100644 index 0000000..9202252 --- /dev/null +++ b/src/features/catalogs/catalogs/index.js @@ -0,0 +1,5 @@ +export { default as catalogRoutes } from './catalog.routes.js'; +export { default as catalogController } from './catalog.controller.js'; +export { default as catalogService } from './catalog.service.js'; +export { default as catalogRepository } from './catalog.repository.js'; +export { default as catalogModel } from './catalog.model.js'; diff --git a/src/features/catalogs/index.js b/src/features/catalogs/index.js new file mode 100644 index 0000000..c67da30 --- /dev/null +++ b/src/features/catalogs/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import catalogsRouter from './catalogs/catalog.routes.js'; + +const router = Router(); + +router.use('/catalogs', catalogsRouter); + +export default router; diff --git a/src/features/categories/categories/categorie.controller.js b/src/features/categories/categories/categorie.controller.js new file mode 100644 index 0000000..0695bbb --- /dev/null +++ b/src/features/categories/categories/categorie.controller.js @@ -0,0 +1,50 @@ +import service from './categorie.service.js'; + +export class CategorieController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Categorie deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new CategorieController(); diff --git a/src/features/categories/categories/categorie.model.js b/src/features/categories/categories/categorie.model.js new file mode 100644 index 0000000..d5b2d83 --- /dev/null +++ b/src/features/categories/categories/categorie.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Categorie extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Categorie.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Categorie', + tableName: 'categories', + timestamps: true, + underscored: true + }); + + return Categorie; +}; diff --git a/src/features/categories/categories/categorie.repository.js b/src/features/categories/categories/categorie.repository.js new file mode 100644 index 0000000..07aad65 --- /dev/null +++ b/src/features/categories/categories/categorie.repository.js @@ -0,0 +1,30 @@ +import { Categorie } from './categorie.model.js'; + +export class CategorieRepository { + async findAll(options = {}) { + return await Categorie.findAll(options); + } + + async findById(id, options = {}) { + return await Categorie.findByPk(id, options); + } + + async create(data, options = {}) { + return await Categorie.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new CategorieRepository(); diff --git a/src/features/categories/categories/categorie.routes.js b/src/features/categories/categories/categorie.routes.js new file mode 100644 index 0000000..eed25ed --- /dev/null +++ b/src/features/categories/categories/categorie.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './categorie.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './categorie.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/categories: + * get: + * summary: Retrieve all categories + * tags: [Categories] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:categories']), + controller.getAll +); + +/** + * @swagger + * /api/v1/categories/{id}: + * get: + * summary: Retrieve a single categorie + * tags: [Categories] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:categories']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/categories: + * post: + * summary: Create a categorie + * tags: [Categories] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:categories']), + createValidation, + validate, + audit('CREATE_CATEGORIE'), + controller.create +); + +/** + * @swagger + * /api/v1/categories/{id}: + * put: + * summary: Update a categorie + * tags: [Categories] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:categories']), + updateValidation, + validate, + audit('UPDATE_CATEGORIE'), + controller.update +); + +/** + * @swagger + * /api/v1/categories/{id}: + * delete: + * summary: Delete a categorie + * tags: [Categories] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:categories']), + deleteValidation, + validate, + audit('DELETE_CATEGORIE'), + controller.delete +); + +export default router; diff --git a/src/features/categories/categories/categorie.service.js b/src/features/categories/categories/categorie.service.js new file mode 100644 index 0000000..be27ebf --- /dev/null +++ b/src/features/categories/categories/categorie.service.js @@ -0,0 +1,75 @@ +import repository from './categorie.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class CategorieService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Categorie not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('categorie:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Categorie', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Categorie not found'); + } + + SocketService.broadcast('categorie:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Categorie', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Categorie not found'); + } + + SocketService.broadcast('categorie:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Categorie', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new CategorieService(); diff --git a/src/features/categories/categories/categorie.validation.js b/src/features/categories/categories/categorie.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/categories/categories/categorie.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/categories/categories/index.js b/src/features/categories/categories/index.js new file mode 100644 index 0000000..452c53d --- /dev/null +++ b/src/features/categories/categories/index.js @@ -0,0 +1,5 @@ +export { default as categorieRoutes } from './categorie.routes.js'; +export { default as categorieController } from './categorie.controller.js'; +export { default as categorieService } from './categorie.service.js'; +export { default as categorieRepository } from './categorie.repository.js'; +export { default as categorieModel } from './categorie.model.js'; diff --git a/src/features/categories/index.js b/src/features/categories/index.js new file mode 100644 index 0000000..aad19ed --- /dev/null +++ b/src/features/categories/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import categoriesRouter from './categories/categorie.routes.js'; + +const router = Router(); + +router.use('/categories', categoriesRouter); + +export default router; diff --git a/src/features/channels/channels/channel.controller.js b/src/features/channels/channels/channel.controller.js new file mode 100644 index 0000000..cba1fa1 --- /dev/null +++ b/src/features/channels/channels/channel.controller.js @@ -0,0 +1,50 @@ +import service from './channel.service.js'; + +export class ChannelController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Channel deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ChannelController(); diff --git a/src/features/channels/channels/channel.model.js b/src/features/channels/channels/channel.model.js new file mode 100644 index 0000000..641f931 --- /dev/null +++ b/src/features/channels/channels/channel.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Channel extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Channel.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Channel', + tableName: 'channels', + timestamps: true, + underscored: true + }); + + return Channel; +}; diff --git a/src/features/channels/channels/channel.repository.js b/src/features/channels/channels/channel.repository.js new file mode 100644 index 0000000..d0b7104 --- /dev/null +++ b/src/features/channels/channels/channel.repository.js @@ -0,0 +1,30 @@ +import { Channel } from './channel.model.js'; + +export class ChannelRepository { + async findAll(options = {}) { + return await Channel.findAll(options); + } + + async findById(id, options = {}) { + return await Channel.findByPk(id, options); + } + + async create(data, options = {}) { + return await Channel.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ChannelRepository(); diff --git a/src/features/channels/channels/channel.routes.js b/src/features/channels/channels/channel.routes.js new file mode 100644 index 0000000..4774486 --- /dev/null +++ b/src/features/channels/channels/channel.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './channel.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './channel.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/channels: + * get: + * summary: Retrieve all channels + * tags: [Channels] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:channels']), + controller.getAll +); + +/** + * @swagger + * /api/v1/channels/{id}: + * get: + * summary: Retrieve a single channel + * tags: [Channels] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:channels']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/channels: + * post: + * summary: Create a channel + * tags: [Channels] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:channels']), + createValidation, + validate, + audit('CREATE_CHANNEL'), + controller.create +); + +/** + * @swagger + * /api/v1/channels/{id}: + * put: + * summary: Update a channel + * tags: [Channels] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:channels']), + updateValidation, + validate, + audit('UPDATE_CHANNEL'), + controller.update +); + +/** + * @swagger + * /api/v1/channels/{id}: + * delete: + * summary: Delete a channel + * tags: [Channels] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:channels']), + deleteValidation, + validate, + audit('DELETE_CHANNEL'), + controller.delete +); + +export default router; diff --git a/src/features/channels/channels/channel.service.js b/src/features/channels/channels/channel.service.js new file mode 100644 index 0000000..3c4b607 --- /dev/null +++ b/src/features/channels/channels/channel.service.js @@ -0,0 +1,75 @@ +import repository from './channel.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ChannelService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Channel not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('channel:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Channel', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Channel not found'); + } + + SocketService.broadcast('channel:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Channel', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Channel not found'); + } + + SocketService.broadcast('channel:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Channel', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ChannelService(); diff --git a/src/features/channels/channels/channel.validation.js b/src/features/channels/channels/channel.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/channels/channels/channel.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/channels/channels/index.js b/src/features/channels/channels/index.js new file mode 100644 index 0000000..e083599 --- /dev/null +++ b/src/features/channels/channels/index.js @@ -0,0 +1,5 @@ +export { default as channelRoutes } from './channel.routes.js'; +export { default as channelController } from './channel.controller.js'; +export { default as channelService } from './channel.service.js'; +export { default as channelRepository } from './channel.repository.js'; +export { default as channelModel } from './channel.model.js'; diff --git a/src/features/channels/index.js b/src/features/channels/index.js new file mode 100644 index 0000000..fd15b73 --- /dev/null +++ b/src/features/channels/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import channelsRouter from './channels/channel.routes.js'; + +const router = Router(); + +router.use('/channels', channelsRouter); + +export default router; diff --git a/src/features/dashboard/index.js b/src/features/dashboard/index.js new file mode 100644 index 0000000..2867f3c --- /dev/null +++ b/src/features/dashboard/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import metricsRouter from './metrics/metric.routes.js'; + +const router = Router(); + +router.use('/metrics', metricsRouter); + +export default router; diff --git a/src/features/dashboard/metrics/index.js b/src/features/dashboard/metrics/index.js new file mode 100644 index 0000000..d5e82cd --- /dev/null +++ b/src/features/dashboard/metrics/index.js @@ -0,0 +1,5 @@ +export { default as metricRoutes } from './metric.routes.js'; +export { default as metricController } from './metric.controller.js'; +export { default as metricService } from './metric.service.js'; +export { default as metricRepository } from './metric.repository.js'; +export { default as metricModel } from './metric.model.js'; diff --git a/src/features/dashboard/metrics/metric.controller.js b/src/features/dashboard/metrics/metric.controller.js new file mode 100644 index 0000000..e9138df --- /dev/null +++ b/src/features/dashboard/metrics/metric.controller.js @@ -0,0 +1,50 @@ +import service from './metric.service.js'; + +export class MetricController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Metric deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new MetricController(); diff --git a/src/features/dashboard/metrics/metric.model.js b/src/features/dashboard/metrics/metric.model.js new file mode 100644 index 0000000..8c3d11f --- /dev/null +++ b/src/features/dashboard/metrics/metric.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Metric extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Metric.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Metric', + tableName: 'metrics', + timestamps: true, + underscored: true + }); + + return Metric; +}; diff --git a/src/features/dashboard/metrics/metric.repository.js b/src/features/dashboard/metrics/metric.repository.js new file mode 100644 index 0000000..036f1b7 --- /dev/null +++ b/src/features/dashboard/metrics/metric.repository.js @@ -0,0 +1,30 @@ +import { Metric } from './metric.model.js'; + +export class MetricRepository { + async findAll(options = {}) { + return await Metric.findAll(options); + } + + async findById(id, options = {}) { + return await Metric.findByPk(id, options); + } + + async create(data, options = {}) { + return await Metric.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new MetricRepository(); diff --git a/src/features/dashboard/metrics/metric.routes.js b/src/features/dashboard/metrics/metric.routes.js new file mode 100644 index 0000000..8dad490 --- /dev/null +++ b/src/features/dashboard/metrics/metric.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './metric.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './metric.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/metrics: + * get: + * summary: Retrieve all metrics + * tags: [Metrics] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:metrics']), + controller.getAll +); + +/** + * @swagger + * /api/v1/metrics/{id}: + * get: + * summary: Retrieve a single metric + * tags: [Metrics] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:metrics']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/metrics: + * post: + * summary: Create a metric + * tags: [Metrics] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:metrics']), + createValidation, + validate, + audit('CREATE_METRIC'), + controller.create +); + +/** + * @swagger + * /api/v1/metrics/{id}: + * put: + * summary: Update a metric + * tags: [Metrics] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:metrics']), + updateValidation, + validate, + audit('UPDATE_METRIC'), + controller.update +); + +/** + * @swagger + * /api/v1/metrics/{id}: + * delete: + * summary: Delete a metric + * tags: [Metrics] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:metrics']), + deleteValidation, + validate, + audit('DELETE_METRIC'), + controller.delete +); + +export default router; diff --git a/src/features/dashboard/metrics/metric.service.js b/src/features/dashboard/metrics/metric.service.js new file mode 100644 index 0000000..81e758b --- /dev/null +++ b/src/features/dashboard/metrics/metric.service.js @@ -0,0 +1,75 @@ +import repository from './metric.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class MetricService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Metric not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('metric:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Metric', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Metric not found'); + } + + SocketService.broadcast('metric:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Metric', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Metric not found'); + } + + SocketService.broadcast('metric:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Metric', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new MetricService(); diff --git a/src/features/dashboard/metrics/metric.validation.js b/src/features/dashboard/metrics/metric.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/dashboard/metrics/metric.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/exports/exports/export.controller.js b/src/features/exports/exports/export.controller.js new file mode 100644 index 0000000..64cb46c --- /dev/null +++ b/src/features/exports/exports/export.controller.js @@ -0,0 +1,50 @@ +import service from './export.service.js'; + +export class ExportController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Export deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ExportController(); diff --git a/src/features/exports/exports/export.model.js b/src/features/exports/exports/export.model.js new file mode 100644 index 0000000..06b1613 --- /dev/null +++ b/src/features/exports/exports/export.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Export extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Export.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Export', + tableName: 'exports', + timestamps: true, + underscored: true + }); + + return Export; +}; diff --git a/src/features/exports/exports/export.repository.js b/src/features/exports/exports/export.repository.js new file mode 100644 index 0000000..c2147bf --- /dev/null +++ b/src/features/exports/exports/export.repository.js @@ -0,0 +1,30 @@ +import { Export } from './export.model.js'; + +export class ExportRepository { + async findAll(options = {}) { + return await Export.findAll(options); + } + + async findById(id, options = {}) { + return await Export.findByPk(id, options); + } + + async create(data, options = {}) { + return await Export.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ExportRepository(); diff --git a/src/features/exports/exports/export.routes.js b/src/features/exports/exports/export.routes.js new file mode 100644 index 0000000..612b097 --- /dev/null +++ b/src/features/exports/exports/export.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './export.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './export.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/exports: + * get: + * summary: Retrieve all exports + * tags: [Exports] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:exports']), + controller.getAll +); + +/** + * @swagger + * /api/v1/exports/{id}: + * get: + * summary: Retrieve a single export + * tags: [Exports] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:exports']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/exports: + * post: + * summary: Create a export + * tags: [Exports] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:exports']), + createValidation, + validate, + audit('CREATE_EXPORT'), + controller.create +); + +/** + * @swagger + * /api/v1/exports/{id}: + * put: + * summary: Update a export + * tags: [Exports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:exports']), + updateValidation, + validate, + audit('UPDATE_EXPORT'), + controller.update +); + +/** + * @swagger + * /api/v1/exports/{id}: + * delete: + * summary: Delete a export + * tags: [Exports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:exports']), + deleteValidation, + validate, + audit('DELETE_EXPORT'), + controller.delete +); + +export default router; diff --git a/src/features/exports/exports/export.service.js b/src/features/exports/exports/export.service.js new file mode 100644 index 0000000..66c4188 --- /dev/null +++ b/src/features/exports/exports/export.service.js @@ -0,0 +1,75 @@ +import repository from './export.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ExportService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Export not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('export:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Export', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Export not found'); + } + + SocketService.broadcast('export:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Export', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Export not found'); + } + + SocketService.broadcast('export:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Export', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ExportService(); diff --git a/src/features/exports/exports/export.validation.js b/src/features/exports/exports/export.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/exports/exports/export.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/exports/exports/index.js b/src/features/exports/exports/index.js new file mode 100644 index 0000000..05eb6cc --- /dev/null +++ b/src/features/exports/exports/index.js @@ -0,0 +1,5 @@ +export { default as exportRoutes } from './export.routes.js'; +export { default as exportController } from './export.controller.js'; +export { default as exportService } from './export.service.js'; +export { default as exportRepository } from './export.repository.js'; +export { default as exportModel } from './export.model.js'; diff --git a/src/features/exports/index.js b/src/features/exports/index.js new file mode 100644 index 0000000..7bcdc1b --- /dev/null +++ b/src/features/exports/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import exportsRouter from './exports/export.routes.js'; + +const router = Router(); + +router.use('/exports', exportsRouter); + +export default router; diff --git a/src/features/imports/imports/import.controller.js b/src/features/imports/imports/import.controller.js new file mode 100644 index 0000000..ffe3f6f --- /dev/null +++ b/src/features/imports/imports/import.controller.js @@ -0,0 +1,50 @@ +import service from './import.service.js'; + +export class ImportController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Import deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ImportController(); diff --git a/src/features/imports/imports/import.model.js b/src/features/imports/imports/import.model.js new file mode 100644 index 0000000..65836a3 --- /dev/null +++ b/src/features/imports/imports/import.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Import extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Import.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Import', + tableName: 'imports', + timestamps: true, + underscored: true + }); + + return Import; +}; diff --git a/src/features/imports/imports/import.repository.js b/src/features/imports/imports/import.repository.js new file mode 100644 index 0000000..c611bae --- /dev/null +++ b/src/features/imports/imports/import.repository.js @@ -0,0 +1,30 @@ +import { Import } from './import.model.js'; + +export class ImportRepository { + async findAll(options = {}) { + return await Import.findAll(options); + } + + async findById(id, options = {}) { + return await Import.findByPk(id, options); + } + + async create(data, options = {}) { + return await Import.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ImportRepository(); diff --git a/src/features/imports/imports/import.routes.js b/src/features/imports/imports/import.routes.js new file mode 100644 index 0000000..7908423 --- /dev/null +++ b/src/features/imports/imports/import.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './import.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './import.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/imports: + * get: + * summary: Retrieve all imports + * tags: [Imports] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:imports']), + controller.getAll +); + +/** + * @swagger + * /api/v1/imports/{id}: + * get: + * summary: Retrieve a single import + * tags: [Imports] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:imports']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/imports: + * post: + * summary: Create a import + * tags: [Imports] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:imports']), + createValidation, + validate, + audit('CREATE_IMPORT'), + controller.create +); + +/** + * @swagger + * /api/v1/imports/{id}: + * put: + * summary: Update a import + * tags: [Imports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:imports']), + updateValidation, + validate, + audit('UPDATE_IMPORT'), + controller.update +); + +/** + * @swagger + * /api/v1/imports/{id}: + * delete: + * summary: Delete a import + * tags: [Imports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:imports']), + deleteValidation, + validate, + audit('DELETE_IMPORT'), + controller.delete +); + +export default router; diff --git a/src/features/imports/imports/import.service.js b/src/features/imports/imports/import.service.js new file mode 100644 index 0000000..7e331e0 --- /dev/null +++ b/src/features/imports/imports/import.service.js @@ -0,0 +1,75 @@ +import repository from './import.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ImportService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Import not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('import:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Import', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Import not found'); + } + + SocketService.broadcast('import:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Import', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Import not found'); + } + + SocketService.broadcast('import:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Import', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ImportService(); diff --git a/src/features/imports/imports/import.validation.js b/src/features/imports/imports/import.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/imports/imports/import.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/imports/imports/index.js b/src/features/imports/imports/index.js new file mode 100644 index 0000000..aebce52 --- /dev/null +++ b/src/features/imports/imports/index.js @@ -0,0 +1,5 @@ +export { default as importRoutes } from './import.routes.js'; +export { default as importController } from './import.controller.js'; +export { default as importService } from './import.service.js'; +export { default as importRepository } from './import.repository.js'; +export { default as importModel } from './import.model.js'; diff --git a/src/features/imports/index.js b/src/features/imports/index.js new file mode 100644 index 0000000..ad3b7ad --- /dev/null +++ b/src/features/imports/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import importsRouter from './imports/import.routes.js'; + +const router = Router(); + +router.use('/imports', importsRouter); + +export default router; diff --git a/src/features/index.js b/src/features/index.js new file mode 100644 index 0000000..c786481 --- /dev/null +++ b/src/features/index.js @@ -0,0 +1,47 @@ +import authenticationRouter from './authentication/index.js'; +import organizationRouter from './organization/index.js'; +import dashboardRouter from './dashboard/index.js'; +import productsRouter from './products/index.js'; +import catalogsRouter from './catalogs/index.js'; +import categoriesRouter from './categories/index.js'; +import brandsRouter from './brands/index.js'; +import attributesRouter from './attributes/index.js'; +import mediaRouter from './media/index.js'; +import pricingRouter from './pricing/index.js'; +import inventoryRouter from './inventory/index.js'; +import suppliersRouter from './suppliers/index.js'; +import localizationRouter from './localization/index.js'; +import workflowsRouter from './workflows/index.js'; +import integrationsRouter from './integrations/index.js'; +import channelsRouter from './channels/index.js'; +import importsRouter from './imports/index.js'; +import exportsRouter from './exports/index.js'; +import notificationsRouter from './notifications/index.js'; +import reportsRouter from './reports/index.js'; +import settingsRouter from './settings/index.js'; +import auditLogsRouter from './auditLogs/index.js'; + +export default function registerRoutes(app) { + app.use('/api/v1/auth', authenticationRouter); +app.use('/api/v1/organization', organizationRouter); +app.use('/api/v1/dashboard', dashboardRouter); +app.use('/api/v1/products', productsRouter); +app.use('/api/v1/catalogs', catalogsRouter); +app.use('/api/v1/categories', categoriesRouter); +app.use('/api/v1/brands', brandsRouter); +app.use('/api/v1/attributes', attributesRouter); +app.use('/api/v1/media', mediaRouter); +app.use('/api/v1/pricing', pricingRouter); +app.use('/api/v1/inventory', inventoryRouter); +app.use('/api/v1/suppliers', suppliersRouter); +app.use('/api/v1/localization', localizationRouter); +app.use('/api/v1/workflows', workflowsRouter); +app.use('/api/v1/integrations', integrationsRouter); +app.use('/api/v1/channels', channelsRouter); +app.use('/api/v1/imports', importsRouter); +app.use('/api/v1/exports', exportsRouter); +app.use('/api/v1/notifications', notificationsRouter); +app.use('/api/v1/reports', reportsRouter); +app.use('/api/v1/settings', settingsRouter); +app.use('/api/v1/auditLogs', auditLogsRouter); +} diff --git a/src/features/integrations/index.js b/src/features/integrations/index.js new file mode 100644 index 0000000..02ad787 --- /dev/null +++ b/src/features/integrations/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import integrationsRouter from './integrations/integration.routes.js'; + +const router = Router(); + +router.use('/integrations', integrationsRouter); + +export default router; diff --git a/src/features/integrations/integrations/index.js b/src/features/integrations/integrations/index.js new file mode 100644 index 0000000..ae38ae7 --- /dev/null +++ b/src/features/integrations/integrations/index.js @@ -0,0 +1,5 @@ +export { default as integrationRoutes } from './integration.routes.js'; +export { default as integrationController } from './integration.controller.js'; +export { default as integrationService } from './integration.service.js'; +export { default as integrationRepository } from './integration.repository.js'; +export { default as integrationModel } from './integration.model.js'; diff --git a/src/features/integrations/integrations/integration.controller.js b/src/features/integrations/integrations/integration.controller.js new file mode 100644 index 0000000..240af68 --- /dev/null +++ b/src/features/integrations/integrations/integration.controller.js @@ -0,0 +1,50 @@ +import service from './integration.service.js'; + +export class IntegrationController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Integration deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new IntegrationController(); diff --git a/src/features/integrations/integrations/integration.model.js b/src/features/integrations/integrations/integration.model.js new file mode 100644 index 0000000..4778600 --- /dev/null +++ b/src/features/integrations/integrations/integration.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Integration extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Integration.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Integration', + tableName: 'integrations', + timestamps: true, + underscored: true + }); + + return Integration; +}; diff --git a/src/features/integrations/integrations/integration.repository.js b/src/features/integrations/integrations/integration.repository.js new file mode 100644 index 0000000..0da3f98 --- /dev/null +++ b/src/features/integrations/integrations/integration.repository.js @@ -0,0 +1,30 @@ +import { Integration } from './integration.model.js'; + +export class IntegrationRepository { + async findAll(options = {}) { + return await Integration.findAll(options); + } + + async findById(id, options = {}) { + return await Integration.findByPk(id, options); + } + + async create(data, options = {}) { + return await Integration.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new IntegrationRepository(); diff --git a/src/features/integrations/integrations/integration.routes.js b/src/features/integrations/integrations/integration.routes.js new file mode 100644 index 0000000..8c8351f --- /dev/null +++ b/src/features/integrations/integrations/integration.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './integration.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './integration.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/integrations: + * get: + * summary: Retrieve all integrations + * tags: [Integrations] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:integrations']), + controller.getAll +); + +/** + * @swagger + * /api/v1/integrations/{id}: + * get: + * summary: Retrieve a single integration + * tags: [Integrations] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:integrations']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/integrations: + * post: + * summary: Create a integration + * tags: [Integrations] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:integrations']), + createValidation, + validate, + audit('CREATE_INTEGRATION'), + controller.create +); + +/** + * @swagger + * /api/v1/integrations/{id}: + * put: + * summary: Update a integration + * tags: [Integrations] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:integrations']), + updateValidation, + validate, + audit('UPDATE_INTEGRATION'), + controller.update +); + +/** + * @swagger + * /api/v1/integrations/{id}: + * delete: + * summary: Delete a integration + * tags: [Integrations] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:integrations']), + deleteValidation, + validate, + audit('DELETE_INTEGRATION'), + controller.delete +); + +export default router; diff --git a/src/features/integrations/integrations/integration.service.js b/src/features/integrations/integrations/integration.service.js new file mode 100644 index 0000000..5c61e20 --- /dev/null +++ b/src/features/integrations/integrations/integration.service.js @@ -0,0 +1,75 @@ +import repository from './integration.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class IntegrationService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Integration not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('integration:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Integration', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Integration not found'); + } + + SocketService.broadcast('integration:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Integration', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Integration not found'); + } + + SocketService.broadcast('integration:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Integration', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new IntegrationService(); diff --git a/src/features/integrations/integrations/integration.validation.js b/src/features/integrations/integrations/integration.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/integrations/integrations/integration.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/inventory/index.js b/src/features/inventory/index.js new file mode 100644 index 0000000..8718bc8 --- /dev/null +++ b/src/features/inventory/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import inventoryRouter from './inventory/inventory.routes.js'; + +const router = Router(); + +router.use('/inventory', inventoryRouter); + +export default router; diff --git a/src/features/inventory/inventory/index.js b/src/features/inventory/inventory/index.js new file mode 100644 index 0000000..e5549d8 --- /dev/null +++ b/src/features/inventory/inventory/index.js @@ -0,0 +1,5 @@ +export { default as inventoryRoutes } from './inventory.routes.js'; +export { default as inventoryController } from './inventory.controller.js'; +export { default as inventoryService } from './inventory.service.js'; +export { default as inventoryRepository } from './inventory.repository.js'; +export { default as inventoryModel } from './inventory.model.js'; diff --git a/src/features/inventory/inventory/inventory.controller.js b/src/features/inventory/inventory/inventory.controller.js new file mode 100644 index 0000000..4ab8909 --- /dev/null +++ b/src/features/inventory/inventory/inventory.controller.js @@ -0,0 +1,50 @@ +import service from './inventory.service.js'; + +export class InventoryController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Inventory deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new InventoryController(); diff --git a/src/features/inventory/inventory/inventory.model.js b/src/features/inventory/inventory/inventory.model.js new file mode 100644 index 0000000..b3314a8 --- /dev/null +++ b/src/features/inventory/inventory/inventory.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Inventory extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Inventory.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Inventory', + tableName: 'inventorys', + timestamps: true, + underscored: true + }); + + return Inventory; +}; diff --git a/src/features/inventory/inventory/inventory.repository.js b/src/features/inventory/inventory/inventory.repository.js new file mode 100644 index 0000000..4035b4f --- /dev/null +++ b/src/features/inventory/inventory/inventory.repository.js @@ -0,0 +1,30 @@ +import { Inventory } from './inventory.model.js'; + +export class InventoryRepository { + async findAll(options = {}) { + return await Inventory.findAll(options); + } + + async findById(id, options = {}) { + return await Inventory.findByPk(id, options); + } + + async create(data, options = {}) { + return await Inventory.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new InventoryRepository(); diff --git a/src/features/inventory/inventory/inventory.routes.js b/src/features/inventory/inventory/inventory.routes.js new file mode 100644 index 0000000..ee88c4c --- /dev/null +++ b/src/features/inventory/inventory/inventory.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './inventory.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './inventory.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/inventorys: + * get: + * summary: Retrieve all inventorys + * tags: [Inventorys] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:inventorys']), + controller.getAll +); + +/** + * @swagger + * /api/v1/inventorys/{id}: + * get: + * summary: Retrieve a single inventory + * tags: [Inventorys] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:inventorys']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/inventorys: + * post: + * summary: Create a inventory + * tags: [Inventorys] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:inventorys']), + createValidation, + validate, + audit('CREATE_INVENTORY'), + controller.create +); + +/** + * @swagger + * /api/v1/inventorys/{id}: + * put: + * summary: Update a inventory + * tags: [Inventorys] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:inventorys']), + updateValidation, + validate, + audit('UPDATE_INVENTORY'), + controller.update +); + +/** + * @swagger + * /api/v1/inventorys/{id}: + * delete: + * summary: Delete a inventory + * tags: [Inventorys] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:inventorys']), + deleteValidation, + validate, + audit('DELETE_INVENTORY'), + controller.delete +); + +export default router; diff --git a/src/features/inventory/inventory/inventory.service.js b/src/features/inventory/inventory/inventory.service.js new file mode 100644 index 0000000..ce1cd71 --- /dev/null +++ b/src/features/inventory/inventory/inventory.service.js @@ -0,0 +1,75 @@ +import repository from './inventory.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class InventoryService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Inventory not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('inventory:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Inventory', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Inventory not found'); + } + + SocketService.broadcast('inventory:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Inventory', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Inventory not found'); + } + + SocketService.broadcast('inventory:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Inventory', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new InventoryService(); diff --git a/src/features/inventory/inventory/inventory.validation.js b/src/features/inventory/inventory/inventory.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/inventory/inventory/inventory.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/localization/index.js b/src/features/localization/index.js new file mode 100644 index 0000000..d000cf0 --- /dev/null +++ b/src/features/localization/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import localizationRouter from './localization/localization.routes.js'; + +const router = Router(); + +router.use('/localization', localizationRouter); + +export default router; diff --git a/src/features/localization/localization/index.js b/src/features/localization/localization/index.js new file mode 100644 index 0000000..4d65153 --- /dev/null +++ b/src/features/localization/localization/index.js @@ -0,0 +1,5 @@ +export { default as localizationRoutes } from './localization.routes.js'; +export { default as localizationController } from './localization.controller.js'; +export { default as localizationService } from './localization.service.js'; +export { default as localizationRepository } from './localization.repository.js'; +export { default as localizationModel } from './localization.model.js'; diff --git a/src/features/localization/localization/localization.controller.js b/src/features/localization/localization/localization.controller.js new file mode 100644 index 0000000..6fedec5 --- /dev/null +++ b/src/features/localization/localization/localization.controller.js @@ -0,0 +1,50 @@ +import service from './localization.service.js'; + +export class LocalizationController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Localization deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new LocalizationController(); diff --git a/src/features/localization/localization/localization.model.js b/src/features/localization/localization/localization.model.js new file mode 100644 index 0000000..da3d3ce --- /dev/null +++ b/src/features/localization/localization/localization.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Localization extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Localization.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Localization', + tableName: 'localizations', + timestamps: true, + underscored: true + }); + + return Localization; +}; diff --git a/src/features/localization/localization/localization.repository.js b/src/features/localization/localization/localization.repository.js new file mode 100644 index 0000000..307ab0b --- /dev/null +++ b/src/features/localization/localization/localization.repository.js @@ -0,0 +1,30 @@ +import { Localization } from './localization.model.js'; + +export class LocalizationRepository { + async findAll(options = {}) { + return await Localization.findAll(options); + } + + async findById(id, options = {}) { + return await Localization.findByPk(id, options); + } + + async create(data, options = {}) { + return await Localization.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new LocalizationRepository(); diff --git a/src/features/localization/localization/localization.routes.js b/src/features/localization/localization/localization.routes.js new file mode 100644 index 0000000..13c520d --- /dev/null +++ b/src/features/localization/localization/localization.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './localization.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './localization.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/localizations: + * get: + * summary: Retrieve all localizations + * tags: [Localizations] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:localizations']), + controller.getAll +); + +/** + * @swagger + * /api/v1/localizations/{id}: + * get: + * summary: Retrieve a single localization + * tags: [Localizations] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:localizations']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/localizations: + * post: + * summary: Create a localization + * tags: [Localizations] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:localizations']), + createValidation, + validate, + audit('CREATE_LOCALIZATION'), + controller.create +); + +/** + * @swagger + * /api/v1/localizations/{id}: + * put: + * summary: Update a localization + * tags: [Localizations] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:localizations']), + updateValidation, + validate, + audit('UPDATE_LOCALIZATION'), + controller.update +); + +/** + * @swagger + * /api/v1/localizations/{id}: + * delete: + * summary: Delete a localization + * tags: [Localizations] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:localizations']), + deleteValidation, + validate, + audit('DELETE_LOCALIZATION'), + controller.delete +); + +export default router; diff --git a/src/features/localization/localization/localization.service.js b/src/features/localization/localization/localization.service.js new file mode 100644 index 0000000..75642ff --- /dev/null +++ b/src/features/localization/localization/localization.service.js @@ -0,0 +1,75 @@ +import repository from './localization.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class LocalizationService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Localization not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('localization:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Localization', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Localization not found'); + } + + SocketService.broadcast('localization:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Localization', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Localization not found'); + } + + SocketService.broadcast('localization:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Localization', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new LocalizationService(); diff --git a/src/features/localization/localization/localization.validation.js b/src/features/localization/localization/localization.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/localization/localization/localization.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/media/index.js b/src/features/media/index.js new file mode 100644 index 0000000..7b75092 --- /dev/null +++ b/src/features/media/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import mediaRouter from './media/media.routes.js'; + +const router = Router(); + +router.use('/media', mediaRouter); + +export default router; diff --git a/src/features/media/media/index.js b/src/features/media/media/index.js new file mode 100644 index 0000000..45031a7 --- /dev/null +++ b/src/features/media/media/index.js @@ -0,0 +1,5 @@ +export { default as mediaRoutes } from './media.routes.js'; +export { default as mediaController } from './media.controller.js'; +export { default as mediaService } from './media.service.js'; +export { default as mediaRepository } from './media.repository.js'; +export { default as mediaModel } from './media.model.js'; diff --git a/src/features/media/media/media.controller.js b/src/features/media/media/media.controller.js new file mode 100644 index 0000000..fc815df --- /dev/null +++ b/src/features/media/media/media.controller.js @@ -0,0 +1,50 @@ +import service from './media.service.js'; + +export class MediaController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Media deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new MediaController(); diff --git a/src/features/media/media/media.model.js b/src/features/media/media/media.model.js new file mode 100644 index 0000000..99e85c4 --- /dev/null +++ b/src/features/media/media/media.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Media extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Media.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Media', + tableName: 'medias', + timestamps: true, + underscored: true + }); + + return Media; +}; diff --git a/src/features/media/media/media.repository.js b/src/features/media/media/media.repository.js new file mode 100644 index 0000000..c5fd29b --- /dev/null +++ b/src/features/media/media/media.repository.js @@ -0,0 +1,30 @@ +import { Media } from './media.model.js'; + +export class MediaRepository { + async findAll(options = {}) { + return await Media.findAll(options); + } + + async findById(id, options = {}) { + return await Media.findByPk(id, options); + } + + async create(data, options = {}) { + return await Media.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new MediaRepository(); diff --git a/src/features/media/media/media.routes.js b/src/features/media/media/media.routes.js new file mode 100644 index 0000000..2c8d5ed --- /dev/null +++ b/src/features/media/media/media.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './media.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './media.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/medias: + * get: + * summary: Retrieve all medias + * tags: [Medias] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:medias']), + controller.getAll +); + +/** + * @swagger + * /api/v1/medias/{id}: + * get: + * summary: Retrieve a single media + * tags: [Medias] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:medias']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/medias: + * post: + * summary: Create a media + * tags: [Medias] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:medias']), + createValidation, + validate, + audit('CREATE_MEDIA'), + controller.create +); + +/** + * @swagger + * /api/v1/medias/{id}: + * put: + * summary: Update a media + * tags: [Medias] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:medias']), + updateValidation, + validate, + audit('UPDATE_MEDIA'), + controller.update +); + +/** + * @swagger + * /api/v1/medias/{id}: + * delete: + * summary: Delete a media + * tags: [Medias] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:medias']), + deleteValidation, + validate, + audit('DELETE_MEDIA'), + controller.delete +); + +export default router; diff --git a/src/features/media/media/media.service.js b/src/features/media/media/media.service.js new file mode 100644 index 0000000..f62115a --- /dev/null +++ b/src/features/media/media/media.service.js @@ -0,0 +1,75 @@ +import repository from './media.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class MediaService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Media not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('media:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Media', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Media not found'); + } + + SocketService.broadcast('media:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Media', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Media not found'); + } + + SocketService.broadcast('media:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Media', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new MediaService(); diff --git a/src/features/media/media/media.validation.js b/src/features/media/media/media.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/media/media/media.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/notifications/index.js b/src/features/notifications/index.js new file mode 100644 index 0000000..dbfc181 --- /dev/null +++ b/src/features/notifications/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import notificationsRouter from './notifications/notification.routes.js'; + +const router = Router(); + +router.use('/notifications', notificationsRouter); + +export default router; diff --git a/src/features/notifications/notifications/index.js b/src/features/notifications/notifications/index.js new file mode 100644 index 0000000..19ef3f2 --- /dev/null +++ b/src/features/notifications/notifications/index.js @@ -0,0 +1,5 @@ +export { default as notificationRoutes } from './notification.routes.js'; +export { default as notificationController } from './notification.controller.js'; +export { default as notificationService } from './notification.service.js'; +export { default as notificationRepository } from './notification.repository.js'; +export { default as notificationModel } from './notification.model.js'; diff --git a/src/features/notifications/notifications/notification.controller.js b/src/features/notifications/notifications/notification.controller.js new file mode 100644 index 0000000..898e458 --- /dev/null +++ b/src/features/notifications/notifications/notification.controller.js @@ -0,0 +1,50 @@ +import service from './notification.service.js'; + +export class NotificationController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Notification deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new NotificationController(); diff --git a/src/features/notifications/notifications/notification.model.js b/src/features/notifications/notifications/notification.model.js new file mode 100644 index 0000000..03779b0 --- /dev/null +++ b/src/features/notifications/notifications/notification.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Notification extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Notification.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Notification', + tableName: 'notifications', + timestamps: true, + underscored: true + }); + + return Notification; +}; diff --git a/src/features/notifications/notifications/notification.repository.js b/src/features/notifications/notifications/notification.repository.js new file mode 100644 index 0000000..e8fac90 --- /dev/null +++ b/src/features/notifications/notifications/notification.repository.js @@ -0,0 +1,30 @@ +import { Notification } from './notification.model.js'; + +export class NotificationRepository { + async findAll(options = {}) { + return await Notification.findAll(options); + } + + async findById(id, options = {}) { + return await Notification.findByPk(id, options); + } + + async create(data, options = {}) { + return await Notification.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new NotificationRepository(); diff --git a/src/features/notifications/notifications/notification.routes.js b/src/features/notifications/notifications/notification.routes.js new file mode 100644 index 0000000..669f189 --- /dev/null +++ b/src/features/notifications/notifications/notification.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './notification.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './notification.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/notifications: + * get: + * summary: Retrieve all notifications + * tags: [Notifications] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:notifications']), + controller.getAll +); + +/** + * @swagger + * /api/v1/notifications/{id}: + * get: + * summary: Retrieve a single notification + * tags: [Notifications] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:notifications']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/notifications: + * post: + * summary: Create a notification + * tags: [Notifications] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:notifications']), + createValidation, + validate, + audit('CREATE_NOTIFICATION'), + controller.create +); + +/** + * @swagger + * /api/v1/notifications/{id}: + * put: + * summary: Update a notification + * tags: [Notifications] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:notifications']), + updateValidation, + validate, + audit('UPDATE_NOTIFICATION'), + controller.update +); + +/** + * @swagger + * /api/v1/notifications/{id}: + * delete: + * summary: Delete a notification + * tags: [Notifications] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:notifications']), + deleteValidation, + validate, + audit('DELETE_NOTIFICATION'), + controller.delete +); + +export default router; diff --git a/src/features/notifications/notifications/notification.service.js b/src/features/notifications/notifications/notification.service.js new file mode 100644 index 0000000..b2e60d1 --- /dev/null +++ b/src/features/notifications/notifications/notification.service.js @@ -0,0 +1,75 @@ +import repository from './notification.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class NotificationService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Notification not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('notification:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Notification', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Notification not found'); + } + + SocketService.broadcast('notification:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Notification', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Notification not found'); + } + + SocketService.broadcast('notification:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Notification', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new NotificationService(); diff --git a/src/features/notifications/notifications/notification.validation.js b/src/features/notifications/notifications/notification.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/notifications/notifications/notification.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/organization/index.js b/src/features/organization/index.js new file mode 100644 index 0000000..ccef4c1 --- /dev/null +++ b/src/features/organization/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import orgRouter from './org/org.routes.js'; + +const router = Router(); + +router.use('/org', orgRouter); + +export default router; diff --git a/src/features/organization/org/index.js b/src/features/organization/org/index.js new file mode 100644 index 0000000..1a2c041 --- /dev/null +++ b/src/features/organization/org/index.js @@ -0,0 +1,5 @@ +export { default as orgRoutes } from './org.routes.js'; +export { default as orgController } from './org.controller.js'; +export { default as orgService } from './org.service.js'; +export { default as orgRepository } from './org.repository.js'; +export { default as orgModel } from './org.model.js'; diff --git a/src/features/organization/org/org.controller.js b/src/features/organization/org/org.controller.js new file mode 100644 index 0000000..4f175f8 --- /dev/null +++ b/src/features/organization/org/org.controller.js @@ -0,0 +1,50 @@ +import service from './org.service.js'; + +export class OrgController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Org deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new OrgController(); diff --git a/src/features/organization/org/org.model.js b/src/features/organization/org/org.model.js new file mode 100644 index 0000000..029b675 --- /dev/null +++ b/src/features/organization/org/org.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Org extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Org.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Org', + tableName: 'orgs', + timestamps: true, + underscored: true + }); + + return Org; +}; diff --git a/src/features/organization/org/org.repository.js b/src/features/organization/org/org.repository.js new file mode 100644 index 0000000..39b2d9b --- /dev/null +++ b/src/features/organization/org/org.repository.js @@ -0,0 +1,30 @@ +import { Org } from './org.model.js'; + +export class OrgRepository { + async findAll(options = {}) { + return await Org.findAll(options); + } + + async findById(id, options = {}) { + return await Org.findByPk(id, options); + } + + async create(data, options = {}) { + return await Org.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new OrgRepository(); diff --git a/src/features/organization/org/org.routes.js b/src/features/organization/org/org.routes.js new file mode 100644 index 0000000..27396ec --- /dev/null +++ b/src/features/organization/org/org.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './org.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './org.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/orgs: + * get: + * summary: Retrieve all orgs + * tags: [Orgs] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:orgs']), + controller.getAll +); + +/** + * @swagger + * /api/v1/orgs/{id}: + * get: + * summary: Retrieve a single org + * tags: [Orgs] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:orgs']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/orgs: + * post: + * summary: Create a org + * tags: [Orgs] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:orgs']), + createValidation, + validate, + audit('CREATE_ORG'), + controller.create +); + +/** + * @swagger + * /api/v1/orgs/{id}: + * put: + * summary: Update a org + * tags: [Orgs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:orgs']), + updateValidation, + validate, + audit('UPDATE_ORG'), + controller.update +); + +/** + * @swagger + * /api/v1/orgs/{id}: + * delete: + * summary: Delete a org + * tags: [Orgs] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:orgs']), + deleteValidation, + validate, + audit('DELETE_ORG'), + controller.delete +); + +export default router; diff --git a/src/features/organization/org/org.service.js b/src/features/organization/org/org.service.js new file mode 100644 index 0000000..c9dfd95 --- /dev/null +++ b/src/features/organization/org/org.service.js @@ -0,0 +1,75 @@ +import repository from './org.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class OrgService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Org not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('org:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Org', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Org not found'); + } + + SocketService.broadcast('org:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Org', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Org not found'); + } + + SocketService.broadcast('org:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Org', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new OrgService(); diff --git a/src/features/organization/org/org.validation.js b/src/features/organization/org/org.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/organization/org/org.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/organization/org/tenant.model.js b/src/features/organization/org/tenant.model.js new file mode 100644 index 0000000..f5d8d39 --- /dev/null +++ b/src/features/organization/org/tenant.model.js @@ -0,0 +1,51 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Tenant extends Model { + static associate(models) {} +} + +export default (sequelize) => { + Tenant.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + tenant_code: { + type: DataTypes.STRING(50), + unique: true + }, + tenant_name: { + type: DataTypes.STRING(150) + }, + domain: { + type: DataTypes.STRING(100), + unique: true + }, + contact_email: { + type: DataTypes.STRING(100) + }, + tenant_website: { + type: DataTypes.STRING(255) + }, + tenant_logo: { + type: DataTypes.STRING(255) + }, + mobile: { + type: DataTypes.STRING(20) + }, + status: { + type: DataTypes.BOOLEAN, + defaultValue: true + } + }, { + sequelize, + modelName: 'Tenant', + tableName: 'tenants', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' + }); + + return Tenant; +}; diff --git a/src/features/pricing/index.js b/src/features/pricing/index.js new file mode 100644 index 0000000..f4b6dff --- /dev/null +++ b/src/features/pricing/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import pricingRouter from './pricing/pricing.routes.js'; + +const router = Router(); + +router.use('/pricing', pricingRouter); + +export default router; diff --git a/src/features/pricing/pricing/index.js b/src/features/pricing/pricing/index.js new file mode 100644 index 0000000..73f07db --- /dev/null +++ b/src/features/pricing/pricing/index.js @@ -0,0 +1,5 @@ +export { default as pricingRoutes } from './pricing.routes.js'; +export { default as pricingController } from './pricing.controller.js'; +export { default as pricingService } from './pricing.service.js'; +export { default as pricingRepository } from './pricing.repository.js'; +export { default as pricingModel } from './pricing.model.js'; diff --git a/src/features/pricing/pricing/pricing.controller.js b/src/features/pricing/pricing/pricing.controller.js new file mode 100644 index 0000000..c2648ad --- /dev/null +++ b/src/features/pricing/pricing/pricing.controller.js @@ -0,0 +1,50 @@ +import service from './pricing.service.js'; + +export class PricingController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Pricing deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new PricingController(); diff --git a/src/features/pricing/pricing/pricing.model.js b/src/features/pricing/pricing/pricing.model.js new file mode 100644 index 0000000..614c93c --- /dev/null +++ b/src/features/pricing/pricing/pricing.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Pricing extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Pricing.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Pricing', + tableName: 'pricings', + timestamps: true, + underscored: true + }); + + return Pricing; +}; diff --git a/src/features/pricing/pricing/pricing.repository.js b/src/features/pricing/pricing/pricing.repository.js new file mode 100644 index 0000000..17065bd --- /dev/null +++ b/src/features/pricing/pricing/pricing.repository.js @@ -0,0 +1,30 @@ +import { Pricing } from './pricing.model.js'; + +export class PricingRepository { + async findAll(options = {}) { + return await Pricing.findAll(options); + } + + async findById(id, options = {}) { + return await Pricing.findByPk(id, options); + } + + async create(data, options = {}) { + return await Pricing.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new PricingRepository(); diff --git a/src/features/pricing/pricing/pricing.routes.js b/src/features/pricing/pricing/pricing.routes.js new file mode 100644 index 0000000..8951a3b --- /dev/null +++ b/src/features/pricing/pricing/pricing.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './pricing.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './pricing.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/pricings: + * get: + * summary: Retrieve all pricings + * tags: [Pricings] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:pricings']), + controller.getAll +); + +/** + * @swagger + * /api/v1/pricings/{id}: + * get: + * summary: Retrieve a single pricing + * tags: [Pricings] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:pricings']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/pricings: + * post: + * summary: Create a pricing + * tags: [Pricings] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:pricings']), + createValidation, + validate, + audit('CREATE_PRICING'), + controller.create +); + +/** + * @swagger + * /api/v1/pricings/{id}: + * put: + * summary: Update a pricing + * tags: [Pricings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:pricings']), + updateValidation, + validate, + audit('UPDATE_PRICING'), + controller.update +); + +/** + * @swagger + * /api/v1/pricings/{id}: + * delete: + * summary: Delete a pricing + * tags: [Pricings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:pricings']), + deleteValidation, + validate, + audit('DELETE_PRICING'), + controller.delete +); + +export default router; diff --git a/src/features/pricing/pricing/pricing.service.js b/src/features/pricing/pricing/pricing.service.js new file mode 100644 index 0000000..81034d2 --- /dev/null +++ b/src/features/pricing/pricing/pricing.service.js @@ -0,0 +1,75 @@ +import repository from './pricing.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class PricingService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Pricing not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('pricing:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Pricing', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Pricing not found'); + } + + SocketService.broadcast('pricing:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Pricing', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Pricing not found'); + } + + SocketService.broadcast('pricing:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Pricing', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new PricingService(); diff --git a/src/features/pricing/pricing/pricing.validation.js b/src/features/pricing/pricing/pricing.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/pricing/pricing/pricing.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/approvals/approval.controller.js b/src/features/products/approvals/approval.controller.js new file mode 100644 index 0000000..12b6d16 --- /dev/null +++ b/src/features/products/approvals/approval.controller.js @@ -0,0 +1,50 @@ +import service from './approval.service.js'; + +export class ApprovalController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Approval deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ApprovalController(); diff --git a/src/features/products/approvals/approval.model.js b/src/features/products/approvals/approval.model.js new file mode 100644 index 0000000..d165508 --- /dev/null +++ b/src/features/products/approvals/approval.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Approval extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Approval.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Approval', + tableName: 'approvals', + timestamps: true, + underscored: true + }); + + return Approval; +}; diff --git a/src/features/products/approvals/approval.repository.js b/src/features/products/approvals/approval.repository.js new file mode 100644 index 0000000..f0a3f01 --- /dev/null +++ b/src/features/products/approvals/approval.repository.js @@ -0,0 +1,30 @@ +import { Approval } from './approval.model.js'; + +export class ApprovalRepository { + async findAll(options = {}) { + return await Approval.findAll(options); + } + + async findById(id, options = {}) { + return await Approval.findByPk(id, options); + } + + async create(data, options = {}) { + return await Approval.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ApprovalRepository(); diff --git a/src/features/products/approvals/approval.routes.js b/src/features/products/approvals/approval.routes.js new file mode 100644 index 0000000..52ae856 --- /dev/null +++ b/src/features/products/approvals/approval.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './approval.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './approval.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/approvals: + * get: + * summary: Retrieve all approvals + * tags: [Approvals] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:approvals']), + controller.getAll +); + +/** + * @swagger + * /api/v1/approvals/{id}: + * get: + * summary: Retrieve a single approval + * tags: [Approvals] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:approvals']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/approvals: + * post: + * summary: Create a approval + * tags: [Approvals] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:approvals']), + createValidation, + validate, + audit('CREATE_APPROVAL'), + controller.create +); + +/** + * @swagger + * /api/v1/approvals/{id}: + * put: + * summary: Update a approval + * tags: [Approvals] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:approvals']), + updateValidation, + validate, + audit('UPDATE_APPROVAL'), + controller.update +); + +/** + * @swagger + * /api/v1/approvals/{id}: + * delete: + * summary: Delete a approval + * tags: [Approvals] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:approvals']), + deleteValidation, + validate, + audit('DELETE_APPROVAL'), + controller.delete +); + +export default router; diff --git a/src/features/products/approvals/approval.service.js b/src/features/products/approvals/approval.service.js new file mode 100644 index 0000000..96e7480 --- /dev/null +++ b/src/features/products/approvals/approval.service.js @@ -0,0 +1,75 @@ +import repository from './approval.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ApprovalService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Approval not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('approval:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Approval', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Approval not found'); + } + + SocketService.broadcast('approval:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Approval', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Approval not found'); + } + + SocketService.broadcast('approval:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Approval', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ApprovalService(); diff --git a/src/features/products/approvals/approval.validation.js b/src/features/products/approvals/approval.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/approvals/approval.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/approvals/index.js b/src/features/products/approvals/index.js new file mode 100644 index 0000000..39a81f0 --- /dev/null +++ b/src/features/products/approvals/index.js @@ -0,0 +1,5 @@ +export { default as approvalRoutes } from './approval.routes.js'; +export { default as approvalController } from './approval.controller.js'; +export { default as approvalService } from './approval.service.js'; +export { default as approvalRepository } from './approval.repository.js'; +export { default as approvalModel } from './approval.model.js'; diff --git a/src/features/products/bundles/bundle.controller.js b/src/features/products/bundles/bundle.controller.js new file mode 100644 index 0000000..ee286ca --- /dev/null +++ b/src/features/products/bundles/bundle.controller.js @@ -0,0 +1,50 @@ +import service from './bundle.service.js'; + +export class BundleController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Bundle deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new BundleController(); diff --git a/src/features/products/bundles/bundle.model.js b/src/features/products/bundles/bundle.model.js new file mode 100644 index 0000000..94e3d8c --- /dev/null +++ b/src/features/products/bundles/bundle.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Bundle extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Bundle.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Bundle', + tableName: 'bundles', + timestamps: true, + underscored: true + }); + + return Bundle; +}; diff --git a/src/features/products/bundles/bundle.repository.js b/src/features/products/bundles/bundle.repository.js new file mode 100644 index 0000000..95ae577 --- /dev/null +++ b/src/features/products/bundles/bundle.repository.js @@ -0,0 +1,30 @@ +import { Bundle } from './bundle.model.js'; + +export class BundleRepository { + async findAll(options = {}) { + return await Bundle.findAll(options); + } + + async findById(id, options = {}) { + return await Bundle.findByPk(id, options); + } + + async create(data, options = {}) { + return await Bundle.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new BundleRepository(); diff --git a/src/features/products/bundles/bundle.routes.js b/src/features/products/bundles/bundle.routes.js new file mode 100644 index 0000000..2a86698 --- /dev/null +++ b/src/features/products/bundles/bundle.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './bundle.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './bundle.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/bundles: + * get: + * summary: Retrieve all bundles + * tags: [Bundles] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:bundles']), + controller.getAll +); + +/** + * @swagger + * /api/v1/bundles/{id}: + * get: + * summary: Retrieve a single bundle + * tags: [Bundles] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:bundles']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/bundles: + * post: + * summary: Create a bundle + * tags: [Bundles] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:bundles']), + createValidation, + validate, + audit('CREATE_BUNDLE'), + controller.create +); + +/** + * @swagger + * /api/v1/bundles/{id}: + * put: + * summary: Update a bundle + * tags: [Bundles] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:bundles']), + updateValidation, + validate, + audit('UPDATE_BUNDLE'), + controller.update +); + +/** + * @swagger + * /api/v1/bundles/{id}: + * delete: + * summary: Delete a bundle + * tags: [Bundles] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:bundles']), + deleteValidation, + validate, + audit('DELETE_BUNDLE'), + controller.delete +); + +export default router; diff --git a/src/features/products/bundles/bundle.service.js b/src/features/products/bundles/bundle.service.js new file mode 100644 index 0000000..56e970a --- /dev/null +++ b/src/features/products/bundles/bundle.service.js @@ -0,0 +1,75 @@ +import repository from './bundle.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class BundleService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Bundle not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('bundle:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Bundle', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Bundle not found'); + } + + SocketService.broadcast('bundle:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Bundle', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Bundle not found'); + } + + SocketService.broadcast('bundle:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Bundle', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new BundleService(); diff --git a/src/features/products/bundles/bundle.validation.js b/src/features/products/bundles/bundle.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/bundles/bundle.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/bundles/index.js b/src/features/products/bundles/index.js new file mode 100644 index 0000000..5828a7f --- /dev/null +++ b/src/features/products/bundles/index.js @@ -0,0 +1,5 @@ +export { default as bundleRoutes } from './bundle.routes.js'; +export { default as bundleController } from './bundle.controller.js'; +export { default as bundleService } from './bundle.service.js'; +export { default as bundleRepository } from './bundle.repository.js'; +export { default as bundleModel } from './bundle.model.js'; diff --git a/src/features/products/configurableProducts/configurableProduct.controller.js b/src/features/products/configurableProducts/configurableProduct.controller.js new file mode 100644 index 0000000..1f5cd36 --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.controller.js @@ -0,0 +1,50 @@ +import service from './configurableProduct.service.js'; + +export class ConfigurableProductController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'ConfigurableProduct deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ConfigurableProductController(); diff --git a/src/features/products/configurableProducts/configurableProduct.model.js b/src/features/products/configurableProducts/configurableProduct.model.js new file mode 100644 index 0000000..b428756 --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class ConfigurableProduct extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + ConfigurableProduct.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'ConfigurableProduct', + tableName: 'configurableproducts', + timestamps: true, + underscored: true + }); + + return ConfigurableProduct; +}; diff --git a/src/features/products/configurableProducts/configurableProduct.repository.js b/src/features/products/configurableProducts/configurableProduct.repository.js new file mode 100644 index 0000000..73a28fa --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.repository.js @@ -0,0 +1,30 @@ +import { ConfigurableProduct } from './configurableProduct.model.js'; + +export class ConfigurableProductRepository { + async findAll(options = {}) { + return await ConfigurableProduct.findAll(options); + } + + async findById(id, options = {}) { + return await ConfigurableProduct.findByPk(id, options); + } + + async create(data, options = {}) { + return await ConfigurableProduct.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ConfigurableProductRepository(); diff --git a/src/features/products/configurableProducts/configurableProduct.routes.js b/src/features/products/configurableProducts/configurableProduct.routes.js new file mode 100644 index 0000000..f368d8f --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './configurableProduct.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './configurableProduct.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/configurableProducts: + * get: + * summary: Retrieve all configurableProducts + * tags: [ConfigurableProducts] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:configurableProducts']), + controller.getAll +); + +/** + * @swagger + * /api/v1/configurableProducts/{id}: + * get: + * summary: Retrieve a single configurableProduct + * tags: [ConfigurableProducts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:configurableProducts']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/configurableProducts: + * post: + * summary: Create a configurableProduct + * tags: [ConfigurableProducts] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:configurableProducts']), + createValidation, + validate, + audit('CREATE_CONFIGURABLEPRODUCT'), + controller.create +); + +/** + * @swagger + * /api/v1/configurableProducts/{id}: + * put: + * summary: Update a configurableProduct + * tags: [ConfigurableProducts] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:configurableProducts']), + updateValidation, + validate, + audit('UPDATE_CONFIGURABLEPRODUCT'), + controller.update +); + +/** + * @swagger + * /api/v1/configurableProducts/{id}: + * delete: + * summary: Delete a configurableProduct + * tags: [ConfigurableProducts] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:configurableProducts']), + deleteValidation, + validate, + audit('DELETE_CONFIGURABLEPRODUCT'), + controller.delete +); + +export default router; diff --git a/src/features/products/configurableProducts/configurableProduct.service.js b/src/features/products/configurableProducts/configurableProduct.service.js new file mode 100644 index 0000000..030e6c3 --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.service.js @@ -0,0 +1,75 @@ +import repository from './configurableProduct.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ConfigurableProductService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('ConfigurableProduct not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('configurableProduct:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'ConfigurableProduct', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('ConfigurableProduct not found'); + } + + SocketService.broadcast('configurableProduct:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'ConfigurableProduct', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('ConfigurableProduct not found'); + } + + SocketService.broadcast('configurableProduct:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'ConfigurableProduct', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ConfigurableProductService(); diff --git a/src/features/products/configurableProducts/configurableProduct.validation.js b/src/features/products/configurableProducts/configurableProduct.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/configurableProducts/configurableProduct.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/configurableProducts/index.js b/src/features/products/configurableProducts/index.js new file mode 100644 index 0000000..760f6d4 --- /dev/null +++ b/src/features/products/configurableProducts/index.js @@ -0,0 +1,5 @@ +export { default as configurableProductRoutes } from './configurableProduct.routes.js'; +export { default as configurableProductController } from './configurableProduct.controller.js'; +export { default as configurableProductService } from './configurableProduct.service.js'; +export { default as configurableProductRepository } from './configurableProduct.repository.js'; +export { default as configurableProductModel } from './configurableProduct.model.js'; diff --git a/src/features/products/digitalProducts/digitalProduct.controller.js b/src/features/products/digitalProducts/digitalProduct.controller.js new file mode 100644 index 0000000..7e3df14 --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.controller.js @@ -0,0 +1,50 @@ +import service from './digitalProduct.service.js'; + +export class DigitalProductController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'DigitalProduct deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new DigitalProductController(); diff --git a/src/features/products/digitalProducts/digitalProduct.model.js b/src/features/products/digitalProducts/digitalProduct.model.js new file mode 100644 index 0000000..15f736a --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class DigitalProduct extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + DigitalProduct.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'DigitalProduct', + tableName: 'digitalproducts', + timestamps: true, + underscored: true + }); + + return DigitalProduct; +}; diff --git a/src/features/products/digitalProducts/digitalProduct.repository.js b/src/features/products/digitalProducts/digitalProduct.repository.js new file mode 100644 index 0000000..7fee17c --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.repository.js @@ -0,0 +1,30 @@ +import { DigitalProduct } from './digitalProduct.model.js'; + +export class DigitalProductRepository { + async findAll(options = {}) { + return await DigitalProduct.findAll(options); + } + + async findById(id, options = {}) { + return await DigitalProduct.findByPk(id, options); + } + + async create(data, options = {}) { + return await DigitalProduct.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new DigitalProductRepository(); diff --git a/src/features/products/digitalProducts/digitalProduct.routes.js b/src/features/products/digitalProducts/digitalProduct.routes.js new file mode 100644 index 0000000..34c15bc --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './digitalProduct.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './digitalProduct.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/digitalProducts: + * get: + * summary: Retrieve all digitalProducts + * tags: [DigitalProducts] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:digitalProducts']), + controller.getAll +); + +/** + * @swagger + * /api/v1/digitalProducts/{id}: + * get: + * summary: Retrieve a single digitalProduct + * tags: [DigitalProducts] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:digitalProducts']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/digitalProducts: + * post: + * summary: Create a digitalProduct + * tags: [DigitalProducts] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:digitalProducts']), + createValidation, + validate, + audit('CREATE_DIGITALPRODUCT'), + controller.create +); + +/** + * @swagger + * /api/v1/digitalProducts/{id}: + * put: + * summary: Update a digitalProduct + * tags: [DigitalProducts] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:digitalProducts']), + updateValidation, + validate, + audit('UPDATE_DIGITALPRODUCT'), + controller.update +); + +/** + * @swagger + * /api/v1/digitalProducts/{id}: + * delete: + * summary: Delete a digitalProduct + * tags: [DigitalProducts] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:digitalProducts']), + deleteValidation, + validate, + audit('DELETE_DIGITALPRODUCT'), + controller.delete +); + +export default router; diff --git a/src/features/products/digitalProducts/digitalProduct.service.js b/src/features/products/digitalProducts/digitalProduct.service.js new file mode 100644 index 0000000..36d71a1 --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.service.js @@ -0,0 +1,75 @@ +import repository from './digitalProduct.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class DigitalProductService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('DigitalProduct not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('digitalProduct:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'DigitalProduct', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('DigitalProduct not found'); + } + + SocketService.broadcast('digitalProduct:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'DigitalProduct', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('DigitalProduct not found'); + } + + SocketService.broadcast('digitalProduct:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'DigitalProduct', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new DigitalProductService(); diff --git a/src/features/products/digitalProducts/digitalProduct.validation.js b/src/features/products/digitalProducts/digitalProduct.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/digitalProducts/digitalProduct.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/digitalProducts/index.js b/src/features/products/digitalProducts/index.js new file mode 100644 index 0000000..1727c9e --- /dev/null +++ b/src/features/products/digitalProducts/index.js @@ -0,0 +1,5 @@ +export { default as digitalProductRoutes } from './digitalProduct.routes.js'; +export { default as digitalProductController } from './digitalProduct.controller.js'; +export { default as digitalProductService } from './digitalProduct.service.js'; +export { default as digitalProductRepository } from './digitalProduct.repository.js'; +export { default as digitalProductModel } from './digitalProduct.model.js'; diff --git a/src/features/products/index.js b/src/features/products/index.js new file mode 100644 index 0000000..9a79972 --- /dev/null +++ b/src/features/products/index.js @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import productsRouter from './products/product.routes.js'; +import variantsRouter from './variants/variant.routes.js'; +import bundlesRouter from './bundles/bundle.routes.js'; +import configurableProductsRouter from './configurableProducts/configurableProduct.routes.js'; +import digitalProductsRouter from './digitalProducts/digitalProduct.routes.js'; +import lifecycleRouter from './lifecycle/lifecycle.routes.js'; +import approvalsRouter from './approvals/approval.routes.js'; +import publishingRouter from './publishing/publishing.routes.js'; + +const router = Router(); + +router.use('/products', productsRouter); +router.use('/variants', variantsRouter); +router.use('/bundles', bundlesRouter); +router.use('/configurableproducts', configurableProductsRouter); +router.use('/digitalproducts', digitalProductsRouter); +router.use('/lifecycle', lifecycleRouter); +router.use('/approvals', approvalsRouter); +router.use('/publishing', publishingRouter); + +export default router; diff --git a/src/features/products/lifecycle/index.js b/src/features/products/lifecycle/index.js new file mode 100644 index 0000000..870e762 --- /dev/null +++ b/src/features/products/lifecycle/index.js @@ -0,0 +1,5 @@ +export { default as lifecycleRoutes } from './lifecycle.routes.js'; +export { default as lifecycleController } from './lifecycle.controller.js'; +export { default as lifecycleService } from './lifecycle.service.js'; +export { default as lifecycleRepository } from './lifecycle.repository.js'; +export { default as lifecycleModel } from './lifecycle.model.js'; diff --git a/src/features/products/lifecycle/lifecycle.controller.js b/src/features/products/lifecycle/lifecycle.controller.js new file mode 100644 index 0000000..2ef4c07 --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.controller.js @@ -0,0 +1,50 @@ +import service from './lifecycle.service.js'; + +export class LifecycleController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Lifecycle deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new LifecycleController(); diff --git a/src/features/products/lifecycle/lifecycle.model.js b/src/features/products/lifecycle/lifecycle.model.js new file mode 100644 index 0000000..2ea1798 --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Lifecycle extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Lifecycle.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Lifecycle', + tableName: 'lifecycles', + timestamps: true, + underscored: true + }); + + return Lifecycle; +}; diff --git a/src/features/products/lifecycle/lifecycle.repository.js b/src/features/products/lifecycle/lifecycle.repository.js new file mode 100644 index 0000000..d049f6d --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.repository.js @@ -0,0 +1,30 @@ +import { Lifecycle } from './lifecycle.model.js'; + +export class LifecycleRepository { + async findAll(options = {}) { + return await Lifecycle.findAll(options); + } + + async findById(id, options = {}) { + return await Lifecycle.findByPk(id, options); + } + + async create(data, options = {}) { + return await Lifecycle.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new LifecycleRepository(); diff --git a/src/features/products/lifecycle/lifecycle.routes.js b/src/features/products/lifecycle/lifecycle.routes.js new file mode 100644 index 0000000..6d75987 --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './lifecycle.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './lifecycle.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/lifecycles: + * get: + * summary: Retrieve all lifecycles + * tags: [Lifecycles] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:lifecycles']), + controller.getAll +); + +/** + * @swagger + * /api/v1/lifecycles/{id}: + * get: + * summary: Retrieve a single lifecycle + * tags: [Lifecycles] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:lifecycles']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/lifecycles: + * post: + * summary: Create a lifecycle + * tags: [Lifecycles] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:lifecycles']), + createValidation, + validate, + audit('CREATE_LIFECYCLE'), + controller.create +); + +/** + * @swagger + * /api/v1/lifecycles/{id}: + * put: + * summary: Update a lifecycle + * tags: [Lifecycles] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:lifecycles']), + updateValidation, + validate, + audit('UPDATE_LIFECYCLE'), + controller.update +); + +/** + * @swagger + * /api/v1/lifecycles/{id}: + * delete: + * summary: Delete a lifecycle + * tags: [Lifecycles] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:lifecycles']), + deleteValidation, + validate, + audit('DELETE_LIFECYCLE'), + controller.delete +); + +export default router; diff --git a/src/features/products/lifecycle/lifecycle.service.js b/src/features/products/lifecycle/lifecycle.service.js new file mode 100644 index 0000000..65cf97b --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.service.js @@ -0,0 +1,75 @@ +import repository from './lifecycle.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class LifecycleService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Lifecycle not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('lifecycle:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Lifecycle', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Lifecycle not found'); + } + + SocketService.broadcast('lifecycle:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Lifecycle', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Lifecycle not found'); + } + + SocketService.broadcast('lifecycle:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Lifecycle', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new LifecycleService(); diff --git a/src/features/products/lifecycle/lifecycle.validation.js b/src/features/products/lifecycle/lifecycle.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/lifecycle/lifecycle.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/products/index.js b/src/features/products/products/index.js new file mode 100644 index 0000000..58d31af --- /dev/null +++ b/src/features/products/products/index.js @@ -0,0 +1,5 @@ +export { default as productRoutes } from './product.routes.js'; +export { default as productController } from './product.controller.js'; +export { default as productService } from './product.service.js'; +export { default as productRepository } from './product.repository.js'; +export { default as productModel } from './product.model.js'; diff --git a/src/features/products/products/product.controller.js b/src/features/products/products/product.controller.js new file mode 100644 index 0000000..7bb50b3 --- /dev/null +++ b/src/features/products/products/product.controller.js @@ -0,0 +1,50 @@ +import service from './product.service.js'; + +export class ProductController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Product deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ProductController(); diff --git a/src/features/products/products/product.model.js b/src/features/products/products/product.model.js new file mode 100644 index 0000000..88f3039 --- /dev/null +++ b/src/features/products/products/product.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Product extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Product.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Product', + tableName: 'products', + timestamps: true, + underscored: true + }); + + return Product; +}; diff --git a/src/features/products/products/product.repository.js b/src/features/products/products/product.repository.js new file mode 100644 index 0000000..80f42ae --- /dev/null +++ b/src/features/products/products/product.repository.js @@ -0,0 +1,30 @@ +import { Product } from './product.model.js'; + +export class ProductRepository { + async findAll(options = {}) { + return await Product.findAll(options); + } + + async findById(id, options = {}) { + return await Product.findByPk(id, options); + } + + async create(data, options = {}) { + return await Product.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ProductRepository(); diff --git a/src/features/products/products/product.routes.js b/src/features/products/products/product.routes.js new file mode 100644 index 0000000..ea10365 --- /dev/null +++ b/src/features/products/products/product.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './product.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './product.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/products: + * get: + * summary: Retrieve all products + * tags: [Products] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:products']), + controller.getAll +); + +/** + * @swagger + * /api/v1/products/{id}: + * get: + * summary: Retrieve a single product + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:products']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/products: + * post: + * summary: Create a product + * tags: [Products] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:products']), + createValidation, + validate, + audit('CREATE_PRODUCT'), + controller.create +); + +/** + * @swagger + * /api/v1/products/{id}: + * put: + * summary: Update a product + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:products']), + updateValidation, + validate, + audit('UPDATE_PRODUCT'), + controller.update +); + +/** + * @swagger + * /api/v1/products/{id}: + * delete: + * summary: Delete a product + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:products']), + deleteValidation, + validate, + audit('DELETE_PRODUCT'), + controller.delete +); + +export default router; diff --git a/src/features/products/products/product.service.js b/src/features/products/products/product.service.js new file mode 100644 index 0000000..bc3bc4f --- /dev/null +++ b/src/features/products/products/product.service.js @@ -0,0 +1,75 @@ +import repository from './product.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ProductService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Product not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('product:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Product', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Product not found'); + } + + SocketService.broadcast('product:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Product', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Product not found'); + } + + SocketService.broadcast('product:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Product', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ProductService(); diff --git a/src/features/products/products/product.validation.js b/src/features/products/products/product.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/products/product.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/publishing/index.js b/src/features/products/publishing/index.js new file mode 100644 index 0000000..82600c0 --- /dev/null +++ b/src/features/products/publishing/index.js @@ -0,0 +1,5 @@ +export { default as publishingRoutes } from './publishing.routes.js'; +export { default as publishingController } from './publishing.controller.js'; +export { default as publishingService } from './publishing.service.js'; +export { default as publishingRepository } from './publishing.repository.js'; +export { default as publishingModel } from './publishing.model.js'; diff --git a/src/features/products/publishing/publishing.controller.js b/src/features/products/publishing/publishing.controller.js new file mode 100644 index 0000000..23adde1 --- /dev/null +++ b/src/features/products/publishing/publishing.controller.js @@ -0,0 +1,50 @@ +import service from './publishing.service.js'; + +export class PublishingController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Publishing deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new PublishingController(); diff --git a/src/features/products/publishing/publishing.model.js b/src/features/products/publishing/publishing.model.js new file mode 100644 index 0000000..e46afc5 --- /dev/null +++ b/src/features/products/publishing/publishing.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Publishing extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Publishing.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Publishing', + tableName: 'publishings', + timestamps: true, + underscored: true + }); + + return Publishing; +}; diff --git a/src/features/products/publishing/publishing.repository.js b/src/features/products/publishing/publishing.repository.js new file mode 100644 index 0000000..9167add --- /dev/null +++ b/src/features/products/publishing/publishing.repository.js @@ -0,0 +1,30 @@ +import { Publishing } from './publishing.model.js'; + +export class PublishingRepository { + async findAll(options = {}) { + return await Publishing.findAll(options); + } + + async findById(id, options = {}) { + return await Publishing.findByPk(id, options); + } + + async create(data, options = {}) { + return await Publishing.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new PublishingRepository(); diff --git a/src/features/products/publishing/publishing.routes.js b/src/features/products/publishing/publishing.routes.js new file mode 100644 index 0000000..4bf2284 --- /dev/null +++ b/src/features/products/publishing/publishing.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './publishing.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './publishing.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/publishings: + * get: + * summary: Retrieve all publishings + * tags: [Publishings] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:publishings']), + controller.getAll +); + +/** + * @swagger + * /api/v1/publishings/{id}: + * get: + * summary: Retrieve a single publishing + * tags: [Publishings] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:publishings']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/publishings: + * post: + * summary: Create a publishing + * tags: [Publishings] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:publishings']), + createValidation, + validate, + audit('CREATE_PUBLISHING'), + controller.create +); + +/** + * @swagger + * /api/v1/publishings/{id}: + * put: + * summary: Update a publishing + * tags: [Publishings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:publishings']), + updateValidation, + validate, + audit('UPDATE_PUBLISHING'), + controller.update +); + +/** + * @swagger + * /api/v1/publishings/{id}: + * delete: + * summary: Delete a publishing + * tags: [Publishings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:publishings']), + deleteValidation, + validate, + audit('DELETE_PUBLISHING'), + controller.delete +); + +export default router; diff --git a/src/features/products/publishing/publishing.service.js b/src/features/products/publishing/publishing.service.js new file mode 100644 index 0000000..d84d0f6 --- /dev/null +++ b/src/features/products/publishing/publishing.service.js @@ -0,0 +1,75 @@ +import repository from './publishing.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class PublishingService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Publishing not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('publishing:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Publishing', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Publishing not found'); + } + + SocketService.broadcast('publishing:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Publishing', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Publishing not found'); + } + + SocketService.broadcast('publishing:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Publishing', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new PublishingService(); diff --git a/src/features/products/publishing/publishing.validation.js b/src/features/products/publishing/publishing.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/publishing/publishing.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/products/variants/index.js b/src/features/products/variants/index.js new file mode 100644 index 0000000..8094f32 --- /dev/null +++ b/src/features/products/variants/index.js @@ -0,0 +1,5 @@ +export { default as variantRoutes } from './variant.routes.js'; +export { default as variantController } from './variant.controller.js'; +export { default as variantService } from './variant.service.js'; +export { default as variantRepository } from './variant.repository.js'; +export { default as variantModel } from './variant.model.js'; diff --git a/src/features/products/variants/variant.controller.js b/src/features/products/variants/variant.controller.js new file mode 100644 index 0000000..0e4cb47 --- /dev/null +++ b/src/features/products/variants/variant.controller.js @@ -0,0 +1,50 @@ +import service from './variant.service.js'; + +export class VariantController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Variant deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new VariantController(); diff --git a/src/features/products/variants/variant.model.js b/src/features/products/variants/variant.model.js new file mode 100644 index 0000000..16cfa12 --- /dev/null +++ b/src/features/products/variants/variant.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Variant extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Variant.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Variant', + tableName: 'variants', + timestamps: true, + underscored: true + }); + + return Variant; +}; diff --git a/src/features/products/variants/variant.repository.js b/src/features/products/variants/variant.repository.js new file mode 100644 index 0000000..0c5693a --- /dev/null +++ b/src/features/products/variants/variant.repository.js @@ -0,0 +1,30 @@ +import { Variant } from './variant.model.js'; + +export class VariantRepository { + async findAll(options = {}) { + return await Variant.findAll(options); + } + + async findById(id, options = {}) { + return await Variant.findByPk(id, options); + } + + async create(data, options = {}) { + return await Variant.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new VariantRepository(); diff --git a/src/features/products/variants/variant.routes.js b/src/features/products/variants/variant.routes.js new file mode 100644 index 0000000..4e3b5af --- /dev/null +++ b/src/features/products/variants/variant.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './variant.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './variant.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/variants: + * get: + * summary: Retrieve all variants + * tags: [Variants] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:variants']), + controller.getAll +); + +/** + * @swagger + * /api/v1/variants/{id}: + * get: + * summary: Retrieve a single variant + * tags: [Variants] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:variants']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/variants: + * post: + * summary: Create a variant + * tags: [Variants] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:variants']), + createValidation, + validate, + audit('CREATE_VARIANT'), + controller.create +); + +/** + * @swagger + * /api/v1/variants/{id}: + * put: + * summary: Update a variant + * tags: [Variants] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:variants']), + updateValidation, + validate, + audit('UPDATE_VARIANT'), + controller.update +); + +/** + * @swagger + * /api/v1/variants/{id}: + * delete: + * summary: Delete a variant + * tags: [Variants] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:variants']), + deleteValidation, + validate, + audit('DELETE_VARIANT'), + controller.delete +); + +export default router; diff --git a/src/features/products/variants/variant.service.js b/src/features/products/variants/variant.service.js new file mode 100644 index 0000000..141a549 --- /dev/null +++ b/src/features/products/variants/variant.service.js @@ -0,0 +1,75 @@ +import repository from './variant.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class VariantService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Variant not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('variant:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Variant', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Variant not found'); + } + + SocketService.broadcast('variant:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Variant', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Variant not found'); + } + + SocketService.broadcast('variant:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Variant', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new VariantService(); diff --git a/src/features/products/variants/variant.validation.js b/src/features/products/variants/variant.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/products/variants/variant.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/reports/index.js b/src/features/reports/index.js new file mode 100644 index 0000000..f139bb0 --- /dev/null +++ b/src/features/reports/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import reportsRouter from './reports/report.routes.js'; + +const router = Router(); + +router.use('/reports', reportsRouter); + +export default router; diff --git a/src/features/reports/reports/index.js b/src/features/reports/reports/index.js new file mode 100644 index 0000000..e8b98fb --- /dev/null +++ b/src/features/reports/reports/index.js @@ -0,0 +1,5 @@ +export { default as reportRoutes } from './report.routes.js'; +export { default as reportController } from './report.controller.js'; +export { default as reportService } from './report.service.js'; +export { default as reportRepository } from './report.repository.js'; +export { default as reportModel } from './report.model.js'; diff --git a/src/features/reports/reports/report.controller.js b/src/features/reports/reports/report.controller.js new file mode 100644 index 0000000..61c05de --- /dev/null +++ b/src/features/reports/reports/report.controller.js @@ -0,0 +1,50 @@ +import service from './report.service.js'; + +export class ReportController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Report deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new ReportController(); diff --git a/src/features/reports/reports/report.model.js b/src/features/reports/reports/report.model.js new file mode 100644 index 0000000..b97a07b --- /dev/null +++ b/src/features/reports/reports/report.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Report extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Report.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Report', + tableName: 'reports', + timestamps: true, + underscored: true + }); + + return Report; +}; diff --git a/src/features/reports/reports/report.repository.js b/src/features/reports/reports/report.repository.js new file mode 100644 index 0000000..5bc9e0b --- /dev/null +++ b/src/features/reports/reports/report.repository.js @@ -0,0 +1,30 @@ +import { Report } from './report.model.js'; + +export class ReportRepository { + async findAll(options = {}) { + return await Report.findAll(options); + } + + async findById(id, options = {}) { + return await Report.findByPk(id, options); + } + + async create(data, options = {}) { + return await Report.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new ReportRepository(); diff --git a/src/features/reports/reports/report.routes.js b/src/features/reports/reports/report.routes.js new file mode 100644 index 0000000..1a07b42 --- /dev/null +++ b/src/features/reports/reports/report.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './report.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './report.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/reports: + * get: + * summary: Retrieve all reports + * tags: [Reports] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:reports']), + controller.getAll +); + +/** + * @swagger + * /api/v1/reports/{id}: + * get: + * summary: Retrieve a single report + * tags: [Reports] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:reports']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/reports: + * post: + * summary: Create a report + * tags: [Reports] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:reports']), + createValidation, + validate, + audit('CREATE_REPORT'), + controller.create +); + +/** + * @swagger + * /api/v1/reports/{id}: + * put: + * summary: Update a report + * tags: [Reports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:reports']), + updateValidation, + validate, + audit('UPDATE_REPORT'), + controller.update +); + +/** + * @swagger + * /api/v1/reports/{id}: + * delete: + * summary: Delete a report + * tags: [Reports] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:reports']), + deleteValidation, + validate, + audit('DELETE_REPORT'), + controller.delete +); + +export default router; diff --git a/src/features/reports/reports/report.service.js b/src/features/reports/reports/report.service.js new file mode 100644 index 0000000..a95ef8e --- /dev/null +++ b/src/features/reports/reports/report.service.js @@ -0,0 +1,75 @@ +import repository from './report.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class ReportService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Report not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('report:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Report', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Report not found'); + } + + SocketService.broadcast('report:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Report', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Report not found'); + } + + SocketService.broadcast('report:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Report', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new ReportService(); diff --git a/src/features/reports/reports/report.validation.js b/src/features/reports/reports/report.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/reports/reports/report.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/settings/index.js b/src/features/settings/index.js new file mode 100644 index 0000000..14f5403 --- /dev/null +++ b/src/features/settings/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import settingsRouter from './settings/setting.routes.js'; + +const router = Router(); + +router.use('/settings', settingsRouter); + +export default router; diff --git a/src/features/settings/settings/index.js b/src/features/settings/settings/index.js new file mode 100644 index 0000000..0a61e36 --- /dev/null +++ b/src/features/settings/settings/index.js @@ -0,0 +1,5 @@ +export { default as settingRoutes } from './setting.routes.js'; +export { default as settingController } from './setting.controller.js'; +export { default as settingService } from './setting.service.js'; +export { default as settingRepository } from './setting.repository.js'; +export { default as settingModel } from './setting.model.js'; diff --git a/src/features/settings/settings/setting.controller.js b/src/features/settings/settings/setting.controller.js new file mode 100644 index 0000000..8d4a0da --- /dev/null +++ b/src/features/settings/settings/setting.controller.js @@ -0,0 +1,50 @@ +import service from './setting.service.js'; + +export class SettingController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Setting deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new SettingController(); diff --git a/src/features/settings/settings/setting.model.js b/src/features/settings/settings/setting.model.js new file mode 100644 index 0000000..5ecae5b --- /dev/null +++ b/src/features/settings/settings/setting.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Setting extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Setting.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Setting', + tableName: 'settings', + timestamps: true, + underscored: true + }); + + return Setting; +}; diff --git a/src/features/settings/settings/setting.repository.js b/src/features/settings/settings/setting.repository.js new file mode 100644 index 0000000..9a5850d --- /dev/null +++ b/src/features/settings/settings/setting.repository.js @@ -0,0 +1,30 @@ +import { Setting } from './setting.model.js'; + +export class SettingRepository { + async findAll(options = {}) { + return await Setting.findAll(options); + } + + async findById(id, options = {}) { + return await Setting.findByPk(id, options); + } + + async create(data, options = {}) { + return await Setting.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new SettingRepository(); diff --git a/src/features/settings/settings/setting.routes.js b/src/features/settings/settings/setting.routes.js new file mode 100644 index 0000000..b869d73 --- /dev/null +++ b/src/features/settings/settings/setting.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './setting.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './setting.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/settings: + * get: + * summary: Retrieve all settings + * tags: [Settings] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:settings']), + controller.getAll +); + +/** + * @swagger + * /api/v1/settings/{id}: + * get: + * summary: Retrieve a single setting + * tags: [Settings] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:settings']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/settings: + * post: + * summary: Create a setting + * tags: [Settings] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:settings']), + createValidation, + validate, + audit('CREATE_SETTING'), + controller.create +); + +/** + * @swagger + * /api/v1/settings/{id}: + * put: + * summary: Update a setting + * tags: [Settings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:settings']), + updateValidation, + validate, + audit('UPDATE_SETTING'), + controller.update +); + +/** + * @swagger + * /api/v1/settings/{id}: + * delete: + * summary: Delete a setting + * tags: [Settings] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:settings']), + deleteValidation, + validate, + audit('DELETE_SETTING'), + controller.delete +); + +export default router; diff --git a/src/features/settings/settings/setting.service.js b/src/features/settings/settings/setting.service.js new file mode 100644 index 0000000..b27226a --- /dev/null +++ b/src/features/settings/settings/setting.service.js @@ -0,0 +1,75 @@ +import repository from './setting.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class SettingService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Setting not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('setting:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Setting', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Setting not found'); + } + + SocketService.broadcast('setting:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Setting', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Setting not found'); + } + + SocketService.broadcast('setting:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Setting', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new SettingService(); diff --git a/src/features/settings/settings/setting.validation.js b/src/features/settings/settings/setting.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/settings/settings/setting.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/suppliers/index.js b/src/features/suppliers/index.js new file mode 100644 index 0000000..3a0dc23 --- /dev/null +++ b/src/features/suppliers/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import suppliersRouter from './suppliers/supplier.routes.js'; + +const router = Router(); + +router.use('/suppliers', suppliersRouter); + +export default router; diff --git a/src/features/suppliers/suppliers/index.js b/src/features/suppliers/suppliers/index.js new file mode 100644 index 0000000..4548e50 --- /dev/null +++ b/src/features/suppliers/suppliers/index.js @@ -0,0 +1,5 @@ +export { default as supplierRoutes } from './supplier.routes.js'; +export { default as supplierController } from './supplier.controller.js'; +export { default as supplierService } from './supplier.service.js'; +export { default as supplierRepository } from './supplier.repository.js'; +export { default as supplierModel } from './supplier.model.js'; diff --git a/src/features/suppliers/suppliers/supplier.controller.js b/src/features/suppliers/suppliers/supplier.controller.js new file mode 100644 index 0000000..df5e7cd --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.controller.js @@ -0,0 +1,50 @@ +import service from './supplier.service.js'; + +export class SupplierController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Supplier deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new SupplierController(); diff --git a/src/features/suppliers/suppliers/supplier.model.js b/src/features/suppliers/suppliers/supplier.model.js new file mode 100644 index 0000000..cdeb3b4 --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Supplier extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Supplier.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Supplier', + tableName: 'suppliers', + timestamps: true, + underscored: true + }); + + return Supplier; +}; diff --git a/src/features/suppliers/suppliers/supplier.repository.js b/src/features/suppliers/suppliers/supplier.repository.js new file mode 100644 index 0000000..c20d1db --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.repository.js @@ -0,0 +1,30 @@ +import { Supplier } from './supplier.model.js'; + +export class SupplierRepository { + async findAll(options = {}) { + return await Supplier.findAll(options); + } + + async findById(id, options = {}) { + return await Supplier.findByPk(id, options); + } + + async create(data, options = {}) { + return await Supplier.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new SupplierRepository(); diff --git a/src/features/suppliers/suppliers/supplier.routes.js b/src/features/suppliers/suppliers/supplier.routes.js new file mode 100644 index 0000000..210f500 --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './supplier.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './supplier.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/suppliers: + * get: + * summary: Retrieve all suppliers + * tags: [Suppliers] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:suppliers']), + controller.getAll +); + +/** + * @swagger + * /api/v1/suppliers/{id}: + * get: + * summary: Retrieve a single supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:suppliers']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/suppliers: + * post: + * summary: Create a supplier + * tags: [Suppliers] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:suppliers']), + createValidation, + validate, + audit('CREATE_SUPPLIER'), + controller.create +); + +/** + * @swagger + * /api/v1/suppliers/{id}: + * put: + * summary: Update a supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:suppliers']), + updateValidation, + validate, + audit('UPDATE_SUPPLIER'), + controller.update +); + +/** + * @swagger + * /api/v1/suppliers/{id}: + * delete: + * summary: Delete a supplier + * tags: [Suppliers] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:suppliers']), + deleteValidation, + validate, + audit('DELETE_SUPPLIER'), + controller.delete +); + +export default router; diff --git a/src/features/suppliers/suppliers/supplier.service.js b/src/features/suppliers/suppliers/supplier.service.js new file mode 100644 index 0000000..3eacdd4 --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.service.js @@ -0,0 +1,75 @@ +import repository from './supplier.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class SupplierService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Supplier not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('supplier:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Supplier', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Supplier not found'); + } + + SocketService.broadcast('supplier:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Supplier', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Supplier not found'); + } + + SocketService.broadcast('supplier:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Supplier', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new SupplierService(); diff --git a/src/features/suppliers/suppliers/supplier.validation.js b/src/features/suppliers/suppliers/supplier.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/suppliers/suppliers/supplier.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/features/workflows/index.js b/src/features/workflows/index.js new file mode 100644 index 0000000..6d4bf96 --- /dev/null +++ b/src/features/workflows/index.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import workflowsRouter from './workflows/workflow.routes.js'; + +const router = Router(); + +router.use('/workflows', workflowsRouter); + +export default router; diff --git a/src/features/workflows/workflows/index.js b/src/features/workflows/workflows/index.js new file mode 100644 index 0000000..18c5320 --- /dev/null +++ b/src/features/workflows/workflows/index.js @@ -0,0 +1,5 @@ +export { default as workflowRoutes } from './workflow.routes.js'; +export { default as workflowController } from './workflow.controller.js'; +export { default as workflowService } from './workflow.service.js'; +export { default as workflowRepository } from './workflow.repository.js'; +export { default as workflowModel } from './workflow.model.js'; diff --git a/src/features/workflows/workflows/workflow.controller.js b/src/features/workflows/workflows/workflow.controller.js new file mode 100644 index 0000000..b63d3aa --- /dev/null +++ b/src/features/workflows/workflows/workflow.controller.js @@ -0,0 +1,50 @@ +import service from './workflow.service.js'; + +export class WorkflowController { + async getAll(req, res, next) { + try { + const records = await service.getAll(req.query); + return res.status(200).json({ success: true, data: records }); + } catch (error) { + next(error); + } + } + + async getById(req, res, next) { + try { + const record = await service.getById(req.params.id); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async create(req, res, next) { + try { + const record = await service.create(req.body, req.user); + return res.status(201).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async update(req, res, next) { + try { + const record = await service.update(req.params.id, req.body, req.user); + return res.status(200).json({ success: true, data: record }); + } catch (error) { + next(error); + } + } + + async delete(req, res, next) { + try { + await service.delete(req.params.id, req.user); + return res.status(200).json({ success: true, message: 'Workflow deleted successfully' }); + } catch (error) { + next(error); + } + } +} + +export default new WorkflowController(); diff --git a/src/features/workflows/workflows/workflow.model.js b/src/features/workflows/workflows/workflow.model.js new file mode 100644 index 0000000..14d8a61 --- /dev/null +++ b/src/features/workflows/workflows/workflow.model.js @@ -0,0 +1,38 @@ +import { Model, DataTypes } from 'sequelize'; + +export class Workflow extends Model { + static associate(models) { + // Define associations here + } +} + +export default (sequelize) => { + Workflow.init({ + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: true + }, + status: { + type: DataTypes.STRING, + defaultValue: 'active' + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true + } + }, { + sequelize, + modelName: 'Workflow', + tableName: 'workflows', + timestamps: true, + underscored: true + }); + + return Workflow; +}; diff --git a/src/features/workflows/workflows/workflow.repository.js b/src/features/workflows/workflows/workflow.repository.js new file mode 100644 index 0000000..989b95e --- /dev/null +++ b/src/features/workflows/workflows/workflow.repository.js @@ -0,0 +1,30 @@ +import { Workflow } from './workflow.model.js'; + +export class WorkflowRepository { + async findAll(options = {}) { + return await Workflow.findAll(options); + } + + async findById(id, options = {}) { + return await Workflow.findByPk(id, options); + } + + async create(data, options = {}) { + return await Workflow.create(data, options); + } + + async update(id, data, options = {}) { + const record = await this.findById(id, options); + if (!record) return null; + return await record.update(data, options); + } + + async delete(id, options = {}) { + const record = await this.findById(id, options); + if (!record) return false; + await record.destroy(options); + return true; + } +} + +export default new WorkflowRepository(); diff --git a/src/features/workflows/workflows/workflow.routes.js b/src/features/workflows/workflows/workflow.routes.js new file mode 100644 index 0000000..aa58355 --- /dev/null +++ b/src/features/workflows/workflows/workflow.routes.js @@ -0,0 +1,126 @@ +import { Router } from 'express'; +import controller from './workflow.controller.js'; +import { authenticate } from '../../../shared/middleware/auth.middleware.js'; +import { validate } from '../../../shared/middleware/validation.middleware.js'; +import { authorize } from '../../../shared/middleware/permission.middleware.js'; +import { audit } from '../../../shared/middleware/audit.middleware.js'; +import { + createValidation, + updateValidation, + deleteValidation, + getByIdValidation +} from './workflow.validation.js'; + +const router = Router(); + +/** + * @swagger + * /api/v1/workflows: + * get: + * summary: Retrieve all workflows + * tags: [Workflows] + * responses: + * 200: + * description: Success + */ +router.get( + '/', + authenticate, + authorize(['read:workflows']), + controller.getAll +); + +/** + * @swagger + * /api/v1/workflows/{id}: + * get: + * summary: Retrieve a single workflow + * tags: [Workflows] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ +router.get( + '/:id', + authenticate, + authorize(['read:workflows']), + getByIdValidation, + validate, + controller.getById +); + +/** + * @swagger + * /api/v1/workflows: + * post: + * summary: Create a workflow + * tags: [Workflows] + * responses: + * 201: + * description: Success + */ +router.post( + '/', + authenticate, + authorize(['write:workflows']), + createValidation, + validate, + audit('CREATE_WORKFLOW'), + controller.create +); + +/** + * @swagger + * /api/v1/workflows/{id}: + * put: + * summary: Update a workflow + * tags: [Workflows] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.put( + '/:id', + authenticate, + authorize(['write:workflows']), + updateValidation, + validate, + audit('UPDATE_WORKFLOW'), + controller.update +); + +/** + * @swagger + * /api/v1/workflows/{id}: + * delete: + * summary: Delete a workflow + * tags: [Workflows] + * parameters: + * - in: path + * name: id + * required: true + * responses: + * 200: + * description: Success + */ +router.delete( + '/:id', + authenticate, + authorize(['write:workflows']), + deleteValidation, + validate, + audit('DELETE_WORKFLOW'), + controller.delete +); + +export default router; diff --git a/src/features/workflows/workflows/workflow.service.js b/src/features/workflows/workflows/workflow.service.js new file mode 100644 index 0000000..d8da1a2 --- /dev/null +++ b/src/features/workflows/workflows/workflow.service.js @@ -0,0 +1,75 @@ +import repository from './workflow.repository.js'; +import { SocketService } from '../../../shared/services/socket.service.js'; +import { AuditService } from '../../../shared/services/audit.service.js'; + +export class WorkflowService { + async getAll(query = {}) { + // Add business logic filtering, pagination, etc. + return await repository.findAll(); + } + + async getById(id) { + const record = await repository.findById(id); + if (!record) { + throw new Error('Workflow not found'); + } + return record; + } + + async create(data, userContext = {}) { + const record = await repository.create(data); + + // Broadcast event + SocketService.broadcast('workflow:created', record); + + // Log audit + await AuditService.log({ + action: 'CREATE', + resource: 'Workflow', + resourceId: record.id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async update(id, data, userContext = {}) { + const record = await repository.update(id, data); + if (!record) { + throw new Error('Workflow not found'); + } + + SocketService.broadcast('workflow:updated', record); + + await AuditService.log({ + action: 'UPDATE', + resource: 'Workflow', + resourceId: id, + userId: userContext.id || 'system', + details: data + }); + + return record; + } + + async delete(id, userContext = {}) { + const deleted = await repository.delete(id); + if (!deleted) { + throw new Error('Workflow not found'); + } + + SocketService.broadcast('workflow:deleted', { id }); + + await AuditService.log({ + action: 'DELETE', + resource: 'Workflow', + resourceId: id, + userId: userContext.id || 'system' + }); + + return true; + } +} + +export default new WorkflowService(); diff --git a/src/features/workflows/workflows/workflow.validation.js b/src/features/workflows/workflows/workflow.validation.js new file mode 100644 index 0000000..8a1aaf4 --- /dev/null +++ b/src/features/workflows/workflows/workflow.validation.js @@ -0,0 +1,32 @@ +import { body, param } from 'express-validator'; + +export const createValidation = [ + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const updateValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required'), + body('name') + .optional() + .isString() + .trim() + .withMessage('Name must be a string') +]; + +export const deleteValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; + +export const getByIdValidation = [ + param('id') + .isUUID() + .withMessage('Valid UUID is required') +]; diff --git a/src/migrations/.gitkeep b/src/migrations/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/migrations/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/migrations/20260629131001-create-tenants.cjs b/src/migrations/20260629131001-create-tenants.cjs new file mode 100644 index 0000000..b8f8a09 --- /dev/null +++ b/src/migrations/20260629131001-create-tenants.cjs @@ -0,0 +1,58 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('tenants', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + tenant_code: { + type: Sequelize.STRING(50), + unique: true + }, + tenant_name: { + type: Sequelize.STRING(150) + }, + domain: { + type: Sequelize.STRING(100), + unique: true + }, + contact_email: { + type: Sequelize.STRING(100) + }, + tenant_website: { + type: Sequelize.STRING(255) + }, + tenant_logo: { + type: Sequelize.STRING(255) + }, + mobile: { + type: Sequelize.STRING(20) + }, + status: { + type: Sequelize.BOOLEAN, + defaultValue: true + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + } + }); + + await queryInterface.addIndex('tenants', ['tenant_code'], { unique: true }); + await queryInterface.addIndex('tenants', ['domain'], { unique: true }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('tenants'); + } +}; diff --git a/src/migrations/20260629131002-create-users.cjs b/src/migrations/20260629131002-create-users.cjs new file mode 100644 index 0000000..14e6849 --- /dev/null +++ b/src/migrations/20260629131002-create-users.cjs @@ -0,0 +1,78 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('users', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + tenant_id: { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: 'tenants', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + user_type: { + type: Sequelize.STRING(20) + }, + user_code: { + type: Sequelize.STRING(50) + }, + user_name: { + type: Sequelize.STRING(100) + }, + email: { + type: Sequelize.STRING(100), + allowNull: false, + unique: true + }, + phone: { + type: Sequelize.STRING(20) + }, + password_hash: { + type: Sequelize.STRING(255), + allowNull: false + }, + profile_picture: { + type: Sequelize.STRING(255) + }, + last_login_at: { + type: Sequelize.DATE + }, + status: { + type: Sequelize.BOOLEAN, + defaultValue: true + }, + created_by: { + type: Sequelize.INTEGER + }, + updated_by: { + type: Sequelize.INTEGER + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + } + }); + + await queryInterface.addIndex('users', ['email'], { unique: true }); + await queryInterface.addIndex('users', ['tenant_id']); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('users'); + } +}; diff --git a/src/migrations/20260629131003-create-permission-nodes.cjs b/src/migrations/20260629131003-create-permission-nodes.cjs new file mode 100644 index 0000000..cc88046 --- /dev/null +++ b/src/migrations/20260629131003-create-permission-nodes.cjs @@ -0,0 +1,89 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('permission_nodes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + node_code: { + type: Sequelize.STRING(50), + allowNull: false, + unique: true + }, + node_name: { + type: Sequelize.STRING(100), + allowNull: false + }, + node_type: { + type: Sequelize.STRING(20) + }, + parent_id: { + type: Sequelize.INTEGER, + references: { + model: 'permission_nodes', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + node_level: { + type: Sequelize.INTEGER, + defaultValue: 1 + }, + display_order: { + type: Sequelize.INTEGER, + defaultValue: 0 + }, + can_view: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_create: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_edit: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_delete: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_alter: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_export: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_import: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + description: { + type: Sequelize.TEXT + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + }, + updated_at: { + type: Sequelize.DATE + } + }); + + await queryInterface.addIndex('permission_nodes', ['node_code'], { unique: true }); + await queryInterface.addIndex('permission_nodes', ['parent_id']); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('permission_nodes'); + } +}; diff --git a/src/migrations/20260629131004-create-roles.cjs b/src/migrations/20260629131004-create-roles.cjs new file mode 100644 index 0000000..702f09b --- /dev/null +++ b/src/migrations/20260629131004-create-roles.cjs @@ -0,0 +1,70 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('roles', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + tenant_id: { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: 'tenants', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + role_code: { + type: Sequelize.STRING(50), + allowNull: false + }, + role_name: { + type: Sequelize.STRING(100), + allowNull: false + }, + description: { + type: Sequelize.TEXT + }, + role_type: { + type: Sequelize.STRING(20), + defaultValue: 'tenant', + allowNull: false + }, + is_system_role: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + status: { + type: Sequelize.BOOLEAN, + defaultValue: true + }, + created_by: { + type: Sequelize.INTEGER + }, + updated_by: { + type: Sequelize.INTEGER + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + } + }); + + await queryInterface.addIndex('roles', ['tenant_id']); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('roles'); + } +}; diff --git a/src/migrations/20260629131005-create-role-permissions.cjs b/src/migrations/20260629131005-create-role-permissions.cjs new file mode 100644 index 0000000..f0d01c3 --- /dev/null +++ b/src/migrations/20260629131005-create-role-permissions.cjs @@ -0,0 +1,79 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('role_permissions', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + role_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'roles', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + node_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'permission_nodes', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + can_view: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_create: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_edit: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_delete: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_alter: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_export: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + can_import: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.NOW + } + }); + + await queryInterface.addIndex('role_permissions', ['role_id']); + await queryInterface.addIndex('role_permissions', ['node_id']); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('role_permissions'); + } +}; diff --git a/src/migrations/20260629131006-create-user-roles.cjs b/src/migrations/20260629131006-create-user-roles.cjs new file mode 100644 index 0000000..77cf825 --- /dev/null +++ b/src/migrations/20260629131006-create-user-roles.cjs @@ -0,0 +1,62 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('user_roles', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + user_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + role_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'roles', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + status: { + type: Sequelize.BOOLEAN, + defaultValue: true + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + } + }); + + await queryInterface.addIndex('user_roles', ['user_id']); + await queryInterface.addIndex('user_roles', ['role_id']); + + // Recommended: prevent duplicate role assignments + await queryInterface.addIndex( + 'user_roles', + ['user_id', 'role_id'], + { unique: true } + ); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('user_roles'); + } +}; diff --git a/src/seeders/.gitkeep b/src/seeders/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/seeders/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/seeders/20260629131000-initial-superadmin.cjs b/src/seeders/20260629131000-initial-superadmin.cjs new file mode 100644 index 0000000..fbb2d45 --- /dev/null +++ b/src/seeders/20260629131000-initial-superadmin.cjs @@ -0,0 +1,147 @@ +'use strict'; +const bcrypt = require('bcrypt'); + +module.exports = { + async up(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + + try { + // 1. Create Root Permission Node (PIM System) + const [pimSystemNode] = await queryInterface.bulkInsert('permission_nodes', [{ + node_code: 'PIM_SYSTEM', + node_name: 'PIM System', + node_type: 'module', + parent_id: null, + node_level: 1, + display_order: 1, + can_view: true, + can_create: false, + can_edit: false, + can_delete: false, + created_at: new Date(), + updated_at: new Date() + }], { returning: true, transaction }); + + // 2. Create child nodes (Settings, Users, Roles) + const childNodes = await queryInterface.bulkInsert('permission_nodes', [ + { + node_code: 'USERS_MANAGEMENT', + node_name: 'Users Management', + node_type: 'feature', + parent_id: pimSystemNode.id, + node_level: 2, + display_order: 1, + can_view: true, + can_create: true, + can_edit: true, + can_delete: true, + created_at: new Date(), + updated_at: new Date() + }, + { + node_code: 'ROLES_MANAGEMENT', + node_name: 'Roles Management', + node_type: 'feature', + parent_id: pimSystemNode.id, + node_level: 2, + display_order: 2, + can_view: true, + can_create: true, + can_edit: true, + can_delete: true, + created_at: new Date(), + updated_at: new Date() + }, + { + node_code: 'TENANTS_MANAGEMENT', + node_name: 'Tenants Management', + node_type: 'feature', + parent_id: pimSystemNode.id, + node_level: 2, + display_order: 3, + can_view: true, + can_create: true, + can_edit: true, + can_delete: true, + created_at: new Date(), + updated_at: new Date() + } + ], { returning: true, transaction }); + + // 3. Create Super Admin Role + const [superAdminRole] = await queryInterface.bulkInsert('roles', [{ + tenant_id: null, + role_code: 'SUPER_ADMIN', + role_name: 'Super Admin', + description: 'System Administrator with full access to all tenant and platform features', + role_type: 'platform', + is_system_role: true, + status: true, + created_at: new Date(), + updated_at: new Date() + }], { returning: true, transaction }); + + // 4. Assign all permissions to Super Admin role + const allNodes = [pimSystemNode, ...childNodes]; + const rolePermissions = allNodes.map(node => ({ + role_id: superAdminRole.id, + node_id: node.id, + can_view: node.can_view, + can_create: node.can_create, + can_edit: node.can_edit, + can_delete: node.can_delete, + can_alter: false, + can_export: false, + can_import: false, + created_at: new Date(), + updated_at: new Date() + })); + + await queryInterface.bulkInsert('role_permissions', rolePermissions, { transaction }); + + // 5. Create Super Admin User + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash('Admin@123', salt); + + const [superAdminUser] = await queryInterface.bulkInsert('users', [{ + tenant_id: null, + email: 'superadmin@maskan.com', + user_name: 'Super Admin', + user_type: 'platform', + password_hash: hashedPassword, + status: true, + created_at: new Date(), + updated_at: new Date() + }], { returning: true, transaction }); + + // 6. Assign Super Admin Role to Super Admin User + await queryInterface.bulkInsert('user_roles', [{ + user_id: superAdminUser.id, + role_id: superAdminRole.id, + status: true, + created_at: new Date(), + updated_at: new Date() + }], { transaction }); + + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + + async down(queryInterface, Sequelize) { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.bulkDelete('user_roles', null, { transaction }); + await queryInterface.bulkDelete('users', { email: 'superadmin@maskan.com' }, { transaction }); + await queryInterface.bulkDelete('role_permissions', null, { transaction }); + await queryInterface.bulkDelete('roles', { role_code: 'SUPER_ADMIN' }, { transaction }); + await queryInterface.bulkDelete('permission_nodes', null, { transaction }); + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/src/shared/config/database.config.cjs b/src/shared/config/database.config.cjs new file mode 100644 index 0000000..221c0f5 --- /dev/null +++ b/src/shared/config/database.config.cjs @@ -0,0 +1,38 @@ +const dotenv = require('dotenv'); +dotenv.config(); + +module.exports = { + development: { + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME || 'maskan_pim', + host: process.env.DB_HOST || '127.0.0.1', + port: process.env.DB_PORT || 5432, + dialect: process.env.DB_DIALECT || 'postgres', + logging: console.log + }, + test: { + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME_TEST || 'maskan_pim_test', + host: process.env.DB_HOST || '127.0.0.1', + port: process.env.DB_PORT || 5432, + dialect: process.env.DB_DIALECT || 'postgres', + logging: false + }, + production: { + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + dialect: process.env.DB_DIALECT || 'postgres', + logging: false, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } + } +}; diff --git a/src/shared/config/database.config.js b/src/shared/config/database.config.js new file mode 100644 index 0000000..a3efe65 --- /dev/null +++ b/src/shared/config/database.config.js @@ -0,0 +1,40 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +const config = { + development: { + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME || 'maskan_pim', + host: process.env.DB_HOST || '127.0.0.1', + port: process.env.DB_PORT || 5432, + dialect: process.env.DB_DIALECT || 'postgres', + logging: console.log + }, + test: { + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || 'postgres', + database: process.env.DB_NAME_TEST || 'maskan_pim_test', + host: process.env.DB_HOST || '127.0.0.1', + port: process.env.DB_PORT || 5432, + dialect: process.env.DB_DIALECT || 'postgres', + logging: false + }, + production: { + username: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DB_NAME, + host: process.env.DB_HOST, + port: process.env.DB_PORT, + dialect: process.env.DB_DIALECT || 'postgres', + logging: false, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } + } +}; + +export default config; diff --git a/src/shared/config/multer.config.js b/src/shared/config/multer.config.js new file mode 100644 index 0000000..476ce15 --- /dev/null +++ b/src/shared/config/multer.config.js @@ -0,0 +1,29 @@ +import multer from 'multer'; +import path from 'path'; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, 'uploads/'); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +export const fileFilter = (req, file, cb) => { + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf', 'text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']; + if (allowedTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error('Invalid file type'), false); + } +}; + +export const uploadConfig = multer({ + storage, + fileFilter, + limits: { + fileSize: 10 * 1024 * 1024 // 10MB + } +}); diff --git a/src/shared/config/storage.config.js b/src/shared/config/storage.config.js new file mode 100644 index 0000000..5c9f21d --- /dev/null +++ b/src/shared/config/storage.config.js @@ -0,0 +1,15 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +export const storageConfig = { + provider: process.env.STORAGE_PROVIDER || 'local', // local, s3, azure, gcs + local: { + uploadDir: 'uploads/' + }, + s3: { + bucket: process.env.AWS_S3_BUCKET, + region: process.env.AWS_REGION, + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY + } +}; diff --git a/src/shared/config/swagger.config.js b/src/shared/config/swagger.config.js new file mode 100644 index 0000000..5791a22 --- /dev/null +++ b/src/shared/config/swagger.config.js @@ -0,0 +1,35 @@ +import swaggerJSDoc from 'swagger-jsdoc'; + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Enterprise PIM API', + version: '1.0.0', + description: 'API Documentation for Maskan Enterprise Product Information Management (PIM)' + }, + servers: [ + { + url: 'http://localhost:5000', + description: 'Development Server' + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT' + } + } + }, + security: [ + { + bearerAuth: [] + } + ] + }, + apis: ['./src/features/**/*.routes.js'] +}; + +export default swaggerJSDoc(options); diff --git a/src/shared/database/connection.js b/src/shared/database/connection.js new file mode 100644 index 0000000..7721536 --- /dev/null +++ b/src/shared/database/connection.js @@ -0,0 +1,36 @@ +import { Sequelize } from 'sequelize'; +import dbConfig from '../config/database.config.cjs'; +import { initializeDatabaseModels } from './models.js'; + +const env = process.env.NODE_ENV || 'development'; +const config = dbConfig[env]; + +let sequelize; + +if (process.env.DATABASE_URL) { + sequelize = new Sequelize(process.env.DATABASE_URL, config); +} else { + sequelize = new Sequelize( + config.database, + config.username, + config.password, + config + ); +} + +export const connectDatabase = async () => { + try { + // Initialize models + initializeDatabaseModels(); + + await sequelize.authenticate(); + console.log('Database connection has been established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + if (env === 'production') { + throw error; + } + } +}; + +export default sequelize; diff --git a/src/shared/database/models.js b/src/shared/database/models.js new file mode 100644 index 0000000..9517a80 --- /dev/null +++ b/src/shared/database/models.js @@ -0,0 +1,132 @@ +import sequelize from './connection.js'; + +const models = {}; + +export const registerModel = (name, modelInit) => { + models[name] = modelInit(sequelize); + return models[name]; +}; + +export const associateModels = () => { + Object.keys(models).forEach((modelName) => { + if (models[modelName].associate) { + models[modelName].associate(models); + } + }); + + // Explicit RBAC Associations + if (models.Tenant && models.User) { + models.User.belongsTo(models.Tenant, { foreignKey: 'tenant_id', as: 'tenant' }); + models.Tenant.hasMany(models.User, { foreignKey: 'tenant_id', as: 'users' }); + } + + if (models.Tenant && models.Role) { + models.Role.belongsTo(models.Tenant, { foreignKey: 'tenant_id', as: 'tenant' }); + models.Tenant.hasMany(models.Role, { foreignKey: 'tenant_id', as: 'roles' }); + } + + if (models.Role && models.PermissionNode && models.RolePermission) { + models.Role.belongsToMany(models.PermissionNode, { + through: models.RolePermission, + foreignKey: 'role_id', + otherKey: 'node_id', + as: 'permissions' + }); + models.PermissionNode.belongsToMany(models.Role, { + through: models.RolePermission, + foreignKey: 'node_id', + otherKey: 'role_id', + as: 'roles' + }); + } + + if (models.User && models.Role && models.UserRole) { + models.User.belongsToMany(models.Role, { + through: models.UserRole, + foreignKey: 'user_id', + otherKey: 'role_id', + as: 'roles' + }); + models.Role.belongsToMany(models.User, { + through: models.UserRole, + foreignKey: 'role_id', + otherKey: 'user_id', + as: 'users' + }); + } +}; + +import tenantModelInit from '../../features/organization/org/tenant.model.js'; +import userModelInit from '../../features/authentication/users/user.model.js'; +import roleModelInit from '../../features/authentication/access/role.model.js'; +import permissionNodeModelInit from '../../features/authentication/access/permissionNode.model.js'; +import rolePermissionModelInit from '../../features/authentication/access/rolePermission.model.js'; +import userRoleModelInit from '../../features/authentication/access/userRole.model.js'; +import orgModelInit from '../../features/organization/org/org.model.js'; +import metricModelInit from '../../features/dashboard/metrics/metric.model.js'; +import productModelInit from '../../features/products/products/product.model.js'; +import variantModelInit from '../../features/products/variants/variant.model.js'; +import bundleModelInit from '../../features/products/bundles/bundle.model.js'; +import configurableProductModelInit from '../../features/products/configurableProducts/configurableProduct.model.js'; +import digitalProductModelInit from '../../features/products/digitalProducts/digitalProduct.model.js'; +import lifecycleModelInit from '../../features/products/lifecycle/lifecycle.model.js'; +import approvalModelInit from '../../features/products/approvals/approval.model.js'; +import publishingModelInit from '../../features/products/publishing/publishing.model.js'; +import catalogModelInit from '../../features/catalogs/catalogs/catalog.model.js'; +import categorieModelInit from '../../features/categories/categories/categorie.model.js'; +import brandModelInit from '../../features/brands/brands/brand.model.js'; +import attributeModelInit from '../../features/attributes/attributes/attribute.model.js'; +import mediaModelInit from '../../features/media/media/media.model.js'; +import pricingModelInit from '../../features/pricing/pricing/pricing.model.js'; +import inventoryModelInit from '../../features/inventory/inventory/inventory.model.js'; +import supplierModelInit from '../../features/suppliers/suppliers/supplier.model.js'; +import localizationModelInit from '../../features/localization/localization/localization.model.js'; +import workflowModelInit from '../../features/workflows/workflows/workflow.model.js'; +import integrationModelInit from '../../features/integrations/integrations/integration.model.js'; +import channelModelInit from '../../features/channels/channels/channel.model.js'; +import importModelInit from '../../features/imports/imports/import.model.js'; +import exportModelInit from '../../features/exports/exports/export.model.js'; +import notificationModelInit from '../../features/notifications/notifications/notification.model.js'; +import reportModelInit from '../../features/reports/reports/report.model.js'; +import settingModelInit from '../../features/settings/settings/setting.model.js'; +import auditLogModelInit from '../../features/auditLogs/auditLogs/auditLog.model.js'; + +export const initializeDatabaseModels = () => { + registerModel('Tenant', tenantModelInit); + registerModel('User', userModelInit); + registerModel('Role', roleModelInit); + registerModel('PermissionNode', permissionNodeModelInit); + registerModel('RolePermission', rolePermissionModelInit); + registerModel('UserRole', userRoleModelInit); + registerModel('Org', orgModelInit); + registerModel('Metric', metricModelInit); + registerModel('Product', productModelInit); + registerModel('Variant', variantModelInit); + registerModel('Bundle', bundleModelInit); + registerModel('ConfigurableProduct', configurableProductModelInit); + registerModel('DigitalProduct', digitalProductModelInit); + registerModel('Lifecycle', lifecycleModelInit); + registerModel('Approval', approvalModelInit); + registerModel('Publishing', publishingModelInit); + registerModel('Catalog', catalogModelInit); + registerModel('Categorie', categorieModelInit); + registerModel('Brand', brandModelInit); + registerModel('Attribute', attributeModelInit); + registerModel('Media', mediaModelInit); + registerModel('Pricing', pricingModelInit); + registerModel('Inventory', inventoryModelInit); + registerModel('Supplier', supplierModelInit); + registerModel('Localization', localizationModelInit); + registerModel('Workflow', workflowModelInit); + registerModel('Integration', integrationModelInit); + registerModel('Channel', channelModelInit); + registerModel('Import', importModelInit); + registerModel('Export', exportModelInit); + registerModel('Notification', notificationModelInit); + registerModel('Report', reportModelInit); + registerModel('Setting', settingModelInit); + registerModel('AuditLog', auditLogModelInit); + associateModels(); +}; + +export { models, sequelize }; diff --git a/src/shared/middleware/audit.middleware.js b/src/shared/middleware/audit.middleware.js new file mode 100644 index 0000000..a56e74e --- /dev/null +++ b/src/shared/middleware/audit.middleware.js @@ -0,0 +1,35 @@ +import { AuditService } from '../services/audit.service.js'; + +export const audit = (action) => { + return async (req, res, next) => { + // Capture details, request info + const originalSend = res.send; + + res.send = function (body) { + res.send = originalSend; + originalSend.call(this, body); + + // Perform non-blocking background logging + try { + const parsedBody = JSON.parse(body); + if (res.statusCode >= 200 && res.statusCode < 300) { + AuditService.log({ + action, + resource: req.baseUrl.split('/').pop(), + resourceId: req.params.id || (parsedBody.data ? parsedBody.data.id : null), + userId: req.user ? req.user.id : 'anonymous', + details: { + method: req.method, + query: req.query, + ip: req.ip + } + }); + } + } catch (e) { + // Suppress audit failure in middleware to not crash app + } + }; + + next(); + }; +}; diff --git a/src/shared/middleware/auth.middleware.js b/src/shared/middleware/auth.middleware.js new file mode 100644 index 0000000..067289e --- /dev/null +++ b/src/shared/middleware/auth.middleware.js @@ -0,0 +1,17 @@ +import jwt from 'jsonwebtoken'; + +export const authenticate = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ success: false, message: 'Authentication token missing or invalid format' }); + } + + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'supersecret'); + req.user = decoded; + next(); + } catch (error) { + return res.status(401).json({ success: false, message: 'Unauthorized or token expired' }); + } +}; diff --git a/src/shared/middleware/error.middleware.js b/src/shared/middleware/error.middleware.js new file mode 100644 index 0000000..e5aeaf1 --- /dev/null +++ b/src/shared/middleware/error.middleware.js @@ -0,0 +1,12 @@ +export const errorMiddleware = (err, req, res, next) => { + console.error(err); + const status = err.status || 500; + const message = err.message || 'Internal Server Error'; + + res.status(status).json({ + success: false, + status, + message, + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined + }); +}; diff --git a/src/shared/middleware/logger.middleware.js b/src/shared/middleware/logger.middleware.js new file mode 100644 index 0000000..a601b64 --- /dev/null +++ b/src/shared/middleware/logger.middleware.js @@ -0,0 +1,3 @@ +import morgan from 'morgan'; + +export const loggerMiddleware = morgan('combined'); diff --git a/src/shared/middleware/permission.middleware.js b/src/shared/middleware/permission.middleware.js new file mode 100644 index 0000000..12e1741 --- /dev/null +++ b/src/shared/middleware/permission.middleware.js @@ -0,0 +1,24 @@ +export const authorize = (requiredPermissions = []) => { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ success: false, message: 'Unauthorized' }); + } + + const userPermissions = req.user.permissions || []; + + // Check if user is super admin with wildcard permission + if (userPermissions.includes('*')) { + return next(); + } + + const hasPermission = requiredPermissions.every((perm) => + userPermissions.includes(perm) + ); + + if (!hasPermission) { + return res.status(403).json({ success: false, message: 'Forbidden' }); + } + + next(); + }; +}; diff --git a/src/shared/middleware/rateLimiter.js b/src/shared/middleware/rateLimiter.js new file mode 100644 index 0000000..243c83c --- /dev/null +++ b/src/shared/middleware/rateLimiter.js @@ -0,0 +1,34 @@ +// Simple memory-based rate limiting placeholder since express-rate-limit is optional +const cache = new Map(); + +export const rateLimiter = (options = {}) => { + const windowMs = options.windowMs || 15 * 60 * 1000; // 15 mins + const max = options.max || 100; // limits to 100 requests per window + + return (req, res, next) => { + const ip = req.ip; + const now = Date.now(); + + if (!cache.has(ip)) { + cache.set(ip, { count: 1, resetTime: now + windowMs }); + return next(); + } + + const data = cache.get(ip); + if (now > data.resetTime) { + data.count = 1; + data.resetTime = now + windowMs; + return next(); + } + + data.count++; + if (data.count > max) { + return res.status(429).json({ + success: false, + message: 'Too many requests, please try again later.' + }); + } + + next(); + }; +}; diff --git a/src/shared/middleware/upload.middleware.js b/src/shared/middleware/upload.middleware.js new file mode 100644 index 0000000..107881d --- /dev/null +++ b/src/shared/middleware/upload.middleware.js @@ -0,0 +1,5 @@ +import { uploadConfig } from '../config/multer.config.js'; + +export const uploadSingle = (fieldName) => uploadConfig.single(fieldName); +export const uploadArray = (fieldName, maxCount) => uploadConfig.array(fieldName, maxCount); +export const uploadFields = (fields) => uploadConfig.fields(fields); diff --git a/src/shared/middleware/validation.middleware.js b/src/shared/middleware/validation.middleware.js new file mode 100644 index 0000000..6f039b9 --- /dev/null +++ b/src/shared/middleware/validation.middleware.js @@ -0,0 +1,12 @@ +import { validationResult } from 'express-validator'; + +export const validate = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ + success: false, + errors: errors.array().map(err => ({ field: err.path, message: err.msg })) + }); + } + next(); +}; diff --git a/src/shared/services/audit.service.js b/src/shared/services/audit.service.js new file mode 100644 index 0000000..675284e --- /dev/null +++ b/src/shared/services/audit.service.js @@ -0,0 +1,10 @@ +export class AuditService { + static async log(auditData) { + // In absolute production code, this inserts a record into AuditLog table + console.log('[AUDIT LOG]:', JSON.stringify({ + timestamp: new Date(), + ...auditData + }, null, 2)); + return true; + } +} diff --git a/src/shared/services/cronJob.service.js b/src/shared/services/cronJob.service.js new file mode 100644 index 0000000..fa4d7ab --- /dev/null +++ b/src/shared/services/cronJob.service.js @@ -0,0 +1,6 @@ +export class CronJobService { + static init() { + console.log('Cron Job Manager Initialized.'); + // Set up placeholders for scheduled syncs/exports/imports + } +} diff --git a/src/shared/services/notification.service.js b/src/shared/services/notification.service.js new file mode 100644 index 0000000..17c0bf5 --- /dev/null +++ b/src/shared/services/notification.service.js @@ -0,0 +1,18 @@ +import { SocketService } from './socket.service.js'; + +export class NotificationService { + static async send(userId, notification) { + console.log(`Sending notification to user ${userId}:`, notification); + + // Broadcast websocket notification + SocketService.to(`user:${userId}`, 'notification:received', notification); + + return true; + } + + static async broadcast(notification) { + console.log('Broadcasting notification:', notification); + SocketService.broadcast('notification:broadcast', notification); + return true; + } +} diff --git a/src/shared/services/search.service.js b/src/shared/services/search.service.js new file mode 100644 index 0000000..755d1a6 --- /dev/null +++ b/src/shared/services/search.service.js @@ -0,0 +1,11 @@ +export class SearchService { + static async search(index, query, filters = {}) { + console.log(`Searching in index ${index} for query: ${query}`); + return []; + } + + static async indexDocument(index, id, document) { + console.log(`Indexing document ${id} in index ${index}`); + return true; + } +} diff --git a/src/shared/services/socket.service.js b/src/shared/services/socket.service.js new file mode 100644 index 0000000..8ecbd3d --- /dev/null +++ b/src/shared/services/socket.service.js @@ -0,0 +1,38 @@ +import { Server } from 'socket.io'; + +class SocketServiceClass { + constructor() { + this.io = null; + } + + init(server) { + this.io = new Server(server, { + cors: { + origin: process.env.CORS_ORIGIN || '*', + methods: ['GET', 'POST'] + } + }); + + this.io.on('connection', (socket) => { + console.log('Client connected to Socket.io: ' + socket.id); + + socket.on('disconnect', () => { + console.log('Client disconnected: ' + socket.id); + }); + }); + } + + broadcast(event, data) { + if (this.io) { + this.io.emit(event, data); + } + } + + to(room, event, data) { + if (this.io) { + this.io.to(room).emit(event, data); + } + } +} + +export const SocketService = new SocketServiceClass(); diff --git a/src/shared/services/upload.service.js b/src/shared/services/upload.service.js new file mode 100644 index 0000000..b54c33d --- /dev/null +++ b/src/shared/services/upload.service.js @@ -0,0 +1,27 @@ +import fs from 'fs'; +import path from 'path'; + +export class UploadService { + static async saveFile(file) { + console.log('Uploading file: ' + file.originalname); + return { + filename: file.filename, + path: file.path, + mimetype: file.mimetype, + size: file.size + }; + } + + static async deleteFile(filePath) { + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + return true; + } + return false; + } catch (e) { + console.error('Failed to delete file:', e); + return false; + } + } +} diff --git a/src/utils/connectors/index.js b/src/utils/connectors/index.js new file mode 100644 index 0000000..e42a828 --- /dev/null +++ b/src/utils/connectors/index.js @@ -0,0 +1 @@ +export * from './redis.connector.js'; diff --git a/src/utils/connectors/redis.connector.js b/src/utils/connectors/redis.connector.js new file mode 100644 index 0000000..1859819 --- /dev/null +++ b/src/utils/connectors/redis.connector.js @@ -0,0 +1,7 @@ +export const connectRedis = async () => { + console.log('Redis client connector connected.'); + return { + get: async (key) => null, + set: async (key, val) => true + }; +}; diff --git a/src/utils/eventBroker/eventBroker.js b/src/utils/eventBroker/eventBroker.js new file mode 100644 index 0000000..a21befa --- /dev/null +++ b/src/utils/eventBroker/eventBroker.js @@ -0,0 +1,12 @@ +import { EventEmitter } from 'events'; + +class EventBrokerClass extends EventEmitter { + publish(event, data) { + this.emit(event, data); + } + subscribe(event, callback) { + this.on(event, callback); + } +} + +export const EventBroker = new EventBrokerClass(); diff --git a/src/utils/eventBroker/index.js b/src/utils/eventBroker/index.js new file mode 100644 index 0000000..307c3ed --- /dev/null +++ b/src/utils/eventBroker/index.js @@ -0,0 +1 @@ +export * from './eventBroker.js'; diff --git a/src/utils/exporters/csv.exporter.js b/src/utils/exporters/csv.exporter.js new file mode 100644 index 0000000..7fcdd25 --- /dev/null +++ b/src/utils/exporters/csv.exporter.js @@ -0,0 +1,5 @@ +export class CsvExporter { + static async export(data) { + return 'id,name,status\n1,placeholder,active'; + } +} diff --git a/src/utils/exporters/index.js b/src/utils/exporters/index.js new file mode 100644 index 0000000..17b50f1 --- /dev/null +++ b/src/utils/exporters/index.js @@ -0,0 +1 @@ +export * from './csv.exporter.js'; diff --git a/src/utils/helpers/ApiError.utils.js b/src/utils/helpers/ApiError.utils.js new file mode 100644 index 0000000..b9d6d31 --- /dev/null +++ b/src/utils/helpers/ApiError.utils.js @@ -0,0 +1,12 @@ +export class ApiError extends Error { + constructor(statusCode, message, isOperational = true, stack = '') { + super(message); + this.statusCode = statusCode; + this.isOperational = isOperational; + if (stack) { + this.stack = stack; + } else { + Error.captureStackTrace(this, this.constructor); + } + } +} diff --git a/src/utils/helpers/common.helper.js b/src/utils/helpers/common.helper.js new file mode 100644 index 0000000..60b1470 --- /dev/null +++ b/src/utils/helpers/common.helper.js @@ -0,0 +1,3 @@ +export const formatResponse = (success, data, message = '') => { + return { success, data, message }; +}; diff --git a/src/utils/helpers/index.js b/src/utils/helpers/index.js new file mode 100644 index 0000000..58a9885 --- /dev/null +++ b/src/utils/helpers/index.js @@ -0,0 +1 @@ +export * from './common.helper.js'; diff --git a/src/utils/helpers/jwt.utils.js b/src/utils/helpers/jwt.utils.js new file mode 100644 index 0000000..27d6ae6 --- /dev/null +++ b/src/utils/helpers/jwt.utils.js @@ -0,0 +1,29 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'supersecretjwtkeythatislongandsecure'; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'; +const JWT_REFRESH_EXPIRES_IN = '7d'; + +export const generateToken = (payload) => { + return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); +}; + +export const generateRefreshToken = (payload) => { + return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN }); +}; + +export const verifyToken = (token) => { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + return null; + } +}; + +export const verifyRefreshToken = (token) => { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + throw new Error('Invalid refresh token'); + } +}; diff --git a/src/utils/imageProcessor/imageProcessor.js b/src/utils/imageProcessor/imageProcessor.js new file mode 100644 index 0000000..e589b63 --- /dev/null +++ b/src/utils/imageProcessor/imageProcessor.js @@ -0,0 +1,6 @@ +export class ImageProcessor { + static async resize(imagePath, options) { + console.log('Resizing image at ' + imagePath); + return imagePath; + } +} diff --git a/src/utils/imageProcessor/index.js b/src/utils/imageProcessor/index.js new file mode 100644 index 0000000..cc79e77 --- /dev/null +++ b/src/utils/imageProcessor/index.js @@ -0,0 +1 @@ +export * from './imageProcessor.js'; diff --git a/src/utils/importers/csv.importer.js b/src/utils/importers/csv.importer.js new file mode 100644 index 0000000..8970e1c --- /dev/null +++ b/src/utils/importers/csv.importer.js @@ -0,0 +1,5 @@ +export class CsvImporter { + static async import(fileContent) { + return []; + } +} diff --git a/src/utils/importers/index.js b/src/utils/importers/index.js new file mode 100644 index 0000000..5e88396 --- /dev/null +++ b/src/utils/importers/index.js @@ -0,0 +1 @@ +export * from './csv.importer.js'; diff --git a/src/utils/search/index.js b/src/utils/search/index.js new file mode 100644 index 0000000..a80ba8f --- /dev/null +++ b/src/utils/search/index.js @@ -0,0 +1 @@ +export * from './searchHelper.js'; diff --git a/src/utils/search/searchHelper.js b/src/utils/search/searchHelper.js new file mode 100644 index 0000000..dc9485b --- /dev/null +++ b/src/utils/search/searchHelper.js @@ -0,0 +1,5 @@ +export class SearchHelper { + static buildSearchQuery(keywords) { + return keywords.split(' ').map(word => `%${word}%`).join(' '); + } +} diff --git a/src/utils/storage/index.js b/src/utils/storage/index.js new file mode 100644 index 0000000..dd7c34d --- /dev/null +++ b/src/utils/storage/index.js @@ -0,0 +1 @@ +export * from './storageHelper.js'; diff --git a/src/utils/storage/storageHelper.js b/src/utils/storage/storageHelper.js new file mode 100644 index 0000000..a93883c --- /dev/null +++ b/src/utils/storage/storageHelper.js @@ -0,0 +1,5 @@ +export class StorageHelper { + static getFileExtension(filename) { + return filename.split('.').pop(); + } +}