mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
refactor: port login auth to TS/SWR (#680)
* refactor: allow existing tsc errors * refactor: add missing component key * refactor: port login auth to TS/SWR * refactor: replace incorrect CREATE_TAG_TYPE with UPDATE_TAG_TYPE * refactor: fix AccessProvider permission mocks * refactor: add types to AccessContext * refactor: fix file extension * refactor: remove default export * refactor: remove unused IAddedUser interface * refactor: comment on the permissions prop * refactor: split auth hooks * feat: auth tests * fix: setup separate e2e tests * fix: naming * fix: lint * fix: spec path * fix: missing store * feat: add more tests Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
608c82870b
commit
213e8950d3
1
frontend/.env
Normal file
1
frontend/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
TSC_COMPILE_ON_ERROR=true
|
@ -1,4 +1,4 @@
|
|||||||
name: e2e-tests
|
name: e2e:auth
|
||||||
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||||
on: [deployment_status]
|
on: [deployment_status]
|
||||||
jobs:
|
jobs:
|
||||||
@ -20,5 +20,6 @@ jobs:
|
|||||||
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development"
|
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development"
|
||||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||||
record: true
|
record: true
|
||||||
|
spec: cypress/integration/auth/auth.spec.js
|
||||||
env:
|
env:
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
25
frontend/.github/workflows/e2e.feature.yml
vendored
Normal file
25
frontend/.github/workflows/e2e.feature.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: e2e:feature
|
||||||
|
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||||
|
on: [deployment_status]
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
# only runs this job on successful deploy
|
||||||
|
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Dump GitHub context
|
||||||
|
env:
|
||||||
|
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||||
|
run: |
|
||||||
|
echo "$GITHUB_CONTEXT"
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Run Cypress
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development"
|
||||||
|
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||||
|
record: true
|
||||||
|
spec: cypress/integration/feature-toggle/feature.spec.js
|
||||||
|
env:
|
||||||
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
204
frontend/cypress/integration/auth/auth.spec.js
Normal file
204
frontend/cypress/integration/auth/auth.spec.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
/* eslint-disable jest/no-conditional-expect */
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// Welcome to Cypress!
|
||||||
|
//
|
||||||
|
// This spec file contains a variety of sample tests
|
||||||
|
// for a todo list app that are designed to demonstrate
|
||||||
|
// the power of writing tests in Cypress.
|
||||||
|
//
|
||||||
|
// To learn more about how Cypress works and
|
||||||
|
// what makes it such an awesome testing tool,
|
||||||
|
// please read our getting started guide:
|
||||||
|
// https://on.cypress.io/introduction-to-cypress
|
||||||
|
|
||||||
|
const username = 'test@test.com';
|
||||||
|
const password = 'qY70$NDcJNXA';
|
||||||
|
|
||||||
|
describe('auth', () => {
|
||||||
|
it('renders the password login', () => {
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: false,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.intercept('POST', '/auth/simple/login', req => {
|
||||||
|
expect(req.body.username).to.equal(username);
|
||||||
|
expect(req.body.password).to.equal(password);
|
||||||
|
}).as('passwordLogin');
|
||||||
|
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').type(username);
|
||||||
|
|
||||||
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').type(password);
|
||||||
|
|
||||||
|
cy.get("[data-test='LOGIN_BUTTON']").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders does not render password login if defaultHidden is true', () => {
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: true,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders google auth when options are specified', () => {
|
||||||
|
const ssoPath = '/auth/google/login';
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: true,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: 'google',
|
||||||
|
message: 'Sign in with Google',
|
||||||
|
path: ssoPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').should('not.exist');
|
||||||
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('[data-test="SSO_LOGIN_BUTTON-google"]')
|
||||||
|
.should('exist')
|
||||||
|
.should('have.attr', 'href', ssoPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders oidc auth when options are specified', () => {
|
||||||
|
const ssoPath = '/auth/oidc/login';
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: true,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: 'oidc',
|
||||||
|
message: 'Sign in with OpenId Connect',
|
||||||
|
path: ssoPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').should('not.exist');
|
||||||
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('[data-test="SSO_LOGIN_BUTTON-oidc"]')
|
||||||
|
.should('exist')
|
||||||
|
.should('have.attr', 'href', ssoPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders saml auth when options are specified', () => {
|
||||||
|
const ssoPath = '/auth/saml/login';
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: true,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: 'saml',
|
||||||
|
message: 'Sign in with SAML 2.0',
|
||||||
|
path: ssoPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').should('not.exist');
|
||||||
|
cy.get('[data-test="LOGIN_PASSWORD_ID"]').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('[data-test="SSO_LOGIN_BUTTON-saml"]')
|
||||||
|
.should('exist')
|
||||||
|
.should('have.attr', 'href', ssoPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can visit forgot password when password auth is enabled', () => {
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: false,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [],
|
||||||
|
path: '/auth/simple/login',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.visit('/forgotten-password');
|
||||||
|
cy.get('[data-test="FORGOTTEN_PASSWORD_FIELD"').type('me@myemail.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders demo auth correctly', () => {
|
||||||
|
const email = 'hello@hello.com';
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: false,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [],
|
||||||
|
path: '/auth/demo/login',
|
||||||
|
type: 'demo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.intercept('POST', '/auth/demo/login', req => {
|
||||||
|
expect(req.body.email).to.equal(email);
|
||||||
|
}).as('passwordLogin');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').type(email);
|
||||||
|
cy.get("[data-test='LOGIN_BUTTON']").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders email auth correctly', () => {
|
||||||
|
const email = 'hello@hello.com';
|
||||||
|
cy.intercept('GET', '/api/admin/user', {
|
||||||
|
statusCode: 401,
|
||||||
|
body: {
|
||||||
|
defaultHidden: false,
|
||||||
|
message: 'You must sign in in order to use Unleash',
|
||||||
|
options: [],
|
||||||
|
path: '/auth/unsecure/login',
|
||||||
|
type: 'unsecure',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.intercept('POST', '/auth/unsecure/login', req => {
|
||||||
|
expect(req.body.email).to.equal(email);
|
||||||
|
}).as('passwordLogin');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
cy.get('[data-test="LOGIN_EMAIL_ID"]').type(email);
|
||||||
|
cy.get("[data-test='LOGIN_BUTTON']").click();
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,11 @@
|
|||||||
import { Map as $MAp } from 'immutable';
|
import { Map as $MAp } from 'immutable';
|
||||||
|
|
||||||
export const createFakeStore = (permissions) => {
|
export const createFakeStore = permissions => {
|
||||||
return {
|
return {
|
||||||
getState: () => ({
|
getState: () => ({
|
||||||
user:
|
user: new $MAp({
|
||||||
new $MAp({
|
permissions,
|
||||||
permissions
|
}),
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,57 +1,37 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
|
||||||
import { RouteComponentProps } from 'react-router';
|
|
||||||
|
|
||||||
import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute';
|
|
||||||
import LayoutPicker from './layout/LayoutPicker/LayoutPicker';
|
|
||||||
|
|
||||||
import { routes } from './menu/routes';
|
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
|
||||||
|
|
||||||
import IAuthStatus from '../interfaces/user';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import NotFound from './common/NotFound/NotFound';
|
|
||||||
import Feedback from './common/Feedback/Feedback';
|
|
||||||
import SWRProvider from './providers/SWRProvider/SWRProvider';
|
|
||||||
import ConditionallyRender from './common/ConditionallyRender';
|
import ConditionallyRender from './common/ConditionallyRender';
|
||||||
import EnvironmentSplash from './common/EnvironmentSplash/EnvironmentSplash';
|
import EnvironmentSplash from './common/EnvironmentSplash/EnvironmentSplash';
|
||||||
|
import Feedback from './common/Feedback/Feedback';
|
||||||
|
import LayoutPicker from './layout/LayoutPicker/LayoutPicker';
|
||||||
import Loader from './common/Loader/Loader';
|
import Loader from './common/Loader/Loader';
|
||||||
import useUser from '../hooks/api/getters/useUser/useUser';
|
import NotFound from './common/NotFound/NotFound';
|
||||||
|
import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute';
|
||||||
|
import SWRProvider from './providers/SWRProvider/SWRProvider';
|
||||||
import ToastRenderer from './common/ToastRenderer/ToastRenderer';
|
import ToastRenderer from './common/ToastRenderer/ToastRenderer';
|
||||||
|
import styles from './styles.module.scss';
|
||||||
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { routes } from './menu/routes';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAuthDetails } from '../hooks/api/getters/useAuth/useAuthDetails';
|
||||||
|
import { useAuthUser } from '../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
import { useAuthSplash } from '../hooks/api/getters/useAuth/useAuthSplash';
|
||||||
|
|
||||||
interface IAppProps extends RouteComponentProps {
|
interface IAppProps extends RouteComponentProps {
|
||||||
user: IAuthStatus;
|
fetchUiBootstrap: () => void;
|
||||||
fetchUiBootstrap: any;
|
|
||||||
}
|
}
|
||||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|
||||||
// because we need the userId when the component load.
|
|
||||||
const { splash, user: userFromUseUser, authDetails } = useUser();
|
|
||||||
|
|
||||||
const [showSplash, setShowSplash] = useState(false);
|
export const App = ({ fetchUiBootstrap }: IAppProps) => {
|
||||||
const [showLoader, setShowLoader] = useState(false);
|
const { splash, refetchSplash } = useAuthSplash();
|
||||||
|
const { authDetails } = useAuthDetails();
|
||||||
|
const { user } = useAuthUser();
|
||||||
|
|
||||||
|
const isLoggedIn = Boolean(user?.id);
|
||||||
|
const hasFetchedAuth = Boolean(authDetails || user);
|
||||||
|
const showEnvSplash = isLoggedIn && splash?.environment === false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUiBootstrap();
|
fetchUiBootstrap();
|
||||||
/* eslint-disable-next-line */
|
}, [fetchUiBootstrap, authDetails?.type]);
|
||||||
}, [user.authDetails?.type]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Temporary duality until redux store is removed
|
|
||||||
if (!isUnauthorized() && !userFromUseUser?.id && !authDetails) {
|
|
||||||
setShowLoader(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowLoader(false);
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [user.authDetails, userFromUseUser.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (splash?.environment === undefined) return;
|
|
||||||
if (!splash?.environment && !isUnauthorized()) {
|
|
||||||
setShowSplash(true);
|
|
||||||
}
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [splash.environment]);
|
|
||||||
|
|
||||||
const renderMainLayoutRoutes = () => {
|
const renderMainLayoutRoutes = () => {
|
||||||
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
||||||
@ -63,10 +43,8 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
.map(renderRoute);
|
.map(renderRoute);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUnauthorized = () => {
|
const isUnauthorized = (): boolean => {
|
||||||
// authDetails only exists if the user is not logged in.
|
return !isLoggedIn;
|
||||||
//if (user?.permissions.length === 0) return true;
|
|
||||||
return user?.authDetails !== undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change this to IRoute once snags with HashRouter and TS is worked out
|
// Change this to IRoute once snags with HashRouter and TS is worked out
|
||||||
@ -91,7 +69,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
<route.component
|
<route.component
|
||||||
{...props}
|
{...props}
|
||||||
isUnauthorized={isUnauthorized}
|
isUnauthorized={isUnauthorized}
|
||||||
authDetails={user.authDetails}
|
authDetails={authDetails}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -99,21 +77,18 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRProvider
|
<SWRProvider isUnauthorized={isUnauthorized}>
|
||||||
isUnauthorized={isUnauthorized}
|
|
||||||
setShowLoader={setShowLoader}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showLoader}
|
condition={!hasFetchedAuth}
|
||||||
show={<Loader />}
|
show={<Loader />}
|
||||||
elseShow={
|
elseShow={
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ToastRenderer />
|
<ToastRenderer />
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showSplash}
|
condition={showEnvSplash}
|
||||||
show={
|
show={
|
||||||
<EnvironmentSplash onFinish={setShowSplash} />
|
<EnvironmentSplash onFinish={refetchSplash} />
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<LayoutPicker location={location}>
|
<LayoutPicker location={location}>
|
||||||
@ -133,9 +108,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
/>
|
/>
|
||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
<Feedback
|
<Feedback openUrl="http://feedback.unleash.run" />
|
||||||
openUrl="http://feedback.unleash.run"
|
|
||||||
/>
|
|
||||||
</LayoutPicker>
|
</LayoutPicker>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -145,10 +118,3 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
</SWRProvider>
|
</SWRProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
|
|
||||||
const mapStateToProps = (state: any) => ({
|
|
||||||
user: state.user.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(App);
|
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import App from './App';
|
import { App } from './App';
|
||||||
|
|
||||||
import { fetchUiBootstrap } from '../store/ui-bootstrap/actions';
|
import { fetchUiBootstrap } from '../store/ui-bootstrap/actions';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
export default connect(null, { fetchUiBootstrap })(App);
|
||||||
fetchUiBootstrap,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => ({
|
|
||||||
user: state.user.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
|
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
|
||||||
|
|
||||||
import AdminMenu from '../menu/AdminMenu';
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
import usePermissions from '../../../hooks/usePermissions';
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
const ApiPage = ({ history }) => {
|
const ApiPage = ({ history }) => {
|
||||||
const { isAdmin } = usePermissions();
|
const { isAdmin } = useContext(AccessContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={isAdmin()}
|
condition={isAdmin}
|
||||||
show={<AdminMenu history={history} />}
|
show={<AdminMenu history={history} />}
|
||||||
/>
|
/>
|
||||||
<ApiTokenList />
|
<ApiTokenList />
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
|
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import usePermissions from '../../../hooks/usePermissions';
|
|
||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import { useStyles } from './BreadcrumbNav.styles';
|
import { useStyles } from './BreadcrumbNav.styles';
|
||||||
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
const BreadcrumbNav = () => {
|
const BreadcrumbNav = () => {
|
||||||
const { isAdmin } = usePermissions();
|
const { isAdmin } = useContext(AccessContext);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ const BreadcrumbNav = () => {
|
|||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
(location.pathname.includes('admin') && isAdmin()) ||
|
(location.pathname.includes('admin') && isAdmin) ||
|
||||||
!location.pathname.includes('admin')
|
!location.pathname.includes('admin')
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
|
@ -2,7 +2,6 @@ import { useContext, useState } from 'react';
|
|||||||
import { Button, IconButton } from '@material-ui/core';
|
import { Button, IconButton } from '@material-ui/core';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CloseIcon from '@material-ui/icons/Close';
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
import { ReactComponent as Logo } from '../../../assets/icons/logo-plain.svg';
|
import { ReactComponent as Logo } from '../../../assets/icons/logo-plain.svg';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import { useStyles } from './Feedback.styles';
|
import { useStyles } from './Feedback.styles';
|
||||||
@ -10,8 +9,8 @@ import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
|||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
import { formatApiPath } from '../../../utils/format-path';
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
import UIContext from '../../../contexts/UIContext';
|
import UIContext from '../../../contexts/UIContext';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
|
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
|
||||||
|
import { useAuthFeedback } from '../../../hooks/api/getters/useAuth/useAuthFeedback';
|
||||||
|
|
||||||
interface IFeedbackProps {
|
interface IFeedbackProps {
|
||||||
openUrl: string;
|
openUrl: string;
|
||||||
@ -19,7 +18,7 @@ interface IFeedbackProps {
|
|||||||
|
|
||||||
const Feedback = ({ openUrl }: IFeedbackProps) => {
|
const Feedback = ({ openUrl }: IFeedbackProps) => {
|
||||||
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
||||||
const { refetch, feedback } = useUser();
|
const { feedback, refetchFeedback } = useAuthFeedback();
|
||||||
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
@ -37,7 +36,7 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ feedbackId }),
|
body: JSON.stringify({ feedbackId }),
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetchFeedback();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
@ -65,7 +64,7 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ feedbackId, neverShow: true }),
|
body: JSON.stringify({ feedbackId, neverShow: true }),
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetchFeedback();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
|
@ -64,7 +64,7 @@ const Splash: React.FC<ISplashProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FiberManualRecordOutlined />;
|
return <FiberManualRecordOutlined key={index} />;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMet
|
|||||||
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../testIds';
|
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../testIds';
|
||||||
import useFeatureTypes from '../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
import useFeatureTypes from '../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
||||||
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
@ -12,6 +11,8 @@ import { trim } from '../../common/util';
|
|||||||
import Input from '../../common/Input/Input';
|
import Input from '../../common/Input/Input';
|
||||||
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||||
|
|
||||||
interface IFeatureToggleForm {
|
interface IFeatureToggleForm {
|
||||||
type: string;
|
type: string;
|
||||||
@ -54,7 +55,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
|||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { featureTypes } = useFeatureTypes();
|
const { featureTypes } = useFeatureTypes();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { permissions } = useUser();
|
const { permissions } = useAuthPermissions()
|
||||||
const editable = mode !== 'Edit';
|
const editable = mode !== 'Edit';
|
||||||
|
|
||||||
const renderToggleDescription = () => {
|
const renderToggleDescription = () => {
|
||||||
@ -114,7 +115,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
|||||||
}}
|
}}
|
||||||
enabled={editable}
|
enabled={editable}
|
||||||
filter={projectFilterGenerator(
|
filter={projectFilterGenerator(
|
||||||
{ permissions },
|
permissions,
|
||||||
CREATE_FEATURE
|
CREATE_FEATURE
|
||||||
)}
|
)}
|
||||||
IconComponent={KeyboardArrowDownOutlined}
|
IconComponent={KeyboardArrowDownOutlined}
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@material-ui/core';
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
|
|
||||||
import FeatureToggleList from '../FeatureToggleList';
|
import FeatureToggleList from '../FeatureToggleList';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
import { createFakeStore } from '../../../../accessStoreFake';
|
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
||||||
import {
|
|
||||||
ADMIN,
|
|
||||||
CREATE_FEATURE,
|
|
||||||
} from '../../../providers/AccessProvider/permissions';
|
|
||||||
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||||
|
|
||||||
jest.mock('../FeatureToggleListItem', () => ({
|
jest.mock('../FeatureToggleListItem', () => ({
|
||||||
@ -29,9 +24,7 @@ test('renders correctly with one feature', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider
|
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}>
|
||||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
|
||||||
>
|
|
||||||
<FeatureToggleList
|
<FeatureToggleList
|
||||||
updateSetting={jest.fn()}
|
updateSetting={jest.fn()}
|
||||||
filter={{}}
|
filter={{}}
|
||||||
@ -59,9 +52,7 @@ test('renders correctly with one feature without permissions', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider
|
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}>
|
||||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
|
||||||
>
|
|
||||||
<FeatureToggleList
|
<FeatureToggleList
|
||||||
filter={{}}
|
filter={{}}
|
||||||
setFilter={jest.fn()}
|
setFilter={jest.fn()}
|
||||||
|
@ -3,7 +3,6 @@ import { useHistory, useParams } from 'react-router';
|
|||||||
import AccessContext from '../../../../../contexts/AccessContext';
|
import AccessContext from '../../../../../contexts/AccessContext';
|
||||||
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||||
import useUser from '../../../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import useToast from '../../../../../hooks/useToast';
|
import useToast from '../../../../../hooks/useToast';
|
||||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||||
import { MOVE_FEATURE_TOGGLE } from '../../../../providers/AccessProvider/permissions';
|
import { MOVE_FEATURE_TOGGLE } from '../../../../providers/AccessProvider/permissions';
|
||||||
@ -12,6 +11,7 @@ import PermissionButton from '../../../../common/PermissionButton/PermissionButt
|
|||||||
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
||||||
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
||||||
import { IPermission } from '../../../../../interfaces/user';
|
import { IPermission } from '../../../../../interfaces/user';
|
||||||
|
import { useAuthPermissions } from '../../../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||||
|
|
||||||
const FeatureSettingsProject = () => {
|
const FeatureSettingsProject = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
@ -21,11 +21,12 @@ const FeatureSettingsProject = () => {
|
|||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const editable = hasAccess(MOVE_FEATURE_TOGGLE, projectId);
|
const editable = hasAccess(MOVE_FEATURE_TOGGLE, projectId);
|
||||||
const { permissions } = useUser();
|
const { permissions = [] } = useAuthPermissions()
|
||||||
const { changeFeatureProject } = useFeatureApi();
|
const { changeFeatureProject } = useFeatureApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (project !== feature.project) {
|
if (project !== feature.project) {
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
@ -43,7 +44,7 @@ const FeatureSettingsProject = () => {
|
|||||||
setProject(projectId);
|
setProject(projectId);
|
||||||
}
|
}
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [permissions?.length]);
|
}, [permissions.length]);
|
||||||
|
|
||||||
const updateProject = async () => {
|
const updateProject = async () => {
|
||||||
const newProject = project;
|
const newProject = project;
|
||||||
|
@ -15,19 +15,19 @@ import { useStyles } from './Header.styles';
|
|||||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { IPermission } from '../../../interfaces/user';
|
import { IPermission } from '../../../interfaces/user';
|
||||||
import NavigationMenu from './NavigationMenu/NavigationMenu';
|
import NavigationMenu from './NavigationMenu/NavigationMenu';
|
||||||
import { getRoutes } from '../routes';
|
import { getRoutes } from '../routes';
|
||||||
import { KeyboardArrowDown } from '@material-ui/icons';
|
import { KeyboardArrowDown } from '@material-ui/icons';
|
||||||
import { filterByFlags } from '../../common/util';
|
import { filterByFlags } from '../../common/util';
|
||||||
|
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [anchorEl, setAnchorEl] = useState();
|
const [anchorEl, setAnchorEl] = useState();
|
||||||
const [anchorElAdvanced, setAnchorElAdvanced] = useState();
|
const [anchorElAdvanced, setAnchorElAdvanced] = useState();
|
||||||
const [admin, setAdmin] = useState(false);
|
const [admin, setAdmin] = useState(false);
|
||||||
const { permissions } = useUser();
|
const { permissions } = useAuthPermissions()
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
@ -39,7 +39,7 @@ const Header = () => {
|
|||||||
const handleCloseAdvanced = () => setAnchorElAdvanced(null);
|
const handleCloseAdvanced = () => setAnchorElAdvanced(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const admin = permissions.find(
|
const admin = permissions?.find(
|
||||||
(element: IPermission) => element.permission === ADMIN
|
(element: IPermission) => element.permission === ADMIN
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import AdminUsers from '../admin/users/UsersAdmin';
|
|||||||
import { AuthSettings } from '../admin/auth/AuthSettings';
|
import { AuthSettings } from '../admin/auth/AuthSettings';
|
||||||
import Login from '../user/Login/Login';
|
import Login from '../user/Login/Login';
|
||||||
import { P, C, E, EEA, RE } from '../common/flags';
|
import { P, C, E, EEA, RE } from '../common/flags';
|
||||||
import NewUser from '../user/NewUser';
|
import { NewUser } from '../user/NewUser/NewUser';
|
||||||
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
import ResetPassword from '../user/ResetPassword/ResetPassword';
|
||||||
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
import ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||||
import ProjectListNew from '../project/ProjectList/ProjectList';
|
import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||||
|
@ -5,13 +5,13 @@ import ProjectForm from '../ProjectForm/ProjectForm';
|
|||||||
import useProjectForm from '../hooks/useProjectForm';
|
import useProjectForm from '../hooks/useProjectForm';
|
||||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useToast from '../../../../hooks/useToast';
|
import useToast from '../../../../hooks/useToast';
|
||||||
import useUser from '../../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||||
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
||||||
|
import { useAuthUser } from '../../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
|
||||||
const CreateProject = () => {
|
const CreateProject = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { refetch } = useUser();
|
const { refetchUser } = useAuthUser();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const {
|
const {
|
||||||
@ -40,7 +40,7 @@ const CreateProject = () => {
|
|||||||
const payload = getProjectPayload();
|
const payload = getProjectPayload();
|
||||||
try {
|
try {
|
||||||
await createProject(payload);
|
await createProject(payload);
|
||||||
refetch();
|
refetchUser();
|
||||||
history.push(`/projects/${projectId}`);
|
history.push(`/projects/${projectId}`);
|
||||||
setToastData({
|
setToastData({
|
||||||
title: 'Project created',
|
title: 'Project created',
|
||||||
|
@ -1,88 +1,111 @@
|
|||||||
import { FC } from 'react';
|
import { ReactElement, ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
import AccessContext, { IAccessContext } from '../../../contexts/AccessContext';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { ADMIN } from './permissions';
|
import { ADMIN } from './permissions';
|
||||||
|
import { IPermission } from '../../../interfaces/user';
|
||||||
|
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||||
|
|
||||||
// TODO: Type up redux store
|
interface IAccessProviderProps {
|
||||||
interface IAccessProvider {
|
children: ReactNode;
|
||||||
store: any;
|
permissions: IPermission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPermission {
|
// TODO(olav): Mock useAuth instead of using props.permissions in tests.
|
||||||
permission: string;
|
const AccessProvider = (props: IAccessProviderProps): ReactElement => {
|
||||||
project?: string | null;
|
const auth = useAuthPermissions();
|
||||||
environment: string | null;
|
const permissions = props.permissions ?? auth.permissions;
|
||||||
}
|
|
||||||
|
|
||||||
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
const isAdmin: boolean = useMemo(() => {
|
||||||
const { permissions } = useUser();
|
return checkAdmin(permissions);
|
||||||
const isAdminHigherOrder = () => {
|
}, [permissions]);
|
||||||
let called = false;
|
|
||||||
let result = false;
|
|
||||||
|
|
||||||
return () => {
|
const hasAccess = useCallback(
|
||||||
if (called) return result;
|
(permission: string, project?: string, environment?: string) => {
|
||||||
const permissions = store.getState().user.get('permissions') || [];
|
return checkPermissions(
|
||||||
result = permissions.some(
|
permissions,
|
||||||
(p: IPermission) => p.permission === ADMIN
|
permission,
|
||||||
|
project,
|
||||||
|
environment
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
[permissions]
|
||||||
|
);
|
||||||
|
|
||||||
if (permissions.length > 0) {
|
const value: IAccessContext = useMemo(
|
||||||
called = true;
|
() => ({
|
||||||
}
|
isAdmin,
|
||||||
};
|
hasAccess,
|
||||||
};
|
}),
|
||||||
|
[isAdmin, hasAccess]
|
||||||
const isAdmin = isAdminHigherOrder();
|
);
|
||||||
|
|
||||||
const hasAccess = (
|
|
||||||
permission: string,
|
|
||||||
project: string,
|
|
||||||
environment?: string
|
|
||||||
) => {
|
|
||||||
const result = permissions.some((p: IPermission) => {
|
|
||||||
if (p.permission === ADMIN) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
p.permission === permission &&
|
|
||||||
(p.project === project || p.project === '*') &&
|
|
||||||
(p.environment === environment || p.environment === '*')
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
p.permission === permission &&
|
|
||||||
(p.project === project || p.project === '*') &&
|
|
||||||
p.environment === null
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
p.permission === permission &&
|
|
||||||
p.project === undefined &&
|
|
||||||
p.environment === null
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const context = { hasAccess, isAdmin };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessContext.Provider value={context}>
|
<AccessContext.Provider value={value}>
|
||||||
{children}
|
{props.children}
|
||||||
</AccessContext.Provider>
|
</AccessContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAdmin = (permissions: IPermission[] | undefined): boolean => {
|
||||||
|
if (!permissions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.some(p => {
|
||||||
|
return p.permission === ADMIN;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPermissions = (
|
||||||
|
permissions: IPermission[] | undefined,
|
||||||
|
permission: string,
|
||||||
|
project?: string,
|
||||||
|
environment?: string
|
||||||
|
): boolean => {
|
||||||
|
if (!permissions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions.some(p => {
|
||||||
|
return checkPermission(p, permission, project, environment);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPermission = (
|
||||||
|
p: IPermission,
|
||||||
|
permission: string,
|
||||||
|
project?: string,
|
||||||
|
environment?: string
|
||||||
|
): boolean => {
|
||||||
|
if (!permission) {
|
||||||
|
console.warn(`Missing permission for AccessProvider: ${permission}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.permission === ADMIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
p.permission === permission &&
|
||||||
|
(p.project === project || p.project === '*') &&
|
||||||
|
(p.environment === environment || p.environment === '*')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
p.permission === permission &&
|
||||||
|
(p.project === project || p.project === '*') &&
|
||||||
|
p.environment === null
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
p.permission === permission &&
|
||||||
|
p.project === undefined &&
|
||||||
|
p.environment === null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AccessProvider;
|
export default AccessProvider;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { USER_CACHE_KEY } from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { mutate, SWRConfig, useSWRConfig } from 'swr';
|
import { mutate, SWRConfig, useSWRConfig } from 'swr';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import { formatApiPath } from '../../../utils/format-path';
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
|
import React from 'react';
|
||||||
|
import { USER_ENDPOINT_PATH } from '../../../hooks/api/getters/useAuth/useAuthEndpoint';
|
||||||
|
|
||||||
interface ISWRProviderProps {
|
interface ISWRProviderProps {
|
||||||
setShowLoader: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isUnauthorized: () => boolean;
|
isUnauthorized: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,20 +14,18 @@ const INVALID_TOKEN_ERROR = 'InvalidTokenError';
|
|||||||
const SWRProvider: React.FC<ISWRProviderProps> = ({
|
const SWRProvider: React.FC<ISWRProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
isUnauthorized,
|
isUnauthorized,
|
||||||
setShowLoader,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { cache } = useSWRConfig();
|
const { cache } = useSWRConfig();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const handleFetchError = error => {
|
const handleFetchError = error => {
|
||||||
setShowLoader(false);
|
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
// Only populate user with authDetails if 401 and
|
// Only populate user with authDetails if 401 and
|
||||||
// error is not invalid token
|
// error is not invalid token
|
||||||
if (error?.info?.name !== INVALID_TOKEN_ERROR) {
|
if (error?.info?.name !== INVALID_TOKEN_ERROR) {
|
||||||
mutate(USER_CACHE_KEY, { ...error.info }, false);
|
mutate(USER_ENDPOINT_PATH, { ...error.info }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@material-ui/core';
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
|
|
||||||
import StrategiesListComponent from '../StrategiesList/StrategiesList';
|
import StrategiesListComponent from '../StrategiesList/StrategiesList';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
||||||
import { createFakeStore } from '../../../accessStoreFake';
|
|
||||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
|
||||||
|
|
||||||
test('renders correctly with one strategy', () => {
|
test('renders correctly with one strategy', () => {
|
||||||
const strategy = {
|
const strategy = {
|
||||||
@ -17,7 +14,7 @@ test('renders correctly with one strategy', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider store={createFakeStore()}>
|
<AccessProvider>
|
||||||
<StrategiesListComponent
|
<StrategiesListComponent
|
||||||
strategies={[strategy]}
|
strategies={[strategy]}
|
||||||
fetchStrategies={jest.fn()}
|
fetchStrategies={jest.fn()}
|
||||||
@ -42,9 +39,7 @@ test('renders correctly with one strategy without permissions', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider
|
<AccessProvider>
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
|
||||||
>
|
|
||||||
<StrategiesListComponent
|
<StrategiesListComponent
|
||||||
strategies={[strategy]}
|
strategies={[strategy]}
|
||||||
fetchStrategies={jest.fn()}
|
fetchStrategies={jest.fn()}
|
||||||
|
@ -4,7 +4,6 @@ import StrategyDetails from '../strategy-details-component';
|
|||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import theme from '../../../themes/main-theme';
|
import theme from '../../../themes/main-theme';
|
||||||
import { createFakeStore } from '../../../accessStoreFake';
|
|
||||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
||||||
|
|
||||||
test('renders correctly with one strategy', () => {
|
test('renders correctly with one strategy', () => {
|
||||||
@ -35,7 +34,7 @@ test('renders correctly with one strategy', () => {
|
|||||||
];
|
];
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AccessProvider store={createFakeStore()}>
|
<AccessProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<StrategyDetails
|
<StrategyDetails
|
||||||
strategyName={'Another'}
|
strategyName={'Another'}
|
||||||
|
@ -4,13 +4,11 @@ import renderer from 'react-test-renderer';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@material-ui/styles';
|
import { ThemeProvider } from '@material-ui/styles';
|
||||||
import theme from '../../../../themes/main-theme';
|
import theme from '../../../../themes/main-theme';
|
||||||
import { createFakeStore } from '../../../../accessStoreFake';
|
|
||||||
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||||
import {
|
import {
|
||||||
ADMIN,
|
ADMIN,
|
||||||
CREATE_TAG_TYPE,
|
|
||||||
UPDATE_TAG_TYPE,
|
|
||||||
DELETE_TAG_TYPE,
|
DELETE_TAG_TYPE,
|
||||||
|
UPDATE_TAG_TYPE,
|
||||||
} from '../../../providers/AccessProvider/permissions';
|
} from '../../../providers/AccessProvider/permissions';
|
||||||
import UIProvider from '../../../providers/UIProvider/UIProvider';
|
import UIProvider from '../../../providers/UIProvider/UIProvider';
|
||||||
|
|
||||||
@ -19,9 +17,7 @@ test('renders an empty list correctly', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<UIProvider>
|
<UIProvider>
|
||||||
<AccessProvider
|
<AccessProvider permissions={[{ permission: ADMIN }]}>
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
|
||||||
>
|
|
||||||
<TagTypeList
|
<TagTypeList
|
||||||
tagTypes={[]}
|
tagTypes={[]}
|
||||||
fetchTagTypes={jest.fn()}
|
fetchTagTypes={jest.fn()}
|
||||||
@ -42,11 +38,10 @@ test('renders a list with elements correctly', () => {
|
|||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<UIProvider>
|
<UIProvider>
|
||||||
<AccessProvider
|
<AccessProvider
|
||||||
store={createFakeStore([
|
permissions={[
|
||||||
{ permission: CREATE_TAG_TYPE },
|
|
||||||
{ permission: UPDATE_TAG_TYPE },
|
{ permission: UPDATE_TAG_TYPE },
|
||||||
{ permission: DELETE_TAG_TYPE },
|
{ permission: DELETE_TAG_TYPE },
|
||||||
])}
|
]}
|
||||||
>
|
>
|
||||||
<TagTypeList
|
<TagTypeList
|
||||||
tagTypes={[
|
tagTypes={[
|
||||||
|
@ -28,7 +28,35 @@ exports[`renders a list with elements correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-7"
|
className="makeStyles-headerActions-7"
|
||||||
/>
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||||
|
disabled={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Add new tag type
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="MuiTouchRipple-root"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -76,7 +104,32 @@ exports[`renders an empty list correctly 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="makeStyles-headerActions-7"
|
className="makeStyles-headerActions-7"
|
||||||
/>
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
|
||||||
|
disabled={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Add new tag type
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -2,8 +2,7 @@ import SimpleAuth from '../SimpleAuth/SimpleAuth';
|
|||||||
import AuthenticationCustomComponent from '../authentication-custom-component';
|
import AuthenticationCustomComponent from '../authentication-custom-component';
|
||||||
import PasswordAuth from '../PasswordAuth/PasswordAuth';
|
import PasswordAuth from '../PasswordAuth/PasswordAuth';
|
||||||
import HostedAuth from '../HostedAuth/HostedAuth';
|
import HostedAuth from '../HostedAuth/HostedAuth';
|
||||||
import DemoAuth from '../DemoAuth';
|
import DemoAuth from '../DemoAuth/DemoAuth';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SIMPLE_TYPE,
|
SIMPLE_TYPE,
|
||||||
DEMO_TYPE,
|
DEMO_TYPE,
|
||||||
@ -11,27 +10,13 @@ import {
|
|||||||
HOSTED_TYPE,
|
HOSTED_TYPE,
|
||||||
} from '../../../constants/authTypes';
|
} from '../../../constants/authTypes';
|
||||||
import SecondaryLoginActions from '../common/SecondaryLoginActions/SecondaryLoginActions';
|
import SecondaryLoginActions from '../common/SecondaryLoginActions/SecondaryLoginActions';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { IUser } from '../../../interfaces/user';
|
|
||||||
import { useHistory } from 'react-router';
|
|
||||||
import useQueryParams from '../../../hooks/useQueryParams';
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
|
import { useAuthDetails } from '../../../hooks/api/getters/useAuth/useAuthDetails';
|
||||||
|
|
||||||
interface IAuthenticationProps {
|
const Authentication = () => {
|
||||||
insecureLogin: (path: string, user: IUser) => void;
|
const { authDetails } = useAuthDetails();
|
||||||
passwordLogin: (path: string, user: IUser) => void;
|
|
||||||
demoLogin: (path: string, user: IUser) => void;
|
|
||||||
history: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Authentication = ({
|
|
||||||
insecureLogin,
|
|
||||||
passwordLogin,
|
|
||||||
demoLogin,
|
|
||||||
}: IAuthenticationProps) => {
|
|
||||||
const { authDetails } = useUser();
|
|
||||||
const history = useHistory();
|
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
|
|
||||||
const error = params.get('errorMsg');
|
const error = params.get('errorMsg');
|
||||||
@ -41,11 +26,7 @@ const Authentication = ({
|
|||||||
if (authDetails.type === PASSWORD_TYPE) {
|
if (authDetails.type === PASSWORD_TYPE) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<PasswordAuth
|
<PasswordAuth authDetails={authDetails} />
|
||||||
passwordLogin={passwordLogin}
|
|
||||||
authDetails={authDetails}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!authDetails.defaultHidden}
|
condition={!authDetails.defaultHidden}
|
||||||
show={<SecondaryLoginActions />}
|
show={<SecondaryLoginActions />}
|
||||||
@ -53,29 +34,13 @@ const Authentication = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else if (authDetails.type === SIMPLE_TYPE) {
|
} else if (authDetails.type === SIMPLE_TYPE) {
|
||||||
content = (
|
content = <SimpleAuth authDetails={authDetails} />;
|
||||||
<SimpleAuth
|
|
||||||
insecureLogin={insecureLogin}
|
|
||||||
authDetails={authDetails}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (authDetails.type === DEMO_TYPE) {
|
} else if (authDetails.type === DEMO_TYPE) {
|
||||||
content = (
|
content = <DemoAuth authDetails={authDetails} />;
|
||||||
<DemoAuth
|
|
||||||
demoLogin={demoLogin}
|
|
||||||
authDetails={authDetails}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (authDetails.type === HOSTED_TYPE) {
|
} else if (authDetails.type === HOSTED_TYPE) {
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<HostedAuth
|
<HostedAuth authDetails={authDetails} />
|
||||||
passwordLogin={passwordLogin}
|
|
||||||
authDetails={authDetails}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!authDetails.defaultHidden}
|
condition={!authDetails.defaultHidden}
|
||||||
show={<SecondaryLoginActions />}
|
show={<SecondaryLoginActions />}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AuthenticationComponent from './Authentication';
|
|
||||||
import {
|
|
||||||
insecureLogin,
|
|
||||||
passwordLogin,
|
|
||||||
demoLogin,
|
|
||||||
} from '../../../store/user/actions';
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, props) => ({
|
|
||||||
demoLogin: (path, user) => demoLogin(path, user)(dispatch),
|
|
||||||
insecureLogin: (path, user) => insecureLogin(path, user)(dispatch),
|
|
||||||
passwordLogin: (path, user) => passwordLogin(path, user)(dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(AuthenticationComponent);
|
|
@ -1,27 +1,31 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, TextField } from '@material-ui/core';
|
import { Button, TextField } from '@material-ui/core';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
|
|
||||||
import styles from './DemoAuth.module.scss';
|
import styles from './DemoAuth.module.scss';
|
||||||
|
|
||||||
import { ReactComponent as Logo } from '../../../assets/img/logo.svg';
|
import { ReactComponent as Logo } from '../../../assets/img/logo.svg';
|
||||||
import { LOGIN_BUTTON, LOGIN_EMAIL_ID } from '../../../testIds';
|
import { LOGIN_BUTTON, LOGIN_EMAIL_ID } from '../../../testIds';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useAuthApi } from '../../../hooks/api/actions/useAuthApi/useAuthApi';
|
||||||
|
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
|
||||||
const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
const DemoAuth = ({ authDetails }) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
const history = useHistory();
|
||||||
|
const { refetchUser } = useAuthUser();
|
||||||
|
const { emailAuth } = useAuthApi();
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const { refetch } = useUser();
|
const handleSubmit = async evt => {
|
||||||
|
|
||||||
const handleSubmit = evt => {
|
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const user = { email };
|
|
||||||
const path = evt.target.action;
|
|
||||||
|
|
||||||
demoLogin(path, user).then(() => {
|
try {
|
||||||
refetch();
|
await emailAuth(authDetails.path, email);
|
||||||
|
refetchUser();
|
||||||
history.push(`/`);
|
history.push(`/`);
|
||||||
});
|
} catch (e) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
@ -30,7 +34,7 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Logo className={styles.logo} />
|
<Logo className={styles.logo} />
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2>Access the Unleash demo instance</h2>
|
<h2>Access the Unleash demo instance</h2>
|
||||||
@ -86,8 +90,6 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
|||||||
|
|
||||||
DemoAuth.propTypes = {
|
DemoAuth.propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
demoLogin: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DemoAuth;
|
export default DemoAuth;
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import DemoAuth from './DemoAuth';
|
|
||||||
|
|
||||||
export default DemoAuth;
|
|
@ -5,6 +5,7 @@ import { SyntheticEvent, useState } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useCommonStyles } from '../../../common.styles';
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
import useLoading from '../../../hooks/useLoading';
|
import useLoading from '../../../hooks/useLoading';
|
||||||
|
import { FORGOTTEN_PASSWORD_FIELD } from '../../../testIds';
|
||||||
import { formatApiPath } from '../../../utils/format-path';
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import DividerText from '../../common/DividerText/DividerText';
|
import DividerText from '../../common/DividerText/DividerText';
|
||||||
@ -96,6 +97,7 @@ const ForgottenPassword = () => {
|
|||||||
placeholder="email"
|
placeholder="email"
|
||||||
type="email"
|
type="email"
|
||||||
data-loading
|
data-loading
|
||||||
|
data-test={FORGOTTEN_PASSWORD_FIELD}
|
||||||
value={email}
|
value={email}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setEmail(e.target.value);
|
setEmail(e.target.value);
|
||||||
|
@ -9,15 +9,17 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
|||||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||||
import DividerText from '../../common/DividerText/DividerText';
|
import DividerText from '../../common/DividerText/DividerText';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import PasswordField from '../../common/PasswordField/PasswordField';
|
import PasswordField from '../../common/PasswordField/PasswordField';
|
||||||
|
import { useAuthApi } from "../../../hooks/api/actions/useAuthApi/useAuthApi";
|
||||||
|
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
|
||||||
const HostedAuth = ({ authDetails, passwordLogin }) => {
|
const HostedAuth = ({ authDetails }) => {
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { refetch } = useUser();
|
const { refetchUser } = useAuthUser();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
|
const { passwordAuth } = useAuthApi()
|
||||||
const [username, setUsername] = useState(params.get('email') || '');
|
const [username, setUsername] = useState(params.get('email') || '');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
@ -45,12 +47,9 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = { username, password };
|
|
||||||
const path = evt.target.action;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await passwordLogin(path, user);
|
await passwordAuth(authDetails.path, username, password);
|
||||||
refetch();
|
refetchUser();
|
||||||
history.push(`/`);
|
history.push(`/`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.statusCode === 404 || error.statusCode === 400) {
|
if (error.statusCode === 404 || error.statusCode === 400) {
|
||||||
@ -86,7 +85,7 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!authDetails.defaultHidden}
|
condition={!authDetails.defaultHidden}
|
||||||
show={
|
show={
|
||||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
className={styles.apiError}
|
className={styles.apiError}
|
||||||
@ -138,8 +137,6 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
|
|
||||||
HostedAuth.propTypes = {
|
HostedAuth.propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
passwordLogin: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HostedAuth;
|
export default HostedAuth;
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import AuthenticationContainer from '../Authentication';
|
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
|
|
||||||
import { useStyles } from './Login.styles';
|
import { useStyles } from './Login.styles';
|
||||||
import useQueryParams from '../../../hooks/useQueryParams';
|
import useQueryParams from '../../../hooks/useQueryParams';
|
||||||
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
||||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
import { DEMO_TYPE } from '../../../constants/authTypes';
|
import { DEMO_TYPE } from '../../../constants/authTypes';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import Authentication from "../Authentication/Authentication";
|
||||||
|
import { useAuthDetails } from '../../../hooks/api/getters/useAuth/useAuthDetails';
|
||||||
|
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { permissions, authDetails } = useUser();
|
const { authDetails } = useAuthDetails();
|
||||||
|
const { permissions } = useAuthPermissions();
|
||||||
const query = useQueryParams();
|
const query = useQueryParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (permissions?.length > 0) {
|
if (permissions?.length) {
|
||||||
history.push('features');
|
history.push('features');
|
||||||
}
|
}
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [permissions.length]);
|
}, [permissions?.length]);
|
||||||
|
|
||||||
const resetPassword = query.get('reset') === 'true';
|
const resetPassword = query.get('reset') === 'true';
|
||||||
return (
|
return (
|
||||||
@ -41,7 +41,7 @@ const Login = () => {
|
|||||||
condition={resetPassword}
|
condition={resetPassword}
|
||||||
show={<ResetPasswordSuccess />}
|
show={<ResetPasswordSuccess />}
|
||||||
/>
|
/>
|
||||||
<AuthenticationContainer history={history} />
|
<Authentication />
|
||||||
</div>
|
</div>
|
||||||
</StandaloneLayout>
|
</StandaloneLayout>
|
||||||
);
|
);
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
import useLoading from '../../../hooks/useLoading';
|
import useLoading from '../../../hooks/useLoading';
|
||||||
import { TextField, Typography } from '@material-ui/core';
|
import { TextField, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
|
import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
|
||||||
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
|
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
|
||||||
|
|
||||||
import { useStyles } from './NewUser.styles';
|
import { useStyles } from './NewUser.styles';
|
||||||
import useResetPassword from '../../../hooks/api/getters/useResetPassword/useResetPassword';
|
import useResetPassword from '../../../hooks/api/getters/useResetPassword/useResetPassword';
|
||||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||||
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||||
import { IAuthStatus } from '../../../interfaces/user';
|
|
||||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||||
import DividerText from '../../common/DividerText/DividerText';
|
import DividerText from '../../common/DividerText/DividerText';
|
||||||
|
import { useAuthDetails } from '../../../hooks/api/getters/useAuth/useAuthDetails';
|
||||||
|
|
||||||
interface INewUserProps {
|
export const NewUser = () => {
|
||||||
user: IAuthStatus;
|
const { authDetails } = useAuthDetails();
|
||||||
}
|
|
||||||
|
|
||||||
const NewUser = ({ user }: INewUserProps) => {
|
|
||||||
const { token, data, loading, setLoading, invalidToken } =
|
const { token, data, loading, setLoading, invalidToken } =
|
||||||
useResetPassword();
|
useResetPassword();
|
||||||
const ref = useLoading(loading);
|
const ref = useLoading(loading);
|
||||||
@ -75,10 +70,9 @@ const NewUser = ({ user }: INewUserProps) => {
|
|||||||
/>
|
/>
|
||||||
<div className={styles.roleContainer}>
|
<div className={styles.roleContainer}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={Boolean(
|
||||||
user?.authDetails?.options?.length >
|
authDetails?.options?.length
|
||||||
0
|
)}
|
||||||
}
|
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<DividerText
|
<DividerText
|
||||||
@ -88,8 +82,7 @@ const NewUser = ({ user }: INewUserProps) => {
|
|||||||
|
|
||||||
<AuthOptions
|
<AuthOptions
|
||||||
options={
|
options={
|
||||||
user?.authDetails
|
authDetails?.options
|
||||||
?.options
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DividerText
|
<DividerText
|
||||||
@ -116,5 +109,3 @@ const NewUser = ({ user }: INewUserProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewUser;
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import NewUser from './NewUser';
|
|
||||||
|
|
||||||
const mapStateToProps = (state: any) => ({
|
|
||||||
user: state.user.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NewUser);
|
|
@ -12,20 +12,22 @@ import DividerText from '../../common/DividerText/DividerText';
|
|||||||
import { Alert } from '@material-ui/lab';
|
import { Alert } from '@material-ui/lab';
|
||||||
import {
|
import {
|
||||||
LOGIN_BUTTON,
|
LOGIN_BUTTON,
|
||||||
LOGIN_PASSWORD_ID,
|
|
||||||
LOGIN_EMAIL_ID,
|
LOGIN_EMAIL_ID,
|
||||||
|
LOGIN_PASSWORD_ID,
|
||||||
} from '../../../testIds';
|
} from '../../../testIds';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import PasswordField from '../../common/PasswordField/PasswordField';
|
import PasswordField from '../../common/PasswordField/PasswordField';
|
||||||
|
import { useAuthApi } from '../../../hooks/api/actions/useAuthApi/useAuthApi';
|
||||||
|
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
|
||||||
const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
const PasswordAuth = ({ authDetails }) => {
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { refetch } = useUser();
|
const { refetchUser } = useAuthUser();
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
const [username, setUsername] = useState(params.get('email') || '');
|
const [username, setUsername] = useState(params.get('email') || '');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const { passwordAuth } = useAuthApi();
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
usernameError: '',
|
usernameError: '',
|
||||||
passwordError: '',
|
passwordError: '',
|
||||||
@ -51,12 +53,9 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = { username, password };
|
|
||||||
const path = evt.target.action;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await passwordLogin(path, user);
|
await passwordAuth(authDetails.path, username, password);
|
||||||
refetch();
|
refetchUser();
|
||||||
history.push(`/`);
|
history.push(`/`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.statusCode === 404 || error.statusCode === 400) {
|
if (error.statusCode === 404 || error.statusCode === 400) {
|
||||||
@ -85,7 +84,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!authDetails.defaultHidden}
|
condition={!authDetails.defaultHidden}
|
||||||
show={
|
show={
|
||||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
<form onSubmit={handleSubmit}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={apiError}
|
condition={apiError}
|
||||||
show={
|
show={
|
||||||
@ -169,8 +168,6 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
|||||||
|
|
||||||
PasswordAuth.propTypes = {
|
PasswordAuth.propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
passwordLogin: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordAuth;
|
export default PasswordAuth;
|
||||||
|
@ -1,23 +1,30 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, TextField } from '@material-ui/core';
|
import { Button, TextField } from '@material-ui/core';
|
||||||
|
|
||||||
import styles from './SimpleAuth.module.scss';
|
import styles from './SimpleAuth.module.scss';
|
||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useAuthApi } from '../../../hooks/api/actions/useAuthApi/useAuthApi';
|
||||||
|
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
import { LOGIN_BUTTON, LOGIN_EMAIL_ID } from '../../../testIds';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
|
||||||
const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
const SimpleAuth = ({ authDetails }) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const { refetch } = useUser();
|
const { refetchUser } = useAuthUser();
|
||||||
|
const { emailAuth } = useAuthApi();
|
||||||
|
const history = useHistory();
|
||||||
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const handleSubmit = evt => {
|
const handleSubmit = async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const user = { email };
|
|
||||||
const path = evt.target.action;
|
|
||||||
|
|
||||||
insecureLogin(path, user).then(() => {
|
try {
|
||||||
refetch();
|
await emailAuth(authDetails.path, email);
|
||||||
|
refetchUser();
|
||||||
history.push(`/`);
|
history.push(`/`);
|
||||||
});
|
} catch (e) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
@ -26,7 +33,7 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<p>{authDetails.message}</p>
|
<p>{authDetails.message}</p>
|
||||||
<p>
|
<p>
|
||||||
@ -50,6 +57,7 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
|||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
type="email"
|
type="email"
|
||||||
|
data-test={LOGIN_EMAIL_ID}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@ -58,8 +66,8 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
data-test="login-submit"
|
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
data-test={LOGIN_BUTTON}
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
@ -71,8 +79,6 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
|||||||
|
|
||||||
SimpleAuth.propTypes = {
|
SimpleAuth.propTypes = {
|
||||||
authDetails: PropTypes.object.isRequired,
|
authDetails: PropTypes.object.isRequired,
|
||||||
insecureLogin: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SimpleAuth;
|
export default SimpleAuth;
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import SimpleAuth from './SimpleAuth';
|
|
||||||
|
|
||||||
export default SimpleAuth;
|
|
@ -1,16 +1,20 @@
|
|||||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
|
||||||
import UserProfile from './UserProfile';
|
import UserProfile from './UserProfile';
|
||||||
import { useLocationSettings } from '../../../hooks/useLocationSettings';
|
import { useLocationSettings } from '../../../hooks/useLocationSettings';
|
||||||
|
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
|
||||||
const UserProfileContainer = () => {
|
const UserProfileContainer = () => {
|
||||||
const user = useUser();
|
|
||||||
const { locationSettings, setLocationSettings } = useLocationSettings();
|
const { locationSettings, setLocationSettings } = useLocationSettings();
|
||||||
|
const { user } = useAuthUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserProfile
|
<UserProfile
|
||||||
locationSettings={locationSettings}
|
locationSettings={locationSettings}
|
||||||
setLocationSettings={setLocationSettings}
|
setLocationSettings={setLocationSettings}
|
||||||
profile={user.user}
|
profile={user}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Button } from '@material-ui/core';
|
import { Button } from '@material-ui/core';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useCommonStyles } from '../../../../common.styles';
|
import { useCommonStyles } from '../../../../common.styles';
|
||||||
import { IAuthOptions } from '../../../../interfaces/user';
|
|
||||||
import { ReactComponent as GoogleSvg } from '../../../../assets/icons/google.svg';
|
import { ReactComponent as GoogleSvg } from '../../../../assets/icons/google.svg';
|
||||||
import LockRounded from '@material-ui/icons/LockRounded';
|
import LockRounded from '@material-ui/icons/LockRounded';
|
||||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||||
|
import { IAuthOptions } from '../../../../hooks/api/getters/useAuth/useAuthEndpoint';
|
||||||
|
import { SSO_LOGIN_BUTTON } from '../../../../testIds';
|
||||||
|
|
||||||
interface IAuthOptionProps {
|
interface IAuthOptionProps {
|
||||||
options?: IAuthOptions[];
|
options?: IAuthOptions[];
|
||||||
@ -28,6 +29,7 @@ const AuthOptions = ({ options }: IAuthOptionProps) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
href={o.path}
|
href={o.path}
|
||||||
size="small"
|
size="small"
|
||||||
|
data-test={`${SSO_LOGIN_BUTTON}-${o.type}`}
|
||||||
style={{ height: '40px', color: '#000' }}
|
style={{ height: '40px', color: '#000' }}
|
||||||
startIcon={
|
startIcon={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const AccessContext = React.createContext()
|
|
||||||
|
|
||||||
export default AccessContext;
|
|
21
frontend/src/contexts/AccessContext.ts
Normal file
21
frontend/src/contexts/AccessContext.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface IAccessContext {
|
||||||
|
isAdmin: boolean;
|
||||||
|
hasAccess: (
|
||||||
|
permission: string,
|
||||||
|
project?: string,
|
||||||
|
environment?: string
|
||||||
|
) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccessPlaceholder = () => {
|
||||||
|
throw new Error('hasAccess called outside AccessContext');
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccessContext = React.createContext<IAccessContext>({
|
||||||
|
isAdmin: false,
|
||||||
|
hasAccess: hasAccessPlaceholder,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AccessContext;
|
@ -1,5 +1,3 @@
|
|||||||
import { IUserPayload } from '../../../../interfaces/user';
|
|
||||||
|
|
||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
import {
|
import {
|
||||||
handleBadRequest,
|
handleBadRequest,
|
||||||
@ -16,6 +14,12 @@ export interface IUserApiErrors {
|
|||||||
validatePassword?: string;
|
validatePassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IUserPayload {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const ADD_USER_ERROR = 'addUser';
|
export const ADD_USER_ERROR = 'addUser';
|
||||||
export const UPDATE_USER_ERROR = 'updateUser';
|
export const UPDATE_USER_ERROR = 'updateUser';
|
||||||
export const REMOVE_USER_ERROR = 'removeUser';
|
export const REMOVE_USER_ERROR = 'removeUser';
|
||||||
|
51
frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx
Normal file
51
frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
type PasswordLogin = (
|
||||||
|
path: string,
|
||||||
|
username: string,
|
||||||
|
password: string
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
type EmailLogin = (path: string, email: string) => Promise<Response>;
|
||||||
|
|
||||||
|
interface IUseAuthApiOutput {
|
||||||
|
passwordAuth: PasswordLogin;
|
||||||
|
emailAuth: EmailLogin;
|
||||||
|
errors: Record<string, string>;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthApi = (): IUseAuthApiOutput => {
|
||||||
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordAuth = (path: string, username: string, password: string) => {
|
||||||
|
const req = createRequest(ensureRelativePath(path), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailAuth = (path: string, email: string) => {
|
||||||
|
const req = createRequest(ensureRelativePath(path), {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
passwordAuth,
|
||||||
|
emailAuth,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureRelativePath = (path: string): string => {
|
||||||
|
return path.replace(/^\//, '');
|
||||||
|
};
|
24
frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts
Normal file
24
frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
IAuthEndpointDetailsResponse,
|
||||||
|
useAuthEndpoint,
|
||||||
|
} from './useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthDetailsOutput {
|
||||||
|
authDetails?: IAuthEndpointDetailsResponse;
|
||||||
|
refetchAuthDetails: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthDetails = (): IUseAuthDetailsOutput => {
|
||||||
|
const auth = useAuthEndpoint();
|
||||||
|
const authDetails =
|
||||||
|
auth.data && 'type' in auth.data ? auth.data : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
authDetails,
|
||||||
|
refetchAuthDetails: auth.refetchAuth,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
};
|
||||||
|
};
|
84
frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts
Normal file
84
frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import useSWR, { mutate } from 'swr';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import { IPermission, IUser } from '../../../../interfaces/user';
|
||||||
|
|
||||||
|
// The auth endpoint returns different things depending on the auth status.
|
||||||
|
// When the user is logged in, the endpoint returns user data and permissions.
|
||||||
|
// When the user is logged out, the endpoint returns details on how to log in.
|
||||||
|
type AuthEndpointResponse =
|
||||||
|
| IAuthEndpointUserResponse
|
||||||
|
| IAuthEndpointDetailsResponse;
|
||||||
|
|
||||||
|
export interface IAuthEndpointUserResponse {
|
||||||
|
user: IUser;
|
||||||
|
feedback: IAuthFeedback[];
|
||||||
|
permissions: IPermission[];
|
||||||
|
splash: IAuthSplash;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthEndpointDetailsResponse {
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
defaultHidden: boolean;
|
||||||
|
options: IAuthOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthOptions {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthFeedback {
|
||||||
|
neverShow: boolean;
|
||||||
|
feedbackId: string;
|
||||||
|
given?: string;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthSplash {
|
||||||
|
[key: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUseAuthEndpointOutput {
|
||||||
|
data?: AuthEndpointResponse;
|
||||||
|
refetchAuth: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This helper hook returns the raw response data from the user auth endpoint.
|
||||||
|
// Check out the other hooks in this directory for more ergonomic alternatives.
|
||||||
|
export const useAuthEndpoint = (): IUseAuthEndpointOutput => {
|
||||||
|
const { data, error } = useSWR<AuthEndpointResponse>(
|
||||||
|
USER_ENDPOINT_PATH,
|
||||||
|
fetchAuthStatus,
|
||||||
|
swrConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
const refetchAuth = useCallback(() => {
|
||||||
|
mutate(USER_ENDPOINT_PATH).catch(console.warn);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
refetchAuth,
|
||||||
|
loading: !error && !data,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAuthStatus = (): Promise<AuthEndpointResponse> => {
|
||||||
|
return fetch(USER_ENDPOINT_PATH).then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
const swrConfig = {
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshInterval: 15000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const USER_ENDPOINT_PATH = formatApiPath(`api/admin/user`);
|
21
frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts
Normal file
21
frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { IAuthFeedback, useAuthEndpoint } from './useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthFeedbackOutput {
|
||||||
|
feedback?: IAuthFeedback[];
|
||||||
|
refetchFeedback: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthFeedback = (): IUseAuthFeedbackOutput => {
|
||||||
|
const auth = useAuthEndpoint();
|
||||||
|
const feedback =
|
||||||
|
auth.data && 'feedback' in auth.data ? auth.data.feedback : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
feedback,
|
||||||
|
refetchFeedback: auth.refetchAuth,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
};
|
||||||
|
};
|
24
frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts
Normal file
24
frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { IPermission } from '../../../../interfaces/user';
|
||||||
|
import { useAuthEndpoint } from './useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthPermissionsOutput {
|
||||||
|
permissions?: IPermission[];
|
||||||
|
refetchPermissions: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthPermissions = (): IUseAuthPermissionsOutput => {
|
||||||
|
const auth = useAuthEndpoint();
|
||||||
|
const permissions =
|
||||||
|
auth.data && 'permissions' in auth.data
|
||||||
|
? auth.data.permissions
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions,
|
||||||
|
refetchPermissions: auth.refetchAuth,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
};
|
||||||
|
};
|
21
frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts
Normal file
21
frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { IAuthSplash, useAuthEndpoint } from './useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthSplashOutput {
|
||||||
|
splash?: IAuthSplash;
|
||||||
|
refetchSplash: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthSplash = (): IUseAuthSplashOutput => {
|
||||||
|
const auth = useAuthEndpoint();
|
||||||
|
const splash =
|
||||||
|
auth.data && 'splash' in auth.data ? auth.data.splash : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
splash,
|
||||||
|
refetchSplash: auth.refetchAuth,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
};
|
||||||
|
};
|
21
frontend/src/hooks/api/getters/useAuth/useAuthUser.ts
Normal file
21
frontend/src/hooks/api/getters/useAuth/useAuthUser.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { IUser } from '../../../../interfaces/user';
|
||||||
|
import { useAuthEndpoint } from './useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthUserOutput {
|
||||||
|
user?: IUser;
|
||||||
|
refetchUser: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthUser = (): IUseAuthUserOutput => {
|
||||||
|
const auth = useAuthEndpoint();
|
||||||
|
const user = auth.data && 'user' in auth.data ? auth.data.user : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
refetchUser: auth.refetchAuth,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
};
|
||||||
|
};
|
@ -1,58 +0,0 @@
|
|||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { formatApiPath } from '../../../../utils/format-path';
|
|
||||||
import { IPermission } from '../../../../interfaces/user';
|
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
|
||||||
|
|
||||||
export const USER_CACHE_KEY = `api/admin/user`;
|
|
||||||
const NO_AUTH_USERNAME = 'unknown';
|
|
||||||
|
|
||||||
const useUser = (
|
|
||||||
options: SWRConfiguration = {
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
refreshInterval: 15000,
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const fetcher = () => {
|
|
||||||
const path = formatApiPath(`api/admin/user`);
|
|
||||||
return fetch(path, {
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
.then(handleErrorResponses('User info'))
|
|
||||||
.then(res => res.json());
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error } = useSWR(USER_CACHE_KEY, fetcher, options);
|
|
||||||
const [loading, setLoading] = useState(!error && !data);
|
|
||||||
|
|
||||||
const refetch = () => {
|
|
||||||
mutate(USER_CACHE_KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(!error && !data);
|
|
||||||
}, [data, error]);
|
|
||||||
|
|
||||||
let user = data?.user;
|
|
||||||
// Set a user id if no authentication is on
|
|
||||||
// to cancel the loader.
|
|
||||||
|
|
||||||
if (data && user?.username === NO_AUTH_USERNAME) {
|
|
||||||
user = { ...user, id: 1 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: user || {},
|
|
||||||
permissions: (data?.permissions || []) as IPermission[],
|
|
||||||
feedback: data?.feedback || [],
|
|
||||||
splash: data?.splash || {},
|
|
||||||
authDetails: data || undefined,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useUser;
|
|
@ -1,8 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import AccessContext from '../contexts/AccessContext';
|
|
||||||
|
|
||||||
const usePermissions = () => {
|
|
||||||
return useContext(AccessContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default usePermissions;
|
|
@ -43,7 +43,7 @@ ReactDOM.render(
|
|||||||
<Provider store={unleashStore}>
|
<Provider store={unleashStore}>
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<UIProvider>
|
<UIProvider>
|
||||||
<AccessProvider store={unleashStore}>
|
<AccessProvider>
|
||||||
<Router basename={`${getBasePath()}`}>
|
<Router basename={`${getBasePath()}`}>
|
||||||
<ThemeProvider theme={mainTheme}>
|
<ThemeProvider theme={mainTheme}>
|
||||||
<StylesProvider injectFirst>
|
<StylesProvider injectFirst>
|
||||||
|
@ -1,34 +1,3 @@
|
|||||||
export interface IAuthStatus {
|
|
||||||
authDetails: IAuthDetails;
|
|
||||||
showDialog: boolean;
|
|
||||||
profile?: IUser;
|
|
||||||
permissions: IPermission[];
|
|
||||||
splash: ISplash;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISplash {
|
|
||||||
[key: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPermission {
|
|
||||||
permission: string;
|
|
||||||
project: string;
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAuthDetails {
|
|
||||||
type: string;
|
|
||||||
path: string;
|
|
||||||
message: string;
|
|
||||||
options: IAuthOptions[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAuthOptions {
|
|
||||||
type: string;
|
|
||||||
message: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
@ -43,14 +12,8 @@ export interface IUser {
|
|||||||
username?: string;
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserPayload {
|
export interface IPermission {
|
||||||
name: string;
|
permission: string;
|
||||||
email: string;
|
project?: string;
|
||||||
id?: string;
|
environment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAddedUser extends IUser {
|
|
||||||
emailSent?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IAuthStatus;
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { fromJS, List, Map } from 'immutable';
|
import { fromJS, List, Map } from 'immutable';
|
||||||
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD, DELETE_APPLICATION } from './actions';
|
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD, DELETE_APPLICATION } from './actions';
|
||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
return fromJS({ list: [], apps: {} });
|
return fromJS({ list: [], apps: {} });
|
||||||
@ -19,9 +18,6 @@ const store = (state = getInitState(), action) => {
|
|||||||
const result = state.removeIn(['list', index]);
|
const result = state.removeIn(['list', index]);
|
||||||
return result.removeIn(['apps', action.appName]);
|
return result.removeIn(['apps', action.appName]);
|
||||||
}
|
}
|
||||||
case USER_LOGOUT:
|
|
||||||
case USER_LOGIN:
|
|
||||||
return getInitState();
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,6 @@ import {
|
|||||||
TOGGLE_FEATURE_TOGGLE,
|
TOGGLE_FEATURE_TOGGLE,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
|
||||||
|
|
||||||
const debug = require('debug')('unleash:feature-store');
|
const debug = require('debug')('unleash:feature-store');
|
||||||
|
|
||||||
const features = (state = new List([]), action) => {
|
const features = (state = new List([]), action) => {
|
||||||
@ -62,10 +60,6 @@ const features = (state = new List([]), action) => {
|
|||||||
case RECEIVE_FEATURE_TOGGLES:
|
case RECEIVE_FEATURE_TOGGLES:
|
||||||
debug(RECEIVE_FEATURE_TOGGLES, action);
|
debug(RECEIVE_FEATURE_TOGGLES, action);
|
||||||
return new List(action.featureToggles.map($Map));
|
return new List(action.featureToggles.map($Map));
|
||||||
case USER_LOGIN:
|
|
||||||
case USER_LOGOUT:
|
|
||||||
debug(USER_LOGOUT, action);
|
|
||||||
return new List([]);
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { combineReducers } from 'redux';
|
|||||||
import features from './feature-toggle';
|
import features from './feature-toggle';
|
||||||
import strategies from './strategy';
|
import strategies from './strategy';
|
||||||
import error from './error';
|
import error from './error';
|
||||||
import user from './user';
|
|
||||||
import applications from './application';
|
import applications from './application';
|
||||||
import projects from './project';
|
import projects from './project';
|
||||||
import apiCalls from './api-calls';
|
import apiCalls from './api-calls';
|
||||||
@ -11,7 +10,6 @@ const unleashStore = combineReducers({
|
|||||||
features,
|
features,
|
||||||
strategies,
|
strategies,
|
||||||
error,
|
error,
|
||||||
user,
|
|
||||||
applications,
|
applications,
|
||||||
projects,
|
projects,
|
||||||
apiCalls,
|
apiCalls,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { List } from 'immutable';
|
import { List } from 'immutable';
|
||||||
import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions';
|
import { RECEIVE_PROJECT, REMOVE_PROJECT, ADD_PROJECT, UPDATE_PROJECT } from './actions';
|
||||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
|
||||||
|
|
||||||
const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', initial: true }];
|
const DEFAULT_PROJECTS = [{ id: 'default', name: 'Default', initial: true }];
|
||||||
|
|
||||||
@ -22,9 +21,6 @@ const strategies = (state = getInitState(), action) => {
|
|||||||
const index = state.findIndex(item => item.id === action.project.id);
|
const index = state.findIndex(item => item.id === action.project.id);
|
||||||
return state.set(index, action.project);
|
return state.set(index, action.project);
|
||||||
}
|
}
|
||||||
case USER_LOGOUT:
|
|
||||||
case USER_LOGIN:
|
|
||||||
return getInitState();
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import api from './api';
|
|
||||||
import { dispatchError } from '../util';
|
|
||||||
export const USER_CHANGE_CURRENT = 'USER_CHANGE_CURRENT';
|
|
||||||
export const USER_LOGOUT = 'USER_LOGOUT';
|
|
||||||
export const USER_LOGIN = 'USER_LOGIN';
|
|
||||||
export const START_FETCH_USER = 'START_FETCH_USER';
|
|
||||||
export const ERROR_FETCH_USER = 'ERROR_FETCH_USER';
|
|
||||||
const debug = require('debug')('unleash:user-actions');
|
|
||||||
|
|
||||||
const updateUser = value => ({
|
|
||||||
type: USER_CHANGE_CURRENT,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleError(error) {
|
|
||||||
debug(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchUser() {
|
|
||||||
debug('Start fetching user');
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: START_FETCH_USER });
|
|
||||||
|
|
||||||
return api
|
|
||||||
.fetchUser()
|
|
||||||
.then(json => dispatch(updateUser(json)))
|
|
||||||
.catch(dispatchError(dispatch, ERROR_FETCH_USER));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insecureLogin(path, user) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: START_FETCH_USER });
|
|
||||||
|
|
||||||
return api
|
|
||||||
.insecureLogin(path, user)
|
|
||||||
.then(json => dispatch(updateUser(json)))
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function demoLogin(path, user) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: START_FETCH_USER });
|
|
||||||
|
|
||||||
return api
|
|
||||||
.demoLogin(path, user)
|
|
||||||
.then(json => dispatch(updateUser(json)))
|
|
||||||
.catch(handleError);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function passwordLogin(path, user) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({ type: START_FETCH_USER });
|
|
||||||
|
|
||||||
return api
|
|
||||||
.passwordLogin(path, user)
|
|
||||||
.then(json => dispatch(updateUser(json)))
|
|
||||||
.then(() => dispatch({ type: USER_LOGIN }));
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import { formatApiPath } from '../../utils/format-path';
|
|
||||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
|
||||||
|
|
||||||
const URI = formatApiPath('api/admin/user');
|
|
||||||
|
|
||||||
function fetchUser() {
|
|
||||||
return fetch(URI, { credentials: 'include' })
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function insecureLogin(path, user) {
|
|
||||||
return fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(user),
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function demoLogin(path, user) {
|
|
||||||
return fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(user),
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
function passwordLogin(path, data) {
|
|
||||||
return fetch(path, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
.then(throwIfNotSuccess)
|
|
||||||
.then(response => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
fetchUser,
|
|
||||||
insecureLogin,
|
|
||||||
demoLogin,
|
|
||||||
passwordLogin,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default api;
|
|
@ -1,28 +0,0 @@
|
|||||||
import { Map as $Map } from 'immutable';
|
|
||||||
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
|
||||||
import { AUTH_REQUIRED } from '../util';
|
|
||||||
|
|
||||||
const userStore = (state = new $Map({ permissions: [] }), action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case USER_CHANGE_CURRENT:
|
|
||||||
state = state
|
|
||||||
.set('profile', action.value.user)
|
|
||||||
.set('permissions', action.value.permissions || [])
|
|
||||||
.set('feedback', action.value.feedback || [])
|
|
||||||
.set('splash', action.value.splash || {})
|
|
||||||
.set('showDialog', false)
|
|
||||||
.set('authDetails', undefined);
|
|
||||||
return state;
|
|
||||||
case AUTH_REQUIRED:
|
|
||||||
state = state
|
|
||||||
.set('authDetails', action.error.body)
|
|
||||||
.set('showDialog', true);
|
|
||||||
return state;
|
|
||||||
case USER_LOGOUT:
|
|
||||||
return new $Map({ permissions: [] });
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default userStore;
|
|
@ -15,6 +15,8 @@ export const CF_CREATE_BTN_ID = 'CF_CREATE_BTN_ID';
|
|||||||
export const LOGIN_EMAIL_ID = 'LOGIN_EMAIL_ID';
|
export const LOGIN_EMAIL_ID = 'LOGIN_EMAIL_ID';
|
||||||
export const LOGIN_BUTTON = 'LOGIN_BUTTON';
|
export const LOGIN_BUTTON = 'LOGIN_BUTTON';
|
||||||
export const LOGIN_PASSWORD_ID = 'LOGIN_PASSWORD_ID';
|
export const LOGIN_PASSWORD_ID = 'LOGIN_PASSWORD_ID';
|
||||||
|
export const SSO_LOGIN_BUTTON = 'SSO_LOGIN_BUTTON';
|
||||||
|
export const FORGOTTEN_PASSWORD_FIELD = 'FORGOTTEN_PASSWORD_FIELD';
|
||||||
|
|
||||||
/* STRATEGY */
|
/* STRATEGY */
|
||||||
export const ADD_NEW_STRATEGY_ID = 'ADD_NEW_STRATEGY_ID';
|
export const ADD_NEW_STRATEGY_ID = 'ADD_NEW_STRATEGY_ID';
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { ADMIN } from '../component/providers/AccessProvider/permissions';
|
import { ADMIN } from '../component/providers/AccessProvider/permissions';
|
||||||
import IAuthStatus, { IPermission } from '../interfaces/user';
|
import { IPermission } from '../interfaces/user';
|
||||||
|
|
||||||
type objectIdx = {
|
type objectIdx = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectFilterGenerator = (
|
export const projectFilterGenerator = (
|
||||||
user: IAuthStatus,
|
permissions: IPermission[] = [],
|
||||||
matcherPermission: string
|
matcherPermission: string
|
||||||
) => {
|
) => {
|
||||||
let admin = false;
|
let admin = false;
|
||||||
const permissionMap: objectIdx = user.permissions.reduce(
|
const permissionMap: objectIdx = permissions.reduce(
|
||||||
(acc: objectIdx, current: IPermission) => {
|
(acc: objectIdx, current: IPermission) => {
|
||||||
if (current.permission === ADMIN) {
|
if (current.permission === ADMIN) {
|
||||||
admin = true;
|
admin = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user