Skip to content
12 changes: 10 additions & 2 deletions Parse-Dashboard/Authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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());
Expand Down
6 changes: 5 additions & 1 deletion Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 7 additions & 1 deletion Parse-Dashboard/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
132 changes: 132 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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: <base64-encoded-secret>
PARSE_MASTER_KEY: <base64-encoded-master-key>
DASHBOARD_PASS: <base64-encoded-password>
```

### 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).
120 changes: 120 additions & 0 deletions examples/mongodb-session-store.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading
Loading