From 1d1de9430301c518d664038a358a70bbbf4b7aed Mon Sep 17 00:00:00 2001 From: Rishichandra Wawhal Date: Wed, 25 Sep 2019 21:16:28 +0530 Subject: [PATCH] better key persistence in console (#2686) * change login flow to handle admin secret persistence * handle headers init state * add tooltip for remember-me * remove log, make label clickable * fix a closure scope bug * handle login verification at route level * update Login.js * refactor * remove extra file * refactor * add id to tooltips * remove adminsecretlabel + update admin secret storage flow * fix heartIcon close handling * . * fix admin secret setting * fix urlPrefix * add admin secret header if not present * update jwt analyzer icon * persist if admin secret header has already been added * set cli console mode as constant * handle CLI admin secret errors * make separate logout page * fix typos * fix typos * fix typos * fix typos * fix cli error * fix login page path --- console/cypress/helpers/dataHelpers.js | 4 +- console/cypress/helpers/eventHelpers.js | 4 +- .../cypress/helpers/remoteSchemaHelpers.js | 4 +- console/src/Globals.js | 113 +++------ console/src/components/AppState.js | 7 +- console/src/components/Common/Common.scss | 4 + .../Common/WarningSymbol/WarningSymbol.js | 2 +- .../src/components/Common/utils/urlUtils.js | 8 + console/src/components/Error/ErrorBoundary.js | 2 +- console/src/components/Login/Actions.js | 67 ++++++ console/src/components/Login/Login.js | 220 ++++++++++++------ console/src/components/Login/Login.scss | 4 +- console/src/components/Main/Actions.js | 81 ------- console/src/components/Main/Main.js | 28 +-- console/src/components/Main/Tooltips.js | 9 +- .../Services/ApiExplorer/Actions.js | 41 ++-- .../Services/ApiExplorer/ApiExplorer.js | 17 +- .../ApiExplorer/ApiExplorerGenerator.js | 18 -- .../ApiExplorer/ApiRequest/ApiRequest.js | 81 +++++-- .../Services/ApiExplorer/ApiRequest/utils.js | 98 +++++++- .../Services/ApiExplorer/ApiRequestWrapper.js | 1 + .../GraphiQLWrapper/GraphiQLWrapper.js | 2 +- .../OneGraphExplorer/OneGraphExplorer.js | 5 +- .../components/Services/ApiExplorer/state.js | 1 + .../ReloadEnumValuesButton.js | 2 +- .../Services/Data/Common/getMigrateUrl.js | 4 +- .../components/Services/Data/DataActions.js | 13 +- .../Services/Data/DataPageContainer.js | 3 +- .../components/Services/Data/DataRouter.js | 1 - .../Data/Function/customFunctionReducer.js | 6 +- .../components/Services/Data/RawSQL/RawSQL.js | 5 +- .../Data/TableModify/ModifyActions.js | 3 +- .../EventTrigger/Common/getMigrateUrl.js | 4 +- .../Services/EventTrigger/EventActions.js | 10 +- .../AllowedQueries/AllowedQueries.scss | 1 - .../MetadataOptions/ClearAdminSecret.js | 69 ------ .../Services/RemoteSchema/Actions.js | 8 +- .../Services/RemoteSchema/Edit/View.js | 2 +- .../{Metadata => Settings}/Actions.js | 4 +- .../AllowedQueries/AddAllowedQuery.js | 0 .../AllowedQueries/AllowedQueries.js | 0 .../AllowedQueries/AllowedQueries.scss | 1 + .../AllowedQueries/AllowedQueriesList.js | 0 .../AllowedQueries/AllowedQueriesNotes.js | 0 .../AllowedQueries/utils.js | 0 .../{Metadata => Settings}/Container.js | 2 +- .../Settings/Logout/ClearAdminSecret.js | 56 +++++ .../Services/Settings/Logout/Logout.js | 46 ++++ .../MetadataOptions/ExportMetadata.js | 2 +- .../MetadataOptions/ImportMetadata.js | 2 +- .../MetadataOptions/MetadataOptions.js | 41 +--- .../MetadataOptions/ReloadMetadata.js | 2 +- .../MetadataOptions/ReloadRemoteSchema.js | 2 +- .../MetadataOptions/ResetMetadata.js | 0 .../MetadataStatus/MetadataStatus.js | 2 +- .../Metadata.scss => Settings/Settings.scss} | 0 .../{Metadata => Settings}/Sidebar.js | 24 +- .../Services/{Metadata => Settings}/State.js | 0 .../Services/{Metadata => Settings}/utils.js | 0 console/src/constants.js | 3 + console/src/helpers/localDev.js | 44 ++-- console/src/reducer.js | 2 +- console/src/routes.js | 51 ++-- console/src/utils/requestAction.js | 26 +-- console/src/utils/validateLogin.js | 113 +++------ 65 files changed, 731 insertions(+), 644 deletions(-) create mode 100644 console/src/components/Login/Actions.js delete mode 100644 console/src/components/Services/ApiExplorer/ApiExplorerGenerator.js delete mode 100644 console/src/components/Services/Metadata/AllowedQueries/AllowedQueries.scss delete mode 100644 console/src/components/Services/Metadata/MetadataOptions/ClearAdminSecret.js rename console/src/components/Services/{Metadata => Settings}/Actions.js (98%) rename console/src/components/Services/{Metadata => Settings}/AllowedQueries/AddAllowedQuery.js (100%) rename console/src/components/Services/{Metadata => Settings}/AllowedQueries/AllowedQueries.js (100%) create mode 100644 console/src/components/Services/Settings/AllowedQueries/AllowedQueries.scss rename console/src/components/Services/{Metadata => Settings}/AllowedQueries/AllowedQueriesList.js (100%) rename console/src/components/Services/{Metadata => Settings}/AllowedQueries/AllowedQueriesNotes.js (100%) rename console/src/components/Services/{Metadata => Settings}/AllowedQueries/utils.js (100%) rename console/src/components/Services/{Metadata => Settings}/Container.js (95%) create mode 100644 console/src/components/Services/Settings/Logout/ClearAdminSecret.js create mode 100644 console/src/components/Services/Settings/Logout/Logout.js rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/ExportMetadata.js (98%) rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/ImportMetadata.js (98%) rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/MetadataOptions.js (69%) rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/ReloadMetadata.js (96%) rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/ReloadRemoteSchema.js (96%) rename console/src/components/Services/{Metadata => Settings}/MetadataOptions/ResetMetadata.js (100%) rename console/src/components/Services/{Metadata => Settings}/MetadataStatus/MetadataStatus.js (99%) rename console/src/components/Services/{Metadata/Metadata.scss => Settings/Settings.scss} (100%) rename console/src/components/Services/{Metadata => Settings}/Sidebar.js (73%) rename console/src/components/Services/{Metadata => Settings}/State.js (100%) rename console/src/components/Services/{Metadata => Settings}/utils.js (100%) diff --git a/console/cypress/helpers/dataHelpers.js b/console/cypress/helpers/dataHelpers.js index a12e74c3de2..2bf5819b2ed 100644 --- a/console/cypress/helpers/dataHelpers.js +++ b/console/cypress/helpers/dataHelpers.js @@ -1,3 +1,5 @@ +import { ADMIN_SECRET_HEADER_KEY } from '../../src/constants'; + export const baseUrl = Cypress.config('baseUrl'); export const dataTypes = [ 'serial', @@ -41,7 +43,7 @@ export const makeDataAPIOptions = (dataApiUrl, key, body) => ({ method: 'POST', url: makeDataAPIUrl(dataApiUrl), headers: { - 'x-hasura-admin-secret': key, + [ADMIN_SECRET_HEADER_KEY]: key, }, body, failOnStatusCode: false, diff --git a/console/cypress/helpers/eventHelpers.js b/console/cypress/helpers/eventHelpers.js index 53c772a39e6..d76d8a6fdb2 100644 --- a/console/cypress/helpers/eventHelpers.js +++ b/console/cypress/helpers/eventHelpers.js @@ -1,3 +1,5 @@ +import { ADMIN_SECRET_HEADER_KEY } from '../../src/constants'; + export const baseUrl = Cypress.config('baseUrl'); export const queryTypes = ['insert', 'update', 'delete']; export const getTriggerName = (i, testName = '') => @@ -14,7 +16,7 @@ export const makeDataAPIOptions = (dataApiUrl, key, body) => ({ method: 'POST', url: makeDataAPIUrl(dataApiUrl), headers: { - 'x-hasura-admin-secret': key, + [ADMIN_SECRET_HEADER_KEY]: key, }, body, failOnStatusCode: false, diff --git a/console/cypress/helpers/remoteSchemaHelpers.js b/console/cypress/helpers/remoteSchemaHelpers.js index 072020c4999..5b6a86bcbab 100644 --- a/console/cypress/helpers/remoteSchemaHelpers.js +++ b/console/cypress/helpers/remoteSchemaHelpers.js @@ -1,3 +1,5 @@ +import { ADMIN_SECRET_HEADER_KEY } from '../../src/constants'; + export const baseUrl = Cypress.config('baseUrl'); export const getRemoteSchemaName = (i, schemaName) => `test-remote-schema-${schemaName}-${i}`; @@ -14,7 +16,7 @@ export const makeDataAPIOptions = (dataApiUrl, key, body) => ({ method: 'POST', url: makeDataAPIUrl(dataApiUrl), headers: { - 'x-hasura-admin-secret': key, + [ADMIN_SECRET_HEADER_KEY]: key, }, body, failOnStatusCode: false, diff --git a/console/src/Globals.js b/console/src/Globals.js index e7c01718cca..34406685066 100644 --- a/console/src/Globals.js +++ b/console/src/Globals.js @@ -1,71 +1,11 @@ import { SERVER_CONSOLE_MODE } from './constants'; +import { getFeaturesCompatibility } from './helpers/versionUtils'; +import { stripTrailingSlash } from './components/Common/utils/urlUtils'; -/* helper tools to format explain json */ +/* set helper tools into window */ -/* eslint-disable */ import sqlFormatter from './helpers/sql-formatter.min'; import hljs from './helpers/highlight.min'; -import { getFeaturesCompatibility } from './helpers/versionUtils'; -/* eslint-enable */ - -/* */ - -const checkExtraSlashes = url => { - if (!url) { - return url; - } - if (url[url.length - 1] === '/') { - return url.slice(0, url.length - 1); - } - return url; -}; - -const globals = { - apiHost: window.__env.apiHost, - apiPort: window.__env.apiPort, - dataApiUrl: checkExtraSlashes(window.__env.dataApiUrl), - devDataApiUrl: window.__env.devDataApiUrl, - nodeEnv: window.__env.nodeEnv, - adminSecret: window.__env.adminSecret || window.__env.accessKey, - isAdminSecretSet: - window.__env.isAdminSecretSet || window.__env.isAccessKeySet, - adminSecretLabel: - window.__env.isAdminSecretSet !== undefined || - window.__env.adminSecret !== undefined - ? 'admin-secret' - : 'access-key', - consoleMode: - window.__env.consoleMode === 'hasuradb' - ? 'server' - : window.__env.consoleMode, - urlPrefix: checkExtraSlashes(window.__env.urlPrefix), - enableTelemetry: window.__env.enableTelemetry, - telemetryTopic: - window.__env.nodeEnv !== 'development' ? 'console' : 'console_test', - assetsPath: window.__env.assetsPath, - serverVersion: window.__env.serverVersion, - consoleAssetVersion: CONSOLE_ASSET_VERSION, - featuresCompatibility: window.__env.serverVersion - ? getFeaturesCompatibility(window.__env.serverVersion) - : null, -}; - -// set defaults -if (!window.__env.urlPrefix) { - globals.urlPrefix = '/'; -} - -if (!window.__env.consoleMode) { - globals.consoleMode = SERVER_CONSOLE_MODE; -} - -if (!globals.adminSecret) { - globals.adminSecret = null; -} - -if (globals.isAdminSecretSet === undefined) { - globals.isAdminSecretSet = false; -} if ( window && @@ -77,31 +17,48 @@ if ( window.hljs = hljs; } +/* initialize globals */ + +const globals = { + apiHost: window.__env.apiHost, + apiPort: window.__env.apiPort, + dataApiUrl: stripTrailingSlash(window.__env.dataApiUrl), + devDataApiUrl: window.__env.devDataApiUrl, + nodeEnv: window.__env.nodeEnv, + adminSecret: window.__env.adminSecret || null, // will be updated after login/logout + isAdminSecretSet: window.__env.isAdminSecretSet || false, + consoleMode: window.__env.consoleMode || SERVER_CONSOLE_MODE, + urlPrefix: stripTrailingSlash(window.__env.urlPrefix) || '', + enableTelemetry: window.__env.enableTelemetry, + telemetryTopic: + window.__env.nodeEnv !== 'development' ? 'console' : 'console_test', + assetsPath: window.__env.assetsPath, + serverVersion: window.__env.serverVersion, + consoleAssetVersion: CONSOLE_ASSET_VERSION, // set during console build + featuresCompatibility: window.__env.serverVersion + ? getFeaturesCompatibility(window.__env.serverVersion) + : null, +}; + if (globals.consoleMode === SERVER_CONSOLE_MODE) { if (globals.nodeEnv !== 'development') { - if (window.__env.consolePath) { - const safeCurrentUrl = checkExtraSlashes(window.location.href); - globals.dataApiUrl = safeCurrentUrl.slice( + const consolePath = window.__env.consolePath; + if (consolePath) { + const currentUrl = stripTrailingSlash(window.location.href); + globals.dataApiUrl = currentUrl.slice( 0, - safeCurrentUrl.lastIndexOf(window.__env.consolePath) + currentUrl.lastIndexOf(consolePath) ); - const currentPath = checkExtraSlashes(window.location.pathname); + + const currentPath = stripTrailingSlash(window.location.pathname); globals.urlPrefix = - currentPath.slice( - 0, - currentPath.lastIndexOf(window.__env.consolePath) - ) + '/console'; + currentPath.slice(0, currentPath.lastIndexOf(consolePath)) + '/console'; } else { const windowUrl = window.location.protocol + '//' + window.location.host; + globals.dataApiUrl = windowUrl; } } - /* - * Require the exact usecase - if (globals.nodeEnv === 'development') { - globals.dataApiUrl = globals.devDataApiUrl; - } - */ } export default globals; diff --git a/console/src/components/AppState.js b/console/src/components/AppState.js index 7408d7f8003..ae8cf762c64 100644 --- a/console/src/components/AppState.js +++ b/console/src/components/AppState.js @@ -1,10 +1,7 @@ import globals from 'Globals'; const stateKey = 'CONSOLE_LOCAL_INFO:' + globals.dataApiUrl; -const CONSOLE_ADMIN_SECRET = - globals.adminSecretLabel === 'admin-secret' - ? 'CONSOLE_ADMIN_SECRET' - : 'CONSOLE_ACCESS_KEY'; +const CONSOLE_ADMIN_SECRET = 'CONSOLE_ADMIN_SECRET'; const loadAppState = () => JSON.parse(window.localStorage.getItem(stateKey)); @@ -21,6 +18,8 @@ const saveAdminSecretState = state => { const clearAdminSecretState = () => { window.localStorage.removeItem(CONSOLE_ADMIN_SECRET); + + globals.adminSecret = null; }; const clearState = () => window.localStorage.removeItem(stateKey); diff --git a/console/src/components/Common/Common.scss b/console/src/components/Common/Common.scss index ebf3e8006fd..850f65a4c51 100644 --- a/console/src/components/Common/Common.scss +++ b/console/src/components/Common/Common.scss @@ -431,6 +431,10 @@ input { margin: 0 !important; } +.remove_margin_top { + margin-top: 0 !important; +} + .display_inline { display: inline-block; } diff --git a/console/src/components/Common/WarningSymbol/WarningSymbol.js b/console/src/components/Common/WarningSymbol/WarningSymbol.js index c4a81dcab38..032c23c230b 100644 --- a/console/src/components/Common/WarningSymbol/WarningSymbol.js +++ b/console/src/components/Common/WarningSymbol/WarningSymbol.js @@ -9,7 +9,7 @@ const WarningSymbol = ({ tooltipPlacement = 'right', customStyle = null, }) => { - const tooltip = {tooltipText}; + const tooltip = {tooltipText}; return (
diff --git a/console/src/components/Common/utils/urlUtils.js b/console/src/components/Common/utils/urlUtils.js index 81dc545ace6..2c506b008a6 100644 --- a/console/src/components/Common/utils/urlUtils.js +++ b/console/src/components/Common/utils/urlUtils.js @@ -1,3 +1,11 @@ export const getPathRoot = path => { return path.split('/')[1]; }; + +export const stripTrailingSlash = url => { + if (url && url.endsWith('/')) { + return url.slice(0, -1); + } + + return url; +}; diff --git a/console/src/components/Error/ErrorBoundary.js b/console/src/components/Error/ErrorBoundary.js index 6ef3ca1156a..315d83e1f73 100644 --- a/console/src/components/Error/ErrorBoundary.js +++ b/console/src/components/Error/ErrorBoundary.js @@ -4,7 +4,7 @@ import { loadInconsistentObjects, redirectToMetadataStatus, isMetadataStatusPage, -} from '../Services/Metadata/Actions'; +} from '../Services/Settings/Actions'; import Spinner from '../Common/Spinner/Spinner'; import PageNotFound, { NotFoundError } from './PageNotFound'; diff --git a/console/src/components/Login/Actions.js b/console/src/components/Login/Actions.js new file mode 100644 index 00000000000..7d524e3c79e --- /dev/null +++ b/console/src/components/Login/Actions.js @@ -0,0 +1,67 @@ +import Endpoints, { globalCookiePolicy } from '../../Endpoints'; +import { UPDATE_DATA_HEADERS } from '../Services/Data/DataActions'; +import { saveAdminSecretState } from '../AppState'; +import { ADMIN_SECRET_HEADER_KEY, CLI_CONSOLE_MODE } from '../../constants'; +import requestAction from '../../utils/requestAction'; +import globals from '../../Globals'; + +export const verifyLogin = ({ + adminSecret, + shouldPersist, + successCallback, + errorCallback, + dispatch, +}) => { + const url = Endpoints.getSchema; + const requestOptions = { + credentials: globalCookiePolicy, + method: 'POST', + headers: { + [ADMIN_SECRET_HEADER_KEY]: adminSecret, + 'content-type': 'application/json', + }, + body: JSON.stringify({ + type: 'select', + args: { + table: { + name: 'hdb_table', + schema: 'hdb_catalog', + }, + columns: ['table_schema'], + where: { table_schema: 'public' }, + limit: 1, + }, + }), + }; + + dispatch(requestAction(url, requestOptions)).then( + () => { + if (adminSecret) { + if (globals.consoleMode !== CLI_CONSOLE_MODE) { + // set admin secret to local storage + if (shouldPersist) { + saveAdminSecretState(adminSecret); + } + + // set admin secret in globals + globals.adminSecret = adminSecret; + } + + // set data headers in redux + dispatch({ + type: UPDATE_DATA_HEADERS, + data: { + 'content-type': 'application/json', + [ADMIN_SECRET_HEADER_KEY]: adminSecret, + }, + }); + } + if (successCallback) { + successCallback(); + } + }, + error => { + errorCallback(error); + } + ); +}; diff --git a/console/src/components/Login/Login.js b/console/src/components/Login/Login.js index 530d439b5bf..47ef5e1bfbe 100644 --- a/console/src/components/Login/Login.js +++ b/console/src/components/Login/Login.js @@ -1,96 +1,162 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { useState } from 'react'; +import { push } from 'react-router-redux'; import Helmet from 'react-helmet'; import Button from '../Common/Button/Button'; import globals from '../../Globals'; -import { loginClicked, UPDATE_ADMIN_SECRET_INPUT } from '../Main/Actions'; +import { verifyLogin } from './Actions'; +import { CLI_CONSOLE_MODE } from '../../constants'; +import { getAdminSecret } from '../Services/ApiExplorer/ApiRequest/utils'; -class Login extends Component { - handleAdminSecret = e => { - this.props.dispatch({ - type: UPDATE_ADMIN_SECRET_INPUT, - data: e.target.value, - }); - }; +const styles = require('./Login.scss'); +const hasuraLogo = require('./blue-logo.svg'); - loginClicked = () => { - this.props.dispatch(loginClicked()); - }; +const Login = ({ dispatch }) => { + // request state + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - render() { - const { loginInProgress, loginError, dispatch } = this.props; + // should persist admin secret + const [shouldPersist, setShouldPersist] = useState(true); - const styles = require('./Login.scss'); - const hasuraLogo = require('./blue-logo.svg'); + // input handler + const [adminSecretInput, setAdminSecretInput] = useState(''); - let loginText = 'Enter'; - if (loginInProgress) { - loginText = ( - - Verifying... -