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
This commit is contained in:
Rishichandra Wawhal 2019-09-25 21:16:28 +05:30 committed by Rikin Kachhia
parent eb0c1f6642
commit 1d1de94303
65 changed files with 731 additions and 644 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

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

View File

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

View File

@ -431,6 +431,10 @@ input {
margin: 0 !important;
}
.remove_margin_top {
margin-top: 0 !important;
}
.display_inline {
display: inline-block;
}

View File

@ -9,7 +9,7 @@ const WarningSymbol = ({
tooltipPlacement = 'right',
customStyle = null,
}) => {
const tooltip = <Tooltip>{tooltipText}</Tooltip>;
const tooltip = <Tooltip id={tooltipText}>{tooltipText}</Tooltip>;
return (
<div className={styles.display_inline}>

View File

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

View File

@ -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';

View File

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

View File

@ -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 = (
<span>
Verifying...
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
</span>
);
} else if (loginError) {
loginText = 'Error. Try again?';
}
const getLoginForm = () => {
const getLoginButtonText = () => {
// login button text
let loginText = 'Enter';
if (loading) {
loginText = (
<span>
Verifying...
<i className="fa fa-spinner fa-spin" aria-hidden="true" />
</span>
);
} else if (error) {
loginText = 'Error. Try again?';
}
return loginText;
};
const toggleShouldPersist = () => setShouldPersist(!shouldPersist);
const onAdminSecretChange = e => setAdminSecretInput(e.target.value);
// form submit handler
const onSubmit = e => {
e.preventDefault();
const successCallback = () => {
setLoading(false);
setError(null);
dispatch(push(globals.urlPrefix));
};
const errorCallback = err => {
setAdminSecretInput('');
setLoading(false);
setError(err);
};
setLoading(true);
verifyLogin({
adminSecret: adminSecretInput,
shouldPersist,
successCallback,
errorCallback,
dispatch,
});
};
return (
<div className={styles.mainWrapper + ' container-fluid'}>
<div className={styles.container + ' container'} id="login">
<div className={styles.loginCenter}>
<Helmet title={'Login | ' + 'Hasura'} />
<div className={styles.hasuraLogo}>
<img src={hasuraLogo} />
</div>
<div className={styles.loginWrapper}>
<form
className="form-horizontal"
onSubmit={e => {
e.preventDefault();
dispatch(loginClicked());
}}
>
<div
className={styles.input_addon_group + ' ' + styles.padd_top}
>
<div className={'input-group ' + styles.input_group}>
<input
onChange={this.handleAdminSecret}
className={styles.form_input + ' form-control'}
type="password"
placeholder={`Enter ${globals.adminSecretLabel}`}
name="password"
/>
</div>
</div>
<div className={styles.signin_btn}>
<Button type="submit" color="green" className="form-control">
{loginText}
</Button>
</div>
</form>
</div>
<form className="form-horizontal" onSubmit={onSubmit}>
<div className={styles.input_addon_group + ' ' + styles.padd_top}>
<div className={'input-group ' + styles.input_group}>
<input
onChange={onAdminSecretChange}
className={styles.form_input + ' form-control'}
type="password"
placeholder={'Enter admin-secret'}
name="password"
/>
</div>
</div>
<div className={styles.login_btn}>
<Button
type="submit"
color="green"
className="form-control"
disabled={loading}
>
{getLoginButtonText()}
</Button>
</div>
<div className={styles.add_pad_left}>
<label className={`${styles.cursorPointer}`}>
<input
type="checkbox"
checked={shouldPersist}
onChange={toggleShouldPersist}
className={`${styles.add_mar_right_small} ${
styles.remove_margin_top
} ${styles.cursorPointer}`}
/>
Remember in this browser
</label>
</div>
</form>
);
};
const getCLIAdminSecretErrorMessage = () => {
const adminSecret = getAdminSecret();
const missingAdminSecretMessage = (
<span>
Seems like your Hasura GraphQL engine instance has an admin-secret
configured.
<br />
Run console with the admin-secret using:
<br />
<br />
hasura console --admin-secret=&lt;your-admin-secret&gt;
</span>
);
const invalidAdminSecretMessage = (
<span>Invalid admin-secret passed from CLI</span>
);
return (
<div className={styles.text_center}>
{adminSecret ? invalidAdminSecretMessage : missingAdminSecretMessage}
</div>
);
};
return (
<div className={styles.mainWrapper + ' container-fluid'}>
<div className={styles.container + ' container'} id="login">
<div className={styles.loginCenter}>
<Helmet title={'Login | ' + 'Hasura'} />
<div className={styles.hasuraLogo}>
<img src={hasuraLogo} />
</div>
<div className={styles.loginWrapper}>
{globals.consoleMode !== CLI_CONSOLE_MODE
? getLoginForm()
: getCLIAdminSecretErrorMessage()}
</div>
</div>
</div>
);
}
}
Login.propTypes = {
dispatch: PropTypes.func.isRequired,
</div>
);
};
const generatedLoginConnector = connect => {
const mapStateToProps = state => {
return {
loginInProgress: state.main.loginInProgress,
loginError: state.main.loginError,
adminSecretError: state.tables.adminSecretError,
};
};
return connect(mapStateToProps)(Login);
return connect()(Login);
};
export default generatedLoginConnector;

View File

@ -16,7 +16,7 @@
}
.loginWrapper {
width: 350px;
width: 400px;
background-color: #fff;
padding: 15px;
border-radius: 5px;
@ -57,7 +57,7 @@
}
}
.signin_btn {
.login_btn {
padding: 0 15px;
padding-bottom: 15px;
padding-top: 10px;

View File

@ -1,16 +1,8 @@
import { push } from 'react-router-redux';
import globals from 'Globals';
import defaultState from './State';
import requestAction from '../../utils/requestAction';
import requestActionPlain from '../../utils/requestActionPlain';
import Endpoints, { globalCookiePolicy } from '../../Endpoints';
import { saveAdminSecretState } from '../AppState';
import {
ADMIN_SECRET_ERROR,
UPDATE_DATA_HEADERS,
} from '../Services/Data/DataActions';
import { getFeaturesCompatibility } from '../../helpers/versionUtils';
import { changeRequestHeader } from '../Services/ApiExplorer/Actions';
const SET_MIGRATION_STATUS_SUCCESS = 'Main/SET_MIGRATION_STATUS_SUCCESS';
const SET_MIGRATION_STATUS_ERROR = 'Main/SET_MIGRATION_STATUS_ERROR';
@ -155,77 +147,6 @@ const loadLatestServerVersion = () => (dispatch, getState) => {
);
};
const validateLogin = isInitialLoad => (dispatch, getState) => {
const url = Endpoints.getSchema;
const currentSchema = getState().tables.currentSchema;
const options = {
credentials: globalCookiePolicy,
method: 'POST',
headers: getState().tables.dataHeaders,
body: JSON.stringify({
type: 'select',
args: {
table: {
name: 'hdb_table',
schema: 'hdb_catalog',
},
columns: ['table_schema'],
where: { table_schema: currentSchema },
limit: 1,
},
}),
};
if (isInitialLoad) {
return dispatch(requestAction(url, options));
}
return dispatch(requestAction(url, options)).then(
() => {
dispatch({ type: LOGIN_IN_PROGRESS, data: false });
dispatch({ type: LOGIN_ERROR, data: false });
dispatch(push(globals.urlPrefix));
},
error => {
dispatch({ type: LOGIN_IN_PROGRESS, data: false });
dispatch({ type: LOGIN_ERROR, data: true });
console.error(
`Failed to validate ${globals.adminSecretLabel} + JSON.stringify(error)`
);
if (error.code !== 'access-denied') {
alert(JSON.stringify(error));
}
}
);
};
const loginClicked = () => (dispatch, getState) => {
// set localstorage
dispatch({ type: LOGIN_IN_PROGRESS, data: true });
const adminSecretInput = getState().main.adminSecretInput;
saveAdminSecretState(adminSecretInput);
// redirect to / to test the adminSecretInput;
const updatedDataHeaders = {
'content-type': 'application/json',
[`x-hasura-${globals.adminSecretLabel}`]: adminSecretInput,
};
Promise.all([
dispatch({ type: ADMIN_SECRET_ERROR, data: false }),
dispatch({ type: UPDATE_DATA_HEADERS, data: updatedDataHeaders }),
dispatch(
changeRequestHeader(
1,
'key',
`x-hasura-${globals.adminSecretLabel}`,
true
)
),
dispatch(changeRequestHeader(1, 'value', adminSecretInput, true)),
// dispatch(push('/'))
]).then(() => {
// make a sample query. check error code and push to /
dispatch(validateLogin(false));
});
};
const updateMigrationModeStatus = () => (dispatch, getState) => {
// make req to hasura cli to update migration mode
dispatch({ type: UPDATE_MIGRATION_MODE_PROGRESS, data: true });
@ -381,10 +302,8 @@ export {
UPDATE_ADMIN_SECRET_INPUT,
loadMigrationStatus,
updateMigrationModeStatus,
loginClicked,
LOGIN_IN_PROGRESS,
LOGIN_ERROR,
validateLogin,
loadServerVersion,
fetchServerConfig,
loadLatestServerVersion,

View File

@ -23,7 +23,7 @@ import { loadConsoleTelemetryOpts } from '../../telemetry/Actions.js';
import {
loadInconsistentObjects,
redirectToMetadataStatus,
} from '../Services/Metadata/Actions';
} from '../Services/Settings/Actions';
import {
getLoveConsentState,
@ -111,14 +111,17 @@ class Main extends React.Component {
}
handleBodyClick(e) {
const heartDropDown = document.getElementById('dropdown_wrapper');
const heartDropDownOpen = document.querySelectorAll(
'#dropdown_wrapper.open'
);
if (
!document.getElementById('dropdown_wrapper').contains(e.target) &&
heartDropDown &&
!heartDropDown.contains(e.target) &&
heartDropDownOpen.length !== 0
) {
document.getElementById('dropdown_wrapper').classList.remove('open');
heartDropDown.classList.remove('open');
}
}
@ -193,17 +196,17 @@ class Main extends React.Component {
return mainContent;
};
const getMetadataSelectedMarker = () => {
const getSettingsSelectedMarker = () => {
let metadataSelectedMarker = null;
if (currentActiveBlock === 'metadata') {
if (currentActiveBlock === 'settings') {
metadataSelectedMarker = <span className={styles.selected} />;
}
return metadataSelectedMarker;
};
const getMetadataIcon = () => {
const getMetadataStatusIcon = () => {
if (metadata.inconsistentObjects.length === 0) {
return <i className={styles.question + ' fa fa-cog'} />;
}
@ -223,10 +226,7 @@ class Main extends React.Component {
const getAdminSecretSection = () => {
let adminSecretHtml = null;
if (
!globals.isAdminSecretSet &&
(globals.adminSecret === '' || globals.adminSecret === null)
) {
if (!(globals.isAdminSecretSet || globals.adminSecret)) {
adminSecretHtml = (
<div className={styles.secureSection}>
<a
@ -430,7 +430,7 @@ class Main extends React.Component {
path,
isDefault = false
) => {
const itemTooltip = <Tooltip>{tooltipText}</Tooltip>;
const itemTooltip = <Tooltip id={tooltipText}>{tooltipText}</Tooltip>;
const block = getPathRoot(path);
@ -504,10 +504,10 @@ class Main extends React.Component {
<div id="dropdown_wrapper" className={styles.clusterInfoWrapper}>
{getAdminSecretSection()}
<Link to="/metadata">
<Link to="/settings">
<div className={styles.helpSection + ' ' + styles.settingsIcon}>
{getMetadataIcon()}
{getMetadataSelectedMarker()}
{getMetadataStatusIcon()}
{getSettingsSelectedMarker()}
</div>
</Link>
<div className={styles.supportSection}>

View File

@ -1,5 +1,3 @@
import globals from '../../Globals';
export const data = 'Data & Schema management';
export const apiExplorer = 'Test the GraphQL APIs';
@ -8,8 +6,5 @@ export const events = 'Manage Event Triggers';
export const remoteSchema = 'Manage Remote Schemas';
export const roles = 'User Roles Summary';
export const secureEndpoint = `This GraphQL endpoint is public. You should add an ${
globals.adminSecretLabel
}`;
export const secureEndpoint =
'This GraphQL endpoint is public. You should add an admin-secret';

View File

@ -11,6 +11,7 @@ import { execute } from 'apollo-link';
import { getHeadersAsJSON } from './utils';
import { saveAppState, clearState } from '../../AppState.js';
import { ADMIN_SECRET_HEADER_KEY } from '../../../constants';
const CHANGE_TAB = 'ApiExplorer/CHANGE_TAB';
const CHANGE_API_SELECTION = 'ApiExplorer/CHANGE_API_SELECTION';
@ -28,7 +29,7 @@ const REQUEST_PARAMS_CHANGED = 'ApiExplorer/REQUEST_PARAMS_CHANGED';
const REQUEST_HEADER_CHANGED = 'ApiExplorer/REQUEST_HEADER_CHANGED';
const REQUEST_HEADER_ADDED = 'ApiExplorer/REQUEST_HEADER_ADDED';
const REQUEST_HEADER_REMOVED = 'ApiExplorer/REQUEST_HEADER_REMOVED';
const SET_INITIAL_HEADER_DATA = 'ApiExplorer/SET_INITIAL_HEADER_DATA';
const SET_REQUEST_HEADERS_BULK = 'ApiExplorer/SET_REQUEST_HEADERS_BULK';
const MAKING_API_REQUEST = 'ApiExplorer/MAKING_API_REQUEST';
const RESET_MAKING_REQUEST = 'ApiExplorer/RESET_MAKING_REQUEST';
@ -217,8 +218,7 @@ const analyzeFetcher = (url, headers) => {
const lHead = t.toLowerCase();
if (
lHead.slice(0, 'x-hasura-'.length) === 'x-hasura-' &&
lHead !== 'x-hasura-access-key' &&
lHead !== 'x-hasura-admin-secret'
lHead !== ADMIN_SECRET_HEADER_KEY
) {
user[lHead] = reqHeaders[t];
delete reqHeaders[t];
@ -237,13 +237,6 @@ const analyzeFetcher = (url, headers) => {
};
/* End of it */
const setInitialHeaderState = headerObj => {
return {
type: SET_INITIAL_HEADER_DATA,
data: headerObj,
};
};
const changeRequestHeader = (index, key, newValue, isDisabled) => {
return (dispatch, getState) => {
const currentState = getState().apiexplorer;
@ -269,6 +262,8 @@ const changeRequestHeader = (index, key, newValue, isDisabled) => {
};
};
const setHeadersBulk = headers => ({ type: SET_REQUEST_HEADERS_BULK, headers });
const removeRequestHeader = index => {
return (dispatch, getState) => {
const currentState = getState().apiexplorer;
@ -513,18 +508,6 @@ const apiExplorerReducer = (state = defaultState, action) => {
},
},
};
case SET_INITIAL_HEADER_DATA:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: [...action.data],
},
},
};
case REQUEST_HEADER_ADDED:
return {
...state,
@ -621,6 +604,18 @@ const apiExplorerReducer = (state = defaultState, action) => {
...state,
headerFocus: true,
};
case SET_REQUEST_HEADERS_BULK:
return {
...state,
displayedApi: {
...state.displayedApi,
request: {
...state.displayedApi.request,
headers: action.headers,
headersInitialised: true,
},
},
};
default:
return state;
}
@ -651,6 +646,6 @@ export {
unfocusTypingHeader,
getRemoteQueries,
analyzeFetcher,
setInitialHeaderState,
verifyJWTToken,
setHeadersBulk,
};

View File

@ -107,4 +107,19 @@ ApiExplorer.propTypes = {
location: PropTypes.object.isRequired,
};
export default ApiExplorer;
const generatedApiExplorer = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
serverVersion: state.main.serverVersion ? state.main.serverVersion : '',
credentials: {},
dataApiExplorerData: { ...state.dataApiExplorer },
dataHeaders: state.tables.dataHeaders,
tables: state.tables.allSchemas,
serverConfig: state.main.serverConfig ? state.main.serverConfig.data : {},
};
};
return connect(mapStateToProps)(ApiExplorer);
};
export default generatedApiExplorer;

View File

@ -1,18 +0,0 @@
import ApiExplorer from './ApiExplorer';
const generatedApiExplorer = connect => {
const mapStateToProps = state => {
return {
...state.apiexplorer,
serverVersion: state.main.serverVersion ? state.main.serverVersion : '',
credentials: {},
dataApiExplorerData: { ...state.dataApiExplorer },
dataHeaders: state.tables.dataHeaders,
tables: state.tables.allSchemas,
serverConfig: state.main.serverConfig ? state.main.serverConfig.data : {},
};
};
return connect(mapStateToProps)(ApiExplorer);
};
export default generatedApiExplorer;

View File

@ -8,19 +8,15 @@ import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import Modal from '../../../Common/Modal/Modal';
import { parseAuthHeader } from './utils';
import {
changeRequestHeader,
removeRequestHeader,
focusHeaderTextbox,
unfocusTypingHeader,
setInitialHeaderState,
verifyJWTToken,
setHeadersBulk,
} from '../Actions';
import globals from '../../../../Globals';
import GraphiQLWrapper from '../GraphiQLWrapper/GraphiQLWrapper';
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
@ -30,11 +26,17 @@ import {
setEndPointSectionIsOpen,
getHeadersSectionIsOpen,
setHeadersSectionIsOpen,
getGraphiQLHeadersFromLocalStorage,
setGraphiQLHeadersInLocalStorage,
persistGraphiQLHeaders,
getPersistedGraphiQLHeaders,
parseAuthHeader,
getDefaultGraphiqlHeaders,
getAdminSecret,
getPersistedAdminSecretHeaderWasAdded,
persistAdminSecretHeaderWasAdded,
} from './utils';
import styles from '../ApiExplorer.scss';
import { ADMIN_SECRET_HEADER_KEY } from '../../../../constants';
const inspectJWTTooltip = (
<Tooltip id="tooltip-inspect-jwt">Decode JWT</Tooltip>
@ -78,19 +80,47 @@ class ApiRequest extends Component {
}
componentDidMount() {
const { headers } = this.props;
const HEADER_FROM_LS = getGraphiQLHeadersFromLocalStorage();
if (HEADER_FROM_LS) {
try {
const initialHeader = JSON.parse(HEADER_FROM_LS);
this.props.dispatch(setInitialHeaderState(initialHeader));
} catch (e) {
console.error(e);
setGraphiQLHeadersInLocalStorage(JSON.stringify(headers));
const handleHeaderInit = () => {
const graphiqlHeaders =
getPersistedGraphiQLHeaders() || getDefaultGraphiqlHeaders();
// if admin secret is set and admin secret header was ever added to headers, add admin secret header if not already present
const adminSecret = getAdminSecret();
const adminSecretHeaderWasAdded = getPersistedAdminSecretHeaderWasAdded();
if (adminSecret && !adminSecretHeaderWasAdded) {
const headerKeys = graphiqlHeaders.map(h => h.key);
if (!headerKeys.includes(ADMIN_SECRET_HEADER_KEY)) {
graphiqlHeaders.push({
key: ADMIN_SECRET_HEADER_KEY,
value: adminSecret,
isActive: true,
isNewHeader: false,
isDisabled: true,
});
}
// set in local storage that admin secret header has been automatically added
persistAdminSecretHeaderWasAdded();
}
} else {
setGraphiQLHeadersInLocalStorage(JSON.stringify(headers));
}
// add an empty placeholder header
graphiqlHeaders.push({
key: '',
value: '',
isActive: true,
isNewHeader: true,
isDisabled: false,
});
// persist headers to local storage
persistGraphiQLHeaders(graphiqlHeaders);
// set headers in redux
this.props.dispatch(setHeadersBulk(graphiqlHeaders));
};
handleHeaderInit();
}
onAnalyzeBearerClose() {
@ -242,7 +272,7 @@ class ApiRequest extends Component {
const index = parseInt(e.target.getAttribute('data-header-id'), 10);
this.props
.dispatch(removeRequestHeader(index))
.then(r => setGraphiQLHeadersInLocalStorage(JSON.stringify(r)));
.then(r => persistGraphiQLHeaders(r));
};
const onHeaderValueChanged = e => {
@ -251,7 +281,7 @@ class ApiRequest extends Component {
const newValue = e.target.value;
this.props
.dispatch(changeRequestHeader(index, key, newValue, false))
.then(r => setGraphiQLHeadersInLocalStorage(JSON.stringify(r)));
.then(r => persistGraphiQLHeaders(r));
};
const onShowAdminSecretClicked = () => {
@ -260,7 +290,7 @@ class ApiRequest extends Component {
return headers.map((header, i) => {
const isAdminSecret =
header.key.toLowerCase() === `x-hasura-${globals.adminSecretLabel}`;
header.key.toLowerCase() === ADMIN_SECRET_HEADER_KEY;
const getHeaderActiveCheckBox = () => {
let headerActiveCheckbox = null;
@ -401,7 +431,7 @@ class ApiRequest extends Component {
} else {
analyzeIcon = (
<i
className={styles.showInspector + ' fa fa-plus-square-o'}
className={styles.showInspector + ' fa fa-user-secret'}
token={token}
data-header-index={i}
onClick={this.analyzeBearerToken}
@ -613,8 +643,8 @@ class ApiRequest extends Component {
claimData =
claimFormat === 'stringified_json'
? generateValidNameSpaceData(
JSON.parse(payload[claimNameSpace])
)
JSON.parse(payload[claimNameSpace])
)
: generateValidNameSpaceData(payload[claimNameSpace]);
} catch (e) {
console.error(e);
@ -712,6 +742,7 @@ ApiRequest.propTypes = {
method: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
headers: PropTypes.array,
dataHeaders: PropTypes.array,
params: PropTypes.string,
dispatch: PropTypes.func.isRequired,
explorerData: PropTypes.object.isRequired,

View File

@ -1,3 +1,10 @@
import globals from '../../../../Globals';
import { loadAdminSecretState } from '../../../AppState';
import {
ADMIN_SECRET_HEADER_KEY,
SERVER_CONSOLE_MODE,
} from '../../../../constants';
export const setEndPointSectionIsOpen = isOpen => {
window.localStorage.setItem('ApiExplorer:EndpointSectionIsOpen', isOpen);
};
@ -26,12 +33,95 @@ export const getHeadersSectionIsOpen = () => {
return isOpen ? isOpen === 'true' : defaultIsOpen;
};
export const setGraphiQLHeadersInLocalStorage = headers => {
window.localStorage.setItem('HASURA_CONSOLE_GRAPHIQL_HEADERS', headers);
export const getAdminSecret = () => {
let adminSecret = null;
if (globals.consoleMode === SERVER_CONSOLE_MODE && globals.isAdminSecretSet) {
const adminSecretFromLS = loadAdminSecretState();
const adminSecretInGlobals = globals.adminSecret;
adminSecret = adminSecretFromLS || adminSecretInGlobals;
} else {
adminSecret = globals.adminSecret;
}
return adminSecret;
};
export const getGraphiQLHeadersFromLocalStorage = () => {
return window.localStorage.getItem('HASURA_CONSOLE_GRAPHIQL_HEADERS');
export const persistAdminSecretHeaderWasAdded = () => {
window.localStorage.setItem('ApiExplorer:AdminSecretHeaderWasAdded', true);
};
export const getPersistedAdminSecretHeaderWasAdded = () => {
const defaultIsSet = false;
const isSet = window.localStorage.getItem(
'ApiExplorer:AdminSecretHeaderWasAdded'
);
return isSet ? isSet === 'true' : defaultIsSet;
};
export const persistGraphiQLHeaders = headers => {
// filter empty headers
const validHeaders = headers.filter(h => h.key);
// remove admin-secret value
const maskedHeaders = validHeaders.map(h => {
const maskedHeader = { ...h };
if (h.key.toLowerCase() === ADMIN_SECRET_HEADER_KEY) {
maskedHeader.value = 'xxx';
}
return maskedHeader;
});
window.localStorage.setItem(
'HASURA_CONSOLE_GRAPHIQL_HEADERS',
JSON.stringify(maskedHeaders)
);
};
export const getPersistedGraphiQLHeaders = () => {
const headersString = window.localStorage.getItem(
'HASURA_CONSOLE_GRAPHIQL_HEADERS'
);
let headers = null;
if (headersString) {
try {
headers = JSON.parse(headersString);
// add admin-secret value
headers = headers.map(h => {
const unmaskedHeader = { ...h };
if (h.key.toLowerCase() === ADMIN_SECRET_HEADER_KEY) {
unmaskedHeader.value = getAdminSecret();
}
return unmaskedHeader;
});
} catch (_) {
console.error('Failed parsing headers from local storage');
}
}
return headers;
};
export const getDefaultGraphiqlHeaders = () => {
const headers = [];
headers.push({
key: 'content-type',
value: 'application/json',
isActive: true,
isNewHeader: false,
isDisabled: false,
});
return headers;
};
export const parseAuthHeader = header => {

View File

@ -61,6 +61,7 @@ class ApiRequestWrapper extends Component {
method={this.props.request.method}
url={this.props.request.url}
headers={this.props.request.headers}
headersInitialised={this.props.request.headersInitialised}
params={this.props.request.params}
explorerData={this.props.explorerData}
dispatch={this.props.dispatch}

View File

@ -33,7 +33,6 @@ class GraphiQLWrapper extends Component {
const { numberOfTables, urlParams, headerFocus } = this.props;
const graphqlNetworkData = this.props.data;
const graphQLFetcher = graphQLParams => {
if (headerFocus) {
return null;
@ -78,6 +77,7 @@ class GraphiQLWrapper extends Component {
renderGraphiql={renderGraphiql}
endpoint={graphqlNetworkData.url}
headers={graphqlNetworkData.headers}
headersInitialised={graphqlNetworkData.headersInitialised}
headerFocus={headerFocus}
urlParams={urlParams}
numberOfTables={numberOfTables}

View File

@ -78,7 +78,10 @@ class OneGraphExplorer extends React.Component {
}
introspect() {
const { endpoint } = this.props;
const { endpoint, headersInitialised } = this.props;
if (!headersInitialised) {
return;
}
const headers = JSON.parse(JSON.stringify(this.props.headers));
this.setState({ loading: true });
fetch(endpoint, {

View File

@ -34,6 +34,7 @@ dataApisContent.push({
method: 'POST',
url: getUrl('/v1/graphql'),
headers: defaultHeader,
headersInitialised: false,
bodyType: 'graphql',
params: JSON.stringify({}, null, 4),
},

View File

@ -1,5 +1,5 @@
import React from 'react';
import ReloadEnumMetadata from '../../../Metadata/MetadataOptions/ReloadMetadata';
import ReloadEnumMetadata from '../../../Settings/MetadataOptions/ReloadMetadata';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Tooltip from 'react-bootstrap/lib/Tooltip';
import styles from '../../../../Common/Common.scss';

View File

@ -1,10 +1,10 @@
import Endpoints from '../../../../Endpoints';
import globals from '../../../../Globals';
import { SERVER_CONSOLE_MODE } from '../../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants';
const returnMigrateUrl = mode => {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
return mode ? Endpoints.hasuractlMigrate : Endpoints.hasuractlMetadata;
} else if (globals.consoleMode === SERVER_CONSOLE_MODE) {
return Endpoints.query;

View File

@ -16,8 +16,8 @@ import {
import dataHeaders from './Common/Headers';
import { loadMigrationStatus } from '../../Main/Actions';
import returnMigrateUrl from './Common/getMigrateUrl';
import { loadInconsistentObjects } from '../Metadata/Actions';
import { filterInconsistentMetadataObjects } from '../Metadata/utils';
import { loadInconsistentObjects } from '../Settings/Actions';
import { filterInconsistentMetadataObjects } from '../Settings/utils';
import globals from '../../../Globals';
import {
@ -32,7 +32,7 @@ import { fetchColumnTypesQuery, fetchColumnDefaultFunctions } from './utils';
import { fetchColumnCastsQuery, convertArrayToJson } from './TableModify/utils';
import { SERVER_CONSOLE_MODE } from '../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
const SET_TABLE = 'Data/SET_TABLE';
const LOAD_FUNCTIONS = 'Data/LOAD_FUNCTIONS';
@ -43,11 +43,12 @@ const LOAD_UNTRACKED_RELATIONS = 'Data/LOAD_UNTRACKED_RELATIONS';
const FETCH_SCHEMA_LIST = 'Data/FETCH_SCHEMA_LIST';
const UPDATE_CURRENT_SCHEMA = 'Data/UPDATE_CURRENT_SCHEMA';
const ADMIN_SECRET_ERROR = 'Data/ADMIN_SECRET_ERROR';
const UPDATE_DATA_HEADERS = 'Data/UPDATE_DATA_HEADERS';
const UPDATE_REMOTE_SCHEMA_MANUAL_REL = 'Data/UPDATE_SCHEMA_MANUAL_REL';
const SET_CONSISTENT_SCHEMA = 'Data/SET_CONSISTENT_SCHEMA';
const SET_CONSISTENT_FUNCTIONS = 'Data/SET_CONSISTENT_FUNCTIONS';
const UPDATE_DATA_HEADERS = 'Data/UPDATE_DATA_HEADERS';
const FETCH_COLUMN_TYPE_INFO = 'Data/FETCH_COLUMN_TYPE_INFO';
const FETCH_COLUMN_TYPE_INFO_FAIL = 'Data/FETCH_COLUMN_TYPE_INFO_FAIL';
const RESET_COLUMN_TYPE_INFO = 'Data/RESET_COLUMN_TYPE_INFO';
@ -491,7 +492,7 @@ const makeMigrationCall = (
let finalReqBody;
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
finalReqBody = upQuery;
} else if (globals.consoleMode === 'cli') {
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
finalReqBody = migrationBody;
}
const url = migrateUrl;
@ -504,7 +505,7 @@ const makeMigrationCall = (
const onSuccess = data => {
if (!shouldSkipSchemaReload) {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
dispatch(loadMigrationStatus()); // don't call for server mode
}
dispatch(updateSchemaInfo());

View File

@ -8,6 +8,7 @@ import DataSubSidebar from './DataSubSidebar';
import { updateCurrentSchema } from './DataActions';
import { NotFoundError } from '../../Error/PageNotFound';
import { CLI_CONSOLE_MODE } from '../../../constants';
const sectionPrefix = '/data';
@ -30,7 +31,7 @@ const DataPageContainer = ({
const currentLocation = location.pathname;
let migrationTab = null;
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
migrationTab = (
<li
role="presentation"

View File

@ -34,7 +34,6 @@ import {
fetchFunctionInit,
UPDATE_CURRENT_SCHEMA,
updateSchemaInfo,
// UPDATE_DATA_HEADERS,
// ADMIN_SECRET_ERROR,
} from './DataActions';

View File

@ -10,7 +10,7 @@ import dataHeaders from '../Common/Headers';
import globals from '../../../../Globals';
import returnMigrateUrl from '../Common/getMigrateUrl';
import { SERVER_CONSOLE_MODE } from '../../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants';
import { loadMigrationStatus } from '../../../Main/Actions';
import { handleMigrationErrors } from '../../EventTrigger/EventActions';
@ -73,7 +73,7 @@ const makeRequest = (
let finalReqBody;
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
finalReqBody = upQuery;
} else if (globals.consoleMode === 'cli') {
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
finalReqBody = migrationBody;
}
const url = migrateUrl;
@ -85,7 +85,7 @@ const makeRequest = (
};
const onSuccess = data => {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
dispatch(loadMigrationStatus()); // don't call for server mode
}
if (successMsg) {

View File

@ -19,6 +19,7 @@ import {
import { modalOpen, modalClose } from './Actions';
import globals from '../../../../Globals';
import './AceEditorFix.css';
import { CLI_CONSOLE_MODE } from '../../../../constants';
const RawSQL = ({
sql,
@ -81,7 +82,7 @@ const RawSQL = ({
if (isMigration && migrationName.length === 0) {
migrationName = 'run_sql_migration';
}
if (!isMigration && globals.consoleMode === 'cli') {
if (!isMigration && globals.consoleMode === CLI_CONSOLE_MODE) {
// if migration is not checked, check if is schema modification
if (isSchemaModification(sql)) {
dispatch(modalOpen());
@ -435,7 +436,7 @@ const RawSQL = ({
return migrationNameSection;
};
if (migrationMode && globals.consoleMode === 'cli') {
if (migrationMode && globals.consoleMode === CLI_CONSOLE_MODE) {
migrationSection = (
<div className={styles.add_mar_top_small}>
{getIsMigrationSection()}

View File

@ -36,6 +36,7 @@ import {
getCreatePkSql,
getDropPkSql,
} from './utils';
import { CLI_CONSOLE_MODE } from '../../../../constants';
const DELETE_PK_WARNING =
'Without a primary key there is no way to uniquely identify a row of a table';
@ -1047,7 +1048,7 @@ const deleteColumnSql = (column, tableSchema) => {
const errorMsg = 'Deleting column failed';
const customOnSuccess = (data, consoleMode, migrationMode) => {
if (consoleMode === 'cli' && migrationMode) {
if (consoleMode === CLI_CONSOLE_MODE && migrationMode) {
// show warning information
dispatch(
showWarningNotification(

View File

@ -1,10 +1,10 @@
import Endpoints from '../../../../Endpoints';
import globals from '../../../../Globals';
import { SERVER_CONSOLE_MODE } from '../../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../../constants';
const returnMigrateUrl = mode => {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
return mode ? Endpoints.hasuractlMigrate : Endpoints.hasuractlMetadata;
} else if (globals.consoleMode === SERVER_CONSOLE_MODE) {
return Endpoints.query;

View File

@ -14,12 +14,12 @@ import { loadMigrationStatus } from '../../Main/Actions';
import returnMigrateUrl from './Common/getMigrateUrl';
import globals from '../../../Globals';
import push from './push';
import { loadInconsistentObjects } from '../Metadata/Actions';
import { filterInconsistentMetadataObjects } from '../Metadata/utils';
import { loadInconsistentObjects } from '../Settings/Actions';
import { filterInconsistentMetadataObjects } from '../Settings/utils';
import { replace } from 'react-router-redux';
import { getEventTriggersQuery } from './utils';
import { SERVER_CONSOLE_MODE } from '../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
import { REQUEST_COMPLETE, REQUEST_ONGOING } from './Modify/Actions';
const SET_TRIGGER = 'Event/SET_TRIGGER';
@ -372,7 +372,7 @@ const makeMigrationCall = (
let finalReqBody;
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
finalReqBody = upQuery;
} else if (globals.consoleMode === 'cli') {
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
finalReqBody = migrationBody;
}
const url = migrateUrl;
@ -384,7 +384,7 @@ const makeMigrationCall = (
};
const onSuccess = () => {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
dispatch(loadMigrationStatus()); // don't call for server mode
}
customOnSuccess();

View File

@ -1 +0,0 @@
@import "../Metadata.scss";

View File

@ -1,69 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { clearAdminSecretState, CONSOLE_ADMIN_SECRET } from '../../../AppState';
import globals from '../../../../Globals';
import {
showSuccessNotification,
showErrorNotification,
} from '../../Common/Notification';
import Button from '../../../Common/Button/Button';
class ClearAdminSecret extends Component {
constructor() {
super();
this.state = {};
this.state.isClearing = false;
}
render() {
const metaDataStyles = require('../Metadata.scss');
return (
<div className={metaDataStyles.display_inline}>
<Button
data-test="data-clear-access-key"
className={metaDataStyles.margin_right}
color="white"
size="sm"
onClick={e => {
e.preventDefault();
this.setState({ isClearing: true });
if (globals.isAdminSecretSet || globals.adminSecret) {
clearAdminSecretState();
this.props.dispatch(
showSuccessNotification(`Cleared ${globals.adminSecretLabel}`)
);
this.setState({ isClearing: false });
this.props.router.push('/login');
} else {
this.setState({ isClearing: false });
const errorMessage = (
<div style={{ padding: '5px' }}>
<div style={{ fontSize: '13px' }}>
No {globals.adminSecretLabel} set
</div>
<br />
<div style={{ fontSize: '13px' }}>
Please look for <code>{CONSOLE_ADMIN_SECRET}</code> key
under window storage and delete it if it exists
</div>
</div>
);
this.props.dispatch(showErrorNotification(errorMessage));
}
}}
>
{this.state.isClearing
? 'Clearing...'
: `Clear ${globals.adminSecretLabel} (logout)`}
</Button>
</div>
);
}
}
ClearAdminSecret.propTypes = {
dispatch: PropTypes.func.isRequired,
dataHeaders: PropTypes.object.isRequired,
};
export default ClearAdminSecret;

View File

@ -7,12 +7,12 @@ import requestAction from '../../../utils/requestAction';
import dataHeaders from '../Data/Common/Headers';
import globals from '../../../Globals';
import returnMigrateUrl from '../Data/Common/getMigrateUrl';
import { SERVER_CONSOLE_MODE } from '../../../constants';
import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
import { loadMigrationStatus } from '../../Main/Actions';
import { handleMigrationErrors } from '../EventTrigger/EventActions';
import { showSuccessNotification } from '../Common/Notification';
import { filterInconsistentMetadataObjects } from '../Metadata/utils';
import { filterInconsistentMetadataObjects } from '../Settings/utils';
/* Action constants */
@ -167,7 +167,7 @@ const makeRequest = (
let finalReqBody;
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
finalReqBody = upQuery;
} else if (globals.consoleMode === 'cli') {
} else if (globals.consoleMode === CLI_CONSOLE_MODE) {
finalReqBody = migrationBody;
}
const url = migrateUrl;
@ -179,7 +179,7 @@ const makeRequest = (
};
const onSuccess = data => {
if (globals.consoleMode === 'cli') {
if (globals.consoleMode === CLI_CONSOLE_MODE) {
dispatch(loadMigrationStatus()); // don't call for server mode
}
// dispatch(loadTriggers());

View File

@ -13,7 +13,7 @@ import {
} from '../Add/addRemoteSchemaReducer';
import { VIEW_REMOTE_SCHEMA } from '../Actions';
import ReloadRemoteSchema from '../../Metadata/MetadataOptions/ReloadRemoteSchema';
import ReloadRemoteSchema from '../../Settings/MetadataOptions/ReloadRemoteSchema';
import { appPrefix } from '../constants';

View File

@ -222,13 +222,13 @@ export const dropInconsistentObjects = () => {
};
export const isMetadataStatusPage = () => {
return window.location.pathname.includes('/metadata/status');
return window.location.pathname.includes('/setting/metadata-status');
};
export const redirectToMetadataStatus = () => {
return dispatch => {
return dispatch(
push(globals.urlPrefix + '/metadata/status?is_redirected=true')
push(globals.urlPrefix + '/settings/metadata-status?is_redirected=true')
);
};
};

View File

@ -0,0 +1 @@
@import "../Settings.scss";

View File

@ -3,7 +3,7 @@ import Sidebar from './Sidebar';
import PageContainer from '../../Common/Layout/PageContainer/PageContainer';
const Container = ({ location, children, metadata }) => {
const helmet = 'Metadata | Hasura';
const helmet = 'Settings | Hasura';
const sidebar = <Sidebar location={location} metadata={metadata} />;

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { clearAdminSecretState } from '../../../AppState';
import { showSuccessNotification } from '../../Common/Notification';
import Button from '../../../Common/Button/Button';
class ClearAdminSecret extends Component {
constructor() {
super();
this.state = {
isClearing: false,
};
}
render() {
const metaDataStyles = require('../Settings.scss');
const { dispatch } = this.props;
const { isClearing } = this.state;
return (
<div className={metaDataStyles.display_inline}>
<Button
data-test="data-clear-access-key"
className={metaDataStyles.margin_right}
color="white"
size="sm"
onClick={e => {
e.preventDefault();
this.setState({ isClearing: true });
clearAdminSecretState();
dispatch(showSuccessNotification('Cleared admin-secret'));
this.setState({ isClearing: false });
this.props.router.push('/login');
}}
>
{isClearing ? 'Clearing...' : 'Logout (clear admin-secret)'}
</Button>
</div>
);
}
}
ClearAdminSecret.propTypes = {
dispatch: PropTypes.func.isRequired,
dataHeaders: PropTypes.object.isRequired,
};
export default ClearAdminSecret;

View File

@ -0,0 +1,46 @@
import React from 'react';
import ClearAdminSecret from './ClearAdminSecret';
const Logout = props => {
const styles = require('../Settings.scss');
return (
<div
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top} ${
styles.metadata_wrapper
} container-fluid`}
>
<div className={styles.subHeader}>
<h2 className={`${styles.heading_text} ${styles.remove_pad_bottom}`}>
Logout (clear admin-secret)
</h2>
</div>
<div>
<div key="access_key_reset_1" className={styles.intro_note}>
<div className={styles.content_width}>
The console caches the admin-secret (HASURA_GRAPHQL_ADMIN_SECRET) in
the browser. You can clear this cache to force a prompt for the
admin-secret when the console is accessed next using this browser.
</div>
</div>
<div key="access_key_reset_2">
<ClearAdminSecret {...props} />
</div>
</div>
</div>
);
};
const mapStateToProps = state => {
return {
...state.main,
metadata: state.metadata,
dataHeaders: { ...state.tables.dataHeaders },
};
};
const logoutConnector = connect => connect(mapStateToProps)(Logout);
export default logoutConnector;

View File

@ -15,7 +15,7 @@ class ExportMetadata extends Component {
this.state.isExporting = false;
}
render() {
const metaDataStyles = require('../Metadata.scss');
const metaDataStyles = require('../Settings.scss');
return (
<div className={metaDataStyles.display_inline}>
<Button

View File

@ -83,7 +83,7 @@ class ImportMetadata extends Component {
});
}
render() {
const metaDataStyles = require('../Metadata.scss');
const metaDataStyles = require('../Settings.scss');
return (
<div className={metaDataStyles.display_inline}>
<Button

View File

@ -1,20 +1,17 @@
import globals from '../../../../Globals';
import React from 'react';
import ExportMetadata from './ExportMetadata';
import ImportMetadata from './ImportMetadata';
import ReloadMetadata from './ReloadMetadata';
import ResetMetadata from './ResetMetadata';
import ClearAdminSecret from './ClearAdminSecret';
import { CONSOLE_ADMIN_SECRET } from '../../../AppState';
const MetadataOptions = props => {
const styles = require('../Metadata.scss');
const styles = require('../Settings.scss');
const getMetadataImportExportSection = () => {
return (
<div>
<div className={styles.intro_note}>
<h4>Import/Export</h4>
<h4>Import/Export metadata</h4>
<div className={styles.content_width}>
Get Hasura metadata as JSON.
</div>
@ -47,7 +44,7 @@ const MetadataOptions = props => {
</div>
<div key="meta_data_3" className={styles.intro_note}>
<h4>Reset Metadata</h4>
<h4>Reset metadata</h4>
<div className={styles.content_width}>
Permanently clear GraphQL Engine's metadata and configure it from
scratch (tracking relevant tables and relationships). This process
@ -62,36 +59,6 @@ const MetadataOptions = props => {
);
};
const getClearSecretSection = () => {
let clearSecretSection = null;
if (window.localStorage[CONSOLE_ADMIN_SECRET]) {
clearSecretSection = (
<div>
<div key="access_key_reset_1" className={styles.intro_note}>
<h4>Clear {globals.adminSecretLabel} (logout)</h4>
<div className={styles.content_width}>
The console caches the {globals.adminSecretLabel} (
{globals.adminSecretLabel === 'access-key'
? 'HASURA_GRAPHQL_ACCESS_KEY'
: 'HASURA_GRAPHQL_ADMIN_SECRET'}
) in the browser. You can clear this cache to force a prompt for
the {globals.adminSecretLabel} when the console is accessed next
using this browser.
</div>
</div>
<div key="access_key_reset_2">
<ClearAdminSecret {...props} />
</div>
</div>
);
}
return clearSecretSection;
};
return (
<div
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top} ${
@ -120,8 +87,6 @@ const MetadataOptions = props => {
{getMetadataImportExportSection()}
{getMetadataUpdateSection()}
{getClearSecretSection()}
</div>
);
};

View File

@ -21,7 +21,7 @@ class ReloadMetadata extends Component {
const { dispatch } = this.props;
const { isReloading } = this.state;
const metaDataStyles = require('../Metadata.scss');
const metaDataStyles = require('../Settings.scss');
const reloadMetadataAndLoadInconsistentMetadata = e => {
e.preventDefault();

View File

@ -17,7 +17,7 @@ class ReloadRemoteSchema extends Component {
render() {
const { dispatch, remoteSchemaName } = this.props;
const { isReloading } = this.state;
const metaDataStyles = require('../Metadata.scss');
const metaDataStyles = require('../Settings.scss');
const reloadRemoteMetadataHandler = () => {
this.setState({ isReloading: true });
dispatch(

View File

@ -6,7 +6,7 @@ import {
showSuccessNotification,
showErrorNotification,
} from '../../Common/Notification';
import metaDataStyles from '../Metadata.scss';
import metaDataStyles from '../Settings.scss';
import styles from '../../../Common/TableCommon/Table.scss';
import CheckIcon from '../../../Common/Icons/Check';
import CrossIcon from '../../../Common/Icons/Cross';

View File

@ -1,16 +1,20 @@
import React from 'react';
import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer';
import { Link } from 'react-router';
import styles from '../../Common/TableCommon/Table.scss';
import CheckIcon from '../../Common/Icons/Check';
import CrossIcon from '../../Common/Icons/Cross';
import globals from '../../../Globals';
import { CLI_CONSOLE_MODE } from '../../../constants';
import { getAdminSecret } from '../ApiExplorer/ApiRequest/utils';
import styles from '../../Common/TableCommon/Table.scss';
const Sidebar = ({ location, metadata }) => {
const sectionsData = [];
sectionsData.push({
key: 'actions',
link: '/metadata/actions',
link: '/settings/metadata-actions',
dataTestVal: 'metadata-actions-link',
title: 'Metadata Actions',
});
@ -20,7 +24,7 @@ const Sidebar = ({ location, metadata }) => {
sectionsData.push({
key: 'status',
link: '/metadata/status',
link: '/settings/metadata-status',
dataTestVal: 'metadata-status-link',
title: (
<div className={styles.display_flex}>
@ -32,11 +36,23 @@ const Sidebar = ({ location, metadata }) => {
sectionsData.push({
key: 'allowed-queries',
link: '/metadata/allowed-queries',
link: '/settings/allowed-queries',
dataTestVal: 'allowed-queries-link',
title: 'Allowed Queries',
});
const adminSecret = getAdminSecret();
if (adminSecret && globals.consoleMode !== CLI_CONSOLE_MODE) {
sectionsData.push({
key: 'logout',
link: '/settings/logout',
dataTestVal: 'logout-page-link',
title: 'Logout (clear admin-secret)',
});
}
const currentLocation = location.pathname;
const sections = [];

View File

@ -1 +1,4 @@
export const SERVER_CONSOLE_MODE = 'server';
export const CLI_CONSOLE_MODE = 'cli';
export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret';

View File

@ -1,38 +1,30 @@
const envObj = `apiHost: '${process.env.API_HOST}',
apiPort: '${process.env.API_PORT}',
dataApiUrl: '${process.env.DATA_API_URL}',
adminSecret: '${process.env.ADMIN_SECRET}',
consoleMode: '${process.env.CONSOLE_MODE}',
nodeEnv: '${process.env.NODE_ENV}',
urlPrefix: '${process.env.URL_PREFIX}',
enableTelemetry: ${process.env.ENABLE_TELEMETRY},
assetsPath: '${process.env.ASSETS_PATH}',
assetsVersion: '${process.env.ASSETS_VERSION}',
serverVersion: '${process.env.SERVER_VERSION}',
cdnAssets: ${process.env.CDN_ASSETS},
`;
let appendObj;
let envObj = `
apiHost: '${process.env.API_HOST}',
apiPort: '${process.env.API_PORT}',
dataApiUrl: '${process.env.DATA_API_URL}',
consoleMode: '${process.env.CONSOLE_MODE}',
nodeEnv: '${process.env.NODE_ENV}',
urlPrefix: '${process.env.URL_PREFIX}',
enableTelemetry: ${process.env.ENABLE_TELEMETRY},
assetsPath: '${process.env.ASSETS_PATH}',
assetsVersion: '${process.env.ASSETS_VERSION}',
serverVersion: '${process.env.SERVER_VERSION}',
cdnAssets: ${process.env.CDN_ASSETS},`;
if (process.env.ADMIN_SECRET !== undefined) {
appendObj = `
adminSecret: '${process.env.ADMIN_SECRET}'`;
envObj += `
adminSecret: '${process.env.ADMIN_SECRET}',`;
} else {
// ADMIN_SECRET is undefined
if (process.env.IS_ADMIN_SECRET_SET !== undefined) {
appendObj = `isAdminSecretSet: ${process.env.IS_ADMIN_SECRET_SET}`;
} else {
// Both ADMIN_SECRET and IS_ADMIN_SECRET_SET is undefined
if (process.env.ACCESS_KEY !== undefined) {
appendObj = `accessKey: ${process.env.ACCESS_KEY}`;
} else {
appendObj = `isAccessKeySet: ${process.env.IS_ACCESS_KEY_SET}`;
}
envObj += `
isAdminSecretSet: ${process.env.IS_ADMIN_SECRET_SET},`;
}
}
const env = `
window.__env={\n\t\t${envObj}\t\t${appendObj}
window.__env={
${envObj}
};
`;

View File

@ -7,7 +7,7 @@ import mainReducer from './components/Main/Actions';
import apiExplorerReducer from 'components/Services/ApiExplorer/Actions';
import progressBarReducer from 'components/App/Actions';
import telemetryReducer from './telemetry/Actions';
import { metadataReducer } from './components/Services/Metadata/Actions';
import { metadataReducer } from './components/Services/Settings/Actions';
import { reducer as notifications } from 'react-notification-system-redux';

View File

@ -19,7 +19,7 @@ import { eventRouterUtils } from './components/Services/EventTrigger';
import { getRemoteSchemaRouter } from './components/Services/RemoteSchema';
import generatedApiExplorer from './components/Services/ApiExplorer/ApiExplorerGenerator';
import generatedApiExplorer from './components/Services/ApiExplorer/ApiExplorer';
import generatedVoyagerConnector from './components/Services/VoyagerView/VoyagerView';
@ -27,34 +27,35 @@ import about from './components/Services/About/About';
import generatedLoginConnector from './components/Login/Login';
import metadataContainer from './components/Services/Metadata/Container';
import metadataOptionsContainer from './components/Services/Metadata/MetadataOptions/MetadataOptions';
import metadataStatusContainer from './components/Services/Metadata/MetadataStatus/MetadataStatus';
import allowedQueriesContainer from './components/Services/Metadata/AllowedQueries/AllowedQueries';
import settingsContainer from './components/Services/Settings/Container';
import metadataOptionsContainer from './components/Services/Settings/MetadataOptions/MetadataOptions';
import metadataStatusContainer from './components/Services/Settings/MetadataStatus/MetadataStatus';
import allowedQueriesContainer from './components/Services/Settings/AllowedQueries/AllowedQueries';
import logoutContainer from './components/Services/Settings/Logout/Logout';
import { showErrorNotification } from './components/Services/Common/Notification';
import { CLI_CONSOLE_MODE } from './constants';
const routes = store => {
// load hasuractl migration status
const requireMigrationStatus = (nextState, replaceState, cb) => {
if (globals.consoleMode === 'cli') {
store.dispatch(loadMigrationStatus()).then(
const { dispatch } = store;
if (globals.consoleMode === CLI_CONSOLE_MODE) {
dispatch(loadMigrationStatus()).then(
() => {
cb();
},
r => {
if (r.code === 'data_api_error') {
if (globals.adminSecret) {
alert('Hasura CLI: ' + r.message);
} else {
alert(
`Looks like CLI is not configured with the ${
globals.adminSecretLabel
}. Please configure and try again`
);
}
dispatch(showErrorNotification('Error', null, r));
} else {
alert(
'Hasura console is not able to reach your Hasura GraphQL engine instance. Please ensure that your ' +
'instance is running and the endpoint is configured correctly.'
dispatch(
showErrorNotification(
'Connection error',
'Hasura console is not able to reach your Hasura GraphQL engine instance. Please ensure that your ' +
'instance is running and the endpoint is configured correctly.'
)
);
}
}
@ -102,17 +103,21 @@ const routes = store => {
component={generatedVoyagerConnector(connect)}
/>
<Route path="about" component={about(connect)} />
<Route path="metadata" component={metadataContainer(connect)}>
<IndexRedirect to="actions" />
<Route path="status" component={metadataStatusContainer(connect)} />
<Route path="settings" component={settingsContainer(connect)}>
<IndexRedirect to="metadata-actions" />
<Route
path="actions"
path="metadata-actions"
component={metadataOptionsContainer(connect)}
/>
<Route
path="metadata-status"
component={metadataStatusContainer(connect)}
/>
<Route
path="allowed-queries"
component={allowedQueriesContainer(connect)}
/>
<Route path="logout" component={logoutContainer(connect)} />
</Route>
{dataRouter}
{eventRouter}

View File

@ -1,18 +1,14 @@
import fetch from 'isomorphic-fetch';
import { push } from 'react-router-redux';
import globals from 'Globals';
import { UPDATE_DATA_HEADERS } from 'components/Services/Data/DataActions';
import {
LOAD_REQUEST,
DONE_REQUEST,
FAILED_REQUEST,
ERROR_REQUEST,
CONNECTION_FAILED,
} from 'components/App/Actions';
import { LOGIN_IN_PROGRESS, LOGIN_ERROR } from 'components/Main/Actions';
const requestAction = (
url,
options,
@ -25,7 +21,7 @@ const requestAction = (
}
return dispatch => {
const p1 = new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
dispatch({ type: LOAD_REQUEST });
fetch(url, options).then(
response => {
@ -59,18 +55,9 @@ const requestAction = (
});
}
if (msg.code && msg.code === 'access-denied') {
dispatch({
type: UPDATE_DATA_HEADERS,
data: {
'content-type': 'application/json',
[`x-hasura-${
globals.adminSecretLabel
}`]: globals.adminSecret,
},
});
dispatch({ type: LOGIN_IN_PROGRESS, data: false });
dispatch({ type: LOGIN_ERROR, data: false });
dispatch(push(globals.urlPrefix + '/login'));
if (window.location.pathname !== globals.urlPrefix + '/login') {
dispatch(push(globals.urlPrefix + '/login'));
}
}
reject(msg);
});
@ -84,13 +71,11 @@ const requestAction = (
});
},
error => {
console.error(error);
console.error('Request error: ', error);
dispatch({ type: FAILED_REQUEST });
dispatch({ type: CONNECTION_FAILED });
if (ERROR) {
dispatch({
type: ERROR,
code: 'server-connection-failed',
message: error.message,
data: error.message,
});
@ -99,7 +84,6 @@ const requestAction = (
}
);
});
return p1;
};
};

View File

@ -1,96 +1,43 @@
import {
loadAdminSecretState,
clearAdminSecretState,
CONSOLE_ADMIN_SECRET,
} from '../components/AppState';
import { clearAdminSecretState } from '../components/AppState';
import globals from '../Globals';
import Endpoints, { globalCookiePolicy } from '../Endpoints';
import requestAction from './requestActionPlain';
import { verifyLogin } from '../components/Login/Actions';
import { UPDATE_DATA_HEADERS } from '../components/Services/Data/DataActions';
import { changeRequestHeader } from '../components/Services/ApiExplorer/Actions';
import { SERVER_CONSOLE_MODE } from '../constants';
const checkValidity = adminSecret => {
return dispatch => {
const url = Endpoints.getSchema;
const currentSchema = 'public';
const headers = {
'content-type': 'application/json',
[`x-hasura-${globals.adminSecretLabel}`]: adminSecret,
};
const options = {
credentials: globalCookiePolicy,
method: 'POST',
headers: headers,
body: JSON.stringify({
type: 'select',
args: {
table: {
name: 'hdb_table',
schema: 'hdb_catalog',
},
columns: ['table_schema'],
where: { table_schema: currentSchema },
limit: 1,
},
}),
};
return dispatch(requestAction(url, options));
};
};
import { getAdminSecret } from '../components/Services/ApiExplorer/ApiRequest/utils';
import { CLI_CONSOLE_MODE } from '../constants';
const validateLogin = ({ dispatch }) => {
return (nextState, replaceState, cb) => {
// Validate isAdminSecretSet env is set by server or adminSecret env is set by cli
// care about admin secret only if it is set
if (globals.isAdminSecretSet || globals.adminSecret) {
let adminSecret = '';
// Check the console mode and retrieve adminSecret accordingly.
if (globals.consoleMode === SERVER_CONSOLE_MODE) {
adminSecret = loadAdminSecretState(CONSOLE_ADMIN_SECRET);
} else {
adminSecret = globals.adminSecret;
}
dispatch(checkValidity(adminSecret))
.then(() => {
return Promise.all([
dispatch({
type: UPDATE_DATA_HEADERS,
data: {
'content-type': 'application/json',
[`x-hasura-${globals.adminSecretLabel}`]: adminSecret,
},
}),
dispatch(
changeRequestHeader(
1,
'key',
`x-hasura-${globals.adminSecretLabel}`,
true
)
),
dispatch(changeRequestHeader(1, 'value', adminSecret, true)),
]);
})
.then(() => {
if (nextState.location.pathname === '/login') {
replaceState('/');
}
cb();
})
.catch(() => {
// Clear state from the localStorage if there exists one
const validationSuccessCallback = () => {
if (nextState.location.pathname === '/login') {
replaceState('/');
}
cb();
};
const validationFailureCallback = () => {
if (globals.consoleMode !== CLI_CONSOLE_MODE) {
clearAdminSecretState();
if (nextState.location.pathname !== '/login') {
replaceState('/login');
}
cb();
});
}
if (nextState.location.pathname !== '/login') {
replaceState('/login');
}
cb();
};
const adminSecret = getAdminSecret();
verifyLogin({
adminSecret,
successCallback: validationSuccessCallback,
errorCallback: validationFailureCallback,
dispatch,
});
} else {
cb();
return;
}
};
};