From efd6d8ee29ad72ffdf1bf04904145a2f1be6fe45 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 12 Mar 2023 13:57:53 +1100 Subject: [PATCH 1/3] feat: add dashboard settings page --- src/dashboard/Dashboard.js | 4 +- src/dashboard/DashboardView.react.js | 7 +- .../DashboardSettings.react.js | 242 ++++++++++++++++++ .../DashboardSettings/DashboardSettings.scss | 28 ++ src/lib/ColumnPreferences.js | 21 +- 5 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js create mode 100644 src/dashboard/Settings/DashboardSettings/DashboardSettings.scss diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js index 9993dc0722..516133dead 100644 --- a/src/dashboard/Dashboard.js +++ b/src/dashboard/Dashboard.js @@ -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. @@ -199,12 +200,13 @@ export default class Dashboard extends React.Component { const SettingsRoute = ( }> + } /> } /> } /> } /> } /> } /> - } /> + } /> ) diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js index 8402dcc04f..b224ac6778 100644 --- a/src/dashboard/DashboardView.react.js +++ b/src/dashboard/DashboardView.react.js @@ -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. /* @@ -292,7 +295,7 @@ export default class DashboardView extends React.Component { ); let content =
{this.renderContent()}
; - const canRoute = [...coreSubsections, ...pushSubsections] + const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections] .map(({ link }) => link.split('/')[1]) .includes(this.state.route); diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js new file mode 100644 index 0000000000..478a411d2f --- /dev/null +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js @@ -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 = ( +
+ } input={ this.setState({ username })} />} /> + + Password + this.setState({ passwordHidden: !this.state.passwordHidden })}> + + + + } + description={ this.generatePassword()}>Generate strong password} + /> + } + input={
+ ); + const columnPreferences = ( +
+
+ +
+
+
+
+ ); + const userData = ( +
+ Add the following data to your Parse Dashboard configuration "users": + {this.state.encrypt &&
Make sure the dashboard option useEncryptedPasswords is set to true.
} +
+ +
+ {this.state.mfa && ( + + )} +
+
+
+ ); + return ( +
+
+ } input={ this.getColumns()} />} /> + } input={ this.setState({ createUserInput: true })} />} /> +
+ {this.state.columnData.show && columnPreferences} + {this.state.createUserInput && createUserInput} + {this.state.newUser.show && userData} + + +
+ ); + } + + renderContent() { + return {}} onSubmit={() => {}} renderForm={() => this.renderForm()} />; + } +} diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss new file mode 100644 index 0000000000..2e1896a191 --- /dev/null +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss @@ -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; +} \ No newline at end of file diff --git a/src/lib/ColumnPreferences.js b/src/lib/ColumnPreferences.js index a9c691d0ef..78619dbe18 100644 --- a/src/lib/ColumnPreferences.js +++ b/src/lib/ColumnPreferences.js @@ -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); @@ -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. @@ -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) || {}; From 62324569e70e50fa9964ed274f277f3d5bfb21a7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 12 Mar 2023 14:02:16 +1100 Subject: [PATCH 2/3] Update DashboardSettings.scss --- src/dashboard/Settings/DashboardSettings/DashboardSettings.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss index 2e1896a191..579c813055 100644 --- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss @@ -25,4 +25,4 @@ .mfa { display: block; margin-top: 10px; -} \ No newline at end of file +} From 768e580d9ee405387339d94ca9b1cb4207b474cf Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 12 Mar 2023 14:09:44 +1100 Subject: [PATCH 3/3] Update DashboardSettings.react.js --- .../Settings/DashboardSettings/DashboardSettings.react.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js index 478a411d2f..3dfc4d4bc7 100644 --- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js @@ -17,7 +17,7 @@ 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 * as OTPAuth from 'otpauth'; import QRCode from 'qrcode'; export default class DashboardSettings extends DashboardView {