@intranefr/superbackend 1.5.3 → 1.6.3
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/cookies.txt +6 -0
- package/cookies1.txt +6 -0
- package/cookies2.txt +6 -0
- package/cookies3.txt +6 -0
- package/cookies4.txt +5 -0
- package/cookies_old.txt +5 -0
- package/cookies_old_test.txt +6 -0
- package/cookies_super.txt +5 -0
- package/cookies_super_test.txt +6 -0
- package/cookies_test.txt +6 -0
- package/index.js +7 -0
- package/package.json +3 -1
- package/plugins/core-waiting-list-migration/README.md +118 -0
- package/plugins/core-waiting-list-migration/index.js +438 -0
- package/plugins/global-settings-presets/index.js +20 -0
- package/plugins/hello-cli/index.js +17 -0
- package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
- package/plugins/ui-components-seeder/components/suiToast.js +186 -0
- package/plugins/ui-components-seeder/index.js +31 -0
- package/public/js/admin-ui-components-preview.js +281 -0
- package/public/js/admin-ui-components.js +408 -0
- package/public/js/llm-provider-model-picker.js +193 -0
- package/public/test-iframe-fix.html +63 -0
- package/public/test-iframe.html +14 -0
- package/src/admin/endpointRegistry.js +68 -0
- package/src/controllers/admin.controller.js +25 -5
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +0 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +454 -153
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/UiComponent.js +2 -0
- package/src/models/User.js +1 -1
- package/src/routes/admin.routes.js +3 -3
- package/src/routes/adminAgents.routes.js +2 -2
- package/src/routes/adminAssets.routes.js +11 -11
- package/src/routes/adminBlog.routes.js +2 -2
- package/src/routes/adminBlogAi.routes.js +2 -2
- package/src/routes/adminBlogAutomation.routes.js +2 -2
- package/src/routes/adminCache.routes.js +2 -2
- package/src/routes/adminConsoleManager.routes.js +2 -2
- package/src/routes/adminCrons.routes.js +2 -2
- package/src/routes/adminDataCleanup.routes.js +26 -0
- package/src/routes/adminDbBrowser.routes.js +2 -2
- package/src/routes/adminEjsVirtual.routes.js +2 -2
- package/src/routes/adminFeatureFlags.routes.js +6 -6
- package/src/routes/adminHeadless.routes.js +2 -2
- package/src/routes/adminHealthChecks.routes.js +2 -2
- package/src/routes/adminI18n.routes.js +2 -2
- package/src/routes/adminJsonConfigs.routes.js +8 -8
- package/src/routes/adminLlm.routes.js +8 -8
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +3 -9
- package/src/routes/adminMigration.routes.js +12 -12
- package/src/routes/adminPages.routes.js +2 -2
- package/src/routes/adminPlugins.routes.js +15 -0
- package/src/routes/adminProxy.routes.js +2 -2
- package/src/routes/adminRateLimits.routes.js +8 -8
- package/src/routes/adminRbac.routes.js +2 -2
- package/src/routes/adminRegistry.routes.js +24 -0
- package/src/routes/adminScripts.routes.js +2 -2
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +2 -2
- package/src/routes/adminTerminals.routes.js +2 -2
- package/src/routes/adminUiComponents.routes.js +2 -2
- package/src/routes/adminUploadNamespaces.routes.js +7 -7
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +2 -2
- package/src/routes/formsAdmin.routes.js +6 -6
- package/src/routes/globalSettings.routes.js +8 -8
- package/src/routes/internalExperiments.routes.js +2 -2
- package/src/routes/notificationAdmin.routes.js +7 -7
- package/src/routes/orgAdmin.routes.js +16 -16
- package/src/routes/pages.routes.js +3 -3
- package/src/routes/registry.routes.js +11 -0
- package/src/routes/stripeAdmin.routes.js +12 -12
- package/src/routes/userAdmin.routes.js +7 -7
- package/src/routes/waitingListAdmin.routes.js +2 -2
- package/src/routes/workflows.routes.js +3 -3
- package/src/services/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +262 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/uiComponents.service.js +180 -0
- package/src/services/waitingListJson.service.js +401 -0
- package/src/utils/rbac/rightsRegistry.js +118 -0
- package/test-access.js +63 -0
- package/test-iframe-fix.html +63 -0
- package/test-iframe.html +14 -0
- package/views/admin-403.ejs +92 -0
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +143 -2
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-login.ejs +286 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +2 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const JsonConfig = require('../models/JsonConfig');
|
|
4
|
+
const { parseJsonOrThrow, clearJsonConfigCache } = require('./jsonConfigs.service');
|
|
5
|
+
|
|
6
|
+
const REGISTRY_CONFIG_KEY = 'open-registry-registries';
|
|
7
|
+
|
|
8
|
+
function sha256(value) {
|
|
9
|
+
return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function nowIso() {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeRegistryId(id) {
|
|
17
|
+
return String(id || '').trim().toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeItem(item = {}) {
|
|
21
|
+
const createdAt = item.created_at || nowIso();
|
|
22
|
+
const updatedAt = nowIso();
|
|
23
|
+
const versions = Array.isArray(item.versions)
|
|
24
|
+
? item.versions.map((v) => Number(v)).filter((v) => Number.isInteger(v) && v > 0)
|
|
25
|
+
: [];
|
|
26
|
+
const latestVersion = Number(item.version);
|
|
27
|
+
if (Number.isInteger(latestVersion) && latestVersion > 0 && !versions.includes(latestVersion)) {
|
|
28
|
+
versions.push(latestVersion);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const safeVersions = Array.from(new Set(versions)).sort((a, b) => a - b);
|
|
32
|
+
const finalVersion = safeVersions.length > 0 ? safeVersions[safeVersions.length - 1] : 1;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: String(item.id || '').trim(),
|
|
36
|
+
name: String(item.name || item.id || '').trim(),
|
|
37
|
+
category: String(item.category || 'general').trim(),
|
|
38
|
+
version: finalVersion,
|
|
39
|
+
versions: safeVersions.length > 0 ? safeVersions : [finalVersion],
|
|
40
|
+
description: String(item.description || '').trim(),
|
|
41
|
+
public: item.public !== false,
|
|
42
|
+
tags: Array.isArray(item.tags) ? item.tags.map((t) => String(t).trim()).filter(Boolean) : [],
|
|
43
|
+
created_at: createdAt,
|
|
44
|
+
updated_at: updatedAt,
|
|
45
|
+
metadata: item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata)
|
|
46
|
+
? item.metadata
|
|
47
|
+
: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function ensureRegistryConfigDoc() {
|
|
52
|
+
const existing = await JsonConfig.findOne({
|
|
53
|
+
$or: [{ slug: REGISTRY_CONFIG_KEY }, { alias: REGISTRY_CONFIG_KEY }],
|
|
54
|
+
});
|
|
55
|
+
if (existing) return existing;
|
|
56
|
+
|
|
57
|
+
const payload = {
|
|
58
|
+
version: 1,
|
|
59
|
+
registries: {},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const doc = await JsonConfig.create({
|
|
63
|
+
title: 'Open Registry Registries',
|
|
64
|
+
slug: REGISTRY_CONFIG_KEY,
|
|
65
|
+
alias: REGISTRY_CONFIG_KEY,
|
|
66
|
+
publicEnabled: false,
|
|
67
|
+
cacheTtlSeconds: 0,
|
|
68
|
+
jsonRaw: JSON.stringify(payload, null, 2),
|
|
69
|
+
jsonHash: sha256(JSON.stringify(payload)),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
clearJsonConfigCache(REGISTRY_CONFIG_KEY);
|
|
73
|
+
return doc;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getConfig() {
|
|
77
|
+
const doc = await ensureRegistryConfigDoc();
|
|
78
|
+
const data = parseJsonOrThrow(String(doc.jsonRaw || '{}'));
|
|
79
|
+
if (!data.registries || typeof data.registries !== 'object') {
|
|
80
|
+
data.registries = {};
|
|
81
|
+
}
|
|
82
|
+
return { doc, data };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function saveConfig(doc, data) {
|
|
86
|
+
doc.jsonRaw = JSON.stringify(data, null, 2);
|
|
87
|
+
doc.jsonHash = sha256(doc.jsonRaw);
|
|
88
|
+
await doc.save();
|
|
89
|
+
clearJsonConfigCache(REGISTRY_CONFIG_KEY);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sanitizeRegistryResponse(registry) {
|
|
93
|
+
return {
|
|
94
|
+
id: registry.id,
|
|
95
|
+
name: registry.name,
|
|
96
|
+
description: registry.description,
|
|
97
|
+
public: registry.public !== false,
|
|
98
|
+
categories: Array.isArray(registry.categories) ? registry.categories : [],
|
|
99
|
+
protocol_version: registry.protocol_version || '1.1.0',
|
|
100
|
+
version: registry.version || '1.0.0',
|
|
101
|
+
created_at: registry.created_at,
|
|
102
|
+
updated_at: registry.updated_at,
|
|
103
|
+
items_count: Object.keys(registry.items || {}).length,
|
|
104
|
+
tokens_count: Array.isArray(registry.tokens) ? registry.tokens.length : 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function ensureRegistry(registryInput) {
|
|
109
|
+
const { doc, data } = await getConfig();
|
|
110
|
+
const id = normalizeRegistryId(registryInput.id);
|
|
111
|
+
if (!id) throw new Error('registry id is required');
|
|
112
|
+
|
|
113
|
+
if (data.registries[id]) {
|
|
114
|
+
return data.registries[id];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const created = nowIso();
|
|
118
|
+
data.registries[id] = {
|
|
119
|
+
id,
|
|
120
|
+
name: String(registryInput.name || id),
|
|
121
|
+
description: String(registryInput.description || ''),
|
|
122
|
+
public: registryInput.public === true,
|
|
123
|
+
categories: Array.isArray(registryInput.categories) && registryInput.categories.length > 0
|
|
124
|
+
? registryInput.categories.map((c) => String(c).trim()).filter(Boolean)
|
|
125
|
+
: ['plugins'],
|
|
126
|
+
protocol_version: '1.1.0',
|
|
127
|
+
version: String(registryInput.version || '1.0.0'),
|
|
128
|
+
items: {},
|
|
129
|
+
tokens: [],
|
|
130
|
+
created_at: created,
|
|
131
|
+
updated_at: created,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await saveConfig(doc, data);
|
|
135
|
+
return data.registries[id];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function listRegistries() {
|
|
139
|
+
const { data } = await getConfig();
|
|
140
|
+
return Object.values(data.registries || {}).map(sanitizeRegistryResponse);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function getRegistry(registryId) {
|
|
144
|
+
const { data } = await getConfig();
|
|
145
|
+
return data.registries[normalizeRegistryId(registryId)] || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function createRegistry(payload) {
|
|
149
|
+
const { doc, data } = await getConfig();
|
|
150
|
+
const id = normalizeRegistryId(payload.id);
|
|
151
|
+
if (!id) {
|
|
152
|
+
const err = new Error('id is required');
|
|
153
|
+
err.code = 'VALIDATION';
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
if (data.registries[id]) {
|
|
157
|
+
const err = new Error('registry already exists');
|
|
158
|
+
err.code = 'CONFLICT';
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const created = nowIso();
|
|
163
|
+
data.registries[id] = {
|
|
164
|
+
id,
|
|
165
|
+
name: String(payload.name || id),
|
|
166
|
+
description: String(payload.description || ''),
|
|
167
|
+
public: payload.public === true,
|
|
168
|
+
categories: Array.isArray(payload.categories) && payload.categories.length > 0
|
|
169
|
+
? payload.categories.map((c) => String(c).trim()).filter(Boolean)
|
|
170
|
+
: ['general'],
|
|
171
|
+
protocol_version: '1.1.0',
|
|
172
|
+
version: String(payload.version || '1.0.0'),
|
|
173
|
+
items: {},
|
|
174
|
+
tokens: [],
|
|
175
|
+
created_at: created,
|
|
176
|
+
updated_at: created,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await saveConfig(doc, data);
|
|
180
|
+
return sanitizeRegistryResponse(data.registries[id]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function updateRegistry(registryId, patch) {
|
|
184
|
+
const { doc, data } = await getConfig();
|
|
185
|
+
const id = normalizeRegistryId(registryId);
|
|
186
|
+
const registry = data.registries[id];
|
|
187
|
+
if (!registry) {
|
|
188
|
+
const err = new Error('registry not found');
|
|
189
|
+
err.code = 'NOT_FOUND';
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (patch.name !== undefined) registry.name = String(patch.name || '').trim() || registry.name;
|
|
194
|
+
if (patch.description !== undefined) registry.description = String(patch.description || '');
|
|
195
|
+
if (patch.public !== undefined) registry.public = Boolean(patch.public);
|
|
196
|
+
if (patch.categories !== undefined && Array.isArray(patch.categories)) {
|
|
197
|
+
registry.categories = patch.categories.map((c) => String(c).trim()).filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
registry.updated_at = nowIso();
|
|
200
|
+
|
|
201
|
+
await saveConfig(doc, data);
|
|
202
|
+
return sanitizeRegistryResponse(registry);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function deleteRegistry(registryId) {
|
|
206
|
+
const { doc, data } = await getConfig();
|
|
207
|
+
const id = normalizeRegistryId(registryId);
|
|
208
|
+
if (!data.registries[id]) {
|
|
209
|
+
const err = new Error('registry not found');
|
|
210
|
+
err.code = 'NOT_FOUND';
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
delete data.registries[id];
|
|
214
|
+
await saveConfig(doc, data);
|
|
215
|
+
return { success: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function makeTokenValue() {
|
|
219
|
+
return crypto.randomBytes(24).toString('hex');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function createToken(registryId, payload = {}) {
|
|
223
|
+
const { doc, data } = await getConfig();
|
|
224
|
+
const id = normalizeRegistryId(registryId);
|
|
225
|
+
const registry = data.registries[id];
|
|
226
|
+
if (!registry) {
|
|
227
|
+
const err = new Error('registry not found');
|
|
228
|
+
err.code = 'NOT_FOUND';
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tokenValue = makeTokenValue();
|
|
233
|
+
const token = {
|
|
234
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
235
|
+
name: String(payload.name || 'token'),
|
|
236
|
+
token_hash: sha256(tokenValue),
|
|
237
|
+
scopes: Array.isArray(payload.scopes) && payload.scopes.length > 0 ? payload.scopes : ['read'],
|
|
238
|
+
enabled: payload.enabled !== false,
|
|
239
|
+
created_at: nowIso(),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
registry.tokens = Array.isArray(registry.tokens) ? registry.tokens : [];
|
|
243
|
+
registry.tokens.push(token);
|
|
244
|
+
registry.updated_at = nowIso();
|
|
245
|
+
|
|
246
|
+
await saveConfig(doc, data);
|
|
247
|
+
return {
|
|
248
|
+
token: { ...token, token_hash: undefined },
|
|
249
|
+
tokenValue,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function deleteToken(registryId, tokenId) {
|
|
254
|
+
const { doc, data } = await getConfig();
|
|
255
|
+
const id = normalizeRegistryId(registryId);
|
|
256
|
+
const registry = data.registries[id];
|
|
257
|
+
if (!registry) {
|
|
258
|
+
const err = new Error('registry not found');
|
|
259
|
+
err.code = 'NOT_FOUND';
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const before = registry.tokens?.length || 0;
|
|
264
|
+
registry.tokens = (registry.tokens || []).filter((t) => t.id !== tokenId);
|
|
265
|
+
if ((registry.tokens || []).length === before) {
|
|
266
|
+
const err = new Error('token not found');
|
|
267
|
+
err.code = 'NOT_FOUND';
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
registry.updated_at = nowIso();
|
|
271
|
+
await saveConfig(doc, data);
|
|
272
|
+
return { success: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function hasValidToken(registry, authHeader) {
|
|
276
|
+
const expectedPrefix = 'Bearer ';
|
|
277
|
+
if (!authHeader || !String(authHeader).startsWith(expectedPrefix)) return false;
|
|
278
|
+
const raw = String(authHeader).slice(expectedPrefix.length).trim();
|
|
279
|
+
if (!raw) return false;
|
|
280
|
+
const hash = sha256(raw);
|
|
281
|
+
return (registry.tokens || []).some((token) => token.enabled !== false && token.token_hash === hash);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function applyListQuery(items, query = {}, includePrivate = false) {
|
|
285
|
+
const category = query.category ? String(query.category) : null;
|
|
286
|
+
const version = query.version !== undefined ? String(query.version) : null;
|
|
287
|
+
const minimal = String(query.minimal || 'false') === 'true';
|
|
288
|
+
const filter = query.filter ? String(query.filter).toLowerCase() : '';
|
|
289
|
+
|
|
290
|
+
let rows = items.filter((item) => includePrivate || item.public !== false);
|
|
291
|
+
|
|
292
|
+
if (category) rows = rows.filter((item) => item.category === category);
|
|
293
|
+
if (filter) {
|
|
294
|
+
rows = rows.filter((item) => {
|
|
295
|
+
const target = `${item.id} ${item.name} ${item.description} ${(item.tags || []).join(' ')}`.toLowerCase();
|
|
296
|
+
return target.includes(filter);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (version === 'latest') {
|
|
301
|
+
rows = rows.map((item) => ({ ...item, version: Math.max(...(item.versions || [item.version])) }));
|
|
302
|
+
} else if (version && Number.isInteger(Number(version))) {
|
|
303
|
+
const v = Number(version);
|
|
304
|
+
rows = rows.filter((item) => (item.versions || []).includes(v));
|
|
305
|
+
rows = rows.map((item) => ({ ...item, version: v }));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (minimal) {
|
|
309
|
+
rows = rows.map(({ metadata, ...rest }) => rest);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const page = Math.max(1, Number(query.page || 1));
|
|
313
|
+
const limit = Math.min(100, Math.max(1, Number(query.limit || 20)));
|
|
314
|
+
const totalItems = rows.length;
|
|
315
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / limit));
|
|
316
|
+
const start = (page - 1) * limit;
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
pagination: {
|
|
320
|
+
page,
|
|
321
|
+
limit,
|
|
322
|
+
total_items: totalItems,
|
|
323
|
+
total_pages: totalPages,
|
|
324
|
+
has_next: page < totalPages,
|
|
325
|
+
has_prev: page > 1,
|
|
326
|
+
category,
|
|
327
|
+
version: version || null,
|
|
328
|
+
},
|
|
329
|
+
items: rows.slice(start, start + limit),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function listItemsForRegistry(registryId, query = {}, authHeader) {
|
|
334
|
+
const registry = await getRegistry(registryId);
|
|
335
|
+
if (!registry) {
|
|
336
|
+
const err = new Error('registry not found');
|
|
337
|
+
err.code = 'NOT_FOUND';
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const canReadPrivate = registry.public === true ? true : hasValidToken(registry, authHeader);
|
|
342
|
+
const items = Object.values(registry.items || {});
|
|
343
|
+
const result = applyListQuery(items, query, canReadPrivate);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
registry: {
|
|
347
|
+
name: registry.name,
|
|
348
|
+
version: registry.version || '1.0.0',
|
|
349
|
+
description: registry.description,
|
|
350
|
+
categories: registry.categories || [],
|
|
351
|
+
protocol_version: registry.protocol_version || '1.1.0',
|
|
352
|
+
},
|
|
353
|
+
pagination: result.pagination,
|
|
354
|
+
items: result.items,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function getAuthStatus(registryId, authHeader) {
|
|
359
|
+
const registry = await getRegistry(registryId);
|
|
360
|
+
if (!registry) {
|
|
361
|
+
const err = new Error('registry not found');
|
|
362
|
+
err.code = 'NOT_FOUND';
|
|
363
|
+
throw err;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (registry.public === true) {
|
|
367
|
+
return {
|
|
368
|
+
public: true,
|
|
369
|
+
requires_auth: false,
|
|
370
|
+
auth_type: 'none',
|
|
371
|
+
scope: 'read',
|
|
372
|
+
message: 'This registry is publicly accessible',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const valid = hasValidToken(registry, authHeader);
|
|
377
|
+
return {
|
|
378
|
+
public: false,
|
|
379
|
+
requires_auth: true,
|
|
380
|
+
auth_type: 'bearer',
|
|
381
|
+
scope: valid ? 'read' : 'none',
|
|
382
|
+
message: valid ? 'Authenticated access granted' : 'Token invalid or expired',
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function upsertItem(registryId, itemPayload) {
|
|
387
|
+
const { doc, data } = await getConfig();
|
|
388
|
+
const id = normalizeRegistryId(registryId);
|
|
389
|
+
const registry = data.registries[id];
|
|
390
|
+
if (!registry) {
|
|
391
|
+
const err = new Error('registry not found');
|
|
392
|
+
err.code = 'NOT_FOUND';
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const normalized = normalizeItem(itemPayload);
|
|
397
|
+
if (!normalized.id) {
|
|
398
|
+
const err = new Error('item id is required');
|
|
399
|
+
err.code = 'VALIDATION';
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const existing = registry.items?.[normalized.id];
|
|
404
|
+
if (existing) {
|
|
405
|
+
normalized.created_at = existing.created_at || normalized.created_at;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
registry.items = registry.items || {};
|
|
409
|
+
registry.items[normalized.id] = normalized;
|
|
410
|
+
registry.updated_at = nowIso();
|
|
411
|
+
await saveConfig(doc, data);
|
|
412
|
+
return normalized;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function deleteItem(registryId, itemId) {
|
|
416
|
+
const { doc, data } = await getConfig();
|
|
417
|
+
const id = normalizeRegistryId(registryId);
|
|
418
|
+
const registry = data.registries[id];
|
|
419
|
+
if (!registry) {
|
|
420
|
+
const err = new Error('registry not found');
|
|
421
|
+
err.code = 'NOT_FOUND';
|
|
422
|
+
throw err;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
registry.items = registry.items || {};
|
|
426
|
+
if (!registry.items[itemId]) {
|
|
427
|
+
const err = new Error('item not found');
|
|
428
|
+
err.code = 'NOT_FOUND';
|
|
429
|
+
throw err;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
delete registry.items[itemId];
|
|
433
|
+
registry.updated_at = nowIso();
|
|
434
|
+
await saveConfig(doc, data);
|
|
435
|
+
return { success: true };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
REGISTRY_CONFIG_KEY,
|
|
440
|
+
ensureRegistry,
|
|
441
|
+
listRegistries,
|
|
442
|
+
getRegistry,
|
|
443
|
+
createRegistry,
|
|
444
|
+
updateRegistry,
|
|
445
|
+
deleteRegistry,
|
|
446
|
+
createToken,
|
|
447
|
+
deleteToken,
|
|
448
|
+
listItemsForRegistry,
|
|
449
|
+
getAuthStatus,
|
|
450
|
+
upsertItem,
|
|
451
|
+
deleteItem,
|
|
452
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const UiComponent = require('../models/UiComponent');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UI Components Service
|
|
5
|
+
* Provides service layer for UI Components operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class UiComponentsService {
|
|
9
|
+
/**
|
|
10
|
+
* Upsert a UI component (create or update)
|
|
11
|
+
* @param {Object} componentData - Component data
|
|
12
|
+
* @param {string} componentData.code - Unique component code
|
|
13
|
+
* @param {string} componentData.name - Component display name
|
|
14
|
+
* @param {string} componentData.html - HTML template
|
|
15
|
+
* @param {string} componentData.css - CSS styles
|
|
16
|
+
* @param {string} componentData.js - JavaScript code
|
|
17
|
+
* @param {string} componentData.usageMarkdown - Usage documentation
|
|
18
|
+
* @param {string} componentData.api - API summary
|
|
19
|
+
* @param {number} componentData.version - Component version
|
|
20
|
+
* @param {boolean} componentData.isActive - Whether component is active
|
|
21
|
+
* @returns {Promise<Object>} The created/updated component
|
|
22
|
+
*/
|
|
23
|
+
async upsertComponent(componentData) {
|
|
24
|
+
try {
|
|
25
|
+
const {
|
|
26
|
+
code,
|
|
27
|
+
name,
|
|
28
|
+
html = '',
|
|
29
|
+
css = '',
|
|
30
|
+
js = '',
|
|
31
|
+
usageMarkdown = '',
|
|
32
|
+
api = null,
|
|
33
|
+
version = 1,
|
|
34
|
+
isActive = true,
|
|
35
|
+
previewExample = null,
|
|
36
|
+
} = componentData;
|
|
37
|
+
|
|
38
|
+
// Validate required fields
|
|
39
|
+
if (!code || typeof code !== 'string') {
|
|
40
|
+
throw new Error('code is required and must be a string');
|
|
41
|
+
}
|
|
42
|
+
if (!name || typeof name !== 'string') {
|
|
43
|
+
throw new Error('name is required and must be a string');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Normalize code to lowercase
|
|
47
|
+
const normalizedCode = String(code).trim().toLowerCase();
|
|
48
|
+
|
|
49
|
+
// Check if component exists
|
|
50
|
+
const existing = await UiComponent.findOne({ code: normalizedCode });
|
|
51
|
+
|
|
52
|
+
if (existing) {
|
|
53
|
+
// Update existing component
|
|
54
|
+
const updateData = {
|
|
55
|
+
name: String(name).trim(),
|
|
56
|
+
html: String(html),
|
|
57
|
+
css: String(css),
|
|
58
|
+
js: String(js),
|
|
59
|
+
usageMarkdown: String(usageMarkdown),
|
|
60
|
+
api,
|
|
61
|
+
version: Number(version) || 1,
|
|
62
|
+
isActive: Boolean(isActive),
|
|
63
|
+
updatedAt: new Date(),
|
|
64
|
+
previewExample,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const updated = await UiComponent.findOneAndUpdate(
|
|
68
|
+
{ code: normalizedCode },
|
|
69
|
+
updateData,
|
|
70
|
+
{ new: true, runValidators: true }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
console.log(`[uiComponents] Updated component: ${normalizedCode}`);
|
|
74
|
+
return updated.toObject();
|
|
75
|
+
} else {
|
|
76
|
+
// Create new component
|
|
77
|
+
const createData = {
|
|
78
|
+
code: normalizedCode,
|
|
79
|
+
name: String(name).trim(),
|
|
80
|
+
html: String(html),
|
|
81
|
+
css: String(css),
|
|
82
|
+
js: String(js),
|
|
83
|
+
usageMarkdown: String(usageMarkdown),
|
|
84
|
+
api,
|
|
85
|
+
version: Number(version) || 1,
|
|
86
|
+
isActive: Boolean(isActive),
|
|
87
|
+
previewExample,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const created = await UiComponent.create(createData);
|
|
91
|
+
console.log(`[uiComponents] Created component: ${normalizedCode}`);
|
|
92
|
+
return created.toObject();
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[uiComponents] Failed to upsert component ${componentData.code}:`, error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a component by code
|
|
102
|
+
* @param {string} code - Component code
|
|
103
|
+
* @returns {Promise<Object|null>} Component data or null if not found
|
|
104
|
+
*/
|
|
105
|
+
async getComponent(code) {
|
|
106
|
+
try {
|
|
107
|
+
const component = await UiComponent.findOne({
|
|
108
|
+
code: String(code).trim().toLowerCase()
|
|
109
|
+
}).lean();
|
|
110
|
+
return component;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`[uiComponents] Failed to get component ${code}:`, error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List all components
|
|
119
|
+
* @param {Object} options - Query options
|
|
120
|
+
* @param {boolean} options.activeOnly - Only return active components
|
|
121
|
+
* @returns {Promise<Array>} Array of components
|
|
122
|
+
*/
|
|
123
|
+
async listComponents(options = {}) {
|
|
124
|
+
try {
|
|
125
|
+
const { activeOnly = false } = options;
|
|
126
|
+
const query = activeOnly ? { isActive: true } : {};
|
|
127
|
+
|
|
128
|
+
const components = await UiComponent.find(query)
|
|
129
|
+
.sort({ updatedAt: -1 })
|
|
130
|
+
.lean();
|
|
131
|
+
|
|
132
|
+
return components;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[uiComponents] Failed to list components:', error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Delete a component by code
|
|
141
|
+
* @param {string} code - Component code
|
|
142
|
+
* @returns {Promise<boolean>} True if deleted, false if not found
|
|
143
|
+
*/
|
|
144
|
+
async deleteComponent(code) {
|
|
145
|
+
try {
|
|
146
|
+
const result = await UiComponent.deleteOne({
|
|
147
|
+
code: String(code).trim().toLowerCase()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (result.deletedCount > 0) {
|
|
151
|
+
console.log(`[uiComponents] Deleted component: ${code}`);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`[uiComponents] Failed to delete component ${code}:`, error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if component exists
|
|
164
|
+
* @param {string} code - Component code
|
|
165
|
+
* @returns {Promise<boolean>} True if component exists
|
|
166
|
+
*/
|
|
167
|
+
async componentExists(code) {
|
|
168
|
+
try {
|
|
169
|
+
const count = await UiComponent.countDocuments({
|
|
170
|
+
code: String(code).trim().toLowerCase()
|
|
171
|
+
});
|
|
172
|
+
return count > 0;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(`[uiComponents] Failed to check component existence ${code}:`, error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = new UiComponentsService();
|