@intranefr/superbackend 1.4.3 → 1.4.4
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 +6 -1
- package/README.md +5 -5
- package/index.js +7 -4
- package/package.json +1 -1
- package/sdk/error-tracking/browser/package.json +4 -3
- package/sdk/error-tracking/browser/src/embed.js +29 -0
- package/src/controllers/admin.controller.js +50 -1
- package/src/controllers/adminMigration.controller.js +5 -1
- package/src/middleware.js +4 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/services/consoleOverride.service.js +291 -0
- package/src/services/email.service.js +17 -1
- package/src/services/webhook.service.js +2 -2
- package/src/services/workflow.service.js +1 -1
- package/src/utils/encryption.js +5 -3
- package/views/admin-coolify-deploy.ejs +1 -1
- package/views/admin-dashboard-home.ejs +1 -1
- package/views/admin-dashboard.ejs +1 -1
- package/views/admin-errors.ejs +2 -2
- package/views/admin-global-settings.ejs +3 -3
- package/views/admin-json-configs.ejs +8 -1
- package/views/admin-llm.ejs +2 -2
- package/views/admin-seo-config.ejs +1 -1
- package/views/admin-test.ejs +3 -3
- package/views/admin-users.ejs +179 -0
- package/views/admin-webhooks.ejs +1 -1
- package/views/admin-workflows.ejs +1 -1
- package/views/partials/admin-assets-script.ejs +3 -3
- package/views/partials/dashboard/palette.ejs +1 -1
package/.env.example
CHANGED
|
@@ -43,5 +43,10 @@ MAX_FILE_SIZE_HARD_CAP=10485760
|
|
|
43
43
|
# S3_REGION=us-east-1
|
|
44
44
|
# S3_ACCESS_KEY_ID=minioadmin
|
|
45
45
|
# S3_SECRET_ACCESS_KEY=minioadmin
|
|
46
|
-
# S3_BUCKET=
|
|
46
|
+
# S3_BUCKET=superbackend
|
|
47
|
+
# Legacy fallback: S3_BUCKET=saasbackend
|
|
47
48
|
# S3_FORCE_PATH_STYLE=true
|
|
49
|
+
|
|
50
|
+
# Encryption key for encrypted settings (new preferred name)
|
|
51
|
+
# SUPERBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
|
52
|
+
# Legacy fallback: SAASBACKEND_ENCRYPTION_KEY=your-32-byte-encryption-key
|
package/README.md
CHANGED
|
@@ -29,13 +29,13 @@ Node.js middleware that gives your project backend superpowers. Handles authenti
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm install superbackend
|
|
32
|
+
npm install @intranefr/superbackend
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
or
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
yarn add superbackend
|
|
38
|
+
yarn add @intranefr/superbackend
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
---
|
|
@@ -45,7 +45,7 @@ yarn add superbackend
|
|
|
45
45
|
```javascript
|
|
46
46
|
require('dotenv').config();
|
|
47
47
|
const express = require('express');
|
|
48
|
-
const { middleware } = require('
|
|
48
|
+
const { middleware } = require('@intranefr/superbackend');
|
|
49
49
|
|
|
50
50
|
const app = express();
|
|
51
51
|
|
|
@@ -101,8 +101,8 @@ Please read the [CONTRIBUTING.md](#) for guidelines.
|
|
|
101
101
|
<img src="https://img.shields.io/badge/Intrane-intranefr-blue?style=flat-square" alt="Intrane"/>
|
|
102
102
|
</a>
|
|
103
103
|
|
|
104
|
-
<a href="https://www.npmjs.com/package/superbackend" target="_blank">
|
|
105
|
-
<img src="https://img.shields.io/npm/v
|
|
104
|
+
<a href="https://www.npmjs.com/package/@intranefr/superbackend" target="_blank">
|
|
105
|
+
<img src="https://img.shields.io/npm/v/@intranefr%2Fsuperbackend?style=flat-square" alt="npm"/>
|
|
106
106
|
</a>
|
|
107
107
|
|
|
108
108
|
## License
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@ require("dotenv").config({ path: process.env.ENV_FILE || ".env" });
|
|
|
2
2
|
const express = require("express");
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Creates the
|
|
5
|
+
* Creates the SuperBackend as Express middleware
|
|
6
6
|
* @param {Object} options - Configuration options
|
|
7
7
|
* @param {string} options.mongodbUri - MongoDB connection string
|
|
8
8
|
* @param {string} options.corsOrigin - CORS origin(s)
|
|
@@ -13,7 +13,7 @@ const express = require("express");
|
|
|
13
13
|
const middleware = require("./src/middleware");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Creates and starts a standalone
|
|
16
|
+
* Creates and starts a standalone SuperBackend server
|
|
17
17
|
* @param {Object} options - Configuration options
|
|
18
18
|
* @param {number} options.port - Port to listen on
|
|
19
19
|
* @param {string} options.mongodbUri - MongoDB connection string
|
|
@@ -28,7 +28,7 @@ function startServer(options = {}) {
|
|
|
28
28
|
|
|
29
29
|
// Start server
|
|
30
30
|
const server = app.listen(PORT, () => {
|
|
31
|
-
console.log(`🚀
|
|
31
|
+
console.log(`🚀 SuperBackend standalone server running on http://localhost:${PORT}`);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
return { app, server };
|
|
@@ -36,8 +36,11 @@ function startServer(options = {}) {
|
|
|
36
36
|
|
|
37
37
|
const saasbackend = {
|
|
38
38
|
server: startServer,
|
|
39
|
+
consoleOverride: require("./src/services/consoleOverride.service"),
|
|
39
40
|
middleware: (options = {}) => {
|
|
40
|
-
|
|
41
|
+
// Set both registries for backward compatibility
|
|
42
|
+
globalThis.superbackend = saasbackend;
|
|
43
|
+
globalThis.saasbackend = saasbackend; // Legacy support
|
|
41
44
|
return middleware(options);
|
|
42
45
|
},
|
|
43
46
|
services: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@
|
|
3
|
-
"version": "
|
|
2
|
+
"name": "@intranefr/superbackend-error-tracking-browser-sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Error tracking SDK for SuperBackend browser applications.",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
}
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
|
-
"build": "esbuild src/embed.js --bundle --format=iife --global-name=
|
|
12
|
+
"build": "esbuild src/embed.js --bundle --format=iife --global-name=superbackendErrorTrackingEmbed --outfile=dist/embed.iife.js --minify"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"esbuild": "^0.27.2"
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { createErrorTrackingClient } from './core.js';
|
|
2
2
|
|
|
3
|
+
function attachToSuperbackendGlobal() {
|
|
4
|
+
const root = (typeof window !== 'undefined' ? window : undefined);
|
|
5
|
+
if (!root) return;
|
|
6
|
+
|
|
7
|
+
if (root.saasbackendErrorTrackingEmbed && !root.superbackendErrorTrackingEmbed) {
|
|
8
|
+
root.superbackendErrorTrackingEmbed = root.saasbackendErrorTrackingEmbed;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
root.superbackend = root.superbackend || {};
|
|
12
|
+
|
|
13
|
+
if (!root.superbackend.errorTracking) {
|
|
14
|
+
root.superbackend.errorTracking = createErrorTrackingClient();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (root.superbackend.errorTracking && typeof root.superbackend.errorTracking.init === 'function') {
|
|
18
|
+
root.superbackend.errorTracking.init();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
function attachToSaasbackendGlobal() {
|
|
4
23
|
const root = (typeof window !== 'undefined' ? window : undefined);
|
|
5
24
|
if (!root) return;
|
|
6
25
|
|
|
26
|
+
// Show deprecation warning in console
|
|
27
|
+
if (console.warn) {
|
|
28
|
+
console.warn('DEPRECATION: Global "window.saasbackend" is deprecated. Use "window.superbackend" instead.');
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
root.saasbackend = root.saasbackend || {};
|
|
8
32
|
|
|
33
|
+
if (root.superbackendErrorTrackingEmbed && !root.saasbackendErrorTrackingEmbed) {
|
|
34
|
+
root.saasbackendErrorTrackingEmbed = root.superbackendErrorTrackingEmbed;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
if (!root.saasbackend.errorTracking) {
|
|
10
38
|
root.saasbackend.errorTracking = createErrorTrackingClient();
|
|
11
39
|
}
|
|
@@ -15,4 +43,5 @@ function attachToSaasbackendGlobal() {
|
|
|
15
43
|
}
|
|
16
44
|
}
|
|
17
45
|
|
|
46
|
+
attachToSuperbackendGlobal();
|
|
18
47
|
attachToSaasbackendGlobal();
|
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
7
7
|
const { generateAccessToken, generateRefreshToken } = require('../utils/jwt');
|
|
8
8
|
const { retryFailedWebhooks, processWebhookEvent } = require('../utils/webhookRetry');
|
|
9
|
+
const { auditMiddleware } = require('../services/auditLogger');
|
|
9
10
|
|
|
10
11
|
// Get all users
|
|
11
12
|
const getUsers = asyncHandler(async (req, res) => {
|
|
@@ -17,6 +18,53 @@ const getUsers = asyncHandler(async (req, res) => {
|
|
|
17
18
|
res.json(users.map(u => u.toJSON()));
|
|
18
19
|
});
|
|
19
20
|
|
|
21
|
+
// Register new user (admin only)
|
|
22
|
+
const registerUser = asyncHandler(async (req, res) => {
|
|
23
|
+
const { email, password, name, role = 'user' } = req.body;
|
|
24
|
+
|
|
25
|
+
// Validation
|
|
26
|
+
if (!email || !password) {
|
|
27
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (password.length < 6) {
|
|
31
|
+
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!['user', 'admin'].includes(role)) {
|
|
35
|
+
return res.status(400).json({ error: 'Role must be either "user" or "admin"' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
39
|
+
if (!emailRegex.test(email)) {
|
|
40
|
+
return res.status(400).json({ error: 'Invalid email format' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if user already exists
|
|
44
|
+
const existingUser = await User.findOne({ email: email.toLowerCase() });
|
|
45
|
+
if (existingUser) {
|
|
46
|
+
return res.status(409).json({ error: 'Email already registered' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create new user
|
|
50
|
+
const user = new User({
|
|
51
|
+
email: email.toLowerCase(),
|
|
52
|
+
passwordHash: password, // Will be hashed by pre-save hook
|
|
53
|
+
name: name || '',
|
|
54
|
+
role: role
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await user.save();
|
|
58
|
+
|
|
59
|
+
// Log the admin action
|
|
60
|
+
console.log(`Admin registered new user: ${user.email} with role: ${user.role}`);
|
|
61
|
+
|
|
62
|
+
res.status(201).json({
|
|
63
|
+
success: true,
|
|
64
|
+
user: user.toJSON()
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
20
68
|
// Get single user
|
|
21
69
|
const getUser = asyncHandler(async (req, res) => {
|
|
22
70
|
const user = await User.findById(req.params.id);
|
|
@@ -290,7 +338,7 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
|
|
|
290
338
|
});
|
|
291
339
|
}
|
|
292
340
|
|
|
293
|
-
// In ref-
|
|
341
|
+
// In ref-superbackend, manage.sh already exists in the root of the repository
|
|
294
342
|
// If it didn't, we would write it here. For this case, we'll just success.
|
|
295
343
|
res.json({
|
|
296
344
|
success: true,
|
|
@@ -307,6 +355,7 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
|
|
|
307
355
|
|
|
308
356
|
module.exports = {
|
|
309
357
|
getUsers,
|
|
358
|
+
registerUser,
|
|
310
359
|
getUser,
|
|
311
360
|
updateUserSubscription,
|
|
312
361
|
updateUserPassword,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
const migrationService = require('../services/migration.service');
|
|
2
2
|
|
|
3
3
|
function getModelRegistry() {
|
|
4
|
-
|
|
4
|
+
// Try new registry first, then fallback to old registry for backward compatibility
|
|
5
|
+
if (globalThis?.saasbackend?.models && !globalThis?.superbackend?.models) {
|
|
6
|
+
console.warn('Deprecation: globalThis.saasbackend is deprecated. Use globalThis.superbackend instead.');
|
|
7
|
+
}
|
|
8
|
+
return globalThis?.superbackend?.models || globalThis?.saasbackend?.models || null;
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
function getModelByName(modelName) {
|
package/src/middleware.js
CHANGED
|
@@ -7,6 +7,7 @@ const ejs = require("ejs");
|
|
|
7
7
|
const { basicAuth } = require("./middleware/auth");
|
|
8
8
|
const endpointRegistry = require("./admin/endpointRegistry");
|
|
9
9
|
const { createFeatureFlagsEjsMiddleware } = require("./services/featureFlags.service");
|
|
10
|
+
const consoleOverride = require("./services/consoleOverride.service");
|
|
10
11
|
const {
|
|
11
12
|
hookConsoleError,
|
|
12
13
|
setupProcessHandlers,
|
|
@@ -30,6 +31,9 @@ function createMiddleware(options = {}) {
|
|
|
30
31
|
const router = express.Router();
|
|
31
32
|
const adminPath = options.adminPath || "/admin";
|
|
32
33
|
|
|
34
|
+
// Initialize console override service early to capture all logs
|
|
35
|
+
consoleOverride.init();
|
|
36
|
+
|
|
33
37
|
if (!errorCaptureInitialized) {
|
|
34
38
|
errorCaptureInitialized = true;
|
|
35
39
|
hookConsoleError();
|
|
@@ -7,6 +7,7 @@ const { basicAuth } = require('../middleware/auth');
|
|
|
7
7
|
router.use(basicAuth);
|
|
8
8
|
|
|
9
9
|
router.get('/users', adminController.getUsers);
|
|
10
|
+
router.post('/users/register', adminController.registerUser);
|
|
10
11
|
router.get('/users/:id', adminController.getUser);
|
|
11
12
|
router.put('/users/:id/subscription', adminController.updateUserSubscription);
|
|
12
13
|
router.patch('/users/:id', adminController.updateUserPassword);
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
let originalConsole = null;
|
|
5
|
+
let logFileStream = null;
|
|
6
|
+
let isActive = false;
|
|
7
|
+
let isWriting = false;
|
|
8
|
+
let memoryInterval = null;
|
|
9
|
+
let logLines = [];
|
|
10
|
+
const MAX_LOG_LINES = 2000;
|
|
11
|
+
|
|
12
|
+
// Store the truly original console methods at module load time
|
|
13
|
+
const TRULY_ORIGINAL_CONSOLE = {
|
|
14
|
+
log: console.log,
|
|
15
|
+
error: console.error,
|
|
16
|
+
warn: console.warn,
|
|
17
|
+
info: console.info,
|
|
18
|
+
debug: console.debug
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Console Override Service
|
|
23
|
+
* Provides dual logging to stdout and file in non-production environments
|
|
24
|
+
*/
|
|
25
|
+
const consoleOverride = {
|
|
26
|
+
/**
|
|
27
|
+
* Initialize console override
|
|
28
|
+
* @param {Object} options - Configuration options
|
|
29
|
+
* @param {string} options.logFile - Log file path (default: 'stdout.log')
|
|
30
|
+
* @param {boolean} options.forceEnable - Force enable regardless of NODE_ENV
|
|
31
|
+
*/
|
|
32
|
+
init(options = {}) {
|
|
33
|
+
if (isActive) {
|
|
34
|
+
return; // Already initialized
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
38
|
+
const forceEnabled = options.forceEnable || process.env.CONSOLE_OVERRIDE_ENABLED === 'true';
|
|
39
|
+
const forceDisabled = process.env.CONSOLE_OVERRIDE_ENABLED === 'false';
|
|
40
|
+
|
|
41
|
+
// Skip if production and not force enabled, or if force disabled
|
|
42
|
+
if ((nodeEnv === 'production' && !forceEnabled) || forceDisabled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const logFile = options.logFile || process.env.CONSOLE_LOG_FILE || 'stdout.log';
|
|
47
|
+
const logPath = path.resolve(process.cwd(), logFile);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Close any existing stream before truncating
|
|
51
|
+
if (logFileStream && !logFileStream.destroyed) {
|
|
52
|
+
logFileStream.end();
|
|
53
|
+
logFileStream = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Wait a bit for stream to fully close, then truncate
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
// Truncate log file on initialization (start with empty file)
|
|
59
|
+
if (fs.existsSync(logPath)) {
|
|
60
|
+
fs.truncateSync(logPath, 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create file stream for appending with error handling
|
|
64
|
+
logFileStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
65
|
+
|
|
66
|
+
// Handle stream errors
|
|
67
|
+
logFileStream.on('error', (error) => {
|
|
68
|
+
if (originalConsole && originalConsole.error && !isWriting) {
|
|
69
|
+
originalConsole.error('❌ Log stream error:', error.message);
|
|
70
|
+
}
|
|
71
|
+
isActive = false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Store original console
|
|
75
|
+
originalConsole = { ...console };
|
|
76
|
+
|
|
77
|
+
// Override console methods
|
|
78
|
+
this._overrideConsoleMethods();
|
|
79
|
+
|
|
80
|
+
// Start memory management interval (1 minute)
|
|
81
|
+
this._startMemoryManagement();
|
|
82
|
+
|
|
83
|
+
isActive = true;
|
|
84
|
+
|
|
85
|
+
// Log initialization using original console to avoid recursion
|
|
86
|
+
const initMsg = `📝 Console override initialized - logging to ${logPath}`;
|
|
87
|
+
originalConsole.log(initMsg);
|
|
88
|
+
this._writeToFile(initMsg);
|
|
89
|
+
}, 10);
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Fallback to console-only logging
|
|
93
|
+
originalConsole = originalConsole || console;
|
|
94
|
+
originalConsole.error('❌ Console override failed:', error.message);
|
|
95
|
+
isActive = false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Override individual console methods
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
_overrideConsoleMethods() {
|
|
104
|
+
const methods = ['log', 'error', 'warn', 'info', 'debug'];
|
|
105
|
+
|
|
106
|
+
methods.forEach(method => {
|
|
107
|
+
console[method] = (...args) => {
|
|
108
|
+
// Call the truly original console method
|
|
109
|
+
TRULY_ORIGINAL_CONSOLE[method](...args);
|
|
110
|
+
|
|
111
|
+
// Write to file if stream is available and not already writing
|
|
112
|
+
if (logFileStream && !logFileStream.destroyed && !isWriting) {
|
|
113
|
+
this._writeToFile(args);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Write message to file
|
|
121
|
+
* @param {string|Array} message - Message to write
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_writeToFile(message) {
|
|
125
|
+
if (!logFileStream || logFileStream.destroyed) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
isWriting = true;
|
|
130
|
+
try {
|
|
131
|
+
const messageStr = Array.isArray(message)
|
|
132
|
+
? message.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ')
|
|
133
|
+
: String(message);
|
|
134
|
+
|
|
135
|
+
// Add to memory buffer
|
|
136
|
+
logLines.push(messageStr);
|
|
137
|
+
|
|
138
|
+
// Only write if stream is available and not in the middle of a rewrite
|
|
139
|
+
if (logFileStream && !logFileStream.destroyed) {
|
|
140
|
+
logFileStream.write(messageStr + '\n');
|
|
141
|
+
}
|
|
142
|
+
} catch (writeError) {
|
|
143
|
+
// Prevent infinite recursion - use original console for errors
|
|
144
|
+
if (originalConsole && originalConsole.error && !isWriting) {
|
|
145
|
+
originalConsole.error('❌ Log write error:', writeError.message);
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
isWriting = false;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Start memory management interval
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_startMemoryManagement() {
|
|
157
|
+
// Clear any existing interval
|
|
158
|
+
if (memoryInterval) {
|
|
159
|
+
clearInterval(memoryInterval);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Set up 1-minute interval to manage log lines
|
|
163
|
+
memoryInterval = setInterval(() => {
|
|
164
|
+
this._manageLogMemory();
|
|
165
|
+
}, 60000); // 1 minute
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Manage log memory by keeping only last MAX_LOG_LINES
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
_manageLogMemory() {
|
|
173
|
+
if (logLines.length > MAX_LOG_LINES) {
|
|
174
|
+
// Keep only the last MAX_LOG_LINES
|
|
175
|
+
const excessLines = logLines.length - MAX_LOG_LINES;
|
|
176
|
+
logLines = logLines.slice(excessLines);
|
|
177
|
+
|
|
178
|
+
// Rewrite the log file with only the recent lines
|
|
179
|
+
this._rewriteLogFile();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Rewrite log file with current memory buffer
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_rewriteLogFile() {
|
|
188
|
+
if (!logFileStream || !logFileStream.path) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const logPath = logFileStream.path;
|
|
194
|
+
|
|
195
|
+
// Write recent lines to file (this truncates the file)
|
|
196
|
+
const fileContent = logLines.join('\n') + '\n';
|
|
197
|
+
fs.writeFileSync(logPath, fileContent, { flag: 'w' });
|
|
198
|
+
|
|
199
|
+
// Reopen stream for appending
|
|
200
|
+
logFileStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
201
|
+
|
|
202
|
+
// Reattach error handler
|
|
203
|
+
logFileStream.on('error', (error) => {
|
|
204
|
+
if (originalConsole && originalConsole.error && !isWriting) {
|
|
205
|
+
originalConsole.error('❌ Log stream error:', error.message);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (originalConsole && originalConsole.error) {
|
|
211
|
+
originalConsole.error('❌ Log file rewrite error:', error.message);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore original console
|
|
218
|
+
*/
|
|
219
|
+
restore() {
|
|
220
|
+
if (!isActive && !originalConsole) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Clear memory management interval
|
|
225
|
+
if (memoryInterval) {
|
|
226
|
+
clearInterval(memoryInterval);
|
|
227
|
+
memoryInterval = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Restore original console methods using the truly original ones
|
|
231
|
+
['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
|
|
232
|
+
console[method] = TRULY_ORIGINAL_CONSOLE[method];
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Close file stream
|
|
236
|
+
if (logFileStream && !logFileStream.destroyed) {
|
|
237
|
+
logFileStream.end();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Reset state
|
|
241
|
+
isActive = false;
|
|
242
|
+
originalConsole = null;
|
|
243
|
+
logFileStream = null;
|
|
244
|
+
logLines = [];
|
|
245
|
+
isWriting = false;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if override is active
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
isActive() {
|
|
253
|
+
return isActive;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get current memory lines count (for testing/debugging)
|
|
258
|
+
* @returns {number}
|
|
259
|
+
*/
|
|
260
|
+
getMemoryLinesCount() {
|
|
261
|
+
return logLines.length;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get current log path
|
|
266
|
+
* @returns {string|null}
|
|
267
|
+
*/
|
|
268
|
+
getLogPath() {
|
|
269
|
+
if (!logFileStream) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return logFileStream.path;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Cleanup on process exit
|
|
277
|
+
process.on('exit', () => {
|
|
278
|
+
consoleOverride.restore();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
process.on('SIGINT', () => {
|
|
282
|
+
consoleOverride.restore();
|
|
283
|
+
process.exit(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
process.on('SIGTERM', () => {
|
|
287
|
+
consoleOverride.restore();
|
|
288
|
+
process.exit(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
module.exports = consoleOverride;
|
|
@@ -74,7 +74,7 @@ const sendEmail = async ({
|
|
|
74
74
|
from ||
|
|
75
75
|
(await getSetting(
|
|
76
76
|
"EMAIL_FROM",
|
|
77
|
-
process.env.EMAIL_FROM || "
|
|
77
|
+
process.env.EMAIL_FROM || "SuperBackend <no-reply@resend.dev>",
|
|
78
78
|
));
|
|
79
79
|
const toArray = Array.isArray(to) ? to : [to];
|
|
80
80
|
|
|
@@ -338,6 +338,20 @@ const replaceTemplateVariables = (template, variables) => {
|
|
|
338
338
|
return result;
|
|
339
339
|
};
|
|
340
340
|
|
|
341
|
+
// Helper to clear cache (for testing)
|
|
342
|
+
const clearCache = () => {
|
|
343
|
+
settingsCache.clear();
|
|
344
|
+
resendClient = null;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Helper to clear cache and reinitialize (for testing)
|
|
348
|
+
const clearCacheAndReinitialize = async () => {
|
|
349
|
+
settingsCache.clear();
|
|
350
|
+
resendClient = null;
|
|
351
|
+
// Reinitialize with current env vars
|
|
352
|
+
await initResend();
|
|
353
|
+
};
|
|
354
|
+
|
|
341
355
|
module.exports = {
|
|
342
356
|
sendEmail,
|
|
343
357
|
sendPasswordResetEmail,
|
|
@@ -348,4 +362,6 @@ module.exports = {
|
|
|
348
362
|
sendSubscriptionEmail,
|
|
349
363
|
sendWaitingListEmail,
|
|
350
364
|
replaceTemplateVariables,
|
|
365
|
+
clearCache,
|
|
366
|
+
clearCacheAndReinitialize,
|
|
351
367
|
};
|
|
@@ -57,7 +57,7 @@ class WebhookService {
|
|
|
57
57
|
headers: {
|
|
58
58
|
'Content-Type': 'application/json',
|
|
59
59
|
'X-SaaS-Signature': signature,
|
|
60
|
-
'User-Agent': '
|
|
60
|
+
'User-Agent': 'SuperBackend-Webhook/1.0'
|
|
61
61
|
},
|
|
62
62
|
timeout: timeout
|
|
63
63
|
}).catch(err => {
|
|
@@ -87,7 +87,7 @@ class WebhookService {
|
|
|
87
87
|
headers: {
|
|
88
88
|
'Content-Type': 'application/json',
|
|
89
89
|
'X-SaaS-Signature': signature,
|
|
90
|
-
'User-Agent': '
|
|
90
|
+
'User-Agent': 'SuperBackend-Webhook/1.0'
|
|
91
91
|
},
|
|
92
92
|
timeout: timeout
|
|
93
93
|
});
|
|
@@ -5,7 +5,7 @@ const { NodeVM } = require('vm2');
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Workflow Service
|
|
8
|
-
* Handles execution of stacked workflow nodes within
|
|
8
|
+
* Handles execution of stacked workflow nodes within SuperBackend.
|
|
9
9
|
*/
|
|
10
10
|
class WorkflowService {
|
|
11
11
|
constructor(workflowId, initialContext = {}) {
|
package/src/utils/encryption.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
3
|
function getEncryptionKey() {
|
|
4
|
-
|
|
4
|
+
// Try new name first, then fallback to old name for backward compatibility
|
|
5
|
+
const raw = process.env.SUPERBACKEND_ENCRYPTION_KEY || process.env.SAASBACKEND_ENCRYPTION_KEY;
|
|
6
|
+
|
|
5
7
|
if (!raw) {
|
|
6
|
-
throw new Error('SAASBACKEND_ENCRYPTION_KEY is required for encrypted settings');
|
|
8
|
+
throw new Error('SUPERBACKEND_ENCRYPTION_KEY (or SAASBACKEND_ENCRYPTION_KEY for compatibility) is required for encrypted settings');
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
let key;
|
|
@@ -23,7 +25,7 @@ function getEncryptionKey() {
|
|
|
23
25
|
|
|
24
26
|
if (key.length !== 32) {
|
|
25
27
|
throw new Error(
|
|
26
|
-
'SAASBACKEND_ENCRYPTION_KEY must be 32 bytes (base64-encoded 32 bytes, hex 64 chars, or 32-char utf8)',
|
|
28
|
+
'SUPERBACKEND_ENCRYPTION_KEY (or SAASBACKEND_ENCRYPTION_KEY) must be 32 bytes (base64-encoded 32 bytes, hex 64 chars, or 32-char utf8)',
|
|
27
29
|
);
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Coolify Headless Deploy -
|
|
6
|
+
<title>Coolify Headless Deploy - SuperBackend</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>SuperBackend Command Center</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
9
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div class="flex items-center gap-4">
|
|
20
20
|
<i class="ti ti-layout-dashboard text-2xl text-blue-600"></i>
|
|
21
21
|
<h1 class="text-xl font-bold text-gray-800">
|
|
22
|
-
|
|
22
|
+
SuperBackend <span class="text-xs font-normal text-gray-500 ml-2 align-middle">(@intranefr/superbackend)</span>
|
|
23
23
|
</h1>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="flex items-center gap-6">
|
package/views/admin-errors.ejs
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
<div>
|
|
51
51
|
<div class="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2">Identify user (JWT header)</div>
|
|
52
52
|
<div class="relative">
|
|
53
|
-
<pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-jwt">
|
|
53
|
+
<pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-jwt">superbackend.errorTracking.config({
|
|
54
54
|
headers: { authorization: "Bearer XXX" }
|
|
55
55
|
})</code></pre>
|
|
56
56
|
<button type="button" class="absolute top-2 right-2 text-xs px-2 py-1 bg-white border border-gray-200 rounded hover:border-blue-500" onclick="copySnippet('snippet-jwt')">Copy</button>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<div>
|
|
61
61
|
<div class="text-xs font-semibold text-gray-600 uppercase tracking-wide mb-2">Future npm package (bundlers)</div>
|
|
62
62
|
<div class="relative">
|
|
63
|
-
<pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-npm">import { createErrorTrackingClient } from '@
|
|
63
|
+
<pre class="p-3 bg-gray-50 rounded text-xs overflow-x-auto"><code id="snippet-npm">import { createErrorTrackingClient } from '@intranefr/superbackend-error-tracking-browser';
|
|
64
64
|
|
|
65
65
|
const client = createErrorTrackingClient({
|
|
66
66
|
endpoint: '/api/log/error',
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<div class="flex justify-between items-center">
|
|
31
31
|
<div>
|
|
32
32
|
<h1 class="text-2xl font-bold text-gray-900">Global Settings Manager</h1>
|
|
33
|
-
<p class="text-sm text-gray-600 mt-1">Configure system-wide settings for
|
|
33
|
+
<p class="text-sm text-gray-600 mt-1">Configure system-wide settings for SuperBackend</p>
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
<h3 class="font-semibold text-blue-900 mb-2">📋 Available Setting Keys</h3>
|
|
67
67
|
<div class="text-sm text-blue-800 space-y-2">
|
|
68
68
|
<p><strong>RESEND_API_KEY</strong> (string) - API key for Resend email service</p>
|
|
69
|
-
<p><strong>EMAIL_FROM</strong> (string) - Default "From" address for emails (e.g., "
|
|
70
|
-
<p><strong>FRONTEND_URL</strong> (string) - Frontend application URL (e.g., "https://app.
|
|
69
|
+
<p><strong>EMAIL_FROM</strong> (string) - Default "From" address for emails (e.g., "SuperBackend <no-reply@yourdomain.com>")</p>
|
|
70
|
+
<p><strong>FRONTEND_URL</strong> (string) - Frontend application URL (e.g., "https://app.superbackend.com")</p>
|
|
71
71
|
<p><strong>EMAIL_PASSWORD_RESET_SUBJECT</strong> (string) - Subject line for password reset emails</p>
|
|
72
72
|
<p><strong>EMAIL_PASSWORD_RESET_HTML</strong> (html) - HTML template for password reset emails</p>
|
|
73
73
|
<p class="pl-4 text-blue-700">Available variables: <code class="bg-blue-100 px-1 rounded">{{resetUrl}}</code></p>
|
|
@@ -229,7 +229,14 @@
|
|
|
229
229
|
function updateSnippet() {
|
|
230
230
|
const slug = $('slug').value || '<slug>';
|
|
231
231
|
const snippet =
|
|
232
|
-
`const
|
|
232
|
+
`const superbackend = require('@intranefr/superbackend');
|
|
233
|
+
|
|
234
|
+
// Works when your host app mounts superbackend.middleware(...) and shares the same process/DB connection
|
|
235
|
+
const { getJsonConfig } = superbackend.services.jsonConfigs;
|
|
236
|
+
|
|
237
|
+
const cfg = await getJsonConfig('${slug}', {
|
|
238
|
+
bypassCache: false,
|
|
239
|
+
});\n`;
|
|
233
240
|
$('snippet').textContent = snippet;
|
|
234
241
|
}
|
|
235
242
|
|
package/views/admin-llm.ejs
CHANGED
|
@@ -369,9 +369,9 @@
|
|
|
369
369
|
<div class="mt-4 bg-blue-50 border-l-4 border-blue-500 p-4 text-sm text-blue-900">
|
|
370
370
|
<p class="font-semibold mb-1">Storage model</p>
|
|
371
371
|
<p>Providers and prompts are stored in <code>GlobalSetting</code> rows as JSON under keys <code>llm.providers</code> and <code>llm.prompts</code>. Use the internal service:</p>
|
|
372
|
-
<pre class="mt-2 bg-blue-100 rounded p-2 text-xs overflow-auto">const
|
|
372
|
+
<pre class="mt-2 bg-blue-100 rounded p-2 text-xs overflow-auto">const superbackend = require('@intranefr/superbackend');
|
|
373
373
|
|
|
374
|
-
const result = await
|
|
374
|
+
const result = await superbackend.services.llm.call('tell_joke', {
|
|
375
375
|
theme: 'universe',
|
|
376
376
|
}, {
|
|
377
377
|
temperature: 0.8,
|
|
@@ -248,7 +248,7 @@
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
function buildSnippet() {
|
|
251
|
-
const snippet = `const { getJsonConfigValueBySlug } = require('
|
|
251
|
+
const snippet = `const { getJsonConfigValueBySlug } = require('@intranefr/superbackend').services.jsonConfigs;\n\nasync function loadSeoConfig() {\n const seo = await getJsonConfigValueBySlug('seo-config');\n return seo;\n}`;
|
|
252
252
|
document.getElementById('dev-snippet').textContent = snippet;
|
|
253
253
|
}
|
|
254
254
|
|
package/views/admin-test.ejs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>SuperBackend API Laboratory</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
|
9
9
|
<style>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
<div class="p-4 border-t border-slate-800 bg-slate-900/50">
|
|
47
47
|
<div class="flex items-center justify-between text-xs text-slate-500 mb-2">
|
|
48
|
-
<span>
|
|
48
|
+
<span>SuperBackend Framework</span>
|
|
49
49
|
<span class="px-2 py-0.5 bg-slate-800 rounded">v1.2.0</span>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
<div class="text-6xl mb-6">🚀</div>
|
|
77
77
|
<h2 class="text-2xl font-bold">Framework Technical Console</h2>
|
|
78
78
|
<p class="text-slate-400 max-w-lg mx-auto">
|
|
79
|
-
Test and debug
|
|
79
|
+
Test and debug SuperBackend internal APIs. Documented endpoints are automatically grouped by service.
|
|
80
80
|
</p>
|
|
81
81
|
<div class="grid grid-cols-3 gap-4 pt-8 max-w-2xl mx-auto">
|
|
82
82
|
<div class="p-4 bg-slate-800/50 rounded-lg border border-slate-700">
|
package/views/admin-users.ejs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
<p class="text-sm text-gray-600 mt-1">Manage system users</p>
|
|
23
23
|
</div>
|
|
24
24
|
<div class="flex items-center gap-4">
|
|
25
|
+
<button id="btn-register-user" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register New User</button>
|
|
25
26
|
</div>
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
@@ -199,6 +200,46 @@
|
|
|
199
200
|
</div>
|
|
200
201
|
</div>
|
|
201
202
|
|
|
203
|
+
<div id="modal-register" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
|
|
204
|
+
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
|
|
205
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Register New User</h3>
|
|
206
|
+
<form id="register-form" class="space-y-4">
|
|
207
|
+
<div>
|
|
208
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Email *</label>
|
|
209
|
+
<input id="register-email" type="email" required class="w-full border rounded px-3 py-2" placeholder="user@example.com">
|
|
210
|
+
</div>
|
|
211
|
+
<div>
|
|
212
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Password *</label>
|
|
213
|
+
<div class="relative">
|
|
214
|
+
<input id="register-password" type="password" required minlength="6" class="w-full border rounded px-3 py-2 pr-10" placeholder="Min 6 characters">
|
|
215
|
+
<button type="button" id="toggle-password" class="absolute right-2 top-2 text-gray-500 hover:text-gray-700">
|
|
216
|
+
<svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
217
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
218
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
219
|
+
</svg>
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
225
|
+
<input id="register-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Optional">
|
|
226
|
+
</div>
|
|
227
|
+
<div>
|
|
228
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
229
|
+
<select id="register-role" class="w-full border rounded px-3 py-2">
|
|
230
|
+
<option value="user">User</option>
|
|
231
|
+
<option value="admin">Admin</option>
|
|
232
|
+
</select>
|
|
233
|
+
</div>
|
|
234
|
+
<div id="register-error" class="hidden text-red-600 text-sm"></div>
|
|
235
|
+
</form>
|
|
236
|
+
<div class="flex justify-end gap-2 mt-6">
|
|
237
|
+
<button id="btn-register-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
|
|
238
|
+
<button id="btn-register-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
202
243
|
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
203
244
|
|
|
204
245
|
<script>
|
|
@@ -417,6 +458,132 @@
|
|
|
417
458
|
} catch (e) { showToast(e.message, 'error'); }
|
|
418
459
|
}
|
|
419
460
|
|
|
461
|
+
function openRegisterModal() {
|
|
462
|
+
document.getElementById('register-email').value = '';
|
|
463
|
+
document.getElementById('register-password').value = '';
|
|
464
|
+
document.getElementById('register-name').value = '';
|
|
465
|
+
document.getElementById('register-role').value = 'user';
|
|
466
|
+
document.getElementById('register-error').classList.add('hidden');
|
|
467
|
+
document.getElementById('modal-register').classList.remove('hidden');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function closeRegisterModal() {
|
|
471
|
+
document.getElementById('modal-register').classList.add('hidden');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function togglePasswordVisibility() {
|
|
475
|
+
const passwordInput = document.getElementById('register-password');
|
|
476
|
+
const eyeIcon = document.getElementById('eye-icon');
|
|
477
|
+
|
|
478
|
+
if (passwordInput.type === 'password') {
|
|
479
|
+
passwordInput.type = 'text';
|
|
480
|
+
eyeIcon.innerHTML = `
|
|
481
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
|
482
|
+
`;
|
|
483
|
+
} else {
|
|
484
|
+
passwordInput.type = 'password';
|
|
485
|
+
eyeIcon.innerHTML = `
|
|
486
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
487
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function registerUser() {
|
|
493
|
+
const email = document.getElementById('register-email').value.trim();
|
|
494
|
+
const password = document.getElementById('register-password').value;
|
|
495
|
+
const name = document.getElementById('register-name').value.trim();
|
|
496
|
+
const role = document.getElementById('register-role').value;
|
|
497
|
+
const errorDiv = document.getElementById('register-error');
|
|
498
|
+
|
|
499
|
+
// Clear previous errors
|
|
500
|
+
errorDiv.classList.add('hidden');
|
|
501
|
+
errorDiv.textContent = '';
|
|
502
|
+
|
|
503
|
+
// Basic validation
|
|
504
|
+
if (!email || !password) {
|
|
505
|
+
errorDiv.textContent = 'Email and password are required';
|
|
506
|
+
errorDiv.classList.remove('hidden');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (password.length < 6) {
|
|
511
|
+
errorDiv.textContent = 'Password must be at least 6 characters';
|
|
512
|
+
errorDiv.classList.remove('hidden');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
517
|
+
if (!emailRegex.test(email)) {
|
|
518
|
+
errorDiv.textContent = 'Invalid email format';
|
|
519
|
+
errorDiv.classList.remove('hidden');
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Disable submit button
|
|
524
|
+
const submitBtn = document.getElementById('btn-register-submit');
|
|
525
|
+
const originalText = submitBtn.textContent;
|
|
526
|
+
submitBtn.disabled = true;
|
|
527
|
+
submitBtn.textContent = 'Registering...';
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
// Determine API base URL considering relative path mounting
|
|
531
|
+
const apiBase = getApiUrl('/api/admin/users/register');
|
|
532
|
+
|
|
533
|
+
const response = await fetch(apiBase, {
|
|
534
|
+
method: 'POST',
|
|
535
|
+
headers: {
|
|
536
|
+
'Content-Type': 'application/json'
|
|
537
|
+
},
|
|
538
|
+
body: JSON.stringify({ email, password, name, role })
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
|
|
543
|
+
if (!response.ok) {
|
|
544
|
+
if (response.status === 401) {
|
|
545
|
+
throw new Error('Admin authentication required. Please refresh the page and log in again.');
|
|
546
|
+
}
|
|
547
|
+
throw new Error(data.error || 'Registration failed');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
showToast(`User ${email} registered successfully`, 'success');
|
|
551
|
+
closeRegisterModal();
|
|
552
|
+
await Promise.all([loadUsers(), loadStats()]);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
errorDiv.textContent = error.message || 'Registration failed';
|
|
555
|
+
errorDiv.classList.remove('hidden');
|
|
556
|
+
} finally {
|
|
557
|
+
submitBtn.disabled = false;
|
|
558
|
+
submitBtn.textContent = originalText;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function getApiUrl(endpoint) {
|
|
563
|
+
// Detect if we're mounted on a relative path
|
|
564
|
+
const pathname = window.location.pathname;
|
|
565
|
+
const pathSegments = pathname.split('/').filter(Boolean);
|
|
566
|
+
|
|
567
|
+
// Check for common mounting patterns
|
|
568
|
+
if (pathSegments.includes('admin')) {
|
|
569
|
+
// If we're in /admin/ context, try to detect the base path
|
|
570
|
+
// Look for patterns like /super/admin/ or /api/admin/
|
|
571
|
+
const adminIndex = pathSegments.indexOf('admin');
|
|
572
|
+
|
|
573
|
+
if (adminIndex > 0) {
|
|
574
|
+
// There's a prefix before 'admin', use it as base
|
|
575
|
+
const basePath = '/' + pathSegments.slice(0, adminIndex).join('/');
|
|
576
|
+
return basePath + endpoint;
|
|
577
|
+
} else {
|
|
578
|
+
// Admin is at root, just use the endpoint
|
|
579
|
+
return endpoint;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Default case - use current origin with endpoint
|
|
584
|
+
return window.location.origin + endpoint;
|
|
585
|
+
}
|
|
586
|
+
|
|
420
587
|
function bindEvents() {
|
|
421
588
|
document.getElementById('btn-refresh').onclick = () => Promise.all([loadUsers(), loadStats()]);
|
|
422
589
|
document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadUsers(); };
|
|
@@ -435,6 +602,18 @@
|
|
|
435
602
|
document.getElementById('btn-modal-save').onclick = saveUser;
|
|
436
603
|
document.getElementById('btn-notify-cancel').onclick = closeNotifyModal;
|
|
437
604
|
document.getElementById('btn-notify-send').onclick = sendNotification;
|
|
605
|
+
|
|
606
|
+
// Registration modal events
|
|
607
|
+
document.getElementById('btn-register-user').onclick = openRegisterModal;
|
|
608
|
+
document.getElementById('btn-register-cancel').onclick = closeRegisterModal;
|
|
609
|
+
document.getElementById('btn-register-submit').onclick = registerUser;
|
|
610
|
+
document.getElementById('toggle-password').onclick = togglePasswordVisibility;
|
|
611
|
+
|
|
612
|
+
// Form submission
|
|
613
|
+
document.getElementById('register-form').onsubmit = (e) => {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
registerUser();
|
|
616
|
+
};
|
|
438
617
|
}
|
|
439
618
|
|
|
440
619
|
bindEvents();
|
package/views/admin-webhooks.ejs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Webhook Management |
|
|
6
|
+
<title>Webhook Management | SuperBackend</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
9
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Workflow Editor -
|
|
6
|
+
<title>Workflow Editor - SuperBackend</title>
|
|
7
7
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
8
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
9
9
|
<style>
|
|
@@ -1436,10 +1436,10 @@
|
|
|
1436
1436
|
const publicUrl = selected?.publicUrl ? `${API_BASE}${selected.publicUrl}` : null;
|
|
1437
1437
|
|
|
1438
1438
|
const snippet =
|
|
1439
|
-
`const
|
|
1439
|
+
`const superbackend = require('@intranefr/superbackend');
|
|
1440
1440
|
|
|
1441
|
-
// Works when your host app mounts
|
|
1442
|
-
const { assets } =
|
|
1441
|
+
// Works when your host app mounts superbackend.middleware(...) and shares the same process/DB connection
|
|
1442
|
+
const { assets } = superbackend.services;
|
|
1443
1443
|
|
|
1444
1444
|
// 1) List assets (metadata)
|
|
1445
1445
|
const { assets: list } = await assets.listAssets({
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<span><kbd class="bg-white border rounded px-1.5 py-0.5">Enter</kbd> Select</span>
|
|
58
58
|
<span><kbd class="bg-white border rounded px-1.5 py-0.5">Esc</kbd> Close</span>
|
|
59
59
|
</div>
|
|
60
|
-
<div>
|
|
60
|
+
<div>SuperBackend <span class="text-[10px] font-normal opacity-60 ml-1">(@intranefr/superbackend)</span></div>
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|