Skip to content

Commit

Permalink
feat: Add visual configurator for Parse Dashboard settings (parse-com…
Browse files Browse the repository at this point in the history
  • Loading branch information
dblythy authored May 21, 2023
1 parent 5abcbc9 commit 228d839
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 5 deletions.
4 changes: 3 additions & 1 deletion src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { setBasePath } from 'lib/AJAX';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import Playground from './Data/Playground/Playground.react';
import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react';

const ShowSchemaOverview = false; //In progress features. Change false to true to work on this feature.

Expand Down Expand Up @@ -199,12 +200,13 @@ export default class Dashboard extends React.Component {

const SettingsRoute = (
<Route element={<SettingsData />}>
<Route path='dashboard' element={<DashboardSettings />} />
<Route path='general' element={<GeneralSettings />} />
<Route path='keys' element={<SecuritySettings />} />
<Route path='users' element={<UsersSettings />} />
<Route path='push' element={<PushSettings />} />
<Route path='hosting' element={<HostingSettings />} />
<Route index element={<Navigate replace to='general' />} />
<Route index element={<Navigate replace to='dashboard' />} />
</Route>
)

Expand Down
7 changes: 5 additions & 2 deletions src/dashboard/DashboardView.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,10 @@ export default class DashboardView extends React.Component {
}
*/

let settingsSections = [];
const settingsSections = [{
name: 'Dashboard',
link: '/settings/dashboard'
}];

// Settings - nothing remotely like this in parse-server yet. Maybe it will arrive soon.
/*
Expand Down Expand Up @@ -292,7 +295,7 @@ export default class DashboardView extends React.Component {
);

let content = <div className={styles.content}>{this.renderContent()}</div>;
const canRoute = [...coreSubsections, ...pushSubsections]
const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections]
.map(({ link }) => link.split('/')[1])
.includes(this.state.route);

Expand Down
242 changes: 242 additions & 0 deletions src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import DashboardView from 'dashboard/DashboardView.react';
import Field from 'components/Field/Field.react';
import Fieldset from 'components/Fieldset/Fieldset.react';
import FlowView from 'components/FlowView/FlowView.react';
import FormButton from 'components/FormButton/FormButton.react';
import Label from 'components/Label/Label.react';
import Button from 'components/Button/Button.react';
import React from 'react';
import styles from 'dashboard/Settings/DashboardSettings/DashboardSettings.scss';
import TextInput from 'components/TextInput/TextInput.react';
import Toggle from 'components/Toggle/Toggle.react';
import Icon from 'components/Icon/Icon.react';
import Dropdown from 'components/Dropdown/Dropdown.react';
import Option from 'components/Dropdown/Option.react';
import Toolbar from 'components/Toolbar/Toolbar.react';
import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';
import Notification from 'dashboard/Data/Browser/Notification.react';
import * as ColumnPreferences from 'lib/ColumnPreferences';
import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth';
import QRCode from 'qrcode';

export default class DashboardSettings extends DashboardView {
constructor() {
super();
this.section = 'App Settings';
this.subsection = 'Dashboard Configuration';

this.state = {
createUserInput: false,
username: '',
password: '',
encrypt: true,
mfa: false,
mfaDigits: 6,
mfaPeriod: 30,
mfaAlgorithm: 'SHA1',
message: null,
passwordInput: '',
passwordHidden: true,
columnData: {
data: '',
show: false,
},
newUser: {
data: '',
show: false,
mfa: '',
},
};
}

getColumns() {
const data = ColumnPreferences.getAllPreferences(this.context.applicationId);
this.setState({
columnData: { data: JSON.stringify(data, null, 2), show: true },
});
}

copy(data, label) {
navigator.clipboard.writeText(data);
this.showNote(`${label} copied to clipboard`);
}

createUser() {
if (!this.state.username) {
this.showNote('Please enter a username');
return;
}
if (!this.state.password) {
this.showNote('Please enter a password');
return;
}

let pass = this.state.password;
if (this.state.encrypt) {
const salt = bcrypt.genSaltSync(10);
pass = bcrypt.hashSync(pass, salt);
}

const user = {
username: this.state.username,
pass,
};

let mfa;
if (this.state.mfa) {
const secret = new OTPAuth.Secret();
const totp = new OTPAuth.TOTP({
issuer: this.context.name,
label: user.username,
algorithm: this.state.mfaAlgorithm || 'SHA1',
digits: this.state.mfaDigits || 6,
period: this.state.mfaPeriod || 30,
secret,
});
mfa = totp.toString();
user.mfa = secret.base32;
if (totp.algorithm !== 'SHA1') {
user.mfaAlgorithm = totp.algorithm;
}
if (totp.digits != 6) {
user.mfaDigits = totp.digits;
}
if (totp.period != 30) {
user.mfaPeriod = totp.period;
}

setTimeout(() => {
const canvas = document.getElementById('canvas');
QRCode.toCanvas(canvas, mfa);
}, 10);
}

this.setState({
newUser: {
show: true,
data: JSON.stringify(user, null, 2),
mfa,
},
});
}

generatePassword() {
let chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let pwordLength = 20;
let password = '';

const array = new Uint32Array(chars.length);
window.crypto.getRandomValues(array);

for (let i = 0; i < pwordLength; i++) {
password += chars[array[i] % chars.length];
}
this.setState({ password });
}

showNote(message) {
if (!message) {
return;
}

clearTimeout(this.noteTimeout);

this.setState({ message });

this.noteTimeout = setTimeout(() => {
this.setState({ message: null });
}, 3500);
}

renderForm() {
const createUserInput = (
<Fieldset legend="New User">
<Field label={<Label text="Username" />} input={<TextInput value={this.state.username} placeholder="Username" onChange={(username) => this.setState({ username })} />} />
<Field
label={
<Label
text={
<div className={styles.password}>
<span>Password</span>
<a onClick={() => this.setState({ passwordHidden: !this.state.passwordHidden })}>
<Icon name={this.state.passwordHidden ? 'visibility' : 'visibility_off'} width={18} height={18} fill="rgba(0,0,0,0.4)" />
</a>
</div>
}
description={<a onClick={() => this.generatePassword()}>Generate strong password</a>}
/>
}
input={<TextInput hidden={this.state.passwordHidden} value={this.state.password} placeholder="Password" onChange={(password) => this.setState({ password })} />}
/>
<Field label={<Label text="Encrypt Password" />} input={<Toggle value={this.state.encrypt} type={Toggle.Types.YES_NO} onChange={(encrypt) => this.setState({ encrypt })} />} />
<Field label={<Label text="Enable MFA" />} input={<Toggle value={this.state.mfa} type={Toggle.Types.YES_NO} onChange={(mfa) => this.setState({ mfa })} />} />
{this.state.mfa && (
<Field
label={<Label text="MFA Algorithm" />}
input={
<Dropdown value={this.state.mfaAlgorithm} onChange={(mfaAlgorithm) => this.setState({ mfaAlgorithm })}>
{['SHA1', 'SHA224', 'SHA256', 'SHA384', 'SHA512', 'SHA3-224', 'SHA3-256', 'SHA3-384', 'SHA3-512'].map((column) => (
<Option key={column} value={column}>
{column}
</Option>
))}
</Dropdown>
}
/>
)}
{this.state.mfa && <Field label={<Label text="MFA Digits" description="How many digits long should the MFA code be" />} input={<TextInput value={`${this.state.mfaDigits}`} placeholder="6" onChange={(mfaDigits) => this.setState({ mfaDigits })} />} />}
{this.state.mfa && <Field label={<Label text="MFA Period" description="How many long should the MFA last for" />} input={<TextInput value={`${this.state.mfaPeriod}`} placeholder="30" onChange={(mfaPeriod) => this.setState({ mfaPeriod })} />} />}
<Field input={<Button color="blue" value="Create" width="120px" onClick={() => this.createUser()} />} />
</Fieldset>
);
const columnPreferences = (
<div>
<div className={styles.columnData}>
<CodeSnippet source={this.state.columnData.data} language="json" />
</div>
<div className={styles.footer}>
<Button color="blue" value="Copy" width="120px" onClick={() => this.copy(this.state.columnData.data, 'Column Preferences')} />
<Button primary={true} value="Done" width="120px" onClick={() => this.setState({ columnData: { data: '', show: false } })} />
</div>
</div>
);
const userData = (
<div className={styles.userData}>
Add the following data to your Parse Dashboard configuration "users":
{this.state.encrypt && <div>Make sure the dashboard option useEncryptedPasswords is set to true.</div>}
<div className={styles.newUser}>
<CodeSnippet source={this.state.newUser.data} language="json" />
</div>
{this.state.mfa && (
<div className={styles.mfa}>
<div>Share this MFA Data with your user:</div>
<a onClick={() => this.copy(this.state.newUser.mfa, 'MFA Data')}>{this.state.newUser.mfa}</a>
<canvas id="canvas" />
</div>
)}
<div className={styles.footer}>
<Button color="blue" value="Copy" width="120px" onClick={() => this.copy(this.state.newUser.data, 'New User')} />
<Button primary={true} value="Done" width="120px" onClick={() => this.setState({ username: '', password: '', passwordHidden: true, mfaAlgorithm: 'SHA1', mfaDigits: 6, mfaPeriod: 30, encrypt: true, createUserInput: false, newUser: { data: '', show: false } })} />
</div>
</div>
);
return (
<div className={styles.settings_page}>
<Fieldset legend="Dashboard Configuration">
<Field label={<Label text="Export Column Preferences" />} input={<FormButton color="blue" value="Export" onClick={() => this.getColumns()} />} />
<Field label={<Label text="Create New User" />} input={<FormButton color="blue" value="Create" onClick={() => this.setState({ createUserInput: true })} />} />
</Fieldset>
{this.state.columnData.show && columnPreferences}
{this.state.createUserInput && createUserInput}
{this.state.newUser.show && userData}
<Toolbar section="Settings" subsection="Dashboard Configuration" />
<Notification note={this.state.message} isErrorNote={false} />
</div>
);
}

renderContent() {
return <FlowView initialFields={{}} initialChanges={{}} footerContents={() => {}} onSubmit={() => {}} renderForm={() => this.renderForm()} />;
}
}
28 changes: 28 additions & 0 deletions src/dashboard/Settings/DashboardSettings/DashboardSettings.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.columnData {
max-height: 50vh;
overflow-y: scroll;
}
.newUser {
max-height: 100px;
overflow-y: scroll;
}
.settings_page {
padding: 120px 0 80px 0;
}
.footer {
display: flex;
padding: 10px;
justify-content: end;
gap: 10px;
}
.password {
display: flex;
gap: 4px;
}
.userData {
padding: 10px;
}
.mfa {
display: block;
margin-top: 10px;
}
21 changes: 19 additions & 2 deletions src/lib/ColumnPreferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ export function getPreferences(appId, className) {
}
}

export function getAllPreferences(appId) {
const storageKeys = Object.keys(localStorage);
const result = {};
for (const key of storageKeys) {
const split = key.split(':')
if (split.length <= 1) {
continue;
}
const className = split.at(-1);
const preferences = getPreferences(appId, className);
if (preferences) {
result[className] = preferences;
}
}
return result;
}

export function getColumnSort(sortBy, appId, className) {
let cachedSort = getPreferences(appId, COLUMN_SORT) || [ { name: className, value: DEFAULT_COLUMN_SORT } ];
let ordering = [].concat(cachedSort);
Expand Down Expand Up @@ -74,7 +91,7 @@ export function getColumnSort(sortBy, appId, className) {
export function getOrder(cols, appId, className, defaultPrefs) {

let prefs = getPreferences(appId, className) || [ { name: 'objectId', width: DEFAULT_WIDTH, visible: true, cached: true } ];

if (defaultPrefs) {

// Check that every default pref is in the prefs array.
Expand All @@ -85,7 +102,7 @@ export function getOrder(cols, appId, className, defaultPrefs) {
}
});

// Iterate over the current prefs
// Iterate over the current prefs
prefs = prefs.map((prefsItem) => {
// Get the default prefs item.
const defaultPrefsItem = defaultPrefs.find(defaultPrefsItem => defaultPrefsItem.name === prefsItem.name) || {};
Expand Down

0 comments on commit 228d839

Please sign in to comment.