mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-11 14:52:55 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
146
packages/noodl-parse-dashboard/Parse-Dashboard/Authentication.js
Normal file
146
packages/noodl-parse-dashboard/Parse-Dashboard/Authentication.js
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
var bcrypt = require('bcryptjs');
|
||||
var passport = require('passport');
|
||||
var LocalStrategy = require('passport-local').Strategy;
|
||||
const OTPAuth = require('otpauth')
|
||||
|
||||
/**
|
||||
* Constructor for Authentication class
|
||||
*
|
||||
* @class Authentication
|
||||
* @param {Object[]} validUsers
|
||||
* @param {boolean} useEncryptedPasswords
|
||||
*/
|
||||
function Authentication(validUsers, useEncryptedPasswords, mountPath) {
|
||||
this.validUsers = validUsers;
|
||||
this.useEncryptedPasswords = useEncryptedPasswords || false;
|
||||
this.mountPath = mountPath;
|
||||
}
|
||||
|
||||
function initialize(app, options) {
|
||||
options = options || {};
|
||||
var self = this;
|
||||
passport.use('local', new LocalStrategy(
|
||||
{passReqToCallback:true},
|
||||
function(req, username, password, cb) {
|
||||
var match = self.authenticate({
|
||||
name: username,
|
||||
pass: password,
|
||||
otpCode: req.body.otpCode
|
||||
});
|
||||
if (!match.matchingUsername) {
|
||||
return cb(null, false, { message: JSON.stringify({ text: 'Invalid username or password' }) });
|
||||
}
|
||||
if (!match.otpValid) {
|
||||
return cb(null, false, { message: JSON.stringify({ text: 'Invalid one-time password.', otpLength: match.otpMissingLength || 6}) });
|
||||
}
|
||||
if (match.otpMissingLength) {
|
||||
return cb(null, false, { message: JSON.stringify({ text: 'Please enter your one-time password.', otpLength: match.otpMissingLength || 6 })});
|
||||
}
|
||||
cb(null, match.matchingUsername);
|
||||
})
|
||||
);
|
||||
|
||||
passport.serializeUser(function(username, cb) {
|
||||
cb(null, username);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(username, cb) {
|
||||
var user = self.authenticate({
|
||||
name: username
|
||||
}, true);
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
var cookieSessionSecret = options.cookieSessionSecret || require('crypto').randomBytes(64).toString('hex');
|
||||
app.use(require('connect-flash')());
|
||||
app.use(require('body-parser').urlencoded({ extended: true }));
|
||||
app.use(require('cookie-session')({
|
||||
key : 'parse_dash',
|
||||
secret : cookieSessionSecret,
|
||||
cookie : {
|
||||
maxAge: (2 * 7 * 24 * 60 * 60 * 1000) // 2 weeks
|
||||
}
|
||||
}));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.post('/login',
|
||||
passport.authenticate('local', {
|
||||
successRedirect: `${self.mountPath}apps`,
|
||||
failureRedirect: `${self.mountPath}login`,
|
||||
failureFlash : true
|
||||
})
|
||||
);
|
||||
|
||||
app.get('/logout', function(req, res){
|
||||
req.logout();
|
||||
res.redirect(`${self.mountPath}login`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the `userToTest`
|
||||
*
|
||||
* @param {Object} userToTest
|
||||
* @returns {Object} Object with `isAuthenticated` and `appsUserHasAccessTo` properties
|
||||
*/
|
||||
function authenticate(userToTest, usernameOnly) {
|
||||
let appsUserHasAccessTo = null;
|
||||
let matchingUsername = null;
|
||||
let isReadOnly = false;
|
||||
let otpMissingLength = false;
|
||||
let otpValid = true;
|
||||
|
||||
//they provided auth
|
||||
let isAuthenticated = userToTest &&
|
||||
//there are configured users
|
||||
this.validUsers &&
|
||||
//the provided auth matches one of the users
|
||||
this.validUsers.find(user => {
|
||||
let isAuthenticated = false;
|
||||
let usernameMatches = userToTest.name == user.user;
|
||||
if (usernameMatches && user.mfa && !usernameOnly) {
|
||||
if (!userToTest.otpCode) {
|
||||
otpMissingLength = user.mfaDigits || 6;
|
||||
} else {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
algorithm: user.mfaAlgorithm || 'SHA1',
|
||||
secret: OTPAuth.Secret.fromBase32(user.mfa),
|
||||
digits: user.mfaDigits,
|
||||
period: user.mfaPeriod,
|
||||
});
|
||||
const valid = totp.validate({
|
||||
token: userToTest.otpCode
|
||||
});
|
||||
if (valid === null) {
|
||||
otpValid = false;
|
||||
otpMissingLength = user.mfaDigits || 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
let passwordMatches = this.useEncryptedPasswords && !usernameOnly ? bcrypt.compareSync(userToTest.pass, user.pass) : userToTest.pass == user.pass;
|
||||
if (usernameMatches && (usernameOnly || passwordMatches)) {
|
||||
isAuthenticated = true;
|
||||
matchingUsername = user.user;
|
||||
// User restricted apps
|
||||
appsUserHasAccessTo = user.apps || null;
|
||||
isReadOnly = !!user.readOnly; // make it true/false
|
||||
}
|
||||
return isAuthenticated;
|
||||
}) ? true : false;
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
matchingUsername,
|
||||
otpMissingLength,
|
||||
otpValid,
|
||||
appsUserHasAccessTo,
|
||||
isReadOnly,
|
||||
};
|
||||
}
|
||||
|
||||
Authentication.prototype.initialize = initialize;
|
||||
Authentication.prototype.authenticate = authenticate;
|
||||
|
||||
module.exports = Authentication;
|
||||
231
packages/noodl-parse-dashboard/Parse-Dashboard/CLI/mfa.js
Normal file
231
packages/noodl-parse-dashboard/Parse-Dashboard/CLI/mfa.js
Normal file
@@ -0,0 +1,231 @@
|
||||
const crypto = require('crypto');
|
||||
const inquirer = require('inquirer');
|
||||
const OTPAuth = require('otpauth');
|
||||
const { copy } = require('./utils.js');
|
||||
const phrases = {
|
||||
enterPassword: 'Enter a password:',
|
||||
enterUsername: 'Enter a username:',
|
||||
enterAppName: 'Enter the app name:',
|
||||
}
|
||||
const getAlgorithm = async () => {
|
||||
let { algorithm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'algorithm',
|
||||
message: 'Which hashing algorithm do you want to use?',
|
||||
default: 'SHA1',
|
||||
choices: [
|
||||
'SHA1',
|
||||
'SHA224',
|
||||
'SHA256',
|
||||
'SHA384',
|
||||
'SHA512',
|
||||
'SHA3-224',
|
||||
'SHA3-256',
|
||||
'SHA3-384',
|
||||
'SHA3-512',
|
||||
'Other'
|
||||
]
|
||||
}
|
||||
]);
|
||||
if (algorithm === 'Other') {
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'algorithm',
|
||||
message: 'Enter the hashing algorithm you want to use:'
|
||||
}
|
||||
]);
|
||||
algorithm = result.algorithm;
|
||||
}
|
||||
const { digits, period } = await inquirer.prompt([
|
||||
{
|
||||
type: 'number',
|
||||
name: 'digits',
|
||||
default: 6,
|
||||
message: 'Enter the number of digits the one-time password should have:'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'period',
|
||||
default: 30,
|
||||
message: 'Enter how long the one-time password should be valid (in seconds):'
|
||||
}
|
||||
])
|
||||
return { algorithm, digits, period};
|
||||
};
|
||||
const generateSecret = ({ app, username, algorithm, digits, period }) => {
|
||||
const secret = new OTPAuth.Secret();
|
||||
const totp = new OTPAuth.TOTP({
|
||||
issuer: app,
|
||||
label: username,
|
||||
algorithm,
|
||||
digits,
|
||||
period,
|
||||
secret
|
||||
});
|
||||
const url = totp.toString();
|
||||
const config = { mfa: secret.base32 };
|
||||
config.app = app;
|
||||
config.url = url;
|
||||
if (algorithm !== 'SHA1') {
|
||||
config.mfaAlgorithm = algorithm;
|
||||
}
|
||||
if (digits != 6) {
|
||||
config.mfaDigits = digits;
|
||||
}
|
||||
if (period != 30) {
|
||||
config.mfaPeriod = period;
|
||||
}
|
||||
return { config };
|
||||
};
|
||||
const showQR = text => {
|
||||
const QRCode = require('qrcode');
|
||||
QRCode.toString(text, { type: 'terminal' }, (err, url) => {
|
||||
console.log(
|
||||
'\n------------------------------------------------------------------------------' +
|
||||
`\n\n${url}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const showInstructions = ({ app, username, passwordCopied, encrypt, config }) => {
|
||||
const {secret, url} = config;
|
||||
const mfaJSON = {...config};
|
||||
delete mfaJSON.url;
|
||||
let orderCounter = 0;
|
||||
const getOrder = () => {
|
||||
orderCounter++;
|
||||
return orderCounter;
|
||||
}
|
||||
console.log(
|
||||
'------------------------------------------------------------------------------' +
|
||||
'\n\nFollow these steps to complete the set-up:'
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n${getOrder()}. Add the following settings for user "${username}" ${app ? `in app "${app}" ` : '' }to the Parse Dashboard configuration.` +
|
||||
`\n\n ${JSON.stringify(mfaJSON)}`
|
||||
);
|
||||
|
||||
if (passwordCopied) {
|
||||
console.log(
|
||||
`\n${getOrder()}. Securely store the generated login password that has been copied to your clipboard.`
|
||||
);
|
||||
}
|
||||
|
||||
if (secret) {
|
||||
console.log(
|
||||
`\n${getOrder()}. Open the authenticator app to scan the QR code above or enter this secret code:` +
|
||||
`\n\n ${secret}` +
|
||||
'\n\n If the secret code generates incorrect one-time passwords, try this alternative:' +
|
||||
`\n\n ${url}` +
|
||||
`\n\n${getOrder()}. Destroy any records of the QR code and the secret code to secure the account.`
|
||||
);
|
||||
}
|
||||
|
||||
if (encrypt) {
|
||||
console.log(
|
||||
`\n${getOrder()}. Make sure that "useEncryptedPasswords" is set to "true" in your dashboard configuration.` +
|
||||
'\n You chose to generate an encrypted password for this user.' +
|
||||
'\n Any existing users with non-encrypted passwords will require newly created, encrypted passwords.'
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
'\n------------------------------------------------------------------------------\n'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
async createUser() {
|
||||
const data = {};
|
||||
|
||||
console.log('');
|
||||
const { username, password } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: phrases.enterUsername
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'password',
|
||||
message: 'Do you want to auto-generate a password?'
|
||||
}
|
||||
]);
|
||||
data.user = username;
|
||||
if (!password) {
|
||||
const { password } = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: phrases.enterPassword
|
||||
}
|
||||
]);
|
||||
data.pass = password;
|
||||
} else {
|
||||
const password = crypto.randomBytes(20).toString('base64');
|
||||
data.pass = password;
|
||||
}
|
||||
const { mfa, encrypt } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'encrypt',
|
||||
message: 'Should the password be encrypted? (strongly recommended, otherwise it is stored in clear-text)'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'mfa',
|
||||
message: 'Do you want to enable multi-factor authentication?'
|
||||
}
|
||||
]);
|
||||
if (encrypt) {
|
||||
// Copy the raw password to clipboard
|
||||
copy(data.pass);
|
||||
|
||||
// Encrypt password
|
||||
const bcrypt = require('bcryptjs');
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
data.pass = bcrypt.hashSync(data.pass, salt);
|
||||
}
|
||||
const config = {};
|
||||
if (mfa) {
|
||||
const { app } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'app',
|
||||
message: phrases.enterAppName
|
||||
}
|
||||
]);
|
||||
const { algorithm, digits, period } = await getAlgorithm();
|
||||
const secret =generateSecret({ app, username, algorithm, digits, period });
|
||||
Object.assign(config, secret.config);
|
||||
showQR(secret.config.url);
|
||||
}
|
||||
config.user = data.user;
|
||||
config.pass = data.pass ;
|
||||
showInstructions({ app: data.app, username, passwordCopied: true, encrypt, config });
|
||||
},
|
||||
async createMFA() {
|
||||
console.log('');
|
||||
const { username, app } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message:
|
||||
'Enter the username for which you want to enable multi-factor authentication:'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'app',
|
||||
message: phrases.enterAppName
|
||||
}
|
||||
]);
|
||||
const { algorithm, digits, period } = await getAlgorithm();
|
||||
|
||||
const { config } = generateSecret({ app, username, algorithm, digits, period });
|
||||
showQR(config.url);
|
||||
// Compose config
|
||||
showInstructions({ app, username, config });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
copy(text) {
|
||||
const proc = require('child_process').spawn('pbcopy');
|
||||
proc.stdin.write(text);
|
||||
proc.stdin.end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const { createUser, createMFA } = require('./CLI/mfa');
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
createMFA
|
||||
};
|
||||
208
packages/noodl-parse-dashboard/Parse-Dashboard/app.js
Normal file
208
packages/noodl-parse-dashboard/Parse-Dashboard/app.js
Normal file
@@ -0,0 +1,208 @@
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const Authentication = require('./Authentication.js');
|
||||
var fs = require('fs');
|
||||
|
||||
var newFeaturesInLatestVersion = [];
|
||||
|
||||
function getMount(mountPath) {
|
||||
mountPath = mountPath || '';
|
||||
if (!mountPath.endsWith('/')) {
|
||||
mountPath += '/';
|
||||
}
|
||||
return mountPath;
|
||||
}
|
||||
|
||||
function checkIfIconsExistForApps(apps, iconsFolder) {
|
||||
for (var i in apps) {
|
||||
var currentApp = apps[i];
|
||||
var iconName = currentApp.iconName;
|
||||
var path = iconsFolder + '/' + iconName;
|
||||
|
||||
fs.stat(path, function(err) {
|
||||
if (err) {
|
||||
if ('ENOENT' == err.code) {// file does not exist
|
||||
console.warn('Icon with file name: ' + iconName +' couldn\'t be found in icons folder!');
|
||||
} else {
|
||||
console.log(
|
||||
'An error occurd while checking for icons, please check permission!');
|
||||
}
|
||||
} else {
|
||||
//every thing was ok so for example you can read it and send it to client
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function(config, options) {
|
||||
options = options || {};
|
||||
var app = express();
|
||||
// Serve public files.
|
||||
app.use(express.static(path.join(__dirname,'parse-dashboard-public')));
|
||||
|
||||
// Allow setting via middleware
|
||||
if (config.trustProxy && app.disabled('trust proxy')) {
|
||||
app.enable('trust proxy');
|
||||
}
|
||||
|
||||
// wait for app to mount in order to get mountpath
|
||||
app.on('mount', function() {
|
||||
const mountPath = getMount(app.mountpath);
|
||||
const users = config.users;
|
||||
const useEncryptedPasswords = config.useEncryptedPasswords ? true : false;
|
||||
const authInstance = new Authentication(users, useEncryptedPasswords, mountPath);
|
||||
authInstance.initialize(app, { cookieSessionSecret: options.cookieSessionSecret });
|
||||
|
||||
// Serve the configuration.
|
||||
app.get('/parse-dashboard-config.json', function(req, res) {
|
||||
let apps = config.apps.map((app) => Object.assign({}, app)); // make a copy
|
||||
let response = {
|
||||
apps: apps,
|
||||
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
|
||||
};
|
||||
|
||||
//Based on advice from Doug Wilson here:
|
||||
//https://github.com/expressjs/express/issues/2518
|
||||
const requestIsLocal =
|
||||
req.connection.remoteAddress === '127.0.0.1' ||
|
||||
req.connection.remoteAddress === '::ffff:127.0.0.1' ||
|
||||
req.connection.remoteAddress === '::1';
|
||||
if (!options.dev && !requestIsLocal) {
|
||||
if (!req.secure && !options.allowInsecureHTTP) {
|
||||
//Disallow HTTP requests except on localhost, to prevent the master key from being transmitted in cleartext
|
||||
return res.send({ success: false, error: 'Parse Dashboard can only be remotely accessed via HTTPS' });
|
||||
}
|
||||
|
||||
if (!users) {
|
||||
//Accessing the dashboard over the internet can only be done with username and password
|
||||
return res.send({ success: false, error: 'Configure a user to access Parse Dashboard remotely' });
|
||||
}
|
||||
}
|
||||
const authentication = req.user;
|
||||
|
||||
const successfulAuth = authentication && authentication.isAuthenticated;
|
||||
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
|
||||
const isReadOnly = authentication && authentication.isReadOnly;
|
||||
// User is full read-only, replace the masterKey by the read-only one
|
||||
if (isReadOnly) {
|
||||
response.apps = response.apps.map((app) => {
|
||||
app.masterKey = app.readOnlyMasterKey;
|
||||
if (!app.masterKey) {
|
||||
throw new Error('You need to provide a readOnlyMasterKey to use read-only features.');
|
||||
}
|
||||
return app;
|
||||
});
|
||||
}
|
||||
|
||||
if (successfulAuth) {
|
||||
if (appsUserHasAccess) {
|
||||
// Restric access to apps defined in user dictionary
|
||||
// If they didn't supply any app id, user will access all apps
|
||||
response.apps = response.apps.filter(function (app) {
|
||||
return appsUserHasAccess.find(appUserHasAccess => {
|
||||
const isSame = app.appId === appUserHasAccess.appId;
|
||||
if (isSame && appUserHasAccess.readOnly) {
|
||||
app.masterKey = app.readOnlyMasterKey;
|
||||
}
|
||||
return isSame;
|
||||
})
|
||||
});
|
||||
}
|
||||
// They provided correct auth
|
||||
return res.json(response);
|
||||
}
|
||||
|
||||
if (users) {
|
||||
//They provided incorrect auth
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
//They didn't provide auth, and have configured the dashboard to not need auth
|
||||
//(ie. didn't supply usernames and passwords)
|
||||
if (requestIsLocal || options.dev) {
|
||||
//Allow no-auth access on localhost only, if they have configured the dashboard to not need auth
|
||||
return res.json(response);
|
||||
}
|
||||
//We shouldn't get here. Fail closed.
|
||||
res.send({ success: false, error: 'Something went wrong.' });
|
||||
});
|
||||
|
||||
// Serve the app icons. Uses the optional `iconsFolder` parameter as
|
||||
// directory name, that was setup in the config file.
|
||||
// We are explicitly not using `__dirpath` here because one may be
|
||||
// running parse-dashboard from globally installed npm.
|
||||
if (config.iconsFolder) {
|
||||
try {
|
||||
var stat = fs.statSync(config.iconsFolder);
|
||||
if (stat.isDirectory()) {
|
||||
app.use('/appicons', express.static(config.iconsFolder));
|
||||
//Check also if the icons really exist
|
||||
checkIfIconsExistForApps(config.apps, config.iconsFolder);
|
||||
}
|
||||
} catch (e) {
|
||||
// Directory doesn't exist or something.
|
||||
console.warn('Iconsfolder at path: ' + config.iconsFolder +
|
||||
' not found!');
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/login', function(req, res) {
|
||||
if (!users || (req.user && req.user.isAuthenticated)) {
|
||||
return res.redirect(`${mountPath}apps`);
|
||||
}
|
||||
|
||||
let errors = req.flash('error');
|
||||
if (errors && errors.length) {
|
||||
errors = `<div id="login_errors" style="display: none;">
|
||||
${errors.join(' ')}
|
||||
</div>`
|
||||
}
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
|
||||
<base href="${mountPath}"/>
|
||||
<script>
|
||||
PARSE_DASHBOARD_PATH = "${mountPath}";
|
||||
</script>
|
||||
<title>Parse Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="login_mount"></div>
|
||||
${errors}
|
||||
<script src="${mountPath}bundles/login.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// For every other request, go to index.html. Let client-side handle the rest.
|
||||
app.get('/*', function(req, res) {
|
||||
if (users && (!req.user || !req.user.isAuthenticated)) {
|
||||
return res.redirect(`${mountPath}login`);
|
||||
}
|
||||
if (users && req.user && req.user.matchingUsername ) {
|
||||
res.append('username', req.user.matchingUsername);
|
||||
}
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="${mountPath}favicon.ico" />
|
||||
<base href="${mountPath}"/>
|
||||
<script>
|
||||
PARSE_DASHBOARD_PATH = "${mountPath}";
|
||||
</script>
|
||||
<title>Parse Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="browser_mount"></div>
|
||||
<script src="${mountPath}bundles/dashboard.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
198
packages/noodl-parse-dashboard/Parse-Dashboard/index.js
Normal file
198
packages/noodl-parse-dashboard/Parse-Dashboard/index.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
// Command line tool for npm start
|
||||
'use strict'
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const parseDashboard = require('./app');
|
||||
const CLIHelper = require('./CLIHelper.js');
|
||||
|
||||
const program = require('commander');
|
||||
program.option('--appId [appId]', 'the app Id of the app you would like to manage.');
|
||||
program.option('--masterKey [masterKey]', 'the master key of the app you would like to manage.');
|
||||
program.option('--serverURL [serverURL]', 'the server url of the app you would like to manage.');
|
||||
program.option('--graphQLServerURL [graphQLServerURL]', 'the GraphQL server url of the app you would like to manage.');
|
||||
program.option('--dev', 'Enable development mode. This will disable authentication and allow non HTTPS connections. DO NOT ENABLE IN PRODUCTION SERVERS');
|
||||
program.option('--appName [appName]', 'the name of the app you would like to manage. Optional.');
|
||||
program.option('--config [config]', 'the path to the configuration file');
|
||||
program.option('--host [host]', 'the host to run parse-dashboard');
|
||||
program.option('--port [port]', 'the port to run parse-dashboard');
|
||||
program.option('--mountPath [mountPath]', 'the mount path to run parse-dashboard');
|
||||
program.option('--allowInsecureHTTP [allowInsecureHTTP]', 'set this flag when you are running the dashboard behind an HTTPS load balancer or proxy with early SSL termination.');
|
||||
program.option('--sslKey [sslKey]', 'the path to the SSL private key.');
|
||||
program.option('--sslCert [sslCert]', 'the path to the SSL certificate.');
|
||||
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.');
|
||||
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');
|
||||
program.option('--createUser', 'helper tool to allow you to generate secure user passwords and secrets. Use this on trusted devices only.');
|
||||
program.option('--createMFA', 'helper tool to allow you to generate multi-factor authentication secrets.');
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
for (const key in options) {
|
||||
const func = CLIHelper[key];
|
||||
if (func && typeof func === 'function') {
|
||||
func();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const host = options.host || process.env.HOST || '0.0.0.0';
|
||||
const port = options.port || process.env.PORT || 4040;
|
||||
const mountPath = options.mountPath || process.env.MOUNT_PATH || '/';
|
||||
const allowInsecureHTTP = options.allowInsecureHTTP || process.env.PARSE_DASHBOARD_ALLOW_INSECURE_HTTP;
|
||||
const cookieSessionSecret = options.cookieSessionSecret || process.env.PARSE_DASHBOARD_COOKIE_SESSION_SECRET;
|
||||
const trustProxy = options.trustProxy || process.env.PARSE_DASHBOARD_TRUST_PROXY;
|
||||
const dev = options.dev;
|
||||
|
||||
if (trustProxy && allowInsecureHTTP) {
|
||||
console.log('Set only trustProxy *or* allowInsecureHTTP, not both. Only one is needed to handle being behind a proxy.');
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let explicitConfigFileProvided = !!options.config;
|
||||
let configFile = null;
|
||||
let configFromCLI = null;
|
||||
let configServerURL = options.serverURL || process.env.PARSE_DASHBOARD_SERVER_URL;
|
||||
let configGraphQLServerURL = options.graphQLServerURL || process.env.PARSE_DASHBOARD_GRAPHQL_SERVER_URL;
|
||||
let configMasterKey = options.masterKey || process.env.PARSE_DASHBOARD_MASTER_KEY;
|
||||
let configAppId = options.appId || process.env.PARSE_DASHBOARD_APP_ID;
|
||||
let configAppName = options.appName || process.env.PARSE_DASHBOARD_APP_NAME;
|
||||
let configUserId = options.userId || process.env.PARSE_DASHBOARD_USER_ID;
|
||||
let configUserPassword = options.userPassword || process.env.PARSE_DASHBOARD_USER_PASSWORD;
|
||||
let configSSLKey = options.sslKey || process.env.PARSE_DASHBOARD_SSL_KEY;
|
||||
let configSSLCert = options.sslCert || process.env.PARSE_DASHBOARD_SSL_CERT;
|
||||
|
||||
function handleSIGs(server) {
|
||||
const signals = {
|
||||
'SIGINT': 2,
|
||||
'SIGTERM': 15
|
||||
};
|
||||
function shutdown(signal, value) {
|
||||
server.close(function () {
|
||||
console.log('server stopped by ' + signal);
|
||||
process.exit(128 + value);
|
||||
});
|
||||
}
|
||||
Object.keys(signals).forEach(function (signal) {
|
||||
process.on(signal, function () {
|
||||
shutdown(signal, signals[signal]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.config && !process.env.PARSE_DASHBOARD_CONFIG) {
|
||||
if (configServerURL && configMasterKey && configAppId) {
|
||||
configFromCLI = {
|
||||
data: {
|
||||
apps: [
|
||||
{
|
||||
appId: configAppId,
|
||||
serverURL: configServerURL,
|
||||
masterKey: configMasterKey,
|
||||
appName: configAppName,
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
if (configGraphQLServerURL) {
|
||||
configFromCLI.data.apps[0].graphQLServerURL = configGraphQLServerURL;
|
||||
}
|
||||
if (configUserId && configUserPassword) {
|
||||
configFromCLI.data.users = [
|
||||
{
|
||||
user: configUserId,
|
||||
pass: configUserPassword,
|
||||
}
|
||||
];
|
||||
}
|
||||
} else if (!configServerURL && !configMasterKey && !configAppName) {
|
||||
configFile = path.join(__dirname, 'parse-dashboard-config.json');
|
||||
}
|
||||
} else if (!options.config && process.env.PARSE_DASHBOARD_CONFIG) {
|
||||
configFromCLI = {
|
||||
data: JSON.parse(process.env.PARSE_DASHBOARD_CONFIG)
|
||||
};
|
||||
} else {
|
||||
configFile = options.config;
|
||||
if (options.appId || options.serverURL || options.masterKey || options.appName || options.graphQLServerURL) {
|
||||
console.log('You must provide either a config file or other CLI options (appName, appId, masterKey, serverURL, and graphQLServerURL); not both.');
|
||||
process.exit(3);
|
||||
}
|
||||
}
|
||||
|
||||
let config = null;
|
||||
let configFilePath = null;
|
||||
if (configFile) {
|
||||
try {
|
||||
config = {
|
||||
data: JSON.parse(fs.readFileSync(configFile, 'utf8'))
|
||||
};
|
||||
configFilePath = path.dirname(configFile);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
console.log('Your config file contains invalid JSON. Exiting.');
|
||||
process.exit(1);
|
||||
} else if (error.code === 'ENOENT') {
|
||||
if (explicitConfigFileProvided) {
|
||||
console.log('Your config file is missing. Exiting.');
|
||||
process.exit(2);
|
||||
} else {
|
||||
console.log('You must provide either a config file or required CLI options (app ID, Master Key, and server URL); not both.');
|
||||
process.exit(3);
|
||||
}
|
||||
} else {
|
||||
console.log('There was a problem with your config. Exiting.');
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
} else if (configFromCLI) {
|
||||
config = configFromCLI;
|
||||
} else {
|
||||
//Failed to load default config file.
|
||||
console.log('You must provide either a config file or an app ID, Master Key, and server URL. See parse-dashboard --help for details.');
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
config.data.apps.forEach(app => {
|
||||
if (!app.appName) {
|
||||
app.appName = app.appId;
|
||||
}
|
||||
});
|
||||
|
||||
if (config.data.iconsFolder && configFilePath) {
|
||||
config.data.iconsFolder = path.join(configFilePath, config.data.iconsFolder);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
if (allowInsecureHTTP || trustProxy || dev) app.enable('trust proxy');
|
||||
|
||||
config.data.trustProxy = trustProxy;
|
||||
let dashboardOptions = { allowInsecureHTTP, cookieSessionSecret, dev };
|
||||
app.use(mountPath, parseDashboard(config.data, dashboardOptions));
|
||||
let server;
|
||||
if(!configSSLKey || !configSSLCert){
|
||||
// Start the server.
|
||||
server = app.listen(port, host, function () {
|
||||
console.log(`The dashboard is now available at http://${server.address().address}:${server.address().port}${mountPath}`);
|
||||
});
|
||||
} else {
|
||||
// Start the server using SSL.
|
||||
var privateKey = fs.readFileSync(configSSLKey);
|
||||
var certificate = fs.readFileSync(configSSLCert);
|
||||
|
||||
server = require('https').createServer({
|
||||
key: privateKey,
|
||||
cert: certificate
|
||||
}, app).listen(port, host, function () {
|
||||
console.log(`The dashboard is now available at https://${server.address().address}:${server.address().port}${mountPath}`);
|
||||
});
|
||||
}
|
||||
handleSIGs(server);
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"serverURL": "http://localhost:1337/parse",
|
||||
"appId": "hello",
|
||||
"masterKey": "world",
|
||||
"appName": "",
|
||||
"iconName": "",
|
||||
"primaryBackgroundColor": "",
|
||||
"secondaryBackgroundColor": ""
|
||||
}
|
||||
],
|
||||
"iconsFolder": "icons"
|
||||
}
|
||||
8
packages/noodl-parse-dashboard/README.md
Normal file
8
packages/noodl-parse-dashboard/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
This package makes it easier to bundle Parse Dashboard with Noodl.
|
||||
|
||||
This repo contains:
|
||||
|
||||
- A custom `package.json` with the dependencies that are required by the express app version of the dashboard
|
||||
- A copy of the `Parse-Dashboard` folder from `/deps/parse-dashboard`, which is the code for the Parse express app. Please don't modify the files in here directly. Update `/deps/parse-dashboard`, push, and then copy the files over here.
|
||||
|
||||
The files for the Parse Dashboard app itself (e.g. react components, css, icons etc) is built in `/deps/parse-dashboard` and then copied over to `/packages/noodl-editor/src/editor/parse-dashboard-public`
|
||||
35
packages/noodl-parse-dashboard/package.json
Normal file
35
packages/noodl-parse-dashboard/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@noodl/noodl-parse-dashboard",
|
||||
"version": "2.7.1",
|
||||
"dependencies": {
|
||||
"bcryptjs": "2.4.3",
|
||||
"connect-flash": "0.1.1",
|
||||
"cookie-session": "2.0.0",
|
||||
"express": "^4.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"otpauth": "^7.1.3",
|
||||
"package-json": "7.0.0",
|
||||
"passport": "0.5.3",
|
||||
"passport-local": "1.0.0",
|
||||
"semver": "^7.3.7"
|
||||
},
|
||||
"parseDashboardFeatures": [
|
||||
"Data Browser",
|
||||
"Cloud Code Viewer",
|
||||
"Cloud Code Jobs Viewer and Runner",
|
||||
"Parse Config",
|
||||
"REST API Console",
|
||||
"GraphQL API Console",
|
||||
"JS Custom Query Console",
|
||||
"Class Level Permissions Editor",
|
||||
"Pointer Permissions Editor",
|
||||
"Send Push Notifications",
|
||||
"Logs Viewer",
|
||||
"Push Status Page",
|
||||
"Relation Editor"
|
||||
],
|
||||
"main": "Parse-Dashboard/app.js",
|
||||
"devDependencies": {
|
||||
"keyv": "^4.5.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user