parse-dashboard-analytics 7.4.2 → 7.4.5
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 +93 -26
- 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');
|
|
@@ -1116,7 +1184,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1116
1184
|
});
|
|
1117
1185
|
|
|
1118
1186
|
// Analytics dashboard routes - Only serve custom analytics if analyticsPage is configured
|
|
1119
|
-
app.get('/apps
|
|
1187
|
+
app.get('/apps/:appId/analytics_dashboard', function(req, res) {
|
|
1120
1188
|
console.log('🎯 Analytics dashboard route hit:', req.path, req.url);
|
|
1121
1189
|
|
|
1122
1190
|
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
@@ -1129,8 +1197,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1129
1197
|
}
|
|
1130
1198
|
|
|
1131
1199
|
// Extract app ID from URL
|
|
1132
|
-
const
|
|
1133
|
-
const appId = appIdMatch ? appIdMatch[1] : null;
|
|
1200
|
+
const appId = req.params.appId;
|
|
1134
1201
|
console.log('📱 Extracted app ID:', appId);
|
|
1135
1202
|
|
|
1136
1203
|
// Find the app configuration
|
|
@@ -1172,7 +1239,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1172
1239
|
});
|
|
1173
1240
|
|
|
1174
1241
|
// Alternative analytics dashboard route (with slash instead of underscore)
|
|
1175
|
-
app.get('/apps
|
|
1242
|
+
app.get('/apps/:appId/analytics/dashboard', function(req, res) {
|
|
1176
1243
|
console.log('🎯 Alternative analytics route hit:', req.path, req.url);
|
|
1177
1244
|
|
|
1178
1245
|
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
@@ -1185,8 +1252,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1185
1252
|
}
|
|
1186
1253
|
|
|
1187
1254
|
// Extract app ID from URL
|
|
1188
|
-
const
|
|
1189
|
-
const appId = appIdMatch ? appIdMatch[1] : null;
|
|
1255
|
+
const appId = req.params.appId;
|
|
1190
1256
|
console.log('📱 Extracted app ID:', appId);
|
|
1191
1257
|
|
|
1192
1258
|
// Find the app configuration
|
|
@@ -1228,7 +1294,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1228
1294
|
});
|
|
1229
1295
|
|
|
1230
1296
|
// For every other request, go to index.html. Let client-side handle the rest.
|
|
1231
|
-
app.get('
|
|
1297
|
+
app.get('{*splat}', Authentication.csrfProtection, function(req, res) {
|
|
1232
1298
|
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
1233
1299
|
const redirect = req.url.replace('/login', '');
|
|
1234
1300
|
if (redirect.length > 1) {
|
|
@@ -1252,6 +1318,7 @@ You have direct access to the Parse database through function calls, so you can
|
|
|
1252
1318
|
</head>
|
|
1253
1319
|
<body>
|
|
1254
1320
|
<div id="browser_mount"></div>
|
|
1321
|
+
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
|
|
1255
1322
|
<script src="${mountPath}bundles/dashboard.bundle.js"></script>
|
|
1256
1323
|
</body>
|
|
1257
1324
|
</html>
|