From 1b2e6b87014d4370871ad78c69bd4bd92bcf65c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:24:37 +0000 Subject: [PATCH 1/9] Initial plan From 0d763ddd9eb683aee02efa09b346d53c5dd0f3d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:31:30 +0000 Subject: [PATCH 2/9] Add sessionStore option for multi-replica deployments Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- Parse-Dashboard/Authentication.js | 12 +++++- Parse-Dashboard/app.js | 6 ++- Parse-Dashboard/server.js | 8 +++- README.md | 71 +++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index 55e274f46..c75aa9a7b 100644 --- a/Parse-Dashboard/Authentication.js +++ b/Parse-Dashboard/Authentication.js @@ -55,9 +55,10 @@ function initialize(app, options) { const cookieSessionSecret = options.cookieSessionSecret || require('crypto').randomBytes(64).toString('hex'); const cookieSessionMaxAge = options.cookieSessionMaxAge; + const sessionStore = options.sessionStore; app.use(require('body-parser').urlencoded({ extended: true })); - app.use(require('express-session')({ + const sessionConfig = { name: 'parse_dash', secret: cookieSessionSecret, resave: false, @@ -67,7 +68,14 @@ function initialize(app, options) { httpOnly: true, sameSite: 'lax', } - })); + }; + + // Add custom session store if provided + if (sessionStore) { + sessionConfig.store = sessionStore; + } + + app.use(require('express-session')(sessionConfig)); app.use(require('connect-flash')()); app.use(passport.initialize()); app.use(passport.session()); diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 144851c38..6e92f59bc 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -82,7 +82,11 @@ module.exports = function(config, options) { const users = config.users; const useEncryptedPasswords = config.useEncryptedPasswords ? true : false; const authInstance = new Authentication(users, useEncryptedPasswords, mountPath); - authInstance.initialize(app, { cookieSessionSecret: options.cookieSessionSecret, cookieSessionMaxAge: options.cookieSessionMaxAge }); + authInstance.initialize(app, { + cookieSessionSecret: options.cookieSessionSecret, + cookieSessionMaxAge: options.cookieSessionMaxAge, + sessionStore: options.sessionStore + }); // CSRF error handler app.use(function (err, req, res, next) { diff --git a/Parse-Dashboard/server.js b/Parse-Dashboard/server.js index 412b5a2eb..b24d6de2d 100644 --- a/Parse-Dashboard/server.js +++ b/Parse-Dashboard/server.js @@ -162,7 +162,13 @@ module.exports = (options) => { if (allowInsecureHTTP || trustProxy || dev) {app.enable('trust proxy');} config.data.trustProxy = trustProxy; - const dashboardOptions = { allowInsecureHTTP, cookieSessionSecret, dev, cookieSessionMaxAge }; + const dashboardOptions = { + allowInsecureHTTP, + cookieSessionSecret, + dev, + cookieSessionMaxAge, + sessionStore: config.data.sessionStore + }; app.use(mountPath, parseDashboard(config.data, dashboardOptions)); let server; if(!configSSLKey || !configSSLCert){ diff --git a/README.md b/README.md index 02b5394df..2bbfcb525 100644 --- a/README.md +++ b/README.md @@ -803,6 +803,77 @@ If you create a new user by running `parse-dashboard --createUser`, you will be Parse Dashboard follows the industry standard and supports the common OTP algorithm `SHA-1` by default, to be compatible with most authenticator apps. If you have specific security requirements regarding TOTP characteristics (algorithm, digit length, time period) you can customize them by using the guided configuration mentioned above. +### Running Multiple Dashboard Replicas + +When deploying Parse Dashboard with multiple replicas behind a load balancer, you need to use a shared session store to ensure that CSRF tokens and user sessions work correctly across all replicas. Without a shared session store, login attempts may fail with "CSRF token validation failed" errors when requests are distributed across different replicas. + +#### Using a Custom Session Store + +Parse Dashboard supports using any session store compatible with [express-session](https://github.com/expressjs/session), such as Redis, MongoDB, or Memcached. Here's an example using Redis with `connect-redis`: + +**Step 1:** Install the required dependencies: + +```bash +npm install connect-redis redis +``` + +**Step 2:** Configure your Parse Dashboard with a session store: + +```javascript +const express = require('express'); +const ParseDashboard = require('parse-dashboard'); +const { createClient } = require('redis'); +const RedisStore = require('connect-redis').default; + +// Create Redis client +const redisClient = createClient({ + url: 'redis://localhost:6379' +}); + +redisClient.connect().catch(console.error); + +// Create Redis store +const sessionStore = new RedisStore({ + client: redisClient, + prefix: 'parse-dashboard:', +}); + +// Configure Parse Dashboard with the session store +const dashboard = new ParseDashboard({ + apps: [ + { + serverURL: 'http://localhost:1337/parse', + appId: 'myAppId', + masterKey: 'myMasterKey', + appName: 'MyApp' + } + ], + users: [ + { + user: 'admin', + pass: 'password' + } + ], + cookieSessionSecret: 'your-session-secret', // Required for multi-replica +}, { + sessionStore: sessionStore // Pass the session store +}); + +const app = express(); +app.use('/dashboard', dashboard); +app.listen(4040); +``` + +**Important Notes:** + +- The `cookieSessionSecret` option must be set to the same value across all replicas to ensure session cookies work correctly. +- If `sessionStore` is not provided, Parse Dashboard will use the default in-memory session store, which only works for single-instance deployments. +- For production deployments with multiple replicas, always configure a shared session store. + +#### Alternative: Using Sticky Sessions + +If you cannot use a shared session store, you can configure your load balancer to use sticky sessions (session affinity), which ensures that requests from the same user are always routed to the same replica. However, using a shared session store is the recommended approach as it provides better reliability and scalability. + ### Separating App Access Based on User Identity If you have configured your dashboard to manage multiple applications, you can restrict the management of apps based on user identity. From dbb1c949b4e0f97b8de497e0de95066194a8baf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:33:23 +0000 Subject: [PATCH 3/9] Add tests for sessionStore functionality Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- src/lib/tests/SessionStore.test.js | 161 +++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/lib/tests/SessionStore.test.js diff --git a/src/lib/tests/SessionStore.test.js b/src/lib/tests/SessionStore.test.js new file mode 100644 index 000000000..e773646f7 --- /dev/null +++ b/src/lib/tests/SessionStore.test.js @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016-present, Parse, LLC + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ +jest.dontMock('../../../Parse-Dashboard/Authentication.js'); +jest.dontMock('../../../Parse-Dashboard/app.js'); + +const express = require('express'); +const session = require('express-session'); +const Authentication = require('../../../Parse-Dashboard/Authentication'); + +describe('SessionStore Integration', () => { + it('uses default in-memory store when sessionStore is not provided', () => { + const app = express(); + const users = [{ user: 'test', pass: 'password' }]; + const auth = new Authentication(users, false, '/'); + + // Mock app.use to capture session configuration + const useSpy = jest.fn(); + app.use = useSpy; + + auth.initialize(app, {}); + + // Find the call that sets up express-session + const sessionCall = useSpy.mock.calls.find(call => + call[0] && call[0].name === 'session' + ); + + expect(sessionCall).toBeDefined(); + // When no store is provided, express-session uses MemoryStore by default + // The session function should be called without a custom store + }); + + it('uses custom session store when sessionStore is provided', () => { + const app = express(); + const users = [{ user: 'test', pass: 'password' }]; + const auth = new Authentication(users, false, '/'); + + // Create a mock session store that implements the Store interface + const Store = session.Store; + class MockStore extends Store { + constructor() { + super(); + } + get(sid, callback) { + callback(null, {}); + } + set(sid, session, callback) { + callback(null); + } + destroy(sid, callback) { + callback(null); + } + } + + const mockStore = new MockStore(); + + // Mock app.use to capture session configuration + const useSpy = jest.fn(); + app.use = useSpy; + + auth.initialize(app, { sessionStore: mockStore }); + + // The session middleware should have been configured + expect(useSpy).toHaveBeenCalled(); + + // Find the call that sets up express-session + const sessionCall = useSpy.mock.calls.find(call => + call[0] && call[0].name === 'session' + ); + + expect(sessionCall).toBeDefined(); + }); + + it('passes sessionStore through app.js to Authentication', () => { + const parseDashboard = require('../../../Parse-Dashboard/app.js'); + + // Create a mock session store that implements the Store interface + const Store = session.Store; + class MockStore extends Store { + constructor() { + super(); + } + get(sid, callback) { + callback(null, {}); + } + set(sid, session, callback) { + callback(null); + } + destroy(sid, callback) { + callback(null); + } + } + + const mockStore = new MockStore(); + + const config = { + apps: [ + { + serverURL: 'http://localhost:1337/parse', + appId: 'testAppId', + masterKey: 'testMasterKey', + appName: 'TestApp', + }, + ], + users: [ + { + user: 'testuser', + pass: 'testpass', + }, + ], + }; + + const options = { + sessionStore: mockStore, + cookieSessionSecret: 'test-secret', + }; + + // Create dashboard app + const dashboardApp = parseDashboard(config, options); + + // The app should be created successfully with the session store + expect(dashboardApp).toBeDefined(); + expect(typeof dashboardApp).toBe('function'); // Express app is a function + }); + + it('maintains backward compatibility without sessionStore option', () => { + const parseDashboard = require('../../../Parse-Dashboard/app.js'); + + const config = { + apps: [ + { + serverURL: 'http://localhost:1337/parse', + appId: 'testAppId', + masterKey: 'testMasterKey', + appName: 'TestApp', + }, + ], + users: [ + { + user: 'testuser', + pass: 'testpass', + }, + ], + }; + + const options = { + cookieSessionSecret: 'test-secret', + }; + + // Create dashboard app without sessionStore option + const dashboardApp = parseDashboard(config, options); + + // The app should be created successfully even without session store + expect(dashboardApp).toBeDefined(); + expect(typeof dashboardApp).toBe('function'); + }); +}); From 3b696084a4769c0b3f3fe3d3a05c26457a265a65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:35:22 +0000 Subject: [PATCH 4/9] Add example configurations for multi-replica deployments Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- examples/README.md | 132 +++++++++++++++++++++++++++++ examples/mongodb-session-store.js | 120 ++++++++++++++++++++++++++ examples/redis-session-store.js | 136 ++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/mongodb-session-store.js create mode 100644 examples/redis-session-store.js diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..827154614 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,132 @@ +# Parse Dashboard Examples + +This directory contains example configurations for Parse Dashboard in various deployment scenarios. + +## Multi-Replica Deployments + +When running Parse Dashboard with multiple replicas behind a load balancer, you need to use a shared session store to ensure CSRF tokens and user sessions work correctly across all replicas. + +### Available Examples + +1. **[redis-session-store.js](./redis-session-store.js)** - Using Redis as the session store + - Fast, in-memory session storage + - Recommended for high-traffic deployments + - Requires: `connect-redis`, `redis` + +2. **[mongodb-session-store.js](./mongodb-session-store.js)** - Using MongoDB as the session store + - Persistent session storage + - Good if you already have MongoDB infrastructure + - Requires: `connect-mongo` + +### Quick Start + +1. Choose an example based on your infrastructure +2. Install the required dependencies: + ```bash + # For Redis + npm install parse-dashboard connect-redis redis + + # For MongoDB + npm install parse-dashboard connect-mongo + ``` +3. Configure environment variables: + ```bash + # For Redis + export REDIS_URL="redis://localhost:6379" + export SESSION_SECRET="your-secret-key-change-this" + export PARSE_SERVER_URL="http://localhost:1337/parse" + export PARSE_APP_ID="myAppId" + export PARSE_MASTER_KEY="myMasterKey" + + # For MongoDB + export MONGODB_URL="mongodb://localhost:27017/parse-dashboard-sessions" + export SESSION_SECRET="your-secret-key-change-this" + export PARSE_SERVER_URL="http://localhost:1337/parse" + export PARSE_APP_ID="myAppId" + export PARSE_MASTER_KEY="myMasterKey" + ``` +4. Run the example: + ```bash + node examples/redis-session-store.js + # or + node examples/mongodb-session-store.js + ``` + +### Important Notes + +- **`SESSION_SECRET` must be the same across all replicas** - This ensures session cookies work correctly +- **Configure your load balancer properly** - Set `trustProxy: true` when behind a reverse proxy +- **Health check endpoint** - All examples include a `/health` endpoint for load balancer health checks +- **Graceful shutdown** - Examples include proper cleanup handlers for SIGTERM and SIGINT signals + +### Kubernetes Deployment + +For Kubernetes deployments, you can use ConfigMaps and Secrets to configure your dashboard: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: parse-dashboard-config +data: + REDIS_URL: "redis://redis-service:6379" + PARSE_SERVER_URL: "http://parse-server-service:1337/parse" + PARSE_APP_ID: "myAppId" +--- +apiVersion: v1 +kind: Secret +metadata: + name: parse-dashboard-secrets +type: Opaque +data: + SESSION_SECRET: + PARSE_MASTER_KEY: + DASHBOARD_PASS: +``` + +### Docker Compose + +For Docker Compose deployments, you can use environment files: + +```yaml +version: '3.8' +services: + redis: + image: redis:latest + ports: + - "6379:6379" + + parse-dashboard: + build: . + environment: + - REDIS_URL=redis://redis:6379 + - SESSION_SECRET=${SESSION_SECRET} + - PARSE_SERVER_URL=${PARSE_SERVER_URL} + - PARSE_APP_ID=${PARSE_APP_ID} + - PARSE_MASTER_KEY=${PARSE_MASTER_KEY} + ports: + - "4040:4040" + depends_on: + - redis + deploy: + replicas: 3 +``` + +### Troubleshooting + +**Issue: "CSRF token validation failed" errors** +- Ensure `SESSION_SECRET` is the same across all replicas +- Verify the session store is accessible from all replicas +- Check that `trustProxy` is enabled when behind a load balancer + +**Issue: Sessions not persisting** +- Verify the session store connection is working +- Check session TTL configuration +- Ensure the session store has enough memory/storage + +**Issue: High memory usage** +- Adjust session TTL to clean up expired sessions +- Use `touchAfter` option (MongoDB) to reduce update frequency +- Monitor session store metrics + +For more information, see the [main README](../README.md#running-multiple-dashboard-replicas). diff --git a/examples/mongodb-session-store.js b/examples/mongodb-session-store.js new file mode 100644 index 000000000..a0d643b68 --- /dev/null +++ b/examples/mongodb-session-store.js @@ -0,0 +1,120 @@ +/** + * Example: Using Parse Dashboard with MongoDB Session Store for Multi-Replica Deployments + * + * This example shows how to configure Parse Dashboard with a MongoDB session store + * to support multiple dashboard replicas behind a load balancer without sticky sessions. + * + * Prerequisites: + * 1. Install required dependencies: + * npm install parse-dashboard connect-mongo + * + * 2. Have a MongoDB server running (e.g., mongodb://localhost:27017) + */ + +const express = require('express'); +const ParseDashboard = require('parse-dashboard'); +const MongoStore = require('connect-mongo'); + +// Configuration +const MONGODB_URL = process.env.MONGODB_URL || 'mongodb://localhost:27017/parse-dashboard-sessions'; +const PORT = process.env.PORT || 4040; +const SESSION_SECRET = process.env.SESSION_SECRET || 'your-secret-key-change-this'; + +// Create MongoDB session store +const sessionStore = MongoStore.create({ + mongoUrl: MONGODB_URL, + collectionName: 'sessions', // Collection name for storing sessions + ttl: 86400, // Session TTL in seconds (24 hours) + autoRemove: 'native', // Let MongoDB's TTL index handle session cleanup + touchAfter: 3600, // Update session only once per hour (unless session data changes) + // Optional: Add connection options + mongoOptions: { + useNewUrlParser: true, + useUnifiedTopology: true, + } +}); + +// Handle store connection events +sessionStore.on('error', (err) => { + console.error('MongoDB Session Store Error:', err); +}); + +sessionStore.on('connected', () => { + console.log('Connected to MongoDB for session storage'); +}); + +// Parse Dashboard configuration +const dashboardConfig = { + apps: [ + { + serverURL: process.env.PARSE_SERVER_URL || 'http://localhost:1337/parse', + appId: process.env.PARSE_APP_ID || 'myAppId', + masterKey: process.env.PARSE_MASTER_KEY || 'myMasterKey', + appName: process.env.PARSE_APP_NAME || 'My Parse App', + // Optional: GraphQL endpoint + // graphQLServerURL: 'http://localhost:1337/graphql', + }, + // Add more apps as needed + ], + users: [ + { + user: process.env.DASHBOARD_USER || 'admin', + pass: process.env.DASHBOARD_PASS || 'password', + // Optional: Restrict access to specific apps + // apps: [{ appId: 'myAppId' }] + }, + ], + // Optional: Use encrypted passwords (recommended for production) + // useEncryptedPasswords: true, +}; + +// Dashboard options +const dashboardOptions = { + allowInsecureHTTP: process.env.ALLOW_INSECURE_HTTP === 'true', + cookieSessionSecret: SESSION_SECRET, // IMPORTANT: Must be the same across all replicas + cookieSessionMaxAge: 86400000, // Session cookie max age in milliseconds (24 hours) + sessionStore: sessionStore, // Use MongoDB session store +}; + +// Create Express app +const app = express(); + +// Trust proxy when running behind a load balancer +app.set('trust proxy', 1); + +// Mount Parse Dashboard +app.use('/dashboard', ParseDashboard(dashboardConfig, dashboardOptions)); + +// Health check endpoint (useful for load balancers) +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Start server +const server = app.listen(PORT, () => { + console.log(`Parse Dashboard is now available at http://localhost:${PORT}/dashboard`); + console.log('Dashboard is configured with MongoDB session store for multi-replica support'); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM signal received: closing HTTP server'); + server.close(() => { + console.log('HTTP server closed'); + sessionStore.close().then(() => { + console.log('MongoDB session store connection closed'); + process.exit(0); + }); + }); +}); + +process.on('SIGINT', () => { + console.log('SIGINT signal received: closing HTTP server'); + server.close(() => { + console.log('HTTP server closed'); + sessionStore.close().then(() => { + console.log('MongoDB session store connection closed'); + process.exit(0); + }); + }); +}); diff --git a/examples/redis-session-store.js b/examples/redis-session-store.js new file mode 100644 index 000000000..93d47b596 --- /dev/null +++ b/examples/redis-session-store.js @@ -0,0 +1,136 @@ +/** + * Example: Using Parse Dashboard with Redis Session Store for Multi-Replica Deployments + * + * This example shows how to configure Parse Dashboard with a Redis session store + * to support multiple dashboard replicas behind a load balancer without sticky sessions. + * + * Prerequisites: + * 1. Install required dependencies: + * npm install parse-dashboard connect-redis redis + * + * 2. Have a Redis server running (e.g., redis://localhost:6379) + */ + +const express = require('express'); +const ParseDashboard = require('parse-dashboard'); +const { createClient } = require('redis'); +const RedisStore = require('connect-redis').default; + +// Configuration +const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; +const PORT = process.env.PORT || 4040; +const SESSION_SECRET = process.env.SESSION_SECRET || 'your-secret-key-change-this'; + +// Create Redis client +const redisClient = createClient({ + url: REDIS_URL, + // Optional: Add reconnect strategy + socket: { + reconnectStrategy: (retries) => { + if (retries > 10) { + console.error('Too many Redis reconnection attempts, giving up'); + return new Error('Redis connection failed'); + } + // Exponential backoff: 50ms, 100ms, 200ms, 400ms, etc. + return Math.min(retries * 50, 3000); + } + } +}); + +// Handle Redis connection errors +redisClient.on('error', (err) => { + console.error('Redis Client Error:', err); +}); + +redisClient.on('connect', () => { + console.log('Connected to Redis'); +}); + +// Connect to Redis +redisClient.connect().catch((err) => { + console.error('Failed to connect to Redis:', err); + process.exit(1); +}); + +// Create Redis store for sessions +const sessionStore = new RedisStore({ + client: redisClient, + prefix: 'parse-dashboard:', // Prefix for all session keys in Redis + ttl: 86400, // Session TTL in seconds (24 hours) +}); + +// Parse Dashboard configuration +const dashboardConfig = { + apps: [ + { + serverURL: process.env.PARSE_SERVER_URL || 'http://localhost:1337/parse', + appId: process.env.PARSE_APP_ID || 'myAppId', + masterKey: process.env.PARSE_MASTER_KEY || 'myMasterKey', + appName: process.env.PARSE_APP_NAME || 'My Parse App', + // Optional: GraphQL endpoint + // graphQLServerURL: 'http://localhost:1337/graphql', + }, + // Add more apps as needed + ], + users: [ + { + user: process.env.DASHBOARD_USER || 'admin', + pass: process.env.DASHBOARD_PASS || 'password', + // Optional: Restrict access to specific apps + // apps: [{ appId: 'myAppId' }] + }, + ], + // Optional: Use encrypted passwords (recommended for production) + // useEncryptedPasswords: true, +}; + +// Dashboard options +const dashboardOptions = { + allowInsecureHTTP: process.env.ALLOW_INSECURE_HTTP === 'true', + cookieSessionSecret: SESSION_SECRET, // IMPORTANT: Must be the same across all replicas + cookieSessionMaxAge: 86400000, // Session cookie max age in milliseconds (24 hours) + sessionStore: sessionStore, // Use Redis session store +}; + +// Create Express app +const app = express(); + +// Trust proxy when running behind a load balancer +app.set('trust proxy', 1); + +// Mount Parse Dashboard +app.use('/dashboard', ParseDashboard(dashboardConfig, dashboardOptions)); + +// Health check endpoint (useful for load balancers) +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Start server +const server = app.listen(PORT, () => { + console.log(`Parse Dashboard is now available at http://localhost:${PORT}/dashboard`); + console.log('Dashboard is configured with Redis session store for multi-replica support'); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM signal received: closing HTTP server'); + server.close(() => { + console.log('HTTP server closed'); + redisClient.quit().then(() => { + console.log('Redis connection closed'); + process.exit(0); + }); + }); +}); + +process.on('SIGINT', () => { + console.log('SIGINT signal received: closing HTTP server'); + server.close(() => { + console.log('HTTP server closed'); + redisClient.quit().then(() => { + console.log('Redis connection closed'); + process.exit(0); + }); + }); +}); From ed889248bb4fb42e06e1d3d2f1f561595b51fd6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:10:28 +0000 Subject: [PATCH 5/9] Remove example files and simplify README with session store list Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- README.md | 58 ++----------- examples/README.md | 132 ----------------------------- examples/mongodb-session-store.js | 120 -------------------------- examples/redis-session-store.js | 136 ------------------------------ 4 files changed, 6 insertions(+), 440 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/mongodb-session-store.js delete mode 100644 examples/redis-session-store.js diff --git a/README.md b/README.md index 2bbfcb525..01a169b29 100644 --- a/README.md +++ b/README.md @@ -809,60 +809,14 @@ When deploying Parse Dashboard with multiple replicas behind a load balancer, yo #### Using a Custom Session Store -Parse Dashboard supports using any session store compatible with [express-session](https://github.com/expressjs/session), such as Redis, MongoDB, or Memcached. Here's an example using Redis with `connect-redis`: +Parse Dashboard supports using any session store compatible with [express-session](https://github.com/expressjs/session). The `sessionStore` option must be configured programmatically when initializing the dashboard. -**Step 1:** Install the required dependencies: +**Suggested Session Stores:** -```bash -npm install connect-redis redis -``` - -**Step 2:** Configure your Parse Dashboard with a session store: - -```javascript -const express = require('express'); -const ParseDashboard = require('parse-dashboard'); -const { createClient } = require('redis'); -const RedisStore = require('connect-redis').default; - -// Create Redis client -const redisClient = createClient({ - url: 'redis://localhost:6379' -}); - -redisClient.connect().catch(console.error); - -// Create Redis store -const sessionStore = new RedisStore({ - client: redisClient, - prefix: 'parse-dashboard:', -}); - -// Configure Parse Dashboard with the session store -const dashboard = new ParseDashboard({ - apps: [ - { - serverURL: 'http://localhost:1337/parse', - appId: 'myAppId', - masterKey: 'myMasterKey', - appName: 'MyApp' - } - ], - users: [ - { - user: 'admin', - pass: 'password' - } - ], - cookieSessionSecret: 'your-session-secret', // Required for multi-replica -}, { - sessionStore: sessionStore // Pass the session store -}); - -const app = express(); -app.use('/dashboard', dashboard); -app.listen(4040); -``` +- [connect-redis](https://www.npmjs.com/package/connect-redis) - Redis session store +- [connect-mongo](https://www.npmjs.com/package/connect-mongo) - MongoDB session store +- [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) - PostgreSQL session store +- [memorystore](https://www.npmjs.com/package/memorystore) - Memory session store with TTL support **Important Notes:** diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 827154614..000000000 --- a/examples/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Parse Dashboard Examples - -This directory contains example configurations for Parse Dashboard in various deployment scenarios. - -## Multi-Replica Deployments - -When running Parse Dashboard with multiple replicas behind a load balancer, you need to use a shared session store to ensure CSRF tokens and user sessions work correctly across all replicas. - -### Available Examples - -1. **[redis-session-store.js](./redis-session-store.js)** - Using Redis as the session store - - Fast, in-memory session storage - - Recommended for high-traffic deployments - - Requires: `connect-redis`, `redis` - -2. **[mongodb-session-store.js](./mongodb-session-store.js)** - Using MongoDB as the session store - - Persistent session storage - - Good if you already have MongoDB infrastructure - - Requires: `connect-mongo` - -### Quick Start - -1. Choose an example based on your infrastructure -2. Install the required dependencies: - ```bash - # For Redis - npm install parse-dashboard connect-redis redis - - # For MongoDB - npm install parse-dashboard connect-mongo - ``` -3. Configure environment variables: - ```bash - # For Redis - export REDIS_URL="redis://localhost:6379" - export SESSION_SECRET="your-secret-key-change-this" - export PARSE_SERVER_URL="http://localhost:1337/parse" - export PARSE_APP_ID="myAppId" - export PARSE_MASTER_KEY="myMasterKey" - - # For MongoDB - export MONGODB_URL="mongodb://localhost:27017/parse-dashboard-sessions" - export SESSION_SECRET="your-secret-key-change-this" - export PARSE_SERVER_URL="http://localhost:1337/parse" - export PARSE_APP_ID="myAppId" - export PARSE_MASTER_KEY="myMasterKey" - ``` -4. Run the example: - ```bash - node examples/redis-session-store.js - # or - node examples/mongodb-session-store.js - ``` - -### Important Notes - -- **`SESSION_SECRET` must be the same across all replicas** - This ensures session cookies work correctly -- **Configure your load balancer properly** - Set `trustProxy: true` when behind a reverse proxy -- **Health check endpoint** - All examples include a `/health` endpoint for load balancer health checks -- **Graceful shutdown** - Examples include proper cleanup handlers for SIGTERM and SIGINT signals - -### Kubernetes Deployment - -For Kubernetes deployments, you can use ConfigMaps and Secrets to configure your dashboard: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: parse-dashboard-config -data: - REDIS_URL: "redis://redis-service:6379" - PARSE_SERVER_URL: "http://parse-server-service:1337/parse" - PARSE_APP_ID: "myAppId" ---- -apiVersion: v1 -kind: Secret -metadata: - name: parse-dashboard-secrets -type: Opaque -data: - SESSION_SECRET: - PARSE_MASTER_KEY: - DASHBOARD_PASS: -``` - -### Docker Compose - -For Docker Compose deployments, you can use environment files: - -```yaml -version: '3.8' -services: - redis: - image: redis:latest - ports: - - "6379:6379" - - parse-dashboard: - build: . - environment: - - REDIS_URL=redis://redis:6379 - - SESSION_SECRET=${SESSION_SECRET} - - PARSE_SERVER_URL=${PARSE_SERVER_URL} - - PARSE_APP_ID=${PARSE_APP_ID} - - PARSE_MASTER_KEY=${PARSE_MASTER_KEY} - ports: - - "4040:4040" - depends_on: - - redis - deploy: - replicas: 3 -``` - -### Troubleshooting - -**Issue: "CSRF token validation failed" errors** -- Ensure `SESSION_SECRET` is the same across all replicas -- Verify the session store is accessible from all replicas -- Check that `trustProxy` is enabled when behind a load balancer - -**Issue: Sessions not persisting** -- Verify the session store connection is working -- Check session TTL configuration -- Ensure the session store has enough memory/storage - -**Issue: High memory usage** -- Adjust session TTL to clean up expired sessions -- Use `touchAfter` option (MongoDB) to reduce update frequency -- Monitor session store metrics - -For more information, see the [main README](../README.md#running-multiple-dashboard-replicas). diff --git a/examples/mongodb-session-store.js b/examples/mongodb-session-store.js deleted file mode 100644 index a0d643b68..000000000 --- a/examples/mongodb-session-store.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Example: Using Parse Dashboard with MongoDB Session Store for Multi-Replica Deployments - * - * This example shows how to configure Parse Dashboard with a MongoDB session store - * to support multiple dashboard replicas behind a load balancer without sticky sessions. - * - * Prerequisites: - * 1. Install required dependencies: - * npm install parse-dashboard connect-mongo - * - * 2. Have a MongoDB server running (e.g., mongodb://localhost:27017) - */ - -const express = require('express'); -const ParseDashboard = require('parse-dashboard'); -const MongoStore = require('connect-mongo'); - -// Configuration -const MONGODB_URL = process.env.MONGODB_URL || 'mongodb://localhost:27017/parse-dashboard-sessions'; -const PORT = process.env.PORT || 4040; -const SESSION_SECRET = process.env.SESSION_SECRET || 'your-secret-key-change-this'; - -// Create MongoDB session store -const sessionStore = MongoStore.create({ - mongoUrl: MONGODB_URL, - collectionName: 'sessions', // Collection name for storing sessions - ttl: 86400, // Session TTL in seconds (24 hours) - autoRemove: 'native', // Let MongoDB's TTL index handle session cleanup - touchAfter: 3600, // Update session only once per hour (unless session data changes) - // Optional: Add connection options - mongoOptions: { - useNewUrlParser: true, - useUnifiedTopology: true, - } -}); - -// Handle store connection events -sessionStore.on('error', (err) => { - console.error('MongoDB Session Store Error:', err); -}); - -sessionStore.on('connected', () => { - console.log('Connected to MongoDB for session storage'); -}); - -// Parse Dashboard configuration -const dashboardConfig = { - apps: [ - { - serverURL: process.env.PARSE_SERVER_URL || 'http://localhost:1337/parse', - appId: process.env.PARSE_APP_ID || 'myAppId', - masterKey: process.env.PARSE_MASTER_KEY || 'myMasterKey', - appName: process.env.PARSE_APP_NAME || 'My Parse App', - // Optional: GraphQL endpoint - // graphQLServerURL: 'http://localhost:1337/graphql', - }, - // Add more apps as needed - ], - users: [ - { - user: process.env.DASHBOARD_USER || 'admin', - pass: process.env.DASHBOARD_PASS || 'password', - // Optional: Restrict access to specific apps - // apps: [{ appId: 'myAppId' }] - }, - ], - // Optional: Use encrypted passwords (recommended for production) - // useEncryptedPasswords: true, -}; - -// Dashboard options -const dashboardOptions = { - allowInsecureHTTP: process.env.ALLOW_INSECURE_HTTP === 'true', - cookieSessionSecret: SESSION_SECRET, // IMPORTANT: Must be the same across all replicas - cookieSessionMaxAge: 86400000, // Session cookie max age in milliseconds (24 hours) - sessionStore: sessionStore, // Use MongoDB session store -}; - -// Create Express app -const app = express(); - -// Trust proxy when running behind a load balancer -app.set('trust proxy', 1); - -// Mount Parse Dashboard -app.use('/dashboard', ParseDashboard(dashboardConfig, dashboardOptions)); - -// Health check endpoint (useful for load balancers) -app.get('/health', (req, res) => { - res.status(200).json({ status: 'ok' }); -}); - -// Start server -const server = app.listen(PORT, () => { - console.log(`Parse Dashboard is now available at http://localhost:${PORT}/dashboard`); - console.log('Dashboard is configured with MongoDB session store for multi-replica support'); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - console.log('SIGTERM signal received: closing HTTP server'); - server.close(() => { - console.log('HTTP server closed'); - sessionStore.close().then(() => { - console.log('MongoDB session store connection closed'); - process.exit(0); - }); - }); -}); - -process.on('SIGINT', () => { - console.log('SIGINT signal received: closing HTTP server'); - server.close(() => { - console.log('HTTP server closed'); - sessionStore.close().then(() => { - console.log('MongoDB session store connection closed'); - process.exit(0); - }); - }); -}); diff --git a/examples/redis-session-store.js b/examples/redis-session-store.js deleted file mode 100644 index 93d47b596..000000000 --- a/examples/redis-session-store.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Example: Using Parse Dashboard with Redis Session Store for Multi-Replica Deployments - * - * This example shows how to configure Parse Dashboard with a Redis session store - * to support multiple dashboard replicas behind a load balancer without sticky sessions. - * - * Prerequisites: - * 1. Install required dependencies: - * npm install parse-dashboard connect-redis redis - * - * 2. Have a Redis server running (e.g., redis://localhost:6379) - */ - -const express = require('express'); -const ParseDashboard = require('parse-dashboard'); -const { createClient } = require('redis'); -const RedisStore = require('connect-redis').default; - -// Configuration -const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; -const PORT = process.env.PORT || 4040; -const SESSION_SECRET = process.env.SESSION_SECRET || 'your-secret-key-change-this'; - -// Create Redis client -const redisClient = createClient({ - url: REDIS_URL, - // Optional: Add reconnect strategy - socket: { - reconnectStrategy: (retries) => { - if (retries > 10) { - console.error('Too many Redis reconnection attempts, giving up'); - return new Error('Redis connection failed'); - } - // Exponential backoff: 50ms, 100ms, 200ms, 400ms, etc. - return Math.min(retries * 50, 3000); - } - } -}); - -// Handle Redis connection errors -redisClient.on('error', (err) => { - console.error('Redis Client Error:', err); -}); - -redisClient.on('connect', () => { - console.log('Connected to Redis'); -}); - -// Connect to Redis -redisClient.connect().catch((err) => { - console.error('Failed to connect to Redis:', err); - process.exit(1); -}); - -// Create Redis store for sessions -const sessionStore = new RedisStore({ - client: redisClient, - prefix: 'parse-dashboard:', // Prefix for all session keys in Redis - ttl: 86400, // Session TTL in seconds (24 hours) -}); - -// Parse Dashboard configuration -const dashboardConfig = { - apps: [ - { - serverURL: process.env.PARSE_SERVER_URL || 'http://localhost:1337/parse', - appId: process.env.PARSE_APP_ID || 'myAppId', - masterKey: process.env.PARSE_MASTER_KEY || 'myMasterKey', - appName: process.env.PARSE_APP_NAME || 'My Parse App', - // Optional: GraphQL endpoint - // graphQLServerURL: 'http://localhost:1337/graphql', - }, - // Add more apps as needed - ], - users: [ - { - user: process.env.DASHBOARD_USER || 'admin', - pass: process.env.DASHBOARD_PASS || 'password', - // Optional: Restrict access to specific apps - // apps: [{ appId: 'myAppId' }] - }, - ], - // Optional: Use encrypted passwords (recommended for production) - // useEncryptedPasswords: true, -}; - -// Dashboard options -const dashboardOptions = { - allowInsecureHTTP: process.env.ALLOW_INSECURE_HTTP === 'true', - cookieSessionSecret: SESSION_SECRET, // IMPORTANT: Must be the same across all replicas - cookieSessionMaxAge: 86400000, // Session cookie max age in milliseconds (24 hours) - sessionStore: sessionStore, // Use Redis session store -}; - -// Create Express app -const app = express(); - -// Trust proxy when running behind a load balancer -app.set('trust proxy', 1); - -// Mount Parse Dashboard -app.use('/dashboard', ParseDashboard(dashboardConfig, dashboardOptions)); - -// Health check endpoint (useful for load balancers) -app.get('/health', (req, res) => { - res.status(200).json({ status: 'ok' }); -}); - -// Start server -const server = app.listen(PORT, () => { - console.log(`Parse Dashboard is now available at http://localhost:${PORT}/dashboard`); - console.log('Dashboard is configured with Redis session store for multi-replica support'); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - console.log('SIGTERM signal received: closing HTTP server'); - server.close(() => { - console.log('HTTP server closed'); - redisClient.quit().then(() => { - console.log('Redis connection closed'); - process.exit(0); - }); - }); -}); - -process.on('SIGINT', () => { - console.log('SIGINT signal received: closing HTTP server'); - server.close(() => { - console.log('HTTP server closed'); - redisClient.quit().then(() => { - console.log('Redis connection closed'); - process.exit(0); - }); - }); -}); From 6c87a73c98472a47c804f2e3195212656ec4d379 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:22:11 +0000 Subject: [PATCH 6/9] Add simple connect-redis example to README Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 01a169b29..6565464e8 100644 --- a/README.md +++ b/README.md @@ -818,6 +818,30 @@ Parse Dashboard supports using any session store compatible with [express-sessio - [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) - PostgreSQL session store - [memorystore](https://www.npmjs.com/package/memorystore) - Memory session store with TTL support +**Example using connect-redis:** + +```javascript +const express = require('express'); +const ParseDashboard = require('parse-dashboard'); +const { createClient } = require('redis'); +const RedisStore = require('connect-redis').default; + +const redisClient = createClient({ url: 'redis://localhost:6379' }); +redisClient.connect(); + +const dashboard = new ParseDashboard({ + apps: [{ serverURL: 'http://localhost:1337/parse', appId: 'myAppId', masterKey: 'myMasterKey', appName: 'MyApp' }], + users: [{ user: 'admin', pass: 'password' }] +}, { + sessionStore: new RedisStore({ client: redisClient }), + cookieSessionSecret: 'your-secret-key' +}); + +const app = express(); +app.use('/dashboard', dashboard); +app.listen(4040); +``` + **Important Notes:** - The `cookieSessionSecret` option must be set to the same value across all replicas to ensure session cookies work correctly. From a2170b44e85fc853f2245ff9f66cfaf017b722b1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:13:05 +0100 Subject: [PATCH 7/9] Apply suggestion from @mtrezza Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6565464e8..8004ac57f 100644 --- a/README.md +++ b/README.md @@ -820,28 +820,28 @@ Parse Dashboard supports using any session store compatible with [express-sessio **Example using connect-redis:** -```javascript +```js const express = require('express'); const ParseDashboard = require('parse-dashboard'); const { createClient } = require('redis'); const RedisStore = require('connect-redis').default; +// Instantiate Redis client const redisClient = createClient({ url: 'redis://localhost:6379' }); redisClient.connect(); +// Instantiate Redis session store +const sessionStore = new RedisStore({ client: redisClient }); + +// Configure dashboard with session store const dashboard = new ParseDashboard({ - apps: [{ serverURL: 'http://localhost:1337/parse', appId: 'myAppId', masterKey: 'myMasterKey', appName: 'MyApp' }], - users: [{ user: 'admin', pass: 'password' }] + apps: [...], + users: [{ user: 'user', pass: 'pass' }], }, { - sessionStore: new RedisStore({ client: redisClient }), - cookieSessionSecret: 'your-secret-key' + sessionStore, + cookieSessionSecret: 'your-secret-key', }); -const app = express(); -app.use('/dashboard', dashboard); -app.listen(4040); -``` - **Important Notes:** - The `cookieSessionSecret` option must be set to the same value across all replicas to ensure session cookies work correctly. From ce564c8f59ea0bf320e38f878ebe1cc330e0a294 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:14:37 +0100 Subject: [PATCH 8/9] Apply suggestion from @mtrezza Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8004ac57f..c6e73d0e4 100644 --- a/README.md +++ b/README.md @@ -836,7 +836,7 @@ const sessionStore = new RedisStore({ client: redisClient }); // Configure dashboard with session store const dashboard = new ParseDashboard({ apps: [...], - users: [{ user: 'user', pass: 'pass' }], + users: [...], }, { sessionStore, cookieSessionSecret: 'your-secret-key', From faaeb2d0a8f4bd8ad71c75438f531a54f362e54a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:22:29 +0000 Subject: [PATCH 9/9] Rename sessionStore to cookieSessionStore across the codebase Co-authored-by: mtrezza <5673677+mtrezza@users.noreply.github.com> --- Parse-Dashboard/Authentication.js | 6 +++--- Parse-Dashboard/app.js | 2 +- Parse-Dashboard/server.js | 2 +- README.md | 6 +++--- src/lib/tests/SessionStore.test.js | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Parse-Dashboard/Authentication.js b/Parse-Dashboard/Authentication.js index c75aa9a7b..fef90b948 100644 --- a/Parse-Dashboard/Authentication.js +++ b/Parse-Dashboard/Authentication.js @@ -55,7 +55,7 @@ function initialize(app, options) { const cookieSessionSecret = options.cookieSessionSecret || require('crypto').randomBytes(64).toString('hex'); const cookieSessionMaxAge = options.cookieSessionMaxAge; - const sessionStore = options.sessionStore; + const cookieSessionStore = options.cookieSessionStore; app.use(require('body-parser').urlencoded({ extended: true })); const sessionConfig = { @@ -71,8 +71,8 @@ function initialize(app, options) { }; // Add custom session store if provided - if (sessionStore) { - sessionConfig.store = sessionStore; + if (cookieSessionStore) { + sessionConfig.store = cookieSessionStore; } app.use(require('express-session')(sessionConfig)); diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 6e92f59bc..cc8e034fe 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -85,7 +85,7 @@ module.exports = function(config, options) { authInstance.initialize(app, { cookieSessionSecret: options.cookieSessionSecret, cookieSessionMaxAge: options.cookieSessionMaxAge, - sessionStore: options.sessionStore + cookieSessionStore: options.cookieSessionStore }); // CSRF error handler diff --git a/Parse-Dashboard/server.js b/Parse-Dashboard/server.js index b24d6de2d..a04fa79ac 100644 --- a/Parse-Dashboard/server.js +++ b/Parse-Dashboard/server.js @@ -167,7 +167,7 @@ module.exports = (options) => { cookieSessionSecret, dev, cookieSessionMaxAge, - sessionStore: config.data.sessionStore + cookieSessionStore: config.data.cookieSessionStore }; app.use(mountPath, parseDashboard(config.data, dashboardOptions)); let server; diff --git a/README.md b/README.md index c6e73d0e4..08e4d7419 100644 --- a/README.md +++ b/README.md @@ -831,21 +831,21 @@ const redisClient = createClient({ url: 'redis://localhost:6379' }); redisClient.connect(); // Instantiate Redis session store -const sessionStore = new RedisStore({ client: redisClient }); +const cookieSessionStore = new RedisStore({ client: redisClient }); // Configure dashboard with session store const dashboard = new ParseDashboard({ apps: [...], users: [...], }, { - sessionStore, + cookieSessionStore, cookieSessionSecret: 'your-secret-key', }); **Important Notes:** - The `cookieSessionSecret` option must be set to the same value across all replicas to ensure session cookies work correctly. -- If `sessionStore` is not provided, Parse Dashboard will use the default in-memory session store, which only works for single-instance deployments. +- If `cookieSessionStore` is not provided, Parse Dashboard will use the default in-memory session store, which only works for single-instance deployments. - For production deployments with multiple replicas, always configure a shared session store. #### Alternative: Using Sticky Sessions diff --git a/src/lib/tests/SessionStore.test.js b/src/lib/tests/SessionStore.test.js index e773646f7..813b6daa9 100644 --- a/src/lib/tests/SessionStore.test.js +++ b/src/lib/tests/SessionStore.test.js @@ -13,7 +13,7 @@ const session = require('express-session'); const Authentication = require('../../../Parse-Dashboard/Authentication'); describe('SessionStore Integration', () => { - it('uses default in-memory store when sessionStore is not provided', () => { + it('uses default in-memory store when cookieSessionStore is not provided', () => { const app = express(); const users = [{ user: 'test', pass: 'password' }]; const auth = new Authentication(users, false, '/'); @@ -34,7 +34,7 @@ describe('SessionStore Integration', () => { // The session function should be called without a custom store }); - it('uses custom session store when sessionStore is provided', () => { + it('uses custom session store when cookieSessionStore is provided', () => { const app = express(); const users = [{ user: 'test', pass: 'password' }]; const auth = new Authentication(users, false, '/'); @@ -62,7 +62,7 @@ describe('SessionStore Integration', () => { const useSpy = jest.fn(); app.use = useSpy; - auth.initialize(app, { sessionStore: mockStore }); + auth.initialize(app, { cookieSessionStore: mockStore }); // The session middleware should have been configured expect(useSpy).toHaveBeenCalled(); @@ -75,7 +75,7 @@ describe('SessionStore Integration', () => { expect(sessionCall).toBeDefined(); }); - it('passes sessionStore through app.js to Authentication', () => { + it('passes cookieSessionStore through app.js to Authentication', () => { const parseDashboard = require('../../../Parse-Dashboard/app.js'); // Create a mock session store that implements the Store interface @@ -115,7 +115,7 @@ describe('SessionStore Integration', () => { }; const options = { - sessionStore: mockStore, + cookieSessionStore: mockStore, cookieSessionSecret: 'test-secret', }; @@ -127,7 +127,7 @@ describe('SessionStore Integration', () => { expect(typeof dashboardApp).toBe('function'); // Express app is a function }); - it('maintains backward compatibility without sessionStore option', () => { + it('maintains backward compatibility without cookieSessionStore option', () => { const parseDashboard = require('../../../Parse-Dashboard/app.js'); const config = { @@ -151,7 +151,7 @@ describe('SessionStore Integration', () => { cookieSessionSecret: 'test-secret', }; - // Create dashboard app without sessionStore option + // Create dashboard app without cookieSessionStore option const dashboardApp = parseDashboard(config, options); // The app should be created successfully even without session store