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.
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 +89 -20
  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');
@@ -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('/*', function(req, res) {
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>