parse-dashboard 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +384 -0
- package/LICENSE +30 -0
- package/Parse-Dashboard/Authentication.js +145 -0
- package/Parse-Dashboard/CLI/mfa.js +225 -0
- package/Parse-Dashboard/CLI/utils.js +7 -0
- package/Parse-Dashboard/CLIHelper.js +6 -0
- package/Parse-Dashboard/app.js +229 -0
- package/Parse-Dashboard/index.js +199 -0
- package/Parse-Dashboard/parse-dashboard-config.json +14 -0
- package/Parse-Dashboard/public/bundles/dashboard.bundle.js +120 -0
- package/Parse-Dashboard/public/bundles/img/151bd0d0acab0c914cf28e7ceba3fed8.jpg +0 -0
- package/Parse-Dashboard/public/bundles/img/4400f45c001b956571860243d332df58.jpg +0 -0
- package/Parse-Dashboard/public/bundles/img/69665d8b6f6f575b2a22907909a456a1.png +0 -0
- package/Parse-Dashboard/public/bundles/img/8c98d067435fab4b09c017471f51648e.png +0 -0
- package/Parse-Dashboard/public/bundles/img/dc9bd65a9fc3e6b981413630496bd936.jpg +0 -0
- package/Parse-Dashboard/public/bundles/img/f8c84bccdc8cdeed8ded6ff40f55f090.jpg +0 -0
- package/Parse-Dashboard/public/bundles/login.bundle.js +30 -0
- package/Parse-Dashboard/public/bundles/sprites.svg +396 -0
- package/Parse-Dashboard/public/favicon.ico +0 -0
- package/README.md +619 -0
- package/bin/parse-dashboard +2 -0
- package/package.json +153 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const OTPAuth = require('otpauth');
|
|
4
|
+
const { copy } = require('./utils.js');
|
|
5
|
+
const phrases = {
|
|
6
|
+
enterPassword: 'Enter a password:',
|
|
7
|
+
enterUsername: 'Enter a username:',
|
|
8
|
+
enterAppName: 'Enter the app name:',
|
|
9
|
+
}
|
|
10
|
+
const getAlgorithm = async () => {
|
|
11
|
+
let { algorithm } = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: 'list',
|
|
14
|
+
name: 'algorithm',
|
|
15
|
+
message: 'Which hashing algorithm do you want to use?',
|
|
16
|
+
default: 'SHA1',
|
|
17
|
+
choices: [
|
|
18
|
+
'SHA1',
|
|
19
|
+
'SHA224',
|
|
20
|
+
'SHA256',
|
|
21
|
+
'SHA384',
|
|
22
|
+
'SHA512',
|
|
23
|
+
'SHA3-224',
|
|
24
|
+
'SHA3-256',
|
|
25
|
+
'SHA3-384',
|
|
26
|
+
'SHA3-512',
|
|
27
|
+
'Other'
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]);
|
|
31
|
+
if (algorithm === 'Other') {
|
|
32
|
+
const result = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'input',
|
|
35
|
+
name: 'algorithm',
|
|
36
|
+
message: 'Enter the hashing algorithm you want to use:'
|
|
37
|
+
}
|
|
38
|
+
]);
|
|
39
|
+
algorithm = result.algorithm;
|
|
40
|
+
}
|
|
41
|
+
const { digits, period } = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'number',
|
|
44
|
+
name: 'digits',
|
|
45
|
+
default: 6,
|
|
46
|
+
message: 'Enter the number of digits the one-time password should have:'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'number',
|
|
50
|
+
name: 'period',
|
|
51
|
+
default: 30,
|
|
52
|
+
message: 'Enter how long the one-time password should be valid (in seconds):'
|
|
53
|
+
}
|
|
54
|
+
])
|
|
55
|
+
return { algorithm, digits, period};
|
|
56
|
+
};
|
|
57
|
+
const generateSecret = ({ app, username, algorithm, digits, period }) => {
|
|
58
|
+
const secret = new OTPAuth.Secret();
|
|
59
|
+
const totp = new OTPAuth.TOTP({
|
|
60
|
+
issuer: app,
|
|
61
|
+
label: username,
|
|
62
|
+
algorithm,
|
|
63
|
+
digits,
|
|
64
|
+
period,
|
|
65
|
+
secret
|
|
66
|
+
});
|
|
67
|
+
const url = totp.toString();
|
|
68
|
+
return { secret: secret.base32, url };
|
|
69
|
+
};
|
|
70
|
+
const showQR = text => {
|
|
71
|
+
const QRCode = require('qrcode');
|
|
72
|
+
QRCode.toString(text, { type: 'terminal' }, (err, url) => {
|
|
73
|
+
console.log(
|
|
74
|
+
'\n------------------------------------------------------------------------------' +
|
|
75
|
+
`\n\n${url}`
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const showInstructions = ({ app, username, passwordCopied, secret, url, encrypt, config }) => {
|
|
81
|
+
let orderCounter = 0;
|
|
82
|
+
const getOrder = () => {
|
|
83
|
+
orderCounter++;
|
|
84
|
+
return orderCounter;
|
|
85
|
+
}
|
|
86
|
+
console.log(
|
|
87
|
+
'------------------------------------------------------------------------------' +
|
|
88
|
+
'\n\nFollow these steps to complete the set-up:'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
console.log(
|
|
92
|
+
`\n${getOrder()}. Add the following settings for user "${username}" ${app ? `in app "${app}" ` : '' }to the Parse Dashboard configuration.` +
|
|
93
|
+
`\n\n ${JSON.stringify(config)}`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (passwordCopied) {
|
|
97
|
+
console.log(
|
|
98
|
+
`\n${getOrder()}. Securely store the generated login password that has been copied to your clipboard.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (secret) {
|
|
103
|
+
console.log(
|
|
104
|
+
`\n${getOrder()}. Open the authenticator app to scan the QR code above or enter this secret code:` +
|
|
105
|
+
`\n\n ${secret}` +
|
|
106
|
+
'\n\n If the secret code generates incorrect one-time passwords, try this alternative:' +
|
|
107
|
+
`\n\n ${url}` +
|
|
108
|
+
`\n\n${getOrder()}. Destroy any records of the QR code and the secret code to secure the account.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (encrypt) {
|
|
113
|
+
console.log(
|
|
114
|
+
`\n${getOrder()}. Make sure that "useEncryptedPasswords" is set to "true" in your dashboard configuration.` +
|
|
115
|
+
'\n You chose to generate an encrypted password for this user.' +
|
|
116
|
+
'\n Any existing users with non-encrypted passwords will require newly created, encrypted passwords.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
console.log(
|
|
120
|
+
'\n------------------------------------------------------------------------------\n'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
async createUser() {
|
|
126
|
+
const data = {};
|
|
127
|
+
|
|
128
|
+
console.log('');
|
|
129
|
+
const { username, password } = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'username',
|
|
133
|
+
message: phrases.enterUsername
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'confirm',
|
|
137
|
+
name: 'password',
|
|
138
|
+
message: 'Do you want to auto-generate a password?'
|
|
139
|
+
}
|
|
140
|
+
]);
|
|
141
|
+
data.user = username;
|
|
142
|
+
if (!password) {
|
|
143
|
+
const { password } = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'password',
|
|
146
|
+
name: 'password',
|
|
147
|
+
message: phrases.enterPassword
|
|
148
|
+
}
|
|
149
|
+
]);
|
|
150
|
+
data.pass = password;
|
|
151
|
+
} else {
|
|
152
|
+
const password = crypto.randomBytes(20).toString('base64');
|
|
153
|
+
data.pass = password;
|
|
154
|
+
}
|
|
155
|
+
const { mfa, encrypt } = await inquirer.prompt([
|
|
156
|
+
{
|
|
157
|
+
type: 'confirm',
|
|
158
|
+
name: 'encrypt',
|
|
159
|
+
message: 'Should the password be encrypted? (strongly recommended, otherwise it is stored in clear-text)'
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'confirm',
|
|
163
|
+
name: 'mfa',
|
|
164
|
+
message: 'Do you want to enable multi-factor authentication?'
|
|
165
|
+
}
|
|
166
|
+
]);
|
|
167
|
+
if (encrypt) {
|
|
168
|
+
// Copy the raw password to clipboard
|
|
169
|
+
copy(data.pass);
|
|
170
|
+
|
|
171
|
+
// Encrypt password
|
|
172
|
+
const bcrypt = require('bcryptjs');
|
|
173
|
+
const salt = bcrypt.genSaltSync(10);
|
|
174
|
+
data.pass = bcrypt.hashSync(data.pass, salt);
|
|
175
|
+
}
|
|
176
|
+
if (mfa) {
|
|
177
|
+
const { app } = await inquirer.prompt([
|
|
178
|
+
{
|
|
179
|
+
type: 'input',
|
|
180
|
+
name: 'app',
|
|
181
|
+
message: phrases.enterAppName
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
const { algorithm, digits, period } = await getAlgorithm();
|
|
185
|
+
const { secret, url } = generateSecret({ app, username, algorithm, digits, period });
|
|
186
|
+
data.mfa = secret;
|
|
187
|
+
data.app = app;
|
|
188
|
+
data.url = url;
|
|
189
|
+
if (algorithm !== 'SHA1') {
|
|
190
|
+
data.mfaAlgorithm = algorithm;
|
|
191
|
+
}
|
|
192
|
+
showQR(data.url);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const config = { mfa: data.mfa, user: data.user, pass: data.pass };
|
|
196
|
+
showInstructions({ app: data.app, username, passwordCopied: true, secret: data.mfa, url: data.url, encrypt, config });
|
|
197
|
+
},
|
|
198
|
+
async createMFA() {
|
|
199
|
+
console.log('');
|
|
200
|
+
const { username, app } = await inquirer.prompt([
|
|
201
|
+
{
|
|
202
|
+
type: 'input',
|
|
203
|
+
name: 'username',
|
|
204
|
+
message:
|
|
205
|
+
'Enter the username for which you want to enable multi-factor authentication:'
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
type: 'input',
|
|
209
|
+
name: 'app',
|
|
210
|
+
message: phrases.enterAppName
|
|
211
|
+
}
|
|
212
|
+
]);
|
|
213
|
+
const { algorithm, digits, period } = await getAlgorithm();
|
|
214
|
+
|
|
215
|
+
const { url, secret } = generateSecret({ app, username, algorithm, digits, period });
|
|
216
|
+
showQR(url);
|
|
217
|
+
|
|
218
|
+
// Compose config
|
|
219
|
+
const config = { mfa: secret };
|
|
220
|
+
if (algorithm !== 'SHA1') {
|
|
221
|
+
config.mfaAlgorithm = algorithm;
|
|
222
|
+
}
|
|
223
|
+
showInstructions({ app, username, secret, url, config });
|
|
224
|
+
}
|
|
225
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const packageJson = require('package-json');
|
|
5
|
+
const csrf = require('csurf');
|
|
6
|
+
const Authentication = require('./Authentication.js');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
|
|
9
|
+
const currentVersionFeatures = require('../package.json').parseDashboardFeatures;
|
|
10
|
+
|
|
11
|
+
var newFeaturesInLatestVersion = [];
|
|
12
|
+
packageJson('parse-dashboard', 'latest').then(latestPackage => {
|
|
13
|
+
if (latestPackage.parseDashboardFeatures instanceof Array) {
|
|
14
|
+
newFeaturesInLatestVersion = latestPackage.parseDashboardFeatures.filter(feature => {
|
|
15
|
+
return currentVersionFeatures.indexOf(feature) === -1;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function getMount(mountPath) {
|
|
21
|
+
mountPath = mountPath || '';
|
|
22
|
+
if (!mountPath.endsWith('/')) {
|
|
23
|
+
mountPath += '/';
|
|
24
|
+
}
|
|
25
|
+
return mountPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkIfIconsExistForApps(apps, iconsFolder) {
|
|
29
|
+
for (var i in apps) {
|
|
30
|
+
var currentApp = apps[i];
|
|
31
|
+
var iconName = currentApp.iconName;
|
|
32
|
+
var path = iconsFolder + '/' + iconName;
|
|
33
|
+
|
|
34
|
+
fs.stat(path, function(err) {
|
|
35
|
+
if (err) {
|
|
36
|
+
if ('ENOENT' == err.code) {// file does not exist
|
|
37
|
+
console.warn('Icon with file name: ' + iconName +' couldn\'t be found in icons folder!');
|
|
38
|
+
} else {
|
|
39
|
+
console.log(
|
|
40
|
+
'An error occurd while checking for icons, please check permission!');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
//every thing was ok so for example you can read it and send it to client
|
|
44
|
+
}
|
|
45
|
+
} );
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = function(config, options) {
|
|
50
|
+
options = options || {};
|
|
51
|
+
var app = express();
|
|
52
|
+
// Serve public files.
|
|
53
|
+
app.use(express.static(path.join(__dirname,'public')));
|
|
54
|
+
|
|
55
|
+
// Allow setting via middleware
|
|
56
|
+
if (config.trustProxy && app.disabled('trust proxy')) {
|
|
57
|
+
app.enable('trust proxy');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// wait for app to mount in order to get mountpath
|
|
61
|
+
app.on('mount', function() {
|
|
62
|
+
const mountPath = getMount(app.mountpath);
|
|
63
|
+
const users = config.users;
|
|
64
|
+
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
|
|
65
|
+
const authInstance = new Authentication(users, useEncryptedPasswords, mountPath);
|
|
66
|
+
authInstance.initialize(app, { cookieSessionSecret: options.cookieSessionSecret });
|
|
67
|
+
|
|
68
|
+
// CSRF error handler
|
|
69
|
+
app.use(function (err, req, res, next) {
|
|
70
|
+
if (err.code !== 'EBADCSRFTOKEN') return next(err)
|
|
71
|
+
|
|
72
|
+
// handle CSRF token errors here
|
|
73
|
+
res.status(403)
|
|
74
|
+
res.send('form tampered with')
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Serve the configuration.
|
|
78
|
+
app.get('/parse-dashboard-config.json', function(req, res) {
|
|
79
|
+
let apps = config.apps.map((app) => Object.assign({}, app)); // make a copy
|
|
80
|
+
let response = {
|
|
81
|
+
apps: apps,
|
|
82
|
+
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//Based on advice from Doug Wilson here:
|
|
86
|
+
//https://github.com/expressjs/express/issues/2518
|
|
87
|
+
const requestIsLocal =
|
|
88
|
+
req.connection.remoteAddress === '127.0.0.1' ||
|
|
89
|
+
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
|
|
90
|
+
req.connection.remoteAddress === '::1';
|
|
91
|
+
if (!options.dev && !requestIsLocal) {
|
|
92
|
+
if (!req.secure && !options.allowInsecureHTTP) {
|
|
93
|
+
//Disallow HTTP requests except on localhost, to prevent the master key from being transmitted in cleartext
|
|
94
|
+
return res.send({ success: false, error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!users) {
|
|
98
|
+
//Accessing the dashboard over the internet can only be done with username and password
|
|
99
|
+
return res.send({ success: false, error: 'Configure a user to access Parse Dashboard remotely' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const authentication = req.user;
|
|
103
|
+
|
|
104
|
+
const successfulAuth = authentication && authentication.isAuthenticated;
|
|
105
|
+
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
|
|
106
|
+
const isReadOnly = authentication && authentication.isReadOnly;
|
|
107
|
+
// User is full read-only, replace the masterKey by the read-only one
|
|
108
|
+
if (isReadOnly) {
|
|
109
|
+
response.apps = response.apps.map((app) => {
|
|
110
|
+
app.masterKey = app.readOnlyMasterKey;
|
|
111
|
+
if (!app.masterKey) {
|
|
112
|
+
throw new Error('You need to provide a readOnlyMasterKey to use read-only features.');
|
|
113
|
+
}
|
|
114
|
+
return app;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (successfulAuth) {
|
|
119
|
+
if (appsUserHasAccess) {
|
|
120
|
+
// Restric access to apps defined in user dictionary
|
|
121
|
+
// If they didn't supply any app id, user will access all apps
|
|
122
|
+
response.apps = response.apps.filter(function (app) {
|
|
123
|
+
return appsUserHasAccess.find(appUserHasAccess => {
|
|
124
|
+
const isSame = app.appId === appUserHasAccess.appId;
|
|
125
|
+
if (isSame && appUserHasAccess.readOnly) {
|
|
126
|
+
app.masterKey = app.readOnlyMasterKey;
|
|
127
|
+
}
|
|
128
|
+
return isSame;
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// They provided correct auth
|
|
133
|
+
return res.json(response);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (users) {
|
|
137
|
+
//They provided incorrect auth
|
|
138
|
+
return res.sendStatus(401);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//They didn't provide auth, and have configured the dashboard to not need auth
|
|
142
|
+
//(ie. didn't supply usernames and passwords)
|
|
143
|
+
if (requestIsLocal || options.dev) {
|
|
144
|
+
//Allow no-auth access on localhost only, if they have configured the dashboard to not need auth
|
|
145
|
+
return res.json(response);
|
|
146
|
+
}
|
|
147
|
+
//We shouldn't get here. Fail closed.
|
|
148
|
+
res.send({ success: false, error: 'Something went wrong.' });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Serve the app icons. Uses the optional `iconsFolder` parameter as
|
|
152
|
+
// directory name, that was setup in the config file.
|
|
153
|
+
// We are explicitly not using `__dirpath` here because one may be
|
|
154
|
+
// running parse-dashboard from globally installed npm.
|
|
155
|
+
if (config.iconsFolder) {
|
|
156
|
+
try {
|
|
157
|
+
var stat = fs.statSync(config.iconsFolder);
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
app.use('/appicons', express.static(config.iconsFolder));
|
|
160
|
+
//Check also if the icons really exist
|
|
161
|
+
checkIfIconsExistForApps(config.apps, config.iconsFolder);
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// Directory doesn't exist or something.
|
|
165
|
+
console.warn('Iconsfolder at path: ' + config.iconsFolder +
|
|
166
|
+
' not found!');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
app.get('/login', csrf(), function(req, res) {
|
|
171
|
+
if (!users || (req.user && req.user.isAuthenticated)) {
|
|
172
|
+
return res.redirect(`${mountPath}apps`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let errors = req.flash('error');
|
|
176
|
+
if (errors && errors.length) {
|
|
177
|
+
errors = `<div id="login_errors" style="display: none;">
|
|
178
|
+
${errors.join(' ')}
|
|
179
|
+
</div>`
|
|
180
|
+
}
|
|
181
|
+
res.send(`<!DOCTYPE html>
|
|
182
|
+
<head>
|
|
183
|
+
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
|
|
184
|
+
<base href="${mountPath}"/>
|
|
185
|
+
<script>
|
|
186
|
+
PARSE_DASHBOARD_PATH = "${mountPath}";
|
|
187
|
+
</script>
|
|
188
|
+
</head>
|
|
189
|
+
<html>
|
|
190
|
+
<title>Parse Dashboard</title>
|
|
191
|
+
<body>
|
|
192
|
+
<div id="login_mount"></div>
|
|
193
|
+
${errors}
|
|
194
|
+
<script id="csrf" type="application/json">"${req.csrfToken()}"</script>
|
|
195
|
+
<script src="${mountPath}bundles/login.bundle.js"></script>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
`);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// For every other request, go to index.html. Let client-side handle the rest.
|
|
202
|
+
app.get('/*', function(req, res) {
|
|
203
|
+
if (users && (!req.user || !req.user.isAuthenticated)) {
|
|
204
|
+
return res.redirect(`${mountPath}login`);
|
|
205
|
+
}
|
|
206
|
+
if (users && req.user && req.user.matchingUsername ) {
|
|
207
|
+
res.append('username', req.user.matchingUsername);
|
|
208
|
+
}
|
|
209
|
+
res.send(`<!DOCTYPE html>
|
|
210
|
+
<head>
|
|
211
|
+
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
|
|
212
|
+
<base href="${mountPath}"/>
|
|
213
|
+
<script>
|
|
214
|
+
PARSE_DASHBOARD_PATH = "${mountPath}";
|
|
215
|
+
</script>
|
|
216
|
+
</head>
|
|
217
|
+
<html>
|
|
218
|
+
<title>Parse Dashboard</title>
|
|
219
|
+
<body>
|
|
220
|
+
<div id="browser_mount"></div>
|
|
221
|
+
<script src="${mountPath}bundles/dashboard.bundle.js"></script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|
|
224
|
+
`);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return app;
|
|
229
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2016-present, Parse, LLC
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the license found in the LICENSE file in
|
|
6
|
+
* the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
// Command line tool for npm start
|
|
9
|
+
'use strict'
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const jsonFile = require('json-file-plus');
|
|
12
|
+
const express = require('express');
|
|
13
|
+
const parseDashboard = require('./app');
|
|
14
|
+
const CLIHelper = require('./CLIHelper.js');
|
|
15
|
+
|
|
16
|
+
const program = require('commander');
|
|
17
|
+
program.option('--appId [appId]', 'the app Id of the app you would like to manage.');
|
|
18
|
+
program.option('--masterKey [masterKey]', 'the master key of the app you would like to manage.');
|
|
19
|
+
program.option('--serverURL [serverURL]', 'the server url of the app you would like to manage.');
|
|
20
|
+
program.option('--graphQLServerURL [graphQLServerURL]', 'the GraphQL server url of the app you would like to manage.');
|
|
21
|
+
program.option('--dev', 'Enable development mode. This will disable authentication and allow non HTTPS connections. DO NOT ENABLE IN PRODUCTION SERVERS');
|
|
22
|
+
program.option('--appName [appName]', 'the name of the app you would like to manage. Optional.');
|
|
23
|
+
program.option('--config [config]', 'the path to the configuration file');
|
|
24
|
+
program.option('--host [host]', 'the host to run parse-dashboard');
|
|
25
|
+
program.option('--port [port]', 'the port to run parse-dashboard');
|
|
26
|
+
program.option('--mountPath [mountPath]', 'the mount path to run parse-dashboard');
|
|
27
|
+
program.option('--allowInsecureHTTP [allowInsecureHTTP]', 'set this flag when you are running the dashboard behind an HTTPS load balancer or proxy with early SSL termination.');
|
|
28
|
+
program.option('--sslKey [sslKey]', 'the path to the SSL private key.');
|
|
29
|
+
program.option('--sslCert [sslCert]', 'the path to the SSL certificate.');
|
|
30
|
+
program.option('--trustProxy [trustProxy]', 'set this flag when you are behind a front-facing proxy, such as when hosting on Heroku. Uses X-Forwarded-* headers to determine the client\'s connection and IP address.');
|
|
31
|
+
program.option('--cookieSessionSecret [cookieSessionSecret]', 'set the cookie session secret, defaults to a random string. You should set that value if you want sessions to work across multiple server, or across restarts');
|
|
32
|
+
program.option('--createUser', 'helper tool to allow you to generate secure user passwords and secrets. Use this on trusted devices only.');
|
|
33
|
+
program.option('--createMFA', 'helper tool to allow you to generate multi-factor authentication secrets.');
|
|
34
|
+
|
|
35
|
+
program.parse(process.argv);
|
|
36
|
+
|
|
37
|
+
for (const key in program) {
|
|
38
|
+
const func = CLIHelper[key];
|
|
39
|
+
if (func && typeof func === 'function') {
|
|
40
|
+
func();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const host = program.host || process.env.HOST || '0.0.0.0';
|
|
46
|
+
const port = program.port || process.env.PORT || 4040;
|
|
47
|
+
const mountPath = program.mountPath || process.env.MOUNT_PATH || '/';
|
|
48
|
+
const allowInsecureHTTP = program.allowInsecureHTTP || process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP;
|
|
49
|
+
const cookieSessionSecret = program.cookieSessionSecret || process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET;
|
|
50
|
+
const trustProxy = program.trustProxy || process.env.PARSE_DASHBOARD_TRUST_PROXY;
|
|
51
|
+
const dev = program.dev;
|
|
52
|
+
|
|
53
|
+
if (trustProxy && allowInsecureHTTP) {
|
|
54
|
+
console.log('Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.');
|
|
55
|
+
process.exit(-1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let explicitConfigFileProvided = !!program.config;
|
|
59
|
+
let configFile = null;
|
|
60
|
+
let configFromCLI = null;
|
|
61
|
+
let configServerURL = program.serverURL || process.env.PARSE_DASHBOARD_SERVER_URL;
|
|
62
|
+
let configGraphQLServerURL = program.graphQLServerURL || process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL;
|
|
63
|
+
let configMasterKey = program.masterKey || process.env.PARSE_DASHBOARD_MASTER_KEY;
|
|
64
|
+
let configAppId = program.appId || process.env.PARSE_DASHBOARD_APP_ID;
|
|
65
|
+
let configAppName = program.appName || process.env.PARSE_DASHBOARD_APP_NAME;
|
|
66
|
+
let configUserId = program.userId || process.env.PARSE_DASHBOARD_USER_ID;
|
|
67
|
+
let configUserPassword = program.userPassword || process.env.PARSE_DASHBOARD_USER_PASSWORD;
|
|
68
|
+
let configSSLKey = program.sslKey || process.env.PARSE_DASHBOARD_SSL_KEY;
|
|
69
|
+
let configSSLCert = program.sslCert || process.env.PARSE_DASHBOARD_SSL_CERT;
|
|
70
|
+
|
|
71
|
+
function handleSIGs(server) {
|
|
72
|
+
const signals = {
|
|
73
|
+
'SIGINT': 2,
|
|
74
|
+
'SIGTERM': 15
|
|
75
|
+
};
|
|
76
|
+
function shutdown(signal, value) {
|
|
77
|
+
server.close(function () {
|
|
78
|
+
console.log('server stopped by ' + signal);
|
|
79
|
+
process.exit(128 + value);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
Object.keys(signals).forEach(function (signal) {
|
|
83
|
+
process.on(signal, function () {
|
|
84
|
+
shutdown(signal, signals[signal]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!program.config && !process.env.PARSE_DASHBOARD_CONFIG) {
|
|
90
|
+
if (configServerURL && configMasterKey && configAppId) {
|
|
91
|
+
configFromCLI = {
|
|
92
|
+
data: {
|
|
93
|
+
apps: [
|
|
94
|
+
{
|
|
95
|
+
appId: configAppId,
|
|
96
|
+
serverURL: configServerURL,
|
|
97
|
+
masterKey: configMasterKey,
|
|
98
|
+
appName: configAppName,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (configGraphQLServerURL) {
|
|
104
|
+
configFromCLI.data.apps[0].graphQLServerURL = configGraphQLServerURL;
|
|
105
|
+
}
|
|
106
|
+
if (configUserId && configUserPassword) {
|
|
107
|
+
configFromCLI.data.users = [
|
|
108
|
+
{
|
|
109
|
+
user: configUserId,
|
|
110
|
+
pass: configUserPassword,
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
} else if (!configServerURL && !configMasterKey && !configAppName) {
|
|
115
|
+
configFile = path.join(__dirname, 'parse-dashboard-config.json');
|
|
116
|
+
}
|
|
117
|
+
} else if (!program.config && process.env.PARSE_DASHBOARD_CONFIG) {
|
|
118
|
+
configFromCLI = {
|
|
119
|
+
data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG)
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
configFile = program.config;
|
|
123
|
+
if (program.appId || program.serverURL || program.masterKey || program.appName || program.graphQLServerURL) {
|
|
124
|
+
console.log('You must provide either a config file or other CLI options (appName, appId, masterKey, serverURL, and graphQLServerURL); not both.');
|
|
125
|
+
process.exit(3);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let p = null;
|
|
130
|
+
let configFilePath = null;
|
|
131
|
+
if (configFile) {
|
|
132
|
+
p = jsonFile(configFile);
|
|
133
|
+
configFilePath = path.dirname(configFile);
|
|
134
|
+
} else if (configFromCLI) {
|
|
135
|
+
p = Promise.resolve(configFromCLI);
|
|
136
|
+
} else {
|
|
137
|
+
//Failed to load default config file.
|
|
138
|
+
console.log('You must provide either a config file or an app ID, Master Key, and server URL. See parse-dashboard --help for details.');
|
|
139
|
+
process.exit(4);
|
|
140
|
+
}
|
|
141
|
+
p.then(config => {
|
|
142
|
+
config.data.apps.forEach(app => {
|
|
143
|
+
if (!app.appName) {
|
|
144
|
+
app.appName = app.appId;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (config.data.iconsFolder && configFilePath) {
|
|
149
|
+
config.data.iconsFolder = path.join(configFilePath, config.data.iconsFolder);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const app = express();
|
|
153
|
+
|
|
154
|
+
if (allowInsecureHTTP || trustProxy || dev) app.enable('trust proxy');
|
|
155
|
+
|
|
156
|
+
config.data.trustProxy = trustProxy;
|
|
157
|
+
let dashboardOptions = { allowInsecureHTTP, cookieSessionSecret, dev };
|
|
158
|
+
app.use(mountPath, parseDashboard(config.data, dashboardOptions));
|
|
159
|
+
let server;
|
|
160
|
+
if(!configSSLKey || !configSSLCert){
|
|
161
|
+
// Start the server.
|
|
162
|
+
server = app.listen(port, host, function () {
|
|
163
|
+
console.log(`The dashboard is now available at http://${server.address().address}:${server.address().port}${mountPath}`);
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
// Start the server using SSL.
|
|
167
|
+
var fs = require('fs');
|
|
168
|
+
var privateKey = fs.readFileSync(configSSLKey);
|
|
169
|
+
var certificate = fs.readFileSync(configSSLCert);
|
|
170
|
+
|
|
171
|
+
server = require('https').createServer({
|
|
172
|
+
key: privateKey,
|
|
173
|
+
cert: certificate
|
|
174
|
+
}, app).listen(port, host, function () {
|
|
175
|
+
console.log(`The dashboard is now available at https://${server.address().address}:${server.address().port}${mountPath}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
handleSIGs(server);
|
|
179
|
+
}, error => {
|
|
180
|
+
if (error instanceof SyntaxError) {
|
|
181
|
+
console.log('Your config file contains invalid JSON. Exiting.');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
} else if (error.code === 'ENOENT') {
|
|
184
|
+
if (explicitConfigFileProvided) {
|
|
185
|
+
console.log('Your config file is missing. Exiting.');
|
|
186
|
+
process.exit(2);
|
|
187
|
+
} else {
|
|
188
|
+
console.log('You must provide either a config file or required CLI options (app ID, Master Key, and server URL); not both.');
|
|
189
|
+
process.exit(3);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
console.log('There was a problem with your config. Exiting.');
|
|
193
|
+
process.exit(-1);
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.catch(error => {
|
|
197
|
+
console.log('There was a problem loading the dashboard. Exiting.', error);
|
|
198
|
+
process.exit(-1);
|
|
199
|
+
});
|