@intranefr/superbackend 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const UiComponent = require('../models/UiComponent');
|
|
2
|
+
const UiComponentProject = require('../models/UiComponentProject');
|
|
3
|
+
const UiComponentProjectComponent = require('../models/UiComponentProjectComponent');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
generateProjectApiKeyPlaintext,
|
|
7
|
+
hashKey,
|
|
8
|
+
} = require('../services/uiComponentsCrypto.service');
|
|
9
|
+
|
|
10
|
+
function randomLowerAlphaNum(len) {
|
|
11
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
12
|
+
let out = '';
|
|
13
|
+
for (let i = 0; i < len; i += 1) out += chars[Math.floor(Math.random() * chars.length)];
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function generateProjectId() {
|
|
18
|
+
return `prj_${randomLowerAlphaNum(16)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseBool(value, fallback) {
|
|
22
|
+
if (value === undefined) return fallback;
|
|
23
|
+
if (typeof value === 'boolean') return value;
|
|
24
|
+
const v = String(value).trim().toLowerCase();
|
|
25
|
+
if (v === 'true' || v === '1' || v === 'yes') return true;
|
|
26
|
+
if (v === 'false' || v === '0' || v === 'no') return false;
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
exports.listProjects = async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const items = await UiComponentProject.find({}).sort({ createdAt: -1 }).lean();
|
|
33
|
+
return res.json({ items });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('UI Components listProjects error:', error);
|
|
36
|
+
return res.status(500).json({ error: 'Failed to list projects' });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
exports.createProject = async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const name = String(req.body?.name || '').trim();
|
|
43
|
+
const projectIdIn = req.body?.projectId !== undefined ? String(req.body.projectId).trim() : '';
|
|
44
|
+
const isPublic = parseBool(req.body?.isPublic, true);
|
|
45
|
+
|
|
46
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
47
|
+
|
|
48
|
+
const projectId = projectIdIn || generateProjectId();
|
|
49
|
+
|
|
50
|
+
const doc = await UiComponentProject.create({
|
|
51
|
+
projectId,
|
|
52
|
+
name,
|
|
53
|
+
isPublic,
|
|
54
|
+
apiKeyHash: null,
|
|
55
|
+
allowedOrigins: Array.isArray(req.body?.allowedOrigins) ? req.body.allowedOrigins : [],
|
|
56
|
+
isActive: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let apiKey = null;
|
|
60
|
+
if (!isPublic) {
|
|
61
|
+
apiKey = generateProjectApiKeyPlaintext();
|
|
62
|
+
doc.apiKeyHash = hashKey(apiKey);
|
|
63
|
+
await doc.save();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return res.status(201).json({ item: doc.toObject(), apiKey });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('UI Components createProject error:', error);
|
|
69
|
+
if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
|
|
70
|
+
if (error?.code === 11000) return res.status(409).json({ error: 'Project already exists' });
|
|
71
|
+
return res.status(500).json({ error: 'Failed to create project' });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
exports.getProject = async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const { projectId } = req.params;
|
|
78
|
+
const item = await UiComponentProject.findOne({ projectId: String(projectId) }).lean();
|
|
79
|
+
if (!item) return res.status(404).json({ error: 'Project not found' });
|
|
80
|
+
|
|
81
|
+
const assigned = await UiComponentProjectComponent.find({ projectId: item.projectId }).lean();
|
|
82
|
+
return res.json({ item, assigned });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('UI Components getProject error:', error);
|
|
85
|
+
return res.status(500).json({ error: 'Failed to load project' });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
exports.updateProject = async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const { projectId } = req.params;
|
|
92
|
+
const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
|
|
93
|
+
if (!doc) return res.status(404).json({ error: 'Project not found' });
|
|
94
|
+
|
|
95
|
+
if (req.body?.name !== undefined) {
|
|
96
|
+
const name = String(req.body.name || '').trim();
|
|
97
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
98
|
+
doc.name = name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (req.body?.isPublic !== undefined) {
|
|
102
|
+
const nextPublic = parseBool(req.body.isPublic, doc.isPublic);
|
|
103
|
+
if (nextPublic !== doc.isPublic) {
|
|
104
|
+
doc.isPublic = nextPublic;
|
|
105
|
+
if (doc.isPublic) {
|
|
106
|
+
doc.apiKeyHash = null;
|
|
107
|
+
} else if (!doc.apiKeyHash) {
|
|
108
|
+
const apiKey = generateProjectApiKeyPlaintext();
|
|
109
|
+
doc.apiKeyHash = hashKey(apiKey);
|
|
110
|
+
await doc.save();
|
|
111
|
+
return res.json({ item: doc.toObject(), apiKey });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (req.body?.allowedOrigins !== undefined) {
|
|
117
|
+
doc.allowedOrigins = Array.isArray(req.body.allowedOrigins) ? req.body.allowedOrigins : [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (req.body?.isActive !== undefined) {
|
|
121
|
+
doc.isActive = Boolean(req.body.isActive);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await doc.save();
|
|
125
|
+
return res.json({ item: doc.toObject(), apiKey: null });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('UI Components updateProject error:', error);
|
|
128
|
+
if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
|
|
129
|
+
return res.status(500).json({ error: 'Failed to update project' });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
exports.rotateProjectKey = async (req, res) => {
|
|
134
|
+
try {
|
|
135
|
+
const { projectId } = req.params;
|
|
136
|
+
const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
|
|
137
|
+
if (!doc) return res.status(404).json({ error: 'Project not found' });
|
|
138
|
+
if (doc.isPublic) return res.status(400).json({ error: 'Project is public' });
|
|
139
|
+
|
|
140
|
+
const apiKey = generateProjectApiKeyPlaintext();
|
|
141
|
+
doc.apiKeyHash = hashKey(apiKey);
|
|
142
|
+
await doc.save();
|
|
143
|
+
|
|
144
|
+
return res.json({ item: doc.toObject(), apiKey });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('UI Components rotateProjectKey error:', error);
|
|
147
|
+
return res.status(500).json({ error: 'Failed to rotate key' });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
exports.deleteProject = async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const { projectId } = req.params;
|
|
154
|
+
const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
|
|
155
|
+
if (!doc) return res.status(404).json({ error: 'Project not found' });
|
|
156
|
+
|
|
157
|
+
await UiComponentProjectComponent.deleteMany({ projectId: doc.projectId });
|
|
158
|
+
await UiComponentProject.deleteOne({ _id: doc._id });
|
|
159
|
+
return res.json({ success: true });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('UI Components deleteProject error:', error);
|
|
162
|
+
return res.status(500).json({ error: 'Failed to delete project' });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
exports.listComponents = async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const items = await UiComponent.find({}).sort({ updatedAt: -1 }).lean();
|
|
169
|
+
return res.json({ items });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('UI Components listComponents error:', error);
|
|
172
|
+
return res.status(500).json({ error: 'Failed to list components' });
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
exports.createComponent = async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
const code = String(req.body?.code || '').trim().toLowerCase();
|
|
179
|
+
const name = String(req.body?.name || '').trim();
|
|
180
|
+
if (!code) return res.status(400).json({ error: 'code is required' });
|
|
181
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
182
|
+
|
|
183
|
+
const doc = await UiComponent.create({
|
|
184
|
+
code,
|
|
185
|
+
name,
|
|
186
|
+
html: String(req.body?.html || ''),
|
|
187
|
+
js: String(req.body?.js || ''),
|
|
188
|
+
css: String(req.body?.css || ''),
|
|
189
|
+
api: req.body?.api !== undefined ? req.body.api : null,
|
|
190
|
+
usageMarkdown: String(req.body?.usageMarkdown || ''),
|
|
191
|
+
version: Number(req.body?.version || 1) || 1,
|
|
192
|
+
isActive: parseBool(req.body?.isActive, true),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return res.status(201).json({ item: doc.toObject() });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('UI Components createComponent error:', error);
|
|
198
|
+
if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
|
|
199
|
+
if (error?.code === 11000) return res.status(409).json({ error: 'Component already exists' });
|
|
200
|
+
return res.status(500).json({ error: 'Failed to create component' });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
exports.getComponent = async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { code } = req.params;
|
|
207
|
+
const item = await UiComponent.findOne({ code: String(code).toLowerCase() }).lean();
|
|
208
|
+
if (!item) return res.status(404).json({ error: 'Component not found' });
|
|
209
|
+
return res.json({ item });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('UI Components getComponent error:', error);
|
|
212
|
+
return res.status(500).json({ error: 'Failed to load component' });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
exports.updateComponent = async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const { code } = req.params;
|
|
219
|
+
const doc = await UiComponent.findOne({ code: String(code).toLowerCase() });
|
|
220
|
+
if (!doc) return res.status(404).json({ error: 'Component not found' });
|
|
221
|
+
|
|
222
|
+
if (req.body?.name !== undefined) {
|
|
223
|
+
const name = String(req.body.name || '').trim();
|
|
224
|
+
if (!name) return res.status(400).json({ error: 'name is required' });
|
|
225
|
+
doc.name = name;
|
|
226
|
+
}
|
|
227
|
+
if (req.body?.html !== undefined) doc.html = String(req.body.html || '');
|
|
228
|
+
if (req.body?.js !== undefined) doc.js = String(req.body.js || '');
|
|
229
|
+
if (req.body?.css !== undefined) doc.css = String(req.body.css || '');
|
|
230
|
+
if (req.body?.api !== undefined) doc.api = req.body.api;
|
|
231
|
+
if (req.body?.usageMarkdown !== undefined) doc.usageMarkdown = String(req.body.usageMarkdown || '');
|
|
232
|
+
|
|
233
|
+
if (req.body?.version !== undefined) {
|
|
234
|
+
const v = Number(req.body.version);
|
|
235
|
+
if (!Number.isFinite(v) || v < 1) return res.status(400).json({ error: 'version must be a positive number' });
|
|
236
|
+
doc.version = v;
|
|
237
|
+
} else {
|
|
238
|
+
doc.version = Number(doc.version || 1) + 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (req.body?.isActive !== undefined) doc.isActive = Boolean(req.body.isActive);
|
|
242
|
+
|
|
243
|
+
await doc.save();
|
|
244
|
+
return res.json({ item: doc.toObject() });
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('UI Components updateComponent error:', error);
|
|
247
|
+
if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
|
|
248
|
+
return res.status(500).json({ error: 'Failed to update component' });
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
exports.deleteComponent = async (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const { code } = req.params;
|
|
255
|
+
const doc = await UiComponent.findOne({ code: String(code).toLowerCase() });
|
|
256
|
+
if (!doc) return res.status(404).json({ error: 'Component not found' });
|
|
257
|
+
|
|
258
|
+
await UiComponentProjectComponent.deleteMany({ componentCode: doc.code });
|
|
259
|
+
await UiComponent.deleteOne({ _id: doc._id });
|
|
260
|
+
return res.json({ success: true });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('UI Components deleteComponent error:', error);
|
|
263
|
+
return res.status(500).json({ error: 'Failed to delete component' });
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
exports.setAssignment = async (req, res) => {
|
|
268
|
+
try {
|
|
269
|
+
const projectId = String(req.params.projectId || '').trim();
|
|
270
|
+
const code = String(req.params.code || '').trim().toLowerCase();
|
|
271
|
+
const enabled = parseBool(req.body?.enabled, true);
|
|
272
|
+
|
|
273
|
+
const project = await UiComponentProject.findOne({ projectId }).lean();
|
|
274
|
+
if (!project) return res.status(404).json({ error: 'Project not found' });
|
|
275
|
+
|
|
276
|
+
const component = await UiComponent.findOne({ code }).lean();
|
|
277
|
+
if (!component) return res.status(404).json({ error: 'Component not found' });
|
|
278
|
+
|
|
279
|
+
const doc = await UiComponentProjectComponent.findOneAndUpdate(
|
|
280
|
+
{ projectId, componentCode: code },
|
|
281
|
+
{ $set: { enabled } },
|
|
282
|
+
{ upsert: true, new: true, setDefaultsOnInsert: true },
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return res.json({ item: doc.toObject() });
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error('UI Components setAssignment error:', error);
|
|
288
|
+
if (error?.code === 11000) return res.status(409).json({ error: 'Assignment already exists' });
|
|
289
|
+
return res.status(500).json({ error: 'Failed to set assignment' });
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
exports.deleteAssignment = async (req, res) => {
|
|
294
|
+
try {
|
|
295
|
+
const projectId = String(req.params.projectId || '').trim();
|
|
296
|
+
const code = String(req.params.code || '').trim().toLowerCase();
|
|
297
|
+
|
|
298
|
+
await UiComponentProjectComponent.deleteOne({ projectId, componentCode: code });
|
|
299
|
+
return res.json({ success: true });
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('UI Components deleteAssignment error:', error);
|
|
302
|
+
return res.status(500).json({ error: 'Failed to delete assignment' });
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
exports.listProjectAssignments = async (req, res) => {
|
|
307
|
+
try {
|
|
308
|
+
const projectId = String(req.params.projectId || '').trim();
|
|
309
|
+
const items = await UiComponentProjectComponent.find({ projectId }).lean();
|
|
310
|
+
return res.json({ items });
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('UI Components listProjectAssignments error:', error);
|
|
313
|
+
return res.status(500).json({ error: 'Failed to list assignments' });
|
|
314
|
+
}
|
|
315
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { proposeComponentEdit } = require('../services/uiComponentsAi.service');
|
|
2
|
+
const { getBasicAuthActor } = require('../services/audit.service');
|
|
3
|
+
|
|
4
|
+
function handleError(res, err) {
|
|
5
|
+
const code = err && err.code;
|
|
6
|
+
if (code === 'VALIDATION') return res.status(400).json({ error: err.message });
|
|
7
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: err.message });
|
|
8
|
+
if (code === 'AI_INVALID') return res.status(500).json({ error: err.message });
|
|
9
|
+
return res.status(500).json({ error: err.message || 'Operation failed' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exports.propose = async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const actor = getBasicAuthActor(req);
|
|
15
|
+
const { code } = req.params;
|
|
16
|
+
|
|
17
|
+
const { prompt, providerKey, model, targets, mode } = req.body || {};
|
|
18
|
+
|
|
19
|
+
const result = await proposeComponentEdit({
|
|
20
|
+
code,
|
|
21
|
+
prompt,
|
|
22
|
+
providerKey,
|
|
23
|
+
model,
|
|
24
|
+
targets,
|
|
25
|
+
mode,
|
|
26
|
+
actor,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return res.json(result);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('[adminUiComponentsAi] propose error', err);
|
|
32
|
+
return handleError(res, err);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const BlogPost = require('../models/BlogPost');
|
|
2
|
+
const {
|
|
3
|
+
extractExcerptFromMarkdown,
|
|
4
|
+
generateUniqueBlogSlug,
|
|
5
|
+
normalizeTags,
|
|
6
|
+
slugify,
|
|
7
|
+
parsePagination,
|
|
8
|
+
} = require('../services/blog.service');
|
|
9
|
+
|
|
10
|
+
function normalizeStringField(value) {
|
|
11
|
+
if (value === undefined) return undefined;
|
|
12
|
+
return String(value || '').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.list = async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const { page, limit, skip } = parsePagination({
|
|
18
|
+
page: req.query.page,
|
|
19
|
+
limit: req.query.limit,
|
|
20
|
+
maxLimit: 200,
|
|
21
|
+
defaultLimit: 50,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const filter = {};
|
|
25
|
+
|
|
26
|
+
const status = String(req.query.status || '').trim();
|
|
27
|
+
if (status) filter.status = status;
|
|
28
|
+
|
|
29
|
+
const tag = String(req.query.tag || '').trim();
|
|
30
|
+
if (tag) filter.tags = tag;
|
|
31
|
+
|
|
32
|
+
const category = String(req.query.category || '').trim();
|
|
33
|
+
if (category) filter.category = category;
|
|
34
|
+
|
|
35
|
+
const q = String(req.query.q || '').trim();
|
|
36
|
+
if (q) {
|
|
37
|
+
const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
|
38
|
+
filter.$or = [{ title: re }, { excerpt: re }, { slug: re }];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const statsBaseFilter = { ...filter };
|
|
42
|
+
delete statsBaseFilter.status;
|
|
43
|
+
|
|
44
|
+
const [items, total, statsTotal, statsDraft, statsScheduled, statsPublished, statsArchived] = await Promise.all([
|
|
45
|
+
BlogPost.find(filter)
|
|
46
|
+
.sort({ updatedAt: -1, createdAt: -1 })
|
|
47
|
+
.select('title slug status excerpt category tags authorName publishedAt scheduledAt updatedAt createdAt')
|
|
48
|
+
.skip(skip)
|
|
49
|
+
.limit(limit)
|
|
50
|
+
.lean(),
|
|
51
|
+
BlogPost.countDocuments(filter),
|
|
52
|
+
BlogPost.countDocuments(statsBaseFilter),
|
|
53
|
+
BlogPost.countDocuments({ ...statsBaseFilter, status: 'draft' }),
|
|
54
|
+
BlogPost.countDocuments({ ...statsBaseFilter, status: 'scheduled' }),
|
|
55
|
+
BlogPost.countDocuments({ ...statsBaseFilter, status: 'published' }),
|
|
56
|
+
BlogPost.countDocuments({ ...statsBaseFilter, status: 'archived' }),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
res.json({
|
|
60
|
+
items,
|
|
61
|
+
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
|
|
62
|
+
stats: {
|
|
63
|
+
total: statsTotal,
|
|
64
|
+
draft: statsDraft,
|
|
65
|
+
scheduled: statsScheduled,
|
|
66
|
+
published: statsPublished,
|
|
67
|
+
archived: statsArchived,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error listing admin blog posts:', error);
|
|
72
|
+
res.status(500).json({ error: 'Failed to list blog posts' });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
exports.suggestions = async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const [categories, authorNames, tagsAgg] = await Promise.all([
|
|
79
|
+
BlogPost.distinct('category', { category: { $ne: '' } }),
|
|
80
|
+
BlogPost.distinct('authorName', { authorName: { $ne: '' } }),
|
|
81
|
+
BlogPost.aggregate([
|
|
82
|
+
{ $unwind: '$tags' },
|
|
83
|
+
{ $match: { tags: { $ne: '' } } },
|
|
84
|
+
{ $group: { _id: '$tags' } },
|
|
85
|
+
{ $project: { _id: 0, tag: '$_id' } },
|
|
86
|
+
]),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const tags = (tagsAgg || []).map((t) => String(t.tag || '')).filter(Boolean);
|
|
90
|
+
|
|
91
|
+
res.json({
|
|
92
|
+
categories: (categories || []).map((x) => String(x || '')).filter(Boolean).sort(),
|
|
93
|
+
tags: tags.sort(),
|
|
94
|
+
authorNames: (authorNames || []).map((x) => String(x || '')).filter(Boolean).sort(),
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error building blog suggestions:', error);
|
|
98
|
+
res.status(500).json({ error: 'Failed to load suggestions' });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
exports.create = async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const payload = req.body || {};
|
|
105
|
+
const title = normalizeStringField(payload.title);
|
|
106
|
+
if (!title) return res.status(400).json({ error: 'title is required' });
|
|
107
|
+
|
|
108
|
+
const markdown = String(payload.markdown || '');
|
|
109
|
+
if (!markdown.trim()) return res.status(400).json({ error: 'markdown is required' });
|
|
110
|
+
|
|
111
|
+
const html = String(payload.html || payload.markdown || '');
|
|
112
|
+
const excerpt =
|
|
113
|
+
normalizeStringField(payload.excerpt) || extractExcerptFromMarkdown(markdown);
|
|
114
|
+
|
|
115
|
+
const desiredSlug = normalizeStringField(payload.slug);
|
|
116
|
+
const slug = desiredSlug ? slugify(desiredSlug) : await generateUniqueBlogSlug(title);
|
|
117
|
+
|
|
118
|
+
const post = await BlogPost.create({
|
|
119
|
+
title,
|
|
120
|
+
slug,
|
|
121
|
+
status: 'draft',
|
|
122
|
+
excerpt,
|
|
123
|
+
markdown,
|
|
124
|
+
html,
|
|
125
|
+
coverImageUrl: normalizeStringField(payload.coverImageUrl) || '',
|
|
126
|
+
category: normalizeStringField(payload.category) || '',
|
|
127
|
+
tags: normalizeTags(payload.tags),
|
|
128
|
+
authorName: normalizeStringField(payload.authorName) || '',
|
|
129
|
+
seoTitle: normalizeStringField(payload.seoTitle) || '',
|
|
130
|
+
seoDescription: normalizeStringField(payload.seoDescription) || '',
|
|
131
|
+
scheduledAt: null,
|
|
132
|
+
publishedAt: null,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
res.status(201).json({ item: post.toObject() });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Error creating blog post:', error);
|
|
138
|
+
res.status(500).json({ error: 'Failed to create blog post' });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
exports.get = async (req, res) => {
|
|
143
|
+
try {
|
|
144
|
+
const post = await BlogPost.findById(req.params.id).lean();
|
|
145
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
146
|
+
res.json({ item: post });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error getting blog post:', error);
|
|
149
|
+
res.status(500).json({ error: 'Failed to get blog post' });
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
exports.update = async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const post = await BlogPost.findById(req.params.id);
|
|
156
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
157
|
+
|
|
158
|
+
const payload = req.body || {};
|
|
159
|
+
|
|
160
|
+
if (payload.title !== undefined) post.title = normalizeStringField(payload.title) || '';
|
|
161
|
+
if (!post.title) return res.status(400).json({ error: 'title is required' });
|
|
162
|
+
|
|
163
|
+
if (payload.slug !== undefined) {
|
|
164
|
+
const desired = slugify(payload.slug);
|
|
165
|
+
post.slug = desired || (await generateUniqueBlogSlug(post.title, { excludeId: post._id }));
|
|
166
|
+
}
|
|
167
|
+
if (!post.slug) {
|
|
168
|
+
post.slug = await generateUniqueBlogSlug(post.title, { excludeId: post._id });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (payload.markdown !== undefined) post.markdown = String(payload.markdown || '');
|
|
172
|
+
if (payload.html !== undefined) post.html = String(payload.html || '');
|
|
173
|
+
|
|
174
|
+
if (payload.excerpt !== undefined) {
|
|
175
|
+
post.excerpt = normalizeStringField(payload.excerpt) || '';
|
|
176
|
+
}
|
|
177
|
+
if (!post.excerpt) {
|
|
178
|
+
post.excerpt = extractExcerptFromMarkdown(post.markdown);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (payload.coverImageUrl !== undefined) post.coverImageUrl = normalizeStringField(payload.coverImageUrl) || '';
|
|
182
|
+
if (payload.category !== undefined) post.category = normalizeStringField(payload.category) || '';
|
|
183
|
+
if (payload.authorName !== undefined) post.authorName = normalizeStringField(payload.authorName) || '';
|
|
184
|
+
if (payload.seoTitle !== undefined) post.seoTitle = normalizeStringField(payload.seoTitle) || '';
|
|
185
|
+
if (payload.seoDescription !== undefined) post.seoDescription = normalizeStringField(payload.seoDescription) || '';
|
|
186
|
+
if (payload.tags !== undefined) post.tags = normalizeTags(payload.tags);
|
|
187
|
+
|
|
188
|
+
await post.save();
|
|
189
|
+
|
|
190
|
+
res.json({ item: post.toObject() });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Error updating blog post:', error);
|
|
193
|
+
res.status(500).json({ error: 'Failed to update blog post' });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
exports.publish = async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const post = await BlogPost.findById(req.params.id);
|
|
200
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
201
|
+
|
|
202
|
+
post.status = 'published';
|
|
203
|
+
post.scheduledAt = null;
|
|
204
|
+
if (!post.publishedAt) post.publishedAt = new Date();
|
|
205
|
+
await post.save();
|
|
206
|
+
|
|
207
|
+
res.json({ item: post.toObject() });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('Error publishing blog post:', error);
|
|
210
|
+
res.status(500).json({ error: 'Failed to publish blog post' });
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
exports.unpublish = async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const post = await BlogPost.findById(req.params.id);
|
|
217
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
218
|
+
|
|
219
|
+
post.status = 'draft';
|
|
220
|
+
post.scheduledAt = null;
|
|
221
|
+
await post.save();
|
|
222
|
+
|
|
223
|
+
res.json({ item: post.toObject() });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('Error unpublishing blog post:', error);
|
|
226
|
+
res.status(500).json({ error: 'Failed to unpublish blog post' });
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
exports.schedule = async (req, res) => {
|
|
231
|
+
try {
|
|
232
|
+
const post = await BlogPost.findById(req.params.id);
|
|
233
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
234
|
+
|
|
235
|
+
const scheduledAtRaw = req.body?.scheduledAt;
|
|
236
|
+
const scheduledAt = scheduledAtRaw ? new Date(scheduledAtRaw) : null;
|
|
237
|
+
if (!scheduledAt || Number.isNaN(scheduledAt.getTime())) {
|
|
238
|
+
return res.status(400).json({ error: 'scheduledAt is required and must be a valid date' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
post.status = 'scheduled';
|
|
242
|
+
post.scheduledAt = scheduledAt;
|
|
243
|
+
await post.save();
|
|
244
|
+
|
|
245
|
+
res.json({ item: post.toObject() });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('Error scheduling blog post:', error);
|
|
248
|
+
res.status(500).json({ error: 'Failed to schedule blog post' });
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
exports.archive = async (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const post = await BlogPost.findById(req.params.id);
|
|
255
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
256
|
+
|
|
257
|
+
post.status = 'archived';
|
|
258
|
+
post.scheduledAt = null;
|
|
259
|
+
await post.save();
|
|
260
|
+
|
|
261
|
+
res.json({ item: post.toObject() });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('Error archiving blog post:', error);
|
|
264
|
+
res.status(500).json({ error: 'Failed to archive blog post' });
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
exports.remove = async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
const post = await BlogPost.findById(req.params.id);
|
|
271
|
+
if (!post) return res.status(404).json({ error: 'Not found' });
|
|
272
|
+
|
|
273
|
+
await BlogPost.deleteOne({ _id: post._id });
|
|
274
|
+
res.json({ deleted: true });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error('Error deleting blog post:', error);
|
|
277
|
+
res.status(500).json({ error: 'Failed to delete blog post' });
|
|
278
|
+
}
|
|
279
|
+
};
|