@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
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const express = require("express");
|
|
|
11
11
|
* @returns {express.Router} Configured Express router
|
|
12
12
|
*/
|
|
13
13
|
const middleware = require("./src/middleware");
|
|
14
|
+
const { attachTerminalWebsocketServer } = require('./src/services/terminalsWs.service');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Creates and starts a standalone SuperBackend server
|
|
@@ -24,13 +25,26 @@ function startServer(options = {}) {
|
|
|
24
25
|
const app = express();
|
|
25
26
|
const PORT = options.port || process.env.PORT || 3000;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
const router = module.exports.middleware(options);
|
|
29
|
+
app.use(router);
|
|
28
30
|
|
|
29
31
|
// Start server
|
|
30
32
|
const server = app.listen(PORT, () => {
|
|
31
33
|
console.log(`🚀 SuperBackend standalone server running on http://localhost:${PORT}`);
|
|
32
34
|
});
|
|
33
35
|
|
|
36
|
+
// Attach WebSocket server via middleware helper or directly
|
|
37
|
+
console.log('[Index] Attaching WebSocket server...');
|
|
38
|
+
if (typeof router.attachWs === 'function') {
|
|
39
|
+
console.log('[Index] Using router.attachWs');
|
|
40
|
+
router.attachWs(server);
|
|
41
|
+
} else {
|
|
42
|
+
// Fallback: attach directly with admin path
|
|
43
|
+
const adminPath = router.adminPath || '/admin';
|
|
44
|
+
console.log('[Index] Using fallback attach with adminPath:', adminPath);
|
|
45
|
+
attachTerminalWebsocketServer(server, { basePathPrefix: adminPath });
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
return { app, server };
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -91,6 +105,7 @@ const saasbackend = {
|
|
|
91
105
|
org: require("./src/middleware/org"),
|
|
92
106
|
i18n: require("./src/services/i18n.service"),
|
|
93
107
|
jsonConfigs: require("./src/services/jsonConfigs.service"),
|
|
108
|
+
terminals: require("./src/services/terminalsWs.service"),
|
|
94
109
|
},
|
|
95
110
|
};
|
|
96
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intranefr/superbackend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Node.js middleware that gives your project backend superpowers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"start:minio": "docker compose -f compose.standalone.yml --profile minio-only up -d minio",
|
|
10
10
|
"minio:envs": "node -e \"console.log(['S3_ENDPOINT=http://localhost:9000','S3_REGION=us-east-1','S3_ACCESS_KEY_ID=minioadmin','S3_SECRET_ACCESS_KEY=minioadmin','S3_BUCKET=saasbackend','S3_FORCE_PATH_STYLE=true'].join('\\n'))\"",
|
|
11
11
|
"build:sdk:error-tracking:browser": "esbuild sdk/error-tracking/browser/src/embed.js --bundle --format=iife --global-name=saasbackendErrorTrackingEmbed --outfile=sdk/error-tracking/browser/dist/embed.iife.js",
|
|
12
|
+
"build:sdk:ui-components:browser": "esbuild sdk/ui-components/browser/src/index.js --bundle --format=iife --outfile=public/sdk/ui-components.iife.js",
|
|
12
13
|
"test": "jest",
|
|
13
14
|
"test:watch": "jest --watch",
|
|
14
15
|
"test:coverage": "jest --coverage"
|
|
@@ -41,11 +42,13 @@
|
|
|
41
42
|
"jsonwebtoken": "^9.0.2",
|
|
42
43
|
"mongoose": "^8.0.0",
|
|
43
44
|
"multer": "^1.4.5-lts.1",
|
|
45
|
+
"node-pty": "^1.1.0",
|
|
44
46
|
"openai": "^4.0.0",
|
|
45
47
|
"resend": "^6.4.0",
|
|
46
48
|
"ssh2-sftp-client": "^12.0.1",
|
|
47
49
|
"stripe": "^14.0.0",
|
|
48
|
-
"vm2": "^3.10.0"
|
|
50
|
+
"vm2": "^3.10.0",
|
|
51
|
+
"ws": "^8.18.0"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"esbuild": "^0.25.0",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// sdk/ui-components/browser/src/index.js
|
|
3
|
+
(function() {
|
|
4
|
+
function toStr(v) {
|
|
5
|
+
return v === void 0 || v === null ? "" : String(v);
|
|
6
|
+
}
|
|
7
|
+
function normalizeApiUrl(apiUrl) {
|
|
8
|
+
const u = toStr(apiUrl).trim();
|
|
9
|
+
if (!u) return "";
|
|
10
|
+
return u.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers["x-project-key"] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
async function fetchJson(url, headers) {
|
|
19
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
let data = null;
|
|
22
|
+
try {
|
|
23
|
+
data = text ? JSON.parse(text) : null;
|
|
24
|
+
} catch {
|
|
25
|
+
data = null;
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const msg = data && data.error ? data.error : "Request failed";
|
|
29
|
+
const err = new Error(msg);
|
|
30
|
+
err.status = res.status;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
function ensureTemplate(code, html) {
|
|
36
|
+
const id = "ui-cmp-" + code;
|
|
37
|
+
let tpl = document.getElementById(id);
|
|
38
|
+
if (!tpl) {
|
|
39
|
+
tpl = document.createElement("template");
|
|
40
|
+
tpl.id = id;
|
|
41
|
+
document.body.appendChild(tpl);
|
|
42
|
+
}
|
|
43
|
+
tpl.innerHTML = toStr(html);
|
|
44
|
+
return tpl;
|
|
45
|
+
}
|
|
46
|
+
function injectCssScoped(code, cssText) {
|
|
47
|
+
const id = "ui-cmp-style-" + code;
|
|
48
|
+
let el = document.getElementById(id);
|
|
49
|
+
if (!el) {
|
|
50
|
+
el = document.createElement("style");
|
|
51
|
+
el.id = id;
|
|
52
|
+
document.head.appendChild(el);
|
|
53
|
+
}
|
|
54
|
+
el.textContent = toStr(cssText);
|
|
55
|
+
}
|
|
56
|
+
function createShadowRootContainer() {
|
|
57
|
+
const host = document.createElement("div");
|
|
58
|
+
host.style.all = "initial";
|
|
59
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
60
|
+
return { host, shadow };
|
|
61
|
+
}
|
|
62
|
+
function defaultMountTarget() {
|
|
63
|
+
return document.body;
|
|
64
|
+
}
|
|
65
|
+
function compileComponentJs(jsCode) {
|
|
66
|
+
const code = toStr(jsCode);
|
|
67
|
+
if (!code.trim()) {
|
|
68
|
+
return function() {
|
|
69
|
+
return {};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return new Function("api", "templateRootEl", "props", code);
|
|
73
|
+
}
|
|
74
|
+
const state = {
|
|
75
|
+
initialized: false,
|
|
76
|
+
projectId: null,
|
|
77
|
+
apiKey: null,
|
|
78
|
+
apiUrl: "",
|
|
79
|
+
cssIsolation: "scoped",
|
|
80
|
+
components: {}
|
|
81
|
+
};
|
|
82
|
+
function registerComponent(def) {
|
|
83
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
84
|
+
if (!code) return;
|
|
85
|
+
const version = def.version;
|
|
86
|
+
const html = def.html;
|
|
87
|
+
const js = def.js;
|
|
88
|
+
const css = def.css;
|
|
89
|
+
ensureTemplate(code, html);
|
|
90
|
+
const component = {
|
|
91
|
+
code,
|
|
92
|
+
version,
|
|
93
|
+
css,
|
|
94
|
+
js,
|
|
95
|
+
create: function(props, options) {
|
|
96
|
+
const opts = options || {};
|
|
97
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
98
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
99
|
+
const tpl = ensureTemplate(code, html);
|
|
100
|
+
const fragment = tpl.content.cloneNode(true);
|
|
101
|
+
let templateRootEl;
|
|
102
|
+
let instanceRoot;
|
|
103
|
+
let shadow = null;
|
|
104
|
+
if (isolation === "shadow") {
|
|
105
|
+
const c = createShadowRootContainer();
|
|
106
|
+
instanceRoot = c.host;
|
|
107
|
+
shadow = c.shadow;
|
|
108
|
+
templateRootEl = shadow;
|
|
109
|
+
if (css) {
|
|
110
|
+
const style = document.createElement("style");
|
|
111
|
+
style.textContent = toStr(css);
|
|
112
|
+
shadow.appendChild(style);
|
|
113
|
+
}
|
|
114
|
+
shadow.appendChild(fragment);
|
|
115
|
+
} else {
|
|
116
|
+
instanceRoot = document.createElement("div");
|
|
117
|
+
templateRootEl = instanceRoot;
|
|
118
|
+
if (css) injectCssScoped(code, css);
|
|
119
|
+
instanceRoot.appendChild(fragment);
|
|
120
|
+
}
|
|
121
|
+
mountEl.appendChild(instanceRoot);
|
|
122
|
+
const api = {
|
|
123
|
+
unmount: function() {
|
|
124
|
+
try {
|
|
125
|
+
instanceRoot.remove();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
mountEl,
|
|
130
|
+
hostEl: instanceRoot,
|
|
131
|
+
shadowRoot: shadow
|
|
132
|
+
};
|
|
133
|
+
const fn = compileComponentJs(js);
|
|
134
|
+
let exported = {};
|
|
135
|
+
try {
|
|
136
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
137
|
+
} catch (e) {
|
|
138
|
+
exported = {
|
|
139
|
+
error: e
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return Object.assign({ api }, exported);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
state.components[code] = component;
|
|
146
|
+
uiCmp[code] = component;
|
|
147
|
+
uiComponents[code] = component;
|
|
148
|
+
}
|
|
149
|
+
async function init(options) {
|
|
150
|
+
const opts = options || {};
|
|
151
|
+
const projectId = toStr(opts.projectId).trim();
|
|
152
|
+
if (!projectId) throw new Error("projectId is required");
|
|
153
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
154
|
+
const apiKey = opts.apiKey;
|
|
155
|
+
state.projectId = projectId;
|
|
156
|
+
state.apiKey = apiKey;
|
|
157
|
+
state.apiUrl = apiUrl;
|
|
158
|
+
const cssIsolation = toStr(opts.cssIsolation || "scoped").trim().toLowerCase();
|
|
159
|
+
state.cssIsolation = cssIsolation === "shadow" ? "shadow" : "scoped";
|
|
160
|
+
const base = state.apiUrl;
|
|
161
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(projectId) + "/manifest";
|
|
162
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
163
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
164
|
+
for (const def of items) {
|
|
165
|
+
registerComponent(def);
|
|
166
|
+
}
|
|
167
|
+
state.initialized = true;
|
|
168
|
+
return { project: data ? data.project : null, count: items.length };
|
|
169
|
+
}
|
|
170
|
+
async function load(code) {
|
|
171
|
+
const c = toStr(code).trim().toLowerCase();
|
|
172
|
+
if (!c) throw new Error("code is required");
|
|
173
|
+
if (!state.projectId) throw new Error("uiCmp not initialized");
|
|
174
|
+
if (state.components[c]) return state.components[c];
|
|
175
|
+
const base = state.apiUrl;
|
|
176
|
+
const url = base + "/api/ui-components/projects/" + encodeURIComponent(state.projectId) + "/components/" + encodeURIComponent(c);
|
|
177
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
178
|
+
if (!data || !data.component) throw new Error("Component not found");
|
|
179
|
+
registerComponent(data.component);
|
|
180
|
+
return state.components[c];
|
|
181
|
+
}
|
|
182
|
+
const uiCmp = {
|
|
183
|
+
init,
|
|
184
|
+
load,
|
|
185
|
+
_state: state
|
|
186
|
+
};
|
|
187
|
+
const uiComponents = uiCmp;
|
|
188
|
+
window.uiCmp = uiCmp;
|
|
189
|
+
window.uiComponents = uiComponents;
|
|
190
|
+
})();
|
|
191
|
+
})();
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function toStr(v) {
|
|
3
|
+
return v === undefined || v === null ? '' : String(v);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function normalizeApiUrl(apiUrl) {
|
|
7
|
+
const u = toStr(apiUrl).trim();
|
|
8
|
+
if (!u) return '';
|
|
9
|
+
return u.replace(/\/$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildHeaders(apiKey) {
|
|
13
|
+
const headers = {};
|
|
14
|
+
const key = toStr(apiKey).trim();
|
|
15
|
+
if (key) headers['x-project-key'] = key;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function fetchJson(url, headers) {
|
|
20
|
+
const res = await fetch(url, { headers: headers || {} });
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let data = null;
|
|
23
|
+
try {
|
|
24
|
+
data = text ? JSON.parse(text) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
data = null;
|
|
27
|
+
}
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const msg = data && data.error ? data.error : 'Request failed';
|
|
30
|
+
const err = new Error(msg);
|
|
31
|
+
err.status = res.status;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureTemplate(code, html) {
|
|
38
|
+
const id = 'ui-cmp-' + code;
|
|
39
|
+
let tpl = document.getElementById(id);
|
|
40
|
+
if (!tpl) {
|
|
41
|
+
tpl = document.createElement('template');
|
|
42
|
+
tpl.id = id;
|
|
43
|
+
document.body.appendChild(tpl);
|
|
44
|
+
}
|
|
45
|
+
tpl.innerHTML = toStr(html);
|
|
46
|
+
return tpl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function injectCssScoped(code, cssText) {
|
|
50
|
+
const id = 'ui-cmp-style-' + code;
|
|
51
|
+
let el = document.getElementById(id);
|
|
52
|
+
if (!el) {
|
|
53
|
+
el = document.createElement('style');
|
|
54
|
+
el.id = id;
|
|
55
|
+
document.head.appendChild(el);
|
|
56
|
+
}
|
|
57
|
+
el.textContent = toStr(cssText);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createShadowRootContainer() {
|
|
61
|
+
const host = document.createElement('div');
|
|
62
|
+
host.style.all = 'initial';
|
|
63
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
64
|
+
return { host, shadow };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function defaultMountTarget() {
|
|
68
|
+
return document.body;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compileComponentJs(jsCode) {
|
|
72
|
+
const code = toStr(jsCode);
|
|
73
|
+
if (!code.trim()) {
|
|
74
|
+
return function () {
|
|
75
|
+
return {};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return new Function('api', 'templateRootEl', 'props', code);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const state = {
|
|
82
|
+
initialized: false,
|
|
83
|
+
projectId: null,
|
|
84
|
+
apiKey: null,
|
|
85
|
+
apiUrl: '',
|
|
86
|
+
cssIsolation: 'scoped',
|
|
87
|
+
components: {},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function registerComponent(def) {
|
|
91
|
+
const code = toStr(def.code).trim().toLowerCase();
|
|
92
|
+
if (!code) return;
|
|
93
|
+
|
|
94
|
+
const version = def.version;
|
|
95
|
+
const html = def.html;
|
|
96
|
+
const js = def.js;
|
|
97
|
+
const css = def.css;
|
|
98
|
+
|
|
99
|
+
ensureTemplate(code, html);
|
|
100
|
+
|
|
101
|
+
const component = {
|
|
102
|
+
code,
|
|
103
|
+
version,
|
|
104
|
+
css,
|
|
105
|
+
js,
|
|
106
|
+
create: function (props, options) {
|
|
107
|
+
const opts = options || {};
|
|
108
|
+
const mountEl = opts.mountEl || defaultMountTarget();
|
|
109
|
+
const isolation = opts.cssIsolation || state.cssIsolation;
|
|
110
|
+
|
|
111
|
+
const tpl = ensureTemplate(code, html);
|
|
112
|
+
const fragment = tpl.content.cloneNode(true);
|
|
113
|
+
|
|
114
|
+
let templateRootEl;
|
|
115
|
+
let instanceRoot;
|
|
116
|
+
let shadow = null;
|
|
117
|
+
|
|
118
|
+
if (isolation === 'shadow') {
|
|
119
|
+
const c = createShadowRootContainer();
|
|
120
|
+
instanceRoot = c.host;
|
|
121
|
+
shadow = c.shadow;
|
|
122
|
+
templateRootEl = shadow;
|
|
123
|
+
if (css) {
|
|
124
|
+
const style = document.createElement('style');
|
|
125
|
+
style.textContent = toStr(css);
|
|
126
|
+
shadow.appendChild(style);
|
|
127
|
+
}
|
|
128
|
+
shadow.appendChild(fragment);
|
|
129
|
+
} else {
|
|
130
|
+
instanceRoot = document.createElement('div');
|
|
131
|
+
templateRootEl = instanceRoot;
|
|
132
|
+
if (css) injectCssScoped(code, css);
|
|
133
|
+
instanceRoot.appendChild(fragment);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mountEl.appendChild(instanceRoot);
|
|
137
|
+
|
|
138
|
+
const api = {
|
|
139
|
+
unmount: function () {
|
|
140
|
+
try {
|
|
141
|
+
instanceRoot.remove();
|
|
142
|
+
} catch {}
|
|
143
|
+
},
|
|
144
|
+
mountEl,
|
|
145
|
+
hostEl: instanceRoot,
|
|
146
|
+
shadowRoot: shadow,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const fn = compileComponentJs(js);
|
|
150
|
+
let exported = {};
|
|
151
|
+
try {
|
|
152
|
+
exported = fn(api, templateRootEl, props || {}) || {};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
exported = {
|
|
155
|
+
error: e,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.assign({ api }, exported);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
state.components[code] = component;
|
|
164
|
+
uiCmp[code] = component;
|
|
165
|
+
uiComponents[code] = component;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function init(options) {
|
|
169
|
+
const opts = options || {};
|
|
170
|
+
const projectId = toStr(opts.projectId).trim();
|
|
171
|
+
if (!projectId) throw new Error('projectId is required');
|
|
172
|
+
|
|
173
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl);
|
|
174
|
+
const apiKey = opts.apiKey;
|
|
175
|
+
|
|
176
|
+
state.projectId = projectId;
|
|
177
|
+
state.apiKey = apiKey;
|
|
178
|
+
state.apiUrl = apiUrl;
|
|
179
|
+
|
|
180
|
+
const cssIsolation = toStr(opts.cssIsolation || 'scoped').trim().toLowerCase();
|
|
181
|
+
state.cssIsolation = cssIsolation === 'shadow' ? 'shadow' : 'scoped';
|
|
182
|
+
|
|
183
|
+
const base = state.apiUrl;
|
|
184
|
+
const url = base + '/api/ui-components/projects/' + encodeURIComponent(projectId) + '/manifest';
|
|
185
|
+
|
|
186
|
+
const data = await fetchJson(url, buildHeaders(apiKey));
|
|
187
|
+
const items = data && Array.isArray(data.components) ? data.components : [];
|
|
188
|
+
|
|
189
|
+
for (const def of items) {
|
|
190
|
+
registerComponent(def);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
state.initialized = true;
|
|
194
|
+
return { project: data ? data.project : null, count: items.length };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function load(code) {
|
|
198
|
+
const c = toStr(code).trim().toLowerCase();
|
|
199
|
+
if (!c) throw new Error('code is required');
|
|
200
|
+
if (!state.projectId) throw new Error('uiCmp not initialized');
|
|
201
|
+
if (state.components[c]) return state.components[c];
|
|
202
|
+
|
|
203
|
+
const base = state.apiUrl;
|
|
204
|
+
const url =
|
|
205
|
+
base +
|
|
206
|
+
'/api/ui-components/projects/' +
|
|
207
|
+
encodeURIComponent(state.projectId) +
|
|
208
|
+
'/components/' +
|
|
209
|
+
encodeURIComponent(c);
|
|
210
|
+
|
|
211
|
+
const data = await fetchJson(url, buildHeaders(state.apiKey));
|
|
212
|
+
if (!data || !data.component) throw new Error('Component not found');
|
|
213
|
+
|
|
214
|
+
registerComponent(data.component);
|
|
215
|
+
return state.components[c];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const uiCmp = {
|
|
219
|
+
init,
|
|
220
|
+
load,
|
|
221
|
+
_state: state,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const uiComponents = uiCmp;
|
|
225
|
+
|
|
226
|
+
window.uiCmp = uiCmp;
|
|
227
|
+
window.uiComponents = uiComponents;
|
|
228
|
+
})();
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const User = require('../models/User');
|
|
2
2
|
const StripeWebhookEvent = require('../models/StripeWebhookEvent');
|
|
3
|
+
const Organization = require('../models/Organization');
|
|
4
|
+
const OrganizationMember = require('../models/OrganizationMember');
|
|
5
|
+
const Asset = require('../models/Asset');
|
|
6
|
+
const Notification = require('../models/Notification');
|
|
7
|
+
const Invite = require('../models/Invite');
|
|
8
|
+
const EmailLog = require('../models/EmailLog');
|
|
9
|
+
const FormSubmission = require('../models/FormSubmission');
|
|
3
10
|
const asyncHandler = require('../utils/asyncHandler');
|
|
4
11
|
const fs = require('fs');
|
|
5
12
|
const path = require('path');
|
|
@@ -353,12 +360,94 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
|
|
|
353
360
|
}
|
|
354
361
|
});
|
|
355
362
|
|
|
363
|
+
// Delete user (admin only)
|
|
364
|
+
const deleteUser = asyncHandler(async (req, res) => {
|
|
365
|
+
|
|
366
|
+
const userId = req.params.id;
|
|
367
|
+
|
|
368
|
+
// 1. Validate user exists
|
|
369
|
+
const user = await User.findById(userId);
|
|
370
|
+
if (!user) {
|
|
371
|
+
return res.status(404).json({ error: 'User not found' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 2. Prevent self-deletion
|
|
375
|
+
// Note: In a real implementation, you'd get the admin ID from req.admin or similar
|
|
376
|
+
// For now, we'll skip this check as the basic auth doesn't provide user identity
|
|
377
|
+
|
|
378
|
+
// 3. Check if this is the last admin
|
|
379
|
+
const adminCount = await User.countDocuments({ role: 'admin' });
|
|
380
|
+
if (user.role === 'admin' && adminCount <= 1) {
|
|
381
|
+
return res.status(400).json({ error: 'Cannot delete the last admin user' });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 4. Cleanup dependencies
|
|
385
|
+
await cleanupUserData(userId);
|
|
386
|
+
|
|
387
|
+
// 5. Delete user
|
|
388
|
+
await User.findByIdAndDelete(userId);
|
|
389
|
+
|
|
390
|
+
// 6. Log action
|
|
391
|
+
console.log(`Admin deleted user: ${user.email} (${userId})`);
|
|
392
|
+
|
|
393
|
+
res.json({ message: 'User deleted successfully' });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Helper function to clean up user data
|
|
397
|
+
async function cleanupUserData(userId) {
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
// Handle organizations owned by user
|
|
401
|
+
const ownedOrgs = await Organization.find({ ownerUserId: userId });
|
|
402
|
+
for (const org of ownedOrgs) {
|
|
403
|
+
// Check if organization has other members
|
|
404
|
+
const memberCount = await OrganizationMember.countDocuments({
|
|
405
|
+
orgId: org._id,
|
|
406
|
+
userId: { $ne: userId }
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (memberCount === 0) {
|
|
410
|
+
// Delete organization if no other members
|
|
411
|
+
await Organization.findByIdAndDelete(org._id);
|
|
412
|
+
console.log(`Deleted organization ${org.name} (${org._id}) - no other members`);
|
|
413
|
+
} else {
|
|
414
|
+
// Remove owner but keep organization
|
|
415
|
+
org.ownerUserId = null;
|
|
416
|
+
await org.save();
|
|
417
|
+
console.log(`Removed owner from organization ${org.name} (${org._id}) - has other members`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Remove from all organization memberships
|
|
422
|
+
await OrganizationMember.deleteMany({ userId: userId });
|
|
423
|
+
|
|
424
|
+
// Delete user's assets
|
|
425
|
+
await Asset.deleteMany({ ownerUserId: userId });
|
|
426
|
+
|
|
427
|
+
// Delete notifications
|
|
428
|
+
await Notification.deleteMany({ userId: userId });
|
|
429
|
+
|
|
430
|
+
// Clean up other references
|
|
431
|
+
await Invite.deleteMany({ createdByUserId: userId });
|
|
432
|
+
await EmailLog.deleteMany({ userId: userId });
|
|
433
|
+
await FormSubmission.deleteMany({ userId: userId });
|
|
434
|
+
|
|
435
|
+
// Note: We keep ActivityLog and AuditEvent for audit purposes
|
|
436
|
+
|
|
437
|
+
console.log(`Completed cleanup for user ${userId}`);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Error during user cleanup:', error);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
356
444
|
module.exports = {
|
|
357
445
|
getUsers,
|
|
358
446
|
registerUser,
|
|
359
447
|
getUser,
|
|
360
448
|
updateUserSubscription,
|
|
361
449
|
updateUserPassword,
|
|
450
|
+
deleteUser,
|
|
362
451
|
reconcileUser,
|
|
363
452
|
generateToken,
|
|
364
453
|
getWebhookEvents,
|
|
@@ -7,6 +7,12 @@ const {
|
|
|
7
7
|
getDynamicModel,
|
|
8
8
|
} = require('../services/headlessModels.service');
|
|
9
9
|
|
|
10
|
+
const {
|
|
11
|
+
listExternalCollections,
|
|
12
|
+
inferExternalModelFromCollection,
|
|
13
|
+
createOrUpdateExternalModel,
|
|
14
|
+
} = require('../services/headlessExternalModels.service');
|
|
15
|
+
|
|
10
16
|
const llmService = require('../services/llm.service');
|
|
11
17
|
const { getSettingValue } = require('../services/globalSettings.service');
|
|
12
18
|
const { createAuditEvent, getBasicAuthActor } = require('../services/audit.service');
|
|
@@ -399,6 +405,82 @@ exports.deleteModel = async (req, res) => {
|
|
|
399
405
|
}
|
|
400
406
|
};
|
|
401
407
|
|
|
408
|
+
// External models (Mongo collections)
|
|
409
|
+
exports.listExternalCollections = async (req, res) => {
|
|
410
|
+
try {
|
|
411
|
+
const q = String(req.query.q || '').trim() || null;
|
|
412
|
+
const includeSystem = String(req.query.includeSystem || '').trim().toLowerCase() === 'true';
|
|
413
|
+
const items = await listExternalCollections({ q, includeSystem });
|
|
414
|
+
return res.json({ items });
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('Error listing external mongo collections:', error);
|
|
417
|
+
return handleServiceError(res, error);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
exports.inferExternalCollection = async (req, res) => {
|
|
422
|
+
try {
|
|
423
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
424
|
+
const collectionName = String(body.collectionName || '').trim();
|
|
425
|
+
const sampleSize = body.sampleSize;
|
|
426
|
+
const result = await inferExternalModelFromCollection({ collectionName, sampleSize });
|
|
427
|
+
return res.json(result);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error('Error inferring external collection schema:', error);
|
|
430
|
+
return handleServiceError(res, error);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
exports.importExternalModel = async (req, res) => {
|
|
435
|
+
try {
|
|
436
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
437
|
+
const collectionName = String(body.collectionName || '').trim();
|
|
438
|
+
const codeIdentifier = String(body.codeIdentifier || '').trim();
|
|
439
|
+
const displayName = String(body.displayName || '').trim();
|
|
440
|
+
const sampleSize = body.sampleSize;
|
|
441
|
+
|
|
442
|
+
const result = await createOrUpdateExternalModel({
|
|
443
|
+
collectionName,
|
|
444
|
+
codeIdentifier,
|
|
445
|
+
displayName,
|
|
446
|
+
sampleSize,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return res.status(result.created ? 201 : 200).json({ item: result.item, inference: result.inference });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('Error importing external model:', error);
|
|
452
|
+
return handleServiceError(res, error);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
exports.syncExternalModel = async (req, res) => {
|
|
457
|
+
try {
|
|
458
|
+
const codeIdentifier = String(req.params.codeIdentifier || '').trim();
|
|
459
|
+
const existing = await getModelDefinitionByCode(codeIdentifier);
|
|
460
|
+
if (!existing) return res.status(404).json({ error: 'Model not found' });
|
|
461
|
+
|
|
462
|
+
const isExternal = existing.sourceType === 'external' || existing.isExternal === true;
|
|
463
|
+
if (!isExternal) {
|
|
464
|
+
return res.status(400).json({ error: 'Model is not external' });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const body = req.body && typeof req.body === 'object' ? req.body : {};
|
|
468
|
+
const sampleSize = body.sampleSize;
|
|
469
|
+
|
|
470
|
+
const result = await createOrUpdateExternalModel({
|
|
471
|
+
collectionName: existing.sourceCollectionName,
|
|
472
|
+
codeIdentifier: existing.codeIdentifier,
|
|
473
|
+
displayName: existing.displayName,
|
|
474
|
+
sampleSize,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return res.json({ item: result.item, inference: result.inference });
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error('Error syncing external model:', error);
|
|
480
|
+
return handleServiceError(res, error);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
402
484
|
exports.validateModelDefinition = async (req, res) => {
|
|
403
485
|
try {
|
|
404
486
|
const body = req.body || {};
|