parse-dashboard-analytics 7.4.1 → 7.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/Parse-Dashboard/Authentication.js +9 -4
- package/Parse-Dashboard/CLI/mfa.js +11 -5
- package/Parse-Dashboard/app.js +89 -20
- package/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
- package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
- package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
- package/Parse-Dashboard/browser-control/README.md +852 -0
- package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
- package/Parse-Dashboard/browser-control/index.js +27 -0
- package/Parse-Dashboard/browser-control/setup.js +405 -0
- package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +27 -33
- package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
- package/Parse-Dashboard/public/bundles/sprites.svg +294 -154
- package/Parse-Dashboard/server.js +33 -2
- package/README.md +475 -117
- package/package.json +68 -53
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const bcrypt = require('bcryptjs');
|
|
3
|
-
const
|
|
3
|
+
const { csrfSync } = require('csrf-sync');
|
|
4
4
|
const passport = require('passport');
|
|
5
5
|
const LocalStrategy = require('passport-local').Strategy;
|
|
6
6
|
const OTPAuth = require('otpauth')
|
|
7
7
|
|
|
8
|
+
const { csrfSynchronisedProtection } = csrfSync({
|
|
9
|
+
getTokenFromRequest: (req) => req.body._csrf || req.headers['x-csrf-token'],
|
|
10
|
+
});
|
|
11
|
+
|
|
8
12
|
/**
|
|
9
13
|
* Constructor for Authentication class
|
|
10
14
|
*
|
|
@@ -27,7 +31,7 @@ function initialize(app, options) {
|
|
|
27
31
|
const match = self.authenticate({
|
|
28
32
|
name: username,
|
|
29
33
|
pass: password,
|
|
30
|
-
otpCode: req.body
|
|
34
|
+
otpCode: req.body?.otpCode
|
|
31
35
|
});
|
|
32
36
|
if (!match.matchingUsername) {
|
|
33
37
|
return cb(null, false, { message: JSON.stringify({ text: 'Invalid username or password' }) });
|
|
@@ -81,11 +85,11 @@ function initialize(app, options) {
|
|
|
81
85
|
app.use(passport.session());
|
|
82
86
|
|
|
83
87
|
app.post('/login',
|
|
84
|
-
|
|
88
|
+
csrfSynchronisedProtection,
|
|
85
89
|
(req,res,next) => {
|
|
86
90
|
let redirect = 'apps';
|
|
87
91
|
let originalRedirect = null;
|
|
88
|
-
if (req.body
|
|
92
|
+
if (req.body?.redirect) {
|
|
89
93
|
originalRedirect = req.body.redirect;
|
|
90
94
|
// Validate redirect to prevent open redirect vulnerability
|
|
91
95
|
if (originalRedirect.includes('://') || originalRedirect.startsWith('//')) {
|
|
@@ -176,5 +180,6 @@ function authenticate(userToTest, usernameOnly) {
|
|
|
176
180
|
|
|
177
181
|
Authentication.prototype.initialize = initialize;
|
|
178
182
|
Authentication.prototype.authenticate = authenticate;
|
|
183
|
+
Authentication.csrfProtection = csrfSynchronisedProtection;
|
|
179
184
|
|
|
180
185
|
module.exports = Authentication;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
-
let inquirer
|
|
3
|
-
|
|
4
|
-
inquirer
|
|
5
|
-
|
|
2
|
+
let inquirer;
|
|
3
|
+
const loadInquirer = async () => {
|
|
4
|
+
if (!inquirer) {
|
|
5
|
+
const mod = await import('inquirer');
|
|
6
|
+
inquirer = mod.default || mod;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
6
9
|
const OTPAuth = require('otpauth');
|
|
7
10
|
const { copy } = require('./utils.js');
|
|
8
11
|
const phrases = {
|
|
@@ -11,9 +14,10 @@ const phrases = {
|
|
|
11
14
|
enterAppName: 'Enter the app name:',
|
|
12
15
|
}
|
|
13
16
|
const getAlgorithm = async () => {
|
|
17
|
+
await loadInquirer();
|
|
14
18
|
let { algorithm } = await inquirer.prompt([
|
|
15
19
|
{
|
|
16
|
-
type: '
|
|
20
|
+
type: 'select',
|
|
17
21
|
name: 'algorithm',
|
|
18
22
|
message: 'Which hashing algorithm do you want to use?',
|
|
19
23
|
default: 'SHA1',
|
|
@@ -141,6 +145,7 @@ const showInstructions = ({ app, username, passwordCopied, encrypt, config }) =>
|
|
|
141
145
|
|
|
142
146
|
module.exports = {
|
|
143
147
|
async createUser() {
|
|
148
|
+
await loadInquirer();
|
|
144
149
|
const data = {};
|
|
145
150
|
|
|
146
151
|
console.log('');
|
|
@@ -210,6 +215,7 @@ module.exports = {
|
|
|
210
215
|
showInstructions({ app: data.app, username, passwordCopied: true, encrypt, config });
|
|
211
216
|
},
|
|
212
217
|
async createMFA() {
|
|
218
|
+
await loadInquirer();
|
|
213
219
|
console.log('');
|
|
214
220
|
const { username, app } = await inquirer.prompt([
|
|
215
221
|
{
|
package/Parse-Dashboard/app.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const express = require('express');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const csrf = require('csurf');
|
|
6
5
|
const Authentication = require('./Authentication.js');
|
|
7
6
|
const fs = require('fs');
|
|
8
7
|
const ConfigKeyCache = require('./configKeyCache.js');
|
|
@@ -88,6 +87,32 @@ module.exports = function(config, options) {
|
|
|
88
87
|
cookieSessionStore: options.cookieSessionStore
|
|
89
88
|
});
|
|
90
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Checks whether a request is from localhost.
|
|
92
|
+
*/
|
|
93
|
+
function isLocalRequest(req) {
|
|
94
|
+
return req.connection.remoteAddress === '127.0.0.1' ||
|
|
95
|
+
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
|
|
96
|
+
req.connection.remoteAddress === '::1';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Middleware that enforces remote access restrictions:
|
|
101
|
+
* - Requires HTTPS for remote requests (unless allowInsecureHTTP is set)
|
|
102
|
+
* - Requires users to be configured for remote access (unless dev mode is enabled)
|
|
103
|
+
*/
|
|
104
|
+
function enforceRemoteAccessRestrictions(req, res, next) {
|
|
105
|
+
if (!options.dev && !isLocalRequest(req)) {
|
|
106
|
+
if (!req.secure && !options.allowInsecureHTTP) {
|
|
107
|
+
return res.status(403).json({ error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
|
|
108
|
+
}
|
|
109
|
+
if (!users) {
|
|
110
|
+
return res.status(401).json({ error: 'Configure a user to access Parse Dashboard remotely' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
next();
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
// CSRF error handler
|
|
92
117
|
app.use(function (err, req, res, next) {
|
|
93
118
|
if (err.code !== 'EBADCSRFTOKEN') {return next(err)}
|
|
@@ -110,13 +135,7 @@ module.exports = function(config, options) {
|
|
|
110
135
|
agent: config.agent,
|
|
111
136
|
};
|
|
112
137
|
|
|
113
|
-
|
|
114
|
-
//https://github.com/expressjs/express/issues/2518
|
|
115
|
-
const requestIsLocal =
|
|
116
|
-
req.connection.remoteAddress === '127.0.0.1' ||
|
|
117
|
-
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
|
|
118
|
-
req.connection.remoteAddress === '::1';
|
|
119
|
-
if (!options.dev && !requestIsLocal) {
|
|
138
|
+
if (!options.dev && !isLocalRequest(req)) {
|
|
120
139
|
if (!req.secure && !options.allowInsecureHTTP) {
|
|
121
140
|
//Disallow HTTP requests except on localhost, to prevent the master key from being transmitted in cleartext
|
|
122
141
|
return res.send({ success: false, error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
|
|
@@ -159,7 +178,8 @@ module.exports = function(config, options) {
|
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
if (typeof app.masterKey === 'function') {
|
|
162
|
-
|
|
181
|
+
const cacheKey = matchingAccess.readOnly ? 'readOnlyMasterKey' : 'masterKey';
|
|
182
|
+
app.masterKey = await ConfigKeyCache.get(app.appId, cacheKey, app.masterKeyTtl, app.masterKey);
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
return app;
|
|
@@ -179,7 +199,7 @@ module.exports = function(config, options) {
|
|
|
179
199
|
|
|
180
200
|
//They didn't provide auth, and have configured the dashboard to not need auth
|
|
181
201
|
//(ie. didn't supply usernames and passwords)
|
|
182
|
-
if (
|
|
202
|
+
if (isLocalRequest(req) || options.dev) {
|
|
183
203
|
//Allow no-auth access on localhost only, if they have configured the dashboard to not need auth
|
|
184
204
|
await Promise.all(
|
|
185
205
|
response.apps.map(async (app) => {
|
|
@@ -198,10 +218,12 @@ module.exports = function(config, options) {
|
|
|
198
218
|
// In-memory conversation storage (consider using Redis in future)
|
|
199
219
|
const conversations = new Map();
|
|
200
220
|
|
|
201
|
-
// Agent API endpoint
|
|
202
|
-
|
|
221
|
+
// Agent API endpoint handler
|
|
222
|
+
async function agentHandler(req, res) {
|
|
203
223
|
try {
|
|
204
|
-
const
|
|
224
|
+
const authentication = req.user;
|
|
225
|
+
|
|
226
|
+
const { message, modelName, conversationId, permissions } = req.body || {};
|
|
205
227
|
const { appId } = req.params;
|
|
206
228
|
|
|
207
229
|
if (!message || typeof message !== 'string' || message.trim() === '') {
|
|
@@ -222,11 +244,40 @@ module.exports = function(config, options) {
|
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
// Find the app in the configuration
|
|
225
|
-
const
|
|
226
|
-
if (!
|
|
247
|
+
const appConfig = config.apps.find(a => (a.appNameForURL || a.appName) === appId);
|
|
248
|
+
if (!appConfig) {
|
|
227
249
|
return res.status(404).json({ error: `App "${appId}" not found` });
|
|
228
250
|
}
|
|
229
251
|
|
|
252
|
+
// Cross-app access control — restrict to apps the authenticated user has access to
|
|
253
|
+
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
|
|
254
|
+
let isPerAppReadOnly = false;
|
|
255
|
+
if (appsUserHasAccess) {
|
|
256
|
+
const matchingAccess = appsUserHasAccess.find(access => access.appId === appConfig.appId);
|
|
257
|
+
if (!matchingAccess) {
|
|
258
|
+
return res.status(403).json({ error: 'Forbidden: you do not have access to this app' });
|
|
259
|
+
}
|
|
260
|
+
isPerAppReadOnly = !!matchingAccess.readOnly;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Determine if the user is read-only (globally or per-app)
|
|
264
|
+
const isReadOnly = (authentication && authentication.isReadOnly) || isPerAppReadOnly;
|
|
265
|
+
|
|
266
|
+
// Build the app context — always shallow copy to avoid mutating the shared config
|
|
267
|
+
const appContext = { ...appConfig };
|
|
268
|
+
if (isReadOnly) {
|
|
269
|
+
if (!appConfig.readOnlyMasterKey) {
|
|
270
|
+
return res.status(400).json({ error: 'You need to provide a readOnlyMasterKey to use read-only features.' });
|
|
271
|
+
}
|
|
272
|
+
appContext.masterKey = appConfig.readOnlyMasterKey;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Resolve function-typed masterKey (supports dynamic key rotation via ConfigKeyCache)
|
|
276
|
+
if (typeof appContext.masterKey === 'function') {
|
|
277
|
+
const cacheKey = isReadOnly ? 'readOnlyMasterKey' : 'masterKey';
|
|
278
|
+
appContext.masterKey = await ConfigKeyCache.get(appContext.appId, cacheKey, appContext.masterKeyTtl, appContext.masterKey);
|
|
279
|
+
}
|
|
280
|
+
|
|
230
281
|
// Find the requested model
|
|
231
282
|
const modelConfig = config.agent.models.find(model => model.name === modelName);
|
|
232
283
|
if (!modelConfig) {
|
|
@@ -259,8 +310,12 @@ module.exports = function(config, options) {
|
|
|
259
310
|
// Array to track database operations for this request
|
|
260
311
|
const operationLog = [];
|
|
261
312
|
|
|
313
|
+
// Read-only users: override client permissions to deny all write operations,
|
|
314
|
+
// preventing privilege escalation via self-authorized permissions in the request body
|
|
315
|
+
const effectivePermissions = isReadOnly ? {} : (permissions || {});
|
|
316
|
+
|
|
262
317
|
// Make request to OpenAI API with app context and conversation history
|
|
263
|
-
const response = await makeOpenAIRequest(message, model, apiKey,
|
|
318
|
+
const response = await makeOpenAIRequest(message, model, apiKey, appContext, conversationHistory, operationLog, effectivePermissions);
|
|
264
319
|
|
|
265
320
|
// Update conversation history with user message and AI response
|
|
266
321
|
conversationHistory.push(
|
|
@@ -281,7 +336,7 @@ module.exports = function(config, options) {
|
|
|
281
336
|
conversationId: finalConversationId,
|
|
282
337
|
debug: {
|
|
283
338
|
timestamp: new Date().toISOString(),
|
|
284
|
-
appId:
|
|
339
|
+
appId: appContext.appId,
|
|
285
340
|
modelUsed: model,
|
|
286
341
|
operations: operationLog
|
|
287
342
|
}
|
|
@@ -292,7 +347,20 @@ module.exports = function(config, options) {
|
|
|
292
347
|
const errorMessage = error.message || 'Provider error';
|
|
293
348
|
res.status(500).json({ error: `Error: ${errorMessage}` });
|
|
294
349
|
}
|
|
295
|
-
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Agent API endpoint — middleware chain: remote access guard → auth check (401) → CSRF validation (403) → handler
|
|
353
|
+
app.post('/apps/:appId/agent',
|
|
354
|
+
enforceRemoteAccessRestrictions,
|
|
355
|
+
(req, res, next) => {
|
|
356
|
+
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
357
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
358
|
+
}
|
|
359
|
+
next();
|
|
360
|
+
},
|
|
361
|
+
Authentication.csrfProtection,
|
|
362
|
+
agentHandler
|
|
363
|
+
);
|
|
296
364
|
|
|
297
365
|
/**
|
|
298
366
|
* Database function tools for the AI agent
|
|
@@ -1065,7 +1133,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1065
1133
|
}
|
|
1066
1134
|
}
|
|
1067
1135
|
|
|
1068
|
-
app.get('/login',
|
|
1136
|
+
app.get('/login', Authentication.csrfProtection, function(req, res) {
|
|
1069
1137
|
let redirectURL = null;
|
|
1070
1138
|
try {
|
|
1071
1139
|
const url = new URL(req.url, 'http://localhost');
|
|
@@ -1228,7 +1296,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1228
1296
|
});
|
|
1229
1297
|
|
|
1230
1298
|
// For every other request, go to index.html. Let client-side handle the rest.
|
|
1231
|
-
app.get('
|
|
1299
|
+
app.get('{*splat}', Authentication.csrfProtection, function(req, res) {
|
|
1232
1300
|
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
1233
1301
|
const redirect = req.url.replace('/login', '');
|
|
1234
1302
|
if (redirect.length > 1) {
|
|
@@ -1252,6 +1320,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1252
1320
|
</head>
|
|
1253
1321
|
<body>
|
|
1254
1322
|
<div id="browser_mount"></div>
|
|
1323
|
+
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
|
|
1255
1324
|
<script src="${mountPath}bundles/dashboard.bundle.js"></script>
|
|
1256
1325
|
</body>
|
|
1257
1326
|
</html>
|