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:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View 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;

View 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 });
}
};

View File

@@ -0,0 +1,7 @@
module.exports = {
copy(text) {
const proc = require('child_process').spawn('pbcopy');
proc.stdin.write(text);
proc.stdin.end();
}
}

View File

@@ -0,0 +1,6 @@
const { createUser, createMFA } = require('./CLI/mfa');
module.exports = {
createUser,
createMFA
};

View 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;
}

View 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);

View File

@@ -0,0 +1,14 @@
{
"apps": [
{
"serverURL": "http://localhost:1337/parse",
"appId": "hello",
"masterKey": "world",
"appName": "",
"iconName": "",
"primaryBackgroundColor": "",
"secondaryBackgroundColor": ""
}
],
"iconsFolder": "icons"
}

View 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`

View 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"
}
}