@intranefr/superbackend 1.4.4 → 1.5.0
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/index.js +16 -1
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +89 -0
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminScripts.controller.js +229 -0
- 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/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +115 -0
- package/src/models/HeadlessModelDefinition.js +10 -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/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- 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 +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +261 -4
- package/views/partials/dashboard/nav-items.ejs +3 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const ScriptDefinition = require('../models/ScriptDefinition');
|
|
2
|
+
const ScriptRun = require('../models/ScriptRun');
|
|
3
|
+
const { startRun, getRunBus } = require('../services/scriptsRunner.service');
|
|
4
|
+
|
|
5
|
+
function toSafeJsonError(error) {
|
|
6
|
+
const msg = error?.message || 'Operation failed';
|
|
7
|
+
const code = error?.code;
|
|
8
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
9
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
10
|
+
if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
|
|
11
|
+
return { status: 500, body: { error: msg } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeEnv(env) {
|
|
15
|
+
const items = Array.isArray(env) ? env : [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const it of items) {
|
|
18
|
+
if (!it || typeof it !== 'object') continue;
|
|
19
|
+
const key = String(it.key || '').trim();
|
|
20
|
+
if (!key) continue;
|
|
21
|
+
out.push({ key, value: String(it.value || '') });
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
exports.listScripts = async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
|
|
29
|
+
res.json({ items });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const safe = toSafeJsonError(err);
|
|
32
|
+
res.status(safe.status).json(safe.body);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.getScript = async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const doc = await ScriptDefinition.findById(req.params.id).lean();
|
|
39
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
40
|
+
res.json({ item: doc });
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const safe = toSafeJsonError(err);
|
|
43
|
+
res.status(safe.status).json(safe.body);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.createScript = async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const payload = req.body || {};
|
|
50
|
+
|
|
51
|
+
const doc = await ScriptDefinition.create({
|
|
52
|
+
name: String(payload.name || '').trim(),
|
|
53
|
+
codeIdentifier: String(payload.codeIdentifier || '').trim(),
|
|
54
|
+
description: String(payload.description || ''),
|
|
55
|
+
type: String(payload.type || '').trim(),
|
|
56
|
+
runner: String(payload.runner || '').trim(),
|
|
57
|
+
script: String(payload.script || ''),
|
|
58
|
+
defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
|
|
59
|
+
env: normalizeEnv(payload.env),
|
|
60
|
+
timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
|
|
61
|
+
enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
res.status(201).json({ item: doc.toObject() });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const safe = toSafeJsonError(err);
|
|
67
|
+
res.status(safe.status).json(safe.body);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.updateScript = async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const payload = req.body || {};
|
|
74
|
+
|
|
75
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
76
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
77
|
+
|
|
78
|
+
if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
|
|
79
|
+
if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
|
|
80
|
+
if (payload.description !== undefined) doc.description = String(payload.description || '');
|
|
81
|
+
if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
|
|
82
|
+
if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
|
|
83
|
+
if (payload.script !== undefined) doc.script = String(payload.script || '');
|
|
84
|
+
if (payload.defaultWorkingDirectory !== undefined) {
|
|
85
|
+
doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
|
|
86
|
+
}
|
|
87
|
+
if (payload.env !== undefined) doc.env = normalizeEnv(payload.env);
|
|
88
|
+
if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
|
|
89
|
+
if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
|
|
90
|
+
|
|
91
|
+
await doc.save();
|
|
92
|
+
res.json({ item: doc.toObject() });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const safe = toSafeJsonError(err);
|
|
95
|
+
res.status(safe.status).json(safe.body);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
exports.deleteScript = async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
102
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
103
|
+
await doc.deleteOne();
|
|
104
|
+
res.json({ ok: true });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const safe = toSafeJsonError(err);
|
|
107
|
+
res.status(safe.status).json(safe.body);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
exports.runScript = async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
114
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
115
|
+
if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
|
|
116
|
+
|
|
117
|
+
const result = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
|
|
118
|
+
res.json(result);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const safe = toSafeJsonError(err);
|
|
121
|
+
res.status(safe.status).json(safe.body);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
exports.getRun = async (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const run = await ScriptRun.findById(req.params.runId).lean();
|
|
128
|
+
if (!run) return res.status(404).json({ error: 'Not found' });
|
|
129
|
+
res.json({ item: run });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const safe = toSafeJsonError(err);
|
|
132
|
+
res.status(safe.status).json(safe.body);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
exports.listRuns = async (req, res) => {
|
|
137
|
+
try {
|
|
138
|
+
const filter = {};
|
|
139
|
+
if (req.query.scriptId) filter.scriptId = req.query.scriptId;
|
|
140
|
+
|
|
141
|
+
const items = await ScriptRun.find(filter)
|
|
142
|
+
.sort({ createdAt: -1 })
|
|
143
|
+
.limit(50)
|
|
144
|
+
.lean();
|
|
145
|
+
|
|
146
|
+
res.json({ items });
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const safe = toSafeJsonError(err);
|
|
149
|
+
res.status(safe.status).json(safe.body);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
exports.streamRun = async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const runId = String(req.params.runId);
|
|
156
|
+
|
|
157
|
+
res.status(200);
|
|
158
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
159
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
160
|
+
res.setHeader('Connection', 'keep-alive');
|
|
161
|
+
|
|
162
|
+
const bus = getRunBus(runId);
|
|
163
|
+
|
|
164
|
+
const since = Number(req.query.since || 0);
|
|
165
|
+
if (bus) {
|
|
166
|
+
const existing = bus.snapshot(since);
|
|
167
|
+
for (const e of existing) {
|
|
168
|
+
res.write(`event: ${e.type}\n`);
|
|
169
|
+
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const onEvent = (e) => {
|
|
173
|
+
res.write(`event: ${e.type}\n`);
|
|
174
|
+
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
175
|
+
};
|
|
176
|
+
const cleanup = () => {
|
|
177
|
+
clearInterval(heartbeat);
|
|
178
|
+
bus.emitter.off('event', onEvent);
|
|
179
|
+
bus.emitter.off('close', onClose);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const onClose = () => {
|
|
183
|
+
cleanup();
|
|
184
|
+
res.end();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const heartbeat = setInterval(() => {
|
|
188
|
+
res.write(`: ping\n\n`);
|
|
189
|
+
}, 15000);
|
|
190
|
+
heartbeat.unref();
|
|
191
|
+
|
|
192
|
+
bus.emitter.on('event', onEvent);
|
|
193
|
+
bus.emitter.once('close', onClose);
|
|
194
|
+
|
|
195
|
+
req.on('close', () => {
|
|
196
|
+
cleanup();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const run = await ScriptRun.findById(runId).lean();
|
|
203
|
+
if (!run) {
|
|
204
|
+
res.write(`event: error\n`);
|
|
205
|
+
res.write(`data: ${JSON.stringify({ error: 'Not found' })}\n\n`);
|
|
206
|
+
return res.end();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (run.outputTail) {
|
|
210
|
+
res.write(`event: log\n`);
|
|
211
|
+
res.write(
|
|
212
|
+
`data: ${JSON.stringify({ seq: 1, type: 'log', ts: new Date().toISOString(), stream: 'stdout', line: run.outputTail })}\n\n`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
res.write(`event: status\n`);
|
|
216
|
+
res.write(
|
|
217
|
+
`data: ${JSON.stringify({ seq: 2, type: 'status', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
|
|
218
|
+
);
|
|
219
|
+
res.write(`event: done\n`);
|
|
220
|
+
res.write(
|
|
221
|
+
`data: ${JSON.stringify({ seq: 3, type: 'done', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
|
|
222
|
+
);
|
|
223
|
+
return res.end();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
res.write(`event: error\n`);
|
|
226
|
+
res.write(`data: ${JSON.stringify({ error: err?.message || 'Stream error' })}\n\n`);
|
|
227
|
+
return res.end();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createSession,
|
|
3
|
+
listSessions,
|
|
4
|
+
killSession,
|
|
5
|
+
} = require('../services/terminals.service');
|
|
6
|
+
|
|
7
|
+
function handleError(res, err) {
|
|
8
|
+
const msg = err?.message || 'Operation failed';
|
|
9
|
+
const code = err?.code;
|
|
10
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: msg });
|
|
11
|
+
if (code === 'LIMIT') return res.status(429).json({ error: msg });
|
|
12
|
+
return res.status(500).json({ error: msg });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.createSession = async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const { cols, rows } = req.body || {};
|
|
18
|
+
const result = createSession({ cols, rows });
|
|
19
|
+
res.json(result);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
handleError(res, err);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
exports.listSessions = async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
res.json({ items: listSessions() });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
handleError(res, err);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.killSession = async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
res.json(killSession(req.params.sessionId));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
handleError(res, err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -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
|
+
};
|