@intranefr/superbackend 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,132 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const envVarSchema = new mongoose.Schema(
4
+ {
5
+ key: { type: String, required: true },
6
+ value: { type: String, required: true },
7
+ },
8
+ { _id: false },
9
+ );
10
+
11
+ const httpHeaderSchema = new mongoose.Schema(
12
+ {
13
+ key: { type: String, required: true },
14
+ value: { type: String, required: true },
15
+ },
16
+ { _id: false },
17
+ );
18
+
19
+ const httpAuthRefSchema = new mongoose.Schema(
20
+ {
21
+ type: { type: String, enum: ['bearer', 'basic', 'none'], default: 'none' },
22
+
23
+ // For basic auth we store the username in the HealthCheck doc,
24
+ // but keep sensitive values in encrypted GlobalSettings.
25
+ username: { type: String },
26
+
27
+ // References to GlobalSetting keys (type='encrypted')
28
+ tokenSettingKey: { type: String },
29
+ passwordSettingKey: { type: String },
30
+ },
31
+ { _id: false },
32
+ );
33
+
34
+ const autoHealActionSchema = new mongoose.Schema(
35
+ {
36
+ type: { type: String, enum: ['http', 'script', 'notify_only'], required: true },
37
+ name: { type: String, default: '' },
38
+
39
+ // http action
40
+ httpMethod: { type: String, enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], default: 'POST' },
41
+ httpUrl: { type: String },
42
+ httpHeaders: { type: [httpHeaderSchema], default: [] },
43
+ httpBody: { type: String, default: '' },
44
+ httpBodyType: { type: String, enum: ['json', 'raw', 'form'], default: 'raw' },
45
+ httpAuth: { type: httpAuthRefSchema, default: () => ({}) },
46
+ timeoutMs: { type: Number },
47
+
48
+ // script action
49
+ scriptId: { type: mongoose.Schema.Types.ObjectId, ref: 'ScriptDefinition' },
50
+ scriptEnv: { type: [envVarSchema], default: [] },
51
+ },
52
+ { _id: false },
53
+ );
54
+
55
+ const healthCheckSchema = new mongoose.Schema(
56
+ {
57
+ name: { type: String, required: true },
58
+ description: { type: String, default: '' },
59
+
60
+ enabled: { type: Boolean, default: true, index: true },
61
+
62
+ cronExpression: { type: String, required: true },
63
+ timezone: { type: String, default: 'UTC' },
64
+ nextRunAt: { type: Date, index: true },
65
+
66
+ checkType: { type: String, enum: ['http', 'script', 'internal'], required: true },
67
+
68
+ // Common
69
+ timeoutMs: { type: Number, default: 30000 },
70
+
71
+ // HTTP check
72
+ httpMethod: { type: String, enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], default: 'GET' },
73
+ httpUrl: { type: String, required: function requiredHttpUrl() { return this.checkType === 'http'; } },
74
+ httpHeaders: { type: [httpHeaderSchema], default: [] },
75
+ httpBody: { type: String, default: '' },
76
+ httpBodyType: { type: String, enum: ['json', 'raw', 'form'], default: 'raw' },
77
+ httpAuth: { type: httpAuthRefSchema, default: () => ({}) },
78
+
79
+ // Script check
80
+ scriptId: { type: mongoose.Schema.Types.ObjectId, ref: 'ScriptDefinition' },
81
+ scriptEnv: { type: [envVarSchema], default: [] },
82
+
83
+ // Evaluation
84
+ expectedStatusCodes: { type: [Number], default: [200] },
85
+ maxLatencyMs: { type: Number },
86
+ bodyMustMatch: { type: String },
87
+ bodyMustNotMatch: { type: String },
88
+
89
+ consecutiveFailuresToOpen: { type: Number, default: 3 },
90
+ consecutiveSuccessesToResolve: { type: Number, default: 2 },
91
+
92
+ retries: { type: Number, default: 0 },
93
+ retryDelayMs: { type: Number, default: 0 },
94
+
95
+ // Alerting
96
+ notifyOnOpen: { type: Boolean, default: true },
97
+ notifyOnResolve: { type: Boolean, default: true },
98
+ notifyOnEscalation: { type: Boolean, default: false },
99
+ notificationChannel: { type: String, enum: ['in_app', 'email', 'both'], default: 'in_app' },
100
+ notifyUserIds: { type: [mongoose.Schema.Types.ObjectId], ref: 'User', default: [] },
101
+ suppressNotificationsWhenAcknowledged: { type: Boolean, default: true },
102
+
103
+ // Auto-heal
104
+ autoHealEnabled: { type: Boolean, default: false },
105
+ autoHealWaitMs: { type: Number, default: 0 },
106
+ autoHealCooldownMs: { type: Number, default: 5 * 60 * 1000 },
107
+ autoHealMaxAttemptsPerIncident: { type: Number, default: 3 },
108
+ autoHealBackoffPolicy: { type: String, enum: ['fixed', 'exponential'], default: 'fixed' },
109
+ autoHealBackoffMs: { type: Number, default: 60 * 1000 },
110
+ autoHealActions: { type: [autoHealActionSchema], default: [] },
111
+
112
+ // Operational
113
+ lastRunAt: { type: Date },
114
+ lastStatus: { type: String, enum: ['healthy', 'unhealthy', 'unknown'], default: 'unknown' },
115
+ lastLatencyMs: { type: Number },
116
+ currentIncidentId: { type: mongoose.Schema.Types.ObjectId, ref: 'HealthIncident' },
117
+
118
+ // Streaks (persisted to avoid expensive queries when deciding to open/resolve incidents)
119
+ consecutiveFailureCount: { type: Number, default: 0 },
120
+ consecutiveSuccessCount: { type: Number, default: 0 },
121
+
122
+ createdBy: { type: String, required: true },
123
+ },
124
+ { timestamps: true, collection: 'health_checks' },
125
+ );
126
+
127
+ healthCheckSchema.index({ enabled: 1, nextRunAt: 1 });
128
+ healthCheckSchema.index({ checkType: 1, enabled: 1 });
129
+
130
+ module.exports =
131
+ mongoose.models.HealthCheck ||
132
+ mongoose.model('HealthCheck', healthCheckSchema);
@@ -0,0 +1,51 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const healthCheckRunSchema = new mongoose.Schema(
4
+ {
5
+ healthCheckId: {
6
+ type: mongoose.Schema.Types.ObjectId,
7
+ ref: 'HealthCheck',
8
+ required: true,
9
+ index: true,
10
+ },
11
+
12
+ status: {
13
+ type: String,
14
+ enum: ['running', 'healthy', 'unhealthy', 'timed_out', 'error'],
15
+ default: 'running',
16
+ index: true,
17
+ },
18
+
19
+ attempt: { type: Number, default: 0 },
20
+
21
+ startedAt: { type: Date, default: Date.now, index: true },
22
+ finishedAt: { type: Date },
23
+ durationMs: { type: Number },
24
+
25
+ latencyMs: { type: Number },
26
+
27
+ httpStatusCode: { type: Number },
28
+ httpResponseHeaders: { type: mongoose.Schema.Types.Mixed },
29
+ responseBodySnippet: { type: String },
30
+
31
+ reason: { type: String },
32
+ errorMessage: { type: String },
33
+
34
+ incidentId: { type: mongoose.Schema.Types.ObjectId, ref: 'HealthIncident' },
35
+ },
36
+ { timestamps: true, collection: 'health_check_runs' },
37
+ );
38
+
39
+ healthCheckRunSchema.index({ healthCheckId: 1, startedAt: -1 });
40
+ healthCheckRunSchema.index({ status: 1, startedAt: -1 });
41
+
42
+ healthCheckRunSchema.pre('save', function preSave(next) {
43
+ if (this.isModified('finishedAt') && this.finishedAt && this.startedAt) {
44
+ this.durationMs = this.finishedAt.getTime() - this.startedAt.getTime();
45
+ }
46
+ next();
47
+ });
48
+
49
+ module.exports =
50
+ mongoose.models.HealthCheckRun ||
51
+ mongoose.model('HealthCheckRun', healthCheckRunSchema);
@@ -0,0 +1,49 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const healthIncidentSchema = new mongoose.Schema(
4
+ {
5
+ healthCheckId: {
6
+ type: mongoose.Schema.Types.ObjectId,
7
+ ref: 'HealthCheck',
8
+ required: true,
9
+ index: true,
10
+ },
11
+
12
+ status: {
13
+ type: String,
14
+ enum: ['open', 'acknowledged', 'resolved'],
15
+ default: 'open',
16
+ index: true,
17
+ },
18
+
19
+ severity: {
20
+ type: String,
21
+ enum: ['warning', 'critical'],
22
+ default: 'warning',
23
+ },
24
+
25
+ openedAt: { type: Date, default: Date.now, index: true },
26
+ acknowledgedAt: { type: Date },
27
+ resolvedAt: { type: Date },
28
+ lastSeenAt: { type: Date, default: Date.now },
29
+
30
+ consecutiveFailureCount: { type: Number, default: 0 },
31
+ consecutiveSuccessCount: { type: Number, default: 0 },
32
+
33
+ lastRunId: { type: mongoose.Schema.Types.ObjectId, ref: 'HealthCheckRun' },
34
+
35
+ summary: { type: String, default: '' },
36
+ lastError: { type: String, default: '' },
37
+
38
+ autoHealAttemptCount: { type: Number, default: 0 },
39
+ lastAutoHealAttemptAt: { type: Date },
40
+ },
41
+ { timestamps: true, collection: 'health_incidents' },
42
+ );
43
+
44
+ healthIncidentSchema.index({ healthCheckId: 1, openedAt: -1 });
45
+ healthIncidentSchema.index({ status: 1, openedAt: -1 });
46
+
47
+ module.exports =
48
+ mongoose.models.HealthIncident ||
49
+ mongoose.model('HealthIncident', healthIncidentSchema);
@@ -0,0 +1,95 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const seoMetaSchema = new mongoose.Schema(
4
+ {
5
+ title: { type: String, default: '' },
6
+ description: { type: String, default: '' },
7
+ keywords: { type: String, default: '' },
8
+ ogImage: { type: String, default: '' },
9
+ canonicalUrl: { type: String, default: '' },
10
+ },
11
+ { _id: false },
12
+ );
13
+
14
+ const pageSchema = new mongoose.Schema(
15
+ {
16
+ slug: {
17
+ type: String,
18
+ required: true,
19
+ index: true,
20
+ },
21
+ collectionId: {
22
+ type: mongoose.Schema.Types.ObjectId,
23
+ ref: 'PageCollection',
24
+ default: null,
25
+ index: true,
26
+ },
27
+ title: {
28
+ type: String,
29
+ required: true,
30
+ },
31
+ templateKey: {
32
+ type: String,
33
+ default: 'default',
34
+ },
35
+ layoutKey: {
36
+ type: String,
37
+ default: 'default',
38
+ },
39
+ blocks: {
40
+ type: mongoose.Schema.Types.Mixed,
41
+ default: [],
42
+ },
43
+ repeat: {
44
+ type: mongoose.Schema.Types.Mixed,
45
+ default: null,
46
+ },
47
+ customCss: {
48
+ type: String,
49
+ default: '',
50
+ },
51
+ customJs: {
52
+ type: String,
53
+ default: '',
54
+ },
55
+ seoMeta: {
56
+ type: seoMetaSchema,
57
+ default: () => ({}),
58
+ },
59
+ tenantId: {
60
+ type: mongoose.Schema.Types.ObjectId,
61
+ ref: 'Organization',
62
+ default: null,
63
+ index: true,
64
+ },
65
+ isGlobal: {
66
+ type: Boolean,
67
+ default: true,
68
+ index: true,
69
+ },
70
+ status: {
71
+ type: String,
72
+ enum: ['draft', 'published', 'archived'],
73
+ default: 'draft',
74
+ index: true,
75
+ },
76
+ publishedAt: {
77
+ type: Date,
78
+ default: null,
79
+ },
80
+ },
81
+ { timestamps: true },
82
+ );
83
+
84
+ pageSchema.index({ slug: 1, collectionId: 1, tenantId: 1 }, { unique: true });
85
+ pageSchema.index({ status: 1, isGlobal: 1 });
86
+ pageSchema.index({ collectionId: 1, status: 1 });
87
+
88
+ pageSchema.virtual('routePath').get(function () {
89
+ return this._routePath || null;
90
+ });
91
+
92
+ pageSchema.set('toJSON', { virtuals: true });
93
+ pageSchema.set('toObject', { virtuals: true });
94
+
95
+ module.exports = mongoose.model('Page', pageSchema);
@@ -0,0 +1,42 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const pageCollectionSchema = new mongoose.Schema(
4
+ {
5
+ slug: {
6
+ type: String,
7
+ required: true,
8
+ index: true,
9
+ },
10
+ name: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ description: {
15
+ type: String,
16
+ default: '',
17
+ },
18
+ tenantId: {
19
+ type: mongoose.Schema.Types.ObjectId,
20
+ ref: 'Organization',
21
+ default: null,
22
+ index: true,
23
+ },
24
+ isGlobal: {
25
+ type: Boolean,
26
+ default: true,
27
+ index: true,
28
+ },
29
+ status: {
30
+ type: String,
31
+ enum: ['active', 'archived'],
32
+ default: 'active',
33
+ index: true,
34
+ },
35
+ },
36
+ { timestamps: true },
37
+ );
38
+
39
+ pageCollectionSchema.index({ slug: 1, tenantId: 1 }, { unique: true });
40
+ pageCollectionSchema.index({ isGlobal: 1, status: 1 });
41
+
42
+ module.exports = mongoose.model('PageCollection', pageCollectionSchema);
@@ -0,0 +1,66 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const proxyRuleSchema = new mongoose.Schema(
4
+ {
5
+ enabled: { type: Boolean, default: true },
6
+ type: { type: String, enum: ['contains', 'regexp'], required: true },
7
+ value: { type: String, required: true },
8
+ applyTo: { type: String, enum: ['targetUrl', 'host', 'path'], default: 'targetUrl' },
9
+ flags: { type: String, default: 'i' },
10
+ },
11
+ { _id: false },
12
+ );
13
+
14
+ const proxyEntrySchema = new mongoose.Schema(
15
+ {
16
+ name: { type: String, default: '' },
17
+ enabled: { type: Boolean, default: false, index: true },
18
+ match: {
19
+ type: {
20
+ type: String,
21
+ enum: ['exact', 'contains', 'regexp'],
22
+ default: 'contains',
23
+ },
24
+ value: { type: String, required: true },
25
+ applyTo: { type: String, enum: ['targetUrl', 'host', 'path'], default: 'host' },
26
+ flags: { type: String, default: 'i' },
27
+ },
28
+ policy: {
29
+ mode: { type: String, enum: ['blacklist', 'whitelist', 'allowAll', 'denyAll'], default: 'whitelist' },
30
+ rules: { type: [proxyRuleSchema], default: [] },
31
+ },
32
+ rateLimit: {
33
+ enabled: { type: Boolean, default: false },
34
+ limiterId: { type: String, default: null },
35
+ },
36
+ cache: {
37
+ enabled: { type: Boolean, default: false },
38
+ ttlSeconds: { type: Number, default: 60 },
39
+ namespace: { type: String, default: 'proxy' },
40
+ methods: { type: [String], default: ['GET', 'HEAD'] },
41
+ keyParts: {
42
+ url: { type: Boolean, default: true },
43
+ query: { type: Boolean, default: true },
44
+ bodyHash: { type: Boolean, default: true },
45
+ headersHash: { type: Boolean, default: true },
46
+ },
47
+ keyHeaderAllowList: { type: [String], default: [] },
48
+ },
49
+ headers: {
50
+ forwardAuthorization: { type: Boolean, default: true },
51
+ forwardCookie: { type: Boolean, default: true },
52
+ allowList: { type: [String], default: [] },
53
+ denyList: { type: [String], default: [] },
54
+ },
55
+ transform: {
56
+ enabled: { type: Boolean, default: false },
57
+ timeoutMs: { type: Number, default: 200 },
58
+ code: { type: String, default: '' },
59
+ },
60
+ },
61
+ { timestamps: true, collection: 'proxy_entries' },
62
+ );
63
+
64
+ proxyEntrySchema.index({ 'match.type': 1, 'match.applyTo': 1 });
65
+
66
+ module.exports = mongoose.models.ProxyEntry || mongoose.model('ProxyEntry', proxyEntrySchema);
@@ -0,0 +1,19 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rateLimitCounterSchema = new mongoose.Schema(
4
+ {
5
+ limiterId: { type: String, required: true, index: true },
6
+ identityKey: { type: String, required: true, index: true },
7
+ windowStart: { type: Date, required: true, index: true },
8
+
9
+ count: { type: Number, default: 0 },
10
+
11
+ expiresAt: { type: Date, default: null },
12
+ },
13
+ { timestamps: true, collection: 'rate_limit_counters' },
14
+ );
15
+
16
+ rateLimitCounterSchema.index({ limiterId: 1, identityKey: 1, windowStart: 1 }, { unique: true });
17
+ rateLimitCounterSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
18
+
19
+ module.exports = mongoose.models.RateLimitCounter || mongoose.model('RateLimitCounter', rateLimitCounterSchema);
@@ -0,0 +1,20 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rateLimitMetricBucketSchema = new mongoose.Schema(
4
+ {
5
+ bucketStart: { type: Date, required: true, index: true },
6
+ limiterId: { type: String, required: true, index: true },
7
+
8
+ checked: { type: Number, default: 0 },
9
+ allowed: { type: Number, default: 0 },
10
+ blocked: { type: Number, default: 0 },
11
+
12
+ expiresAt: { type: Date, default: null, index: true },
13
+ },
14
+ { timestamps: true, collection: 'rate_limit_metric_buckets' },
15
+ );
16
+
17
+ rateLimitMetricBucketSchema.index({ limiterId: 1, bucketStart: 1 }, { unique: true });
18
+ rateLimitMetricBucketSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
19
+
20
+ module.exports = mongoose.models.RateLimitMetricBucket || mongoose.model('RateLimitMetricBucket', rateLimitMetricBucketSchema);
@@ -0,0 +1,25 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacGrantSchema = new mongoose.Schema(
4
+ {
5
+ subjectType: { type: String, enum: ['user', 'role', 'group', 'org'], required: true, index: true },
6
+ subjectId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
7
+
8
+ scopeType: { type: String, enum: ['global', 'org'], required: true, index: true },
9
+ scopeId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
10
+
11
+ right: { type: String, required: true, trim: true, index: true },
12
+ effect: { type: String, enum: ['allow', 'deny'], default: 'allow', index: true },
13
+
14
+ createdByActorType: { type: String, default: null },
15
+ createdByActorId: { type: String, default: null },
16
+ },
17
+ { timestamps: true, collection: 'rbac_grants' },
18
+ );
19
+
20
+ rbacGrantSchema.index(
21
+ { subjectType: 1, subjectId: 1, scopeType: 1, scopeId: 1, right: 1 },
22
+ { unique: true },
23
+ );
24
+
25
+ module.exports = mongoose.models.RbacGrant || mongoose.model('RbacGrant', rbacGrantSchema);
@@ -0,0 +1,16 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacGroupSchema = new mongoose.Schema(
4
+ {
5
+ name: { type: String, required: true, trim: true },
6
+ description: { type: String, default: '', trim: true },
7
+ status: { type: String, enum: ['active', 'disabled'], default: 'active', index: true },
8
+ isGlobal: { type: Boolean, default: true, index: true },
9
+ orgId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
10
+ },
11
+ { timestamps: true, collection: 'rbac_groups' },
12
+ );
13
+
14
+ rbacGroupSchema.index({ isGlobal: 1, orgId: 1, name: 1 });
15
+
16
+ module.exports = mongoose.models.RbacGroup || mongoose.model('RbacGroup', rbacGroupSchema);
@@ -0,0 +1,13 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacGroupMemberSchema = new mongoose.Schema(
4
+ {
5
+ groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'RbacGroup', required: true, index: true },
6
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true },
7
+ },
8
+ { timestamps: true, collection: 'rbac_group_members' },
9
+ );
10
+
11
+ rbacGroupMemberSchema.index({ groupId: 1, userId: 1 }, { unique: true });
12
+
13
+ module.exports = mongoose.models.RbacGroupMember || mongoose.model('RbacGroupMember', rbacGroupMemberSchema);
@@ -0,0 +1,13 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacGroupRoleSchema = new mongoose.Schema(
4
+ {
5
+ groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'RbacGroup', required: true, index: true },
6
+ roleId: { type: mongoose.Schema.Types.ObjectId, ref: 'RbacRole', required: true, index: true },
7
+ },
8
+ { timestamps: true, collection: 'rbac_group_roles' },
9
+ );
10
+
11
+ rbacGroupRoleSchema.index({ groupId: 1, roleId: 1 }, { unique: true });
12
+
13
+ module.exports = mongoose.models.RbacGroupRole || mongoose.model('RbacGroupRole', rbacGroupRoleSchema);
@@ -0,0 +1,25 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacRoleSchema = new mongoose.Schema(
4
+ {
5
+ key: { type: String, required: true, trim: true, lowercase: true, index: true },
6
+ name: { type: String, required: true, trim: true },
7
+ description: { type: String, default: '', trim: true },
8
+ status: { type: String, enum: ['active', 'disabled'], default: 'active', index: true },
9
+ isGlobal: { type: Boolean, default: true, index: true },
10
+ orgId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
11
+ },
12
+ { timestamps: true, collection: 'rbac_roles' },
13
+ );
14
+
15
+ rbacRoleSchema.index({ isGlobal: 1, orgId: 1, key: 1 });
16
+ rbacRoleSchema.index(
17
+ { key: 1 },
18
+ { unique: true, partialFilterExpression: { isGlobal: true } },
19
+ );
20
+ rbacRoleSchema.index(
21
+ { orgId: 1, key: 1 },
22
+ { unique: true, partialFilterExpression: { isGlobal: false } },
23
+ );
24
+
25
+ module.exports = mongoose.models.RbacRole || mongoose.model('RbacRole', rbacRoleSchema);
@@ -0,0 +1,13 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const rbacUserRoleSchema = new mongoose.Schema(
4
+ {
5
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true },
6
+ roleId: { type: mongoose.Schema.Types.ObjectId, ref: 'RbacRole', required: true, index: true },
7
+ },
8
+ { timestamps: true, collection: 'rbac_user_roles' },
9
+ );
10
+
11
+ rbacUserRoleSchema.index({ userId: 1, roleId: 1 }, { unique: true });
12
+
13
+ module.exports = mongoose.models.RbacUserRole || mongoose.model('RbacUserRole', rbacUserRoleSchema);
@@ -16,6 +16,7 @@ const scriptDefinitionSchema = new mongoose.Schema(
16
16
  type: { type: String, enum: ['bash', 'node', 'browser'], required: true },
17
17
  runner: { type: String, enum: ['host', 'vm2', 'browser'], required: true },
18
18
  script: { type: String, required: true },
19
+ scriptFormat: { type: String, enum: ['string', 'base64'], default: 'string' },
19
20
  defaultWorkingDirectory: { type: String, default: '' },
20
21
  env: { type: [envVarSchema], default: [] },
21
22
  timeoutMs: { type: Number, default: 5 * 60 * 1000 },
@@ -26,6 +26,8 @@ const webhookSchema = new mongoose.Schema({
26
26
  'organization.updated',
27
27
  'member.added',
28
28
  'form.submitted',
29
+ 'experiment.winner_changed',
30
+ 'experiment.status_changed',
29
31
  'audit.event'
30
32
  ]
31
33
  }],
@@ -25,4 +25,6 @@ router.post('/stripe-webhooks/retry', adminController.retryFailedWebhookEvents);
25
25
  router.post('/stripe-webhooks/:id/retry', adminController.retrySingleWebhookEvent);
26
26
  router.get('/stripe-webhooks-stats', adminController.getWebhookStats);
27
27
 
28
+ router.post('/users/email/token', adminController.generateTokenForEmail);
29
+
28
30
  module.exports = router;
@@ -0,0 +1,21 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ const { basicAuth } = require('../middleware/auth');
5
+ const controller = require('../controllers/blogAdmin.controller');
6
+
7
+ router.use(basicAuth);
8
+ router.use(express.json({ limit: '2mb' }));
9
+
10
+ router.get('/blog-posts', controller.list);
11
+ router.get('/blog-posts/suggestions', controller.suggestions);
12
+ router.post('/blog-posts', controller.create);
13
+ router.get('/blog-posts/:id', controller.get);
14
+ router.put('/blog-posts/:id', controller.update);
15
+ router.put('/blog-posts/:id/publish', controller.publish);
16
+ router.put('/blog-posts/:id/unpublish', controller.unpublish);
17
+ router.put('/blog-posts/:id/schedule', controller.schedule);
18
+ router.put('/blog-posts/:id/archive', controller.archive);
19
+ router.delete('/blog-posts/:id', controller.remove);
20
+
21
+ module.exports = router;
@@ -0,0 +1,16 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ const { basicAuth } = require('../middleware/auth');
5
+ const controller = require('../controllers/blogAiAdmin.controller');
6
+ const rateLimiter = require('../services/rateLimiter.service');
7
+
8
+ router.use(basicAuth);
9
+ router.use(express.json({ limit: '2mb' }));
10
+
11
+ router.post('/blog-ai/generate-field', rateLimiter.limit('blogAiLimiter'), controller.generateField);
12
+ router.post('/blog-ai/generate-all', rateLimiter.limit('blogAiLimiter'), controller.generateAll);
13
+ router.post('/blog-ai/format-markdown', rateLimiter.limit('blogAiLimiter'), controller.formatMarkdown);
14
+ router.post('/blog-ai/refine-markdown', rateLimiter.limit('blogAiLimiter'), controller.refineMarkdown);
15
+
16
+ module.exports = router;