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.
Files changed (43) hide show
  1. package/Parse-Dashboard/Authentication.js +9 -4
  2. package/Parse-Dashboard/CLI/mfa.js +11 -5
  3. package/Parse-Dashboard/app.js +93 -26
  4. package/Parse-Dashboard/browser-control/BrowserControlAPI.js +468 -0
  5. package/Parse-Dashboard/browser-control/BrowserEventStream.js +294 -0
  6. package/Parse-Dashboard/browser-control/BrowserSessionManager.js +304 -0
  7. package/Parse-Dashboard/browser-control/README.md +852 -0
  8. package/Parse-Dashboard/browser-control/ServerOrchestrator.js +334 -0
  9. package/Parse-Dashboard/browser-control/index.js +27 -0
  10. package/Parse-Dashboard/browser-control/setup.js +405 -0
  11. package/Parse-Dashboard/public/bundles/120.bundle.js +1 -1
  12. package/Parse-Dashboard/public/bundles/183.bundle.js +1 -1
  13. package/Parse-Dashboard/public/bundles/19.bundle.js +1 -1
  14. package/Parse-Dashboard/public/bundles/221.bundle.js +1 -1
  15. package/Parse-Dashboard/public/bundles/372.bundle.js +1 -1
  16. package/Parse-Dashboard/public/bundles/448.bundle.js +1 -1
  17. package/Parse-Dashboard/public/bundles/573.bundle.js +1 -1
  18. package/Parse-Dashboard/public/bundles/578.bundle.js +1 -1
  19. package/Parse-Dashboard/public/bundles/584.bundle.js +1 -1
  20. package/Parse-Dashboard/public/bundles/629.bundle.js +1 -1
  21. package/Parse-Dashboard/public/bundles/643.bundle.js +1 -1
  22. package/Parse-Dashboard/public/bundles/647.bundle.js +1 -1
  23. package/Parse-Dashboard/public/bundles/701.bundle.js +1 -1
  24. package/Parse-Dashboard/public/bundles/729.bundle.js +1 -1
  25. package/Parse-Dashboard/public/bundles/75.bundle.js +1 -1
  26. package/Parse-Dashboard/public/bundles/753.bundle.js +1 -1
  27. package/Parse-Dashboard/public/bundles/817.bundle.js +1 -1
  28. package/Parse-Dashboard/public/bundles/844.bundle.js +1 -1
  29. package/Parse-Dashboard/public/bundles/862.bundle.js +1 -1
  30. package/Parse-Dashboard/public/bundles/866.bundle.js +1 -1
  31. package/Parse-Dashboard/public/bundles/874.bundle.js +1 -1
  32. package/Parse-Dashboard/public/bundles/881.bundle.js +1 -1
  33. package/Parse-Dashboard/public/bundles/929.bundle.js +1 -1
  34. package/Parse-Dashboard/public/bundles/950.bundle.js +1 -1
  35. package/Parse-Dashboard/public/bundles/97.bundle.js +1 -1
  36. package/Parse-Dashboard/public/bundles/976.bundle.js +1 -1
  37. package/Parse-Dashboard/public/bundles/dashboard.bundle.js +1 -1
  38. package/Parse-Dashboard/public/bundles/dashboard.bundle.js.LICENSE.txt +27 -33
  39. package/Parse-Dashboard/public/bundles/login.bundle.js +1 -1
  40. package/Parse-Dashboard/public/bundles/sprites.svg +294 -154
  41. package/Parse-Dashboard/server.js +33 -2
  42. package/README.md +475 -117
  43. package/package.json +68 -53
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
  const bcrypt = require('bcryptjs');
3
- const csrf = require('csurf');
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.otpCode
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
- csrf(),
88
+ csrfSynchronisedProtection,
85
89
  (req,res,next) => {
86
90
  let redirect = 'apps';
87
91
  let originalRedirect = null;
88
- if (req.body.redirect) {
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 = require('inquirer');
3
- if (inquirer.default) {
4
- inquirer = inquirer.default;
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: 'list',
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
  {
@@ -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
- //Based on advice from Doug Wilson here:
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
- app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey);
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 (requestIsLocal || options.dev) {
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 for handling AI requests - scoped to specific app
202
- app.post('/apps/:appId/agent', async (req, res) => {
221
+ // Agent API endpoint handler
222
+ async function agentHandler(req, res) {
203
223
  try {
204
- const { message, modelName, conversationId, permissions } = req.body;
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 app = config.apps.find(app => (app.appNameForURL || app.appName) === appId);
226
- if (!app) {
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, app, conversationHistory, operationLog, permissions);
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: app.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', csrf(), function(req, res) {
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/*/analytics_dashboard', function(req, res) {
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 appIdMatch = req.path.match(/\/apps\/([^\/]+)\/analytics_dashboard/);
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/*/analytics/dashboard', function(req, res) {
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 appIdMatch = req.path.match(/\/apps\/([^\/]+)\/analytics\/dashboard/);
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('/*', function(req, res) {
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>