mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +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
|
||||
on: [deployment_status]
|
||||
jobs:
|
||||
@ -20,5 +20,6 @@ jobs:
|
||||
env: AUTH_TOKEN=${{ secrets.UNLEASH_TOKEN }},DEFAULT_ENV="development"
|
||||
config: baseUrl=${{ github.event.deployment_status.target_url }}
|
||||
record: true
|
||||
spec: cypress/integration/auth/auth.spec.js
|
||||
env:
|
||||
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';
|
||||
|
||||
export const createFakeStore = (permissions) => {
|
||||
export const createFakeStore = permissions => {
|
||||
return {
|
||||
getState: () => ({
|
||||
user:
|
||||
new $MAp({
|
||||
permissions
|
||||
})
|
||||
user: new $MAp({
|
||||
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 EnvironmentSplash from './common/EnvironmentSplash/EnvironmentSplash';
|
||||
import Feedback from './common/Feedback/Feedback';
|
||||
import LayoutPicker from './layout/LayoutPicker/LayoutPicker';
|
||||
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 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 {
|
||||
user: IAuthStatus;
|
||||
fetchUiBootstrap: any;
|
||||
fetchUiBootstrap: () => void;
|
||||
}
|
||||
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);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
export const App = ({ fetchUiBootstrap }: IAppProps) => {
|
||||
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(() => {
|
||||
fetchUiBootstrap();
|
||||
/* eslint-disable-next-line */
|
||||
}, [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]);
|
||||
}, [fetchUiBootstrap, authDetails?.type]);
|
||||
|
||||
const renderMainLayoutRoutes = () => {
|
||||
return routes.filter(route => route.layout === 'main').map(renderRoute);
|
||||
@ -63,10 +43,8 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
.map(renderRoute);
|
||||
};
|
||||
|
||||
const isUnauthorized = () => {
|
||||
// authDetails only exists if the user is not logged in.
|
||||
//if (user?.permissions.length === 0) return true;
|
||||
return user?.authDetails !== undefined;
|
||||
const isUnauthorized = (): boolean => {
|
||||
return !isLoggedIn;
|
||||
};
|
||||
|
||||
// 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
|
||||
{...props}
|
||||
isUnauthorized={isUnauthorized}
|
||||
authDetails={user.authDetails}
|
||||
authDetails={authDetails}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -99,21 +77,18 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SWRProvider
|
||||
isUnauthorized={isUnauthorized}
|
||||
setShowLoader={setShowLoader}
|
||||
>
|
||||
<SWRProvider isUnauthorized={isUnauthorized}>
|
||||
<ConditionallyRender
|
||||
condition={showLoader}
|
||||
condition={!hasFetchedAuth}
|
||||
show={<Loader />}
|
||||
elseShow={
|
||||
<div className={styles.container}>
|
||||
<ToastRenderer />
|
||||
|
||||
<ConditionallyRender
|
||||
condition={showSplash}
|
||||
condition={showEnvSplash}
|
||||
show={
|
||||
<EnvironmentSplash onFinish={setShowSplash} />
|
||||
<EnvironmentSplash onFinish={refetchSplash} />
|
||||
}
|
||||
elseShow={
|
||||
<LayoutPicker location={location}>
|
||||
@ -133,9 +108,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
/>
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
<Feedback
|
||||
openUrl="http://feedback.unleash.run"
|
||||
/>
|
||||
<Feedback openUrl="http://feedback.unleash.run" />
|
||||
</LayoutPicker>
|
||||
}
|
||||
/>
|
||||
@ -145,10 +118,3 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
</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 App from './App';
|
||||
|
||||
import { App } from './App';
|
||||
import { fetchUiBootstrap } from '../store/ui-bootstrap/actions';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUiBootstrap,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any) => ({
|
||||
user: state.user.toJS(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
export default connect(null, { fetchUiBootstrap })(App);
|
||||
|
@ -1,17 +1,17 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import ApiTokenList from '../api-token/ApiTokenList/ApiTokenList';
|
||||
|
||||
import AdminMenu from '../menu/AdminMenu';
|
||||
import usePermissions from '../../../hooks/usePermissions';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const ApiPage = ({ history }) => {
|
||||
const { isAdmin } = usePermissions();
|
||||
const { isAdmin } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConditionallyRender
|
||||
condition={isAdmin()}
|
||||
condition={isAdmin}
|
||||
show={<AdminMenu history={history} />}
|
||||
/>
|
||||
<ApiTokenList />
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Breadcrumbs from '@material-ui/core/Breadcrumbs';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import usePermissions from '../../../hooks/usePermissions';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
import { useStyles } from './BreadcrumbNav.styles';
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const BreadcrumbNav = () => {
|
||||
const { isAdmin } = usePermissions();
|
||||
const { isAdmin } = useContext(AccessContext);
|
||||
const styles = useStyles();
|
||||
const location = useLocation();
|
||||
|
||||
@ -23,16 +24,16 @@ const BreadcrumbNav = () => {
|
||||
item !== 'copy' &&
|
||||
item !== 'strategies' &&
|
||||
item !== 'features' &&
|
||||
item !== 'features2' &&
|
||||
item !== 'create-toggle'&&
|
||||
item !== 'settings'
|
||||
item !== 'features2' &&
|
||||
item !== 'create-toggle'&&
|
||||
item !== 'settings'
|
||||
|
||||
);
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
(location.pathname.includes('admin') && isAdmin()) ||
|
||||
(location.pathname.includes('admin') && isAdmin) ||
|
||||
!location.pathname.includes('admin')
|
||||
}
|
||||
show={
|
||||
|
@ -2,7 +2,6 @@ import { useContext, useState } from 'react';
|
||||
import { Button, IconButton } from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
import { ReactComponent as Logo } from '../../../assets/icons/logo-plain.svg';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import { useStyles } from './Feedback.styles';
|
||||
@ -10,8 +9,8 @@ import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||
import ConditionallyRender from '../ConditionallyRender';
|
||||
import { formatApiPath } from '../../../utils/format-path';
|
||||
import UIContext from '../../../contexts/UIContext';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
|
||||
import { useAuthFeedback } from '../../../hooks/api/getters/useAuth/useAuthFeedback';
|
||||
|
||||
interface IFeedbackProps {
|
||||
openUrl: string;
|
||||
@ -19,7 +18,7 @@ interface IFeedbackProps {
|
||||
|
||||
const Feedback = ({ openUrl }: IFeedbackProps) => {
|
||||
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
||||
const { refetch, feedback } = useUser();
|
||||
const { feedback, refetchFeedback } = useAuthFeedback();
|
||||
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
||||
const styles = useStyles();
|
||||
const commonStyles = useCommonStyles();
|
||||
@ -37,7 +36,7 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
||||
},
|
||||
body: JSON.stringify({ feedbackId }),
|
||||
});
|
||||
await refetch();
|
||||
await refetchFeedback();
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
setShowFeedback(false);
|
||||
@ -65,7 +64,7 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
||||
},
|
||||
body: JSON.stringify({ feedbackId, neverShow: true }),
|
||||
});
|
||||
await refetch();
|
||||
await refetchFeedback();
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
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 useFeatureTypes from '../../../hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||
import { KeyboardArrowDownOutlined } from '@material-ui/icons';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { projectFilterGenerator } from '../../../utils/project-filter-generator';
|
||||
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
@ -12,6 +11,8 @@ import { trim } from '../../common/util';
|
||||
import Input from '../../common/Input/Input';
|
||||
import { CREATE_FEATURE } from '../../providers/AccessProvider/permissions';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||
|
||||
interface IFeatureToggleForm {
|
||||
type: string;
|
||||
@ -54,7 +55,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
||||
const styles = useStyles();
|
||||
const { featureTypes } = useFeatureTypes();
|
||||
const history = useHistory();
|
||||
const { permissions } = useUser();
|
||||
const { permissions } = useAuthPermissions()
|
||||
const editable = mode !== 'Edit';
|
||||
|
||||
const renderToggleDescription = () => {
|
||||
@ -114,7 +115,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
||||
}}
|
||||
enabled={editable}
|
||||
filter={projectFilterGenerator(
|
||||
{ permissions },
|
||||
permissions,
|
||||
CREATE_FEATURE
|
||||
)}
|
||||
IconComponent={KeyboardArrowDownOutlined}
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
import FeatureToggleList from '../FeatureToggleList';
|
||||
import renderer from 'react-test-renderer';
|
||||
import theme from '../../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../../accessStoreFake';
|
||||
import {
|
||||
ADMIN,
|
||||
CREATE_FEATURE,
|
||||
} from '../../../providers/AccessProvider/permissions';
|
||||
import { CREATE_FEATURE } from '../../../providers/AccessProvider/permissions';
|
||||
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||
|
||||
jest.mock('../FeatureToggleListItem', () => ({
|
||||
@ -29,9 +24,7 @@ test('renders correctly with one feature', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
||||
>
|
||||
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}>
|
||||
<FeatureToggleList
|
||||
updateSetting={jest.fn()}
|
||||
filter={{}}
|
||||
@ -59,9 +52,7 @@ test('renders correctly with one feature without permissions', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: CREATE_FEATURE }])}
|
||||
>
|
||||
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}>
|
||||
<FeatureToggleList
|
||||
filter={{}}
|
||||
setFilter={jest.fn()}
|
||||
|
@ -3,7 +3,6 @@ import { useHistory, useParams } from 'react-router';
|
||||
import AccessContext from '../../../../../contexts/AccessContext';
|
||||
import useFeatureApi from '../../../../../hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useFeature from '../../../../../hooks/api/getters/useFeature/useFeature';
|
||||
import useUser from '../../../../../hooks/api/getters/useUser/useUser';
|
||||
import useToast from '../../../../../hooks/useToast';
|
||||
import { IFeatureViewParams } from '../../../../../interfaces/params';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../../../../providers/AccessProvider/permissions';
|
||||
@ -12,6 +11,7 @@ import PermissionButton from '../../../../common/PermissionButton/PermissionButt
|
||||
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
||||
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
||||
import { IPermission } from '../../../../../interfaces/user';
|
||||
import { useAuthPermissions } from '../../../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||
|
||||
const FeatureSettingsProject = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
@ -21,11 +21,12 @@ const FeatureSettingsProject = () => {
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
const editable = hasAccess(MOVE_FEATURE_TOGGLE, projectId);
|
||||
const { permissions } = useUser();
|
||||
const { permissions = [] } = useAuthPermissions()
|
||||
const { changeFeatureProject } = useFeatureApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (project !== feature.project) {
|
||||
setDirty(true);
|
||||
@ -43,7 +44,7 @@ const FeatureSettingsProject = () => {
|
||||
setProject(projectId);
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [permissions?.length]);
|
||||
}, [permissions.length]);
|
||||
|
||||
const updateProject = async () => {
|
||||
const newProject = project;
|
||||
|
@ -15,19 +15,19 @@ import { useStyles } from './Header.styles';
|
||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { IPermission } from '../../../interfaces/user';
|
||||
import NavigationMenu from './NavigationMenu/NavigationMenu';
|
||||
import { getRoutes } from '../routes';
|
||||
import { KeyboardArrowDown } from '@material-ui/icons';
|
||||
import { filterByFlags } from '../../common/util';
|
||||
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||
|
||||
const Header = () => {
|
||||
const theme = useTheme();
|
||||
const [anchorEl, setAnchorEl] = useState();
|
||||
const [anchorElAdvanced, setAnchorElAdvanced] = useState();
|
||||
const [admin, setAdmin] = useState(false);
|
||||
const { permissions } = useUser();
|
||||
const { permissions } = useAuthPermissions()
|
||||
const commonStyles = useCommonStyles();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const smallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@ -39,7 +39,7 @@ const Header = () => {
|
||||
const handleCloseAdvanced = () => setAnchorElAdvanced(null);
|
||||
|
||||
useEffect(() => {
|
||||
const admin = permissions.find(
|
||||
const admin = permissions?.find(
|
||||
(element: IPermission) => element.permission === ADMIN
|
||||
);
|
||||
|
||||
|
@ -15,7 +15,7 @@ import AdminUsers from '../admin/users/UsersAdmin';
|
||||
import { AuthSettings } from '../admin/auth/AuthSettings';
|
||||
import Login from '../user/Login/Login';
|
||||
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 ForgottenPassword from '../user/ForgottenPassword/ForgottenPassword';
|
||||
import ProjectListNew from '../project/ProjectList/ProjectList';
|
||||
|
@ -5,13 +5,13 @@ import ProjectForm from '../ProjectForm/ProjectForm';
|
||||
import useProjectForm from '../hooks/useProjectForm';
|
||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
import useUser from '../../../../hooks/api/getters/useUser/useUser';
|
||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
||||
import { useAuthUser } from '../../../../hooks/api/getters/useAuth/useAuthUser';
|
||||
|
||||
const CreateProject = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetch } = useUser();
|
||||
const { refetchUser } = useAuthUser();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const history = useHistory();
|
||||
const {
|
||||
@ -40,7 +40,7 @@ const CreateProject = () => {
|
||||
const payload = getProjectPayload();
|
||||
try {
|
||||
await createProject(payload);
|
||||
refetch();
|
||||
refetchUser();
|
||||
history.push(`/projects/${projectId}`);
|
||||
setToastData({
|
||||
title: 'Project created',
|
||||
|
@ -1,88 +1,111 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import AccessContext from '../../../contexts/AccessContext';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { ReactElement, ReactNode, useCallback, useMemo } from 'react';
|
||||
import AccessContext, { IAccessContext } from '../../../contexts/AccessContext';
|
||||
import { ADMIN } from './permissions';
|
||||
import { IPermission } from '../../../interfaces/user';
|
||||
import { useAuthPermissions } from '../../../hooks/api/getters/useAuth/useAuthPermissions';
|
||||
|
||||
// TODO: Type up redux store
|
||||
interface IAccessProvider {
|
||||
store: any;
|
||||
interface IAccessProviderProps {
|
||||
children: ReactNode;
|
||||
permissions: IPermission[];
|
||||
}
|
||||
|
||||
interface IPermission {
|
||||
permission: string;
|
||||
project?: string | null;
|
||||
environment: string | null;
|
||||
}
|
||||
// TODO(olav): Mock useAuth instead of using props.permissions in tests.
|
||||
const AccessProvider = (props: IAccessProviderProps): ReactElement => {
|
||||
const auth = useAuthPermissions();
|
||||
const permissions = props.permissions ?? auth.permissions;
|
||||
|
||||
const AccessProvider: FC<IAccessProvider> = ({ store, children }) => {
|
||||
const { permissions } = useUser();
|
||||
const isAdminHigherOrder = () => {
|
||||
let called = false;
|
||||
let result = false;
|
||||
const isAdmin: boolean = useMemo(() => {
|
||||
return checkAdmin(permissions);
|
||||
}, [permissions]);
|
||||
|
||||
return () => {
|
||||
if (called) return result;
|
||||
const permissions = store.getState().user.get('permissions') || [];
|
||||
result = permissions.some(
|
||||
(p: IPermission) => p.permission === ADMIN
|
||||
const hasAccess = useCallback(
|
||||
(permission: string, project?: string, environment?: string) => {
|
||||
return checkPermissions(
|
||||
permissions,
|
||||
permission,
|
||||
project,
|
||||
environment
|
||||
);
|
||||
},
|
||||
[permissions]
|
||||
);
|
||||
|
||||
if (permissions.length > 0) {
|
||||
called = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
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 };
|
||||
const value: IAccessContext = useMemo(
|
||||
() => ({
|
||||
isAdmin,
|
||||
hasAccess,
|
||||
}),
|
||||
[isAdmin, hasAccess]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccessContext.Provider value={context}>
|
||||
{children}
|
||||
<AccessContext.Provider value={value}>
|
||||
{props.children}
|
||||
</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;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { USER_CACHE_KEY } from '../../../hooks/api/getters/useUser/useUser';
|
||||
import { mutate, SWRConfig, useSWRConfig } from 'swr';
|
||||
import { useHistory } from 'react-router';
|
||||
import useToast from '../../../hooks/useToast';
|
||||
import { formatApiPath } from '../../../utils/format-path';
|
||||
import React from 'react';
|
||||
import { USER_ENDPOINT_PATH } from '../../../hooks/api/getters/useAuth/useAuthEndpoint';
|
||||
|
||||
interface ISWRProviderProps {
|
||||
setShowLoader: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isUnauthorized: () => boolean;
|
||||
}
|
||||
|
||||
@ -14,20 +14,18 @@ const INVALID_TOKEN_ERROR = 'InvalidTokenError';
|
||||
const SWRProvider: React.FC<ISWRProviderProps> = ({
|
||||
children,
|
||||
isUnauthorized,
|
||||
setShowLoader,
|
||||
}) => {
|
||||
const { cache } = useSWRConfig();
|
||||
const history = useHistory();
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const handleFetchError = error => {
|
||||
setShowLoader(false);
|
||||
if (error.status === 401) {
|
||||
const path = location.pathname;
|
||||
// Only populate user with authDetails if 401 and
|
||||
// error is not invalid token
|
||||
if (error?.info?.name !== INVALID_TOKEN_ERROR) {
|
||||
mutate(USER_CACHE_KEY, { ...error.info }, false);
|
||||
mutate(USER_ENDPOINT_PATH, { ...error.info }, false);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -1,13 +1,10 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@material-ui/core';
|
||||
|
||||
import StrategiesListComponent from '../StrategiesList/StrategiesList';
|
||||
import renderer from 'react-test-renderer';
|
||||
import theme from '../../../themes/main-theme';
|
||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
||||
import { createFakeStore } from '../../../accessStoreFake';
|
||||
import { ADMIN } from '../../providers/AccessProvider/permissions';
|
||||
|
||||
test('renders correctly with one strategy', () => {
|
||||
const strategy = {
|
||||
@ -17,7 +14,7 @@ test('renders correctly with one strategy', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider store={createFakeStore()}>
|
||||
<AccessProvider>
|
||||
<StrategiesListComponent
|
||||
strategies={[strategy]}
|
||||
fetchStrategies={jest.fn()}
|
||||
@ -42,9 +39,7 @@ test('renders correctly with one strategy without permissions', () => {
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<AccessProvider>
|
||||
<StrategiesListComponent
|
||||
strategies={[strategy]}
|
||||
fetchStrategies={jest.fn()}
|
||||
|
@ -4,7 +4,6 @@ import StrategyDetails from '../strategy-details-component';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import theme from '../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../accessStoreFake';
|
||||
import AccessProvider from '../../providers/AccessProvider/AccessProvider';
|
||||
|
||||
test('renders correctly with one strategy', () => {
|
||||
@ -35,7 +34,7 @@ test('renders correctly with one strategy', () => {
|
||||
];
|
||||
const tree = renderer.create(
|
||||
<MemoryRouter>
|
||||
<AccessProvider store={createFakeStore()}>
|
||||
<AccessProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<StrategyDetails
|
||||
strategyName={'Another'}
|
||||
|
@ -4,13 +4,11 @@ import renderer from 'react-test-renderer';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@material-ui/styles';
|
||||
import theme from '../../../../themes/main-theme';
|
||||
import { createFakeStore } from '../../../../accessStoreFake';
|
||||
import AccessProvider from '../../../providers/AccessProvider/AccessProvider';
|
||||
import {
|
||||
ADMIN,
|
||||
CREATE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
DELETE_TAG_TYPE,
|
||||
UPDATE_TAG_TYPE,
|
||||
} from '../../../providers/AccessProvider/permissions';
|
||||
import UIProvider from '../../../providers/UIProvider/UIProvider';
|
||||
|
||||
@ -19,9 +17,7 @@ test('renders an empty list correctly', () => {
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<UIProvider>
|
||||
<AccessProvider
|
||||
store={createFakeStore([{ permission: ADMIN }])}
|
||||
>
|
||||
<AccessProvider permissions={[{ permission: ADMIN }]}>
|
||||
<TagTypeList
|
||||
tagTypes={[]}
|
||||
fetchTagTypes={jest.fn()}
|
||||
@ -42,11 +38,10 @@ test('renders a list with elements correctly', () => {
|
||||
<MemoryRouter>
|
||||
<UIProvider>
|
||||
<AccessProvider
|
||||
store={createFakeStore([
|
||||
{ permission: CREATE_TAG_TYPE },
|
||||
permissions={[
|
||||
{ permission: UPDATE_TAG_TYPE },
|
||||
{ permission: DELETE_TAG_TYPE },
|
||||
])}
|
||||
]}
|
||||
>
|
||||
<TagTypeList
|
||||
tagTypes={[
|
||||
|
@ -28,7 +28,35 @@ exports[`renders a list with elements correctly 1`] = `
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
@ -76,7 +104,32 @@ exports[`renders an empty list correctly 1`] = `
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
|
@ -2,8 +2,7 @@ import SimpleAuth from '../SimpleAuth/SimpleAuth';
|
||||
import AuthenticationCustomComponent from '../authentication-custom-component';
|
||||
import PasswordAuth from '../PasswordAuth/PasswordAuth';
|
||||
import HostedAuth from '../HostedAuth/HostedAuth';
|
||||
import DemoAuth from '../DemoAuth';
|
||||
|
||||
import DemoAuth from '../DemoAuth/DemoAuth';
|
||||
import {
|
||||
SIMPLE_TYPE,
|
||||
DEMO_TYPE,
|
||||
@ -11,27 +10,13 @@ import {
|
||||
HOSTED_TYPE,
|
||||
} from '../../../constants/authTypes';
|
||||
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 ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useAuthDetails } from '../../../hooks/api/getters/useAuth/useAuthDetails';
|
||||
|
||||
interface IAuthenticationProps {
|
||||
insecureLogin: (path: string, user: IUser) => void;
|
||||
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 Authentication = () => {
|
||||
const { authDetails } = useAuthDetails();
|
||||
const params = useQueryParams();
|
||||
|
||||
const error = params.get('errorMsg');
|
||||
@ -41,11 +26,7 @@ const Authentication = ({
|
||||
if (authDetails.type === PASSWORD_TYPE) {
|
||||
content = (
|
||||
<>
|
||||
<PasswordAuth
|
||||
passwordLogin={passwordLogin}
|
||||
authDetails={authDetails}
|
||||
history={history}
|
||||
/>
|
||||
<PasswordAuth authDetails={authDetails} />
|
||||
<ConditionallyRender
|
||||
condition={!authDetails.defaultHidden}
|
||||
show={<SecondaryLoginActions />}
|
||||
@ -53,29 +34,13 @@ const Authentication = ({
|
||||
</>
|
||||
);
|
||||
} else if (authDetails.type === SIMPLE_TYPE) {
|
||||
content = (
|
||||
<SimpleAuth
|
||||
insecureLogin={insecureLogin}
|
||||
authDetails={authDetails}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
content = <SimpleAuth authDetails={authDetails} />;
|
||||
} else if (authDetails.type === DEMO_TYPE) {
|
||||
content = (
|
||||
<DemoAuth
|
||||
demoLogin={demoLogin}
|
||||
authDetails={authDetails}
|
||||
history={history}
|
||||
/>
|
||||
);
|
||||
content = <DemoAuth authDetails={authDetails} />;
|
||||
} else if (authDetails.type === HOSTED_TYPE) {
|
||||
content = (
|
||||
<>
|
||||
<HostedAuth
|
||||
passwordLogin={passwordLogin}
|
||||
authDetails={authDetails}
|
||||
history={history}
|
||||
/>
|
||||
<HostedAuth authDetails={authDetails} />
|
||||
<ConditionallyRender
|
||||
condition={!authDetails.defaultHidden}
|
||||
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 PropTypes from 'prop-types';
|
||||
import { Button, TextField } from '@material-ui/core';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
|
||||
import styles from './DemoAuth.module.scss';
|
||||
|
||||
import { ReactComponent as Logo } from '../../../assets/img/logo.svg';
|
||||
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 history = useHistory();
|
||||
const { refetchUser } = useAuthUser();
|
||||
const { emailAuth } = useAuthApi();
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const { refetch } = useUser();
|
||||
|
||||
const handleSubmit = evt => {
|
||||
const handleSubmit = async evt => {
|
||||
evt.preventDefault();
|
||||
const user = { email };
|
||||
const path = evt.target.action;
|
||||
|
||||
demoLogin(path, user).then(() => {
|
||||
refetch();
|
||||
try {
|
||||
await emailAuth(authDetails.path, email);
|
||||
refetchUser();
|
||||
history.push(`/`);
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
@ -30,7 +34,7 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Logo className={styles.logo} />
|
||||
<div className={styles.container}>
|
||||
<h2>Access the Unleash demo instance</h2>
|
||||
@ -86,8 +90,6 @@ const DemoAuth = ({ demoLogin, history, authDetails }) => {
|
||||
|
||||
DemoAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
demoLogin: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
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 { useCommonStyles } from '../../../common.styles';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import { FORGOTTEN_PASSWORD_FIELD } from '../../../testIds';
|
||||
import { formatApiPath } from '../../../utils/format-path';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import DividerText from '../../common/DividerText/DividerText';
|
||||
@ -96,6 +97,7 @@ const ForgottenPassword = () => {
|
||||
placeholder="email"
|
||||
type="email"
|
||||
data-loading
|
||||
data-test={FORGOTTEN_PASSWORD_FIELD}
|
||||
value={email}
|
||||
onChange={e => {
|
||||
setEmail(e.target.value);
|
||||
|
@ -9,15 +9,17 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||
import DividerText from '../../common/DividerText/DividerText';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
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 styles = useStyles();
|
||||
const { refetch } = useUser();
|
||||
const { refetchUser } = useAuthUser();
|
||||
const history = useHistory();
|
||||
const params = useQueryParams();
|
||||
const { passwordAuth } = useAuthApi()
|
||||
const [username, setUsername] = useState(params.get('email') || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errors, setErrors] = useState({
|
||||
@ -45,12 +47,9 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = { username, password };
|
||||
const path = evt.target.action;
|
||||
|
||||
try {
|
||||
await passwordLogin(path, user);
|
||||
refetch();
|
||||
await passwordAuth(authDetails.path, username, password);
|
||||
refetchUser();
|
||||
history.push(`/`);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404 || error.statusCode === 400) {
|
||||
@ -86,7 +85,7 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
||||
<ConditionallyRender
|
||||
condition={!authDetails.defaultHidden}
|
||||
show={
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
className={styles.apiError}
|
||||
@ -138,8 +137,6 @@ const HostedAuth = ({ authDetails, passwordLogin }) => {
|
||||
|
||||
HostedAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
passwordLogin: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default HostedAuth;
|
||||
|
@ -1,28 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import AuthenticationContainer from '../Authentication';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
|
||||
import { useStyles } from './Login.styles';
|
||||
import useQueryParams from '../../../hooks/useQueryParams';
|
||||
import ResetPasswordSuccess from '../common/ResetPasswordSuccess/ResetPasswordSuccess';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
import { DEMO_TYPE } from '../../../constants/authTypes';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
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 styles = useStyles();
|
||||
const { permissions, authDetails } = useUser();
|
||||
const { authDetails } = useAuthDetails();
|
||||
const { permissions } = useAuthPermissions();
|
||||
const query = useQueryParams();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (permissions?.length > 0) {
|
||||
if (permissions?.length) {
|
||||
history.push('features');
|
||||
}
|
||||
/* eslint-disable-next-line */
|
||||
}, [permissions.length]);
|
||||
}, [permissions?.length]);
|
||||
|
||||
const resetPassword = query.get('reset') === 'true';
|
||||
return (
|
||||
@ -41,7 +41,7 @@ const Login = () => {
|
||||
condition={resetPassword}
|
||||
show={<ResetPasswordSuccess />}
|
||||
/>
|
||||
<AuthenticationContainer history={history} />
|
||||
<Authentication />
|
||||
</div>
|
||||
</StandaloneLayout>
|
||||
);
|
||||
|
@ -1,23 +1,18 @@
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import { TextField, Typography } from '@material-ui/core';
|
||||
|
||||
import StandaloneBanner from '../StandaloneBanner/StandaloneBanner';
|
||||
import ResetPasswordDetails from '../common/ResetPasswordDetails/ResetPasswordDetails';
|
||||
|
||||
import { useStyles } from './NewUser.styles';
|
||||
import useResetPassword from '../../../hooks/api/getters/useResetPassword/useResetPassword';
|
||||
import StandaloneLayout from '../common/StandaloneLayout/StandaloneLayout';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import InvalidToken from '../common/InvalidToken/InvalidToken';
|
||||
import { IAuthStatus } from '../../../interfaces/user';
|
||||
import AuthOptions from '../common/AuthOptions/AuthOptions';
|
||||
import DividerText from '../../common/DividerText/DividerText';
|
||||
import { useAuthDetails } from '../../../hooks/api/getters/useAuth/useAuthDetails';
|
||||
|
||||
interface INewUserProps {
|
||||
user: IAuthStatus;
|
||||
}
|
||||
|
||||
const NewUser = ({ user }: INewUserProps) => {
|
||||
export const NewUser = () => {
|
||||
const { authDetails } = useAuthDetails();
|
||||
const { token, data, loading, setLoading, invalidToken } =
|
||||
useResetPassword();
|
||||
const ref = useLoading(loading);
|
||||
@ -75,10 +70,9 @@ const NewUser = ({ user }: INewUserProps) => {
|
||||
/>
|
||||
<div className={styles.roleContainer}>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
user?.authDetails?.options?.length >
|
||||
0
|
||||
}
|
||||
condition={Boolean(
|
||||
authDetails?.options?.length
|
||||
)}
|
||||
show={
|
||||
<>
|
||||
<DividerText
|
||||
@ -88,8 +82,7 @@ const NewUser = ({ user }: INewUserProps) => {
|
||||
|
||||
<AuthOptions
|
||||
options={
|
||||
user?.authDetails
|
||||
?.options
|
||||
authDetails?.options
|
||||
}
|
||||
/>
|
||||
<DividerText
|
||||
@ -116,5 +109,3 @@ const NewUser = ({ user }: INewUserProps) => {
|
||||
</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 {
|
||||
LOGIN_BUTTON,
|
||||
LOGIN_PASSWORD_ID,
|
||||
LOGIN_EMAIL_ID,
|
||||
LOGIN_PASSWORD_ID,
|
||||
} from '../../../testIds';
|
||||
import useUser from '../../../hooks/api/getters/useUser/useUser';
|
||||
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 styles = useStyles();
|
||||
const history = useHistory();
|
||||
const { refetch } = useUser();
|
||||
const { refetchUser } = useAuthUser();
|
||||
const params = useQueryParams();
|
||||
const [username, setUsername] = useState(params.get('email') || '');
|
||||
const [password, setPassword] = useState('');
|
||||
const { passwordAuth } = useAuthApi();
|
||||
const [errors, setErrors] = useState({
|
||||
usernameError: '',
|
||||
passwordError: '',
|
||||
@ -51,12 +53,9 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = { username, password };
|
||||
const path = evt.target.action;
|
||||
|
||||
try {
|
||||
await passwordLogin(path, user);
|
||||
refetch();
|
||||
await passwordAuth(authDetails.path, username, password);
|
||||
refetchUser();
|
||||
history.push(`/`);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404 || error.statusCode === 400) {
|
||||
@ -85,7 +84,7 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
<ConditionallyRender
|
||||
condition={!authDetails.defaultHidden}
|
||||
show={
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ConditionallyRender
|
||||
condition={apiError}
|
||||
show={
|
||||
@ -169,8 +168,6 @@ const PasswordAuth = ({ authDetails, passwordLogin }) => {
|
||||
|
||||
PasswordAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
passwordLogin: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default PasswordAuth;
|
||||
|
@ -1,23 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, TextField } from '@material-ui/core';
|
||||
|
||||
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 { refetch } = useUser();
|
||||
const { refetchUser } = useAuthUser();
|
||||
const { emailAuth } = useAuthApi();
|
||||
const history = useHistory();
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const handleSubmit = evt => {
|
||||
const handleSubmit = async evt => {
|
||||
evt.preventDefault();
|
||||
const user = { email };
|
||||
const path = evt.target.action;
|
||||
|
||||
insecureLogin(path, user).then(() => {
|
||||
refetch();
|
||||
try {
|
||||
await emailAuth(authDetails.path, email);
|
||||
refetchUser();
|
||||
history.push(`/`);
|
||||
});
|
||||
} catch (e) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
@ -26,7 +33,7 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} action={authDetails.path}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.container}>
|
||||
<p>{authDetails.message}</p>
|
||||
<p>
|
||||
@ -50,6 +57,7 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
data-test={LOGIN_EMAIL_ID}
|
||||
/>
|
||||
<br />
|
||||
|
||||
@ -58,8 +66,8 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
data-test="login-submit"
|
||||
className={styles.button}
|
||||
data-test={LOGIN_BUTTON}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
@ -71,8 +79,6 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => {
|
||||
|
||||
SimpleAuth.propTypes = {
|
||||
authDetails: PropTypes.object.isRequired,
|
||||
insecureLogin: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
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 { useLocationSettings } from '../../../hooks/useLocationSettings';
|
||||
import { useAuthUser } from '../../../hooks/api/getters/useAuth/useAuthUser';
|
||||
|
||||
const UserProfileContainer = () => {
|
||||
const user = useUser();
|
||||
const { locationSettings, setLocationSettings } = useLocationSettings();
|
||||
const { user } = useAuthUser();
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserProfile
|
||||
locationSettings={locationSettings}
|
||||
setLocationSettings={setLocationSettings}
|
||||
profile={user.user}
|
||||
profile={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Button } from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import { useCommonStyles } from '../../../../common.styles';
|
||||
import { IAuthOptions } from '../../../../interfaces/user';
|
||||
import { ReactComponent as GoogleSvg } from '../../../../assets/icons/google.svg';
|
||||
import LockRounded from '@material-ui/icons/LockRounded';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import { IAuthOptions } from '../../../../hooks/api/getters/useAuth/useAuthEndpoint';
|
||||
import { SSO_LOGIN_BUTTON } from '../../../../testIds';
|
||||
|
||||
interface IAuthOptionProps {
|
||||
options?: IAuthOptions[];
|
||||
@ -28,6 +29,7 @@ const AuthOptions = ({ options }: IAuthOptionProps) => {
|
||||
variant="outlined"
|
||||
href={o.path}
|
||||
size="small"
|
||||
data-test={`${SSO_LOGIN_BUTTON}-${o.type}`}
|
||||
style={{ height: '40px', color: '#000' }}
|
||||
startIcon={
|
||||
<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 {
|
||||
handleBadRequest,
|
||||
@ -16,6 +14,12 @@ export interface IUserApiErrors {
|
||||
validatePassword?: string;
|
||||
}
|
||||
|
||||
interface IUserPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const ADD_USER_ERROR = 'addUser';
|
||||
export const UPDATE_USER_ERROR = 'updateUser';
|
||||
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}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<UIProvider>
|
||||
<AccessProvider store={unleashStore}>
|
||||
<AccessProvider>
|
||||
<Router basename={`${getBasePath()}`}>
|
||||
<ThemeProvider theme={mainTheme}>
|
||||
<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 {
|
||||
id: number;
|
||||
email: string;
|
||||
@ -43,14 +12,8 @@ export interface IUser {
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface IUserPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
id?: string;
|
||||
export interface IPermission {
|
||||
permission: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
export interface IAddedUser extends IUser {
|
||||
emailSent?: boolean;
|
||||
}
|
||||
|
||||
export default IAuthStatus;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { fromJS, List, Map } from 'immutable';
|
||||
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION, UPDATE_APPLICATION_FIELD, DELETE_APPLICATION } from './actions';
|
||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||
|
||||
function getInitState() {
|
||||
return fromJS({ list: [], apps: {} });
|
||||
@ -19,9 +18,6 @@ const store = (state = getInitState(), action) => {
|
||||
const result = state.removeIn(['list', index]);
|
||||
return result.removeIn(['apps', action.appName]);
|
||||
}
|
||||
case USER_LOGOUT:
|
||||
case USER_LOGIN:
|
||||
return getInitState();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -9,8 +9,6 @@ import {
|
||||
TOGGLE_FEATURE_TOGGLE,
|
||||
} from './actions';
|
||||
|
||||
import { USER_LOGOUT, USER_LOGIN } from '../user/actions';
|
||||
|
||||
const debug = require('debug')('unleash:feature-store');
|
||||
|
||||
const features = (state = new List([]), action) => {
|
||||
@ -62,10 +60,6 @@ const features = (state = new List([]), action) => {
|
||||
case RECEIVE_FEATURE_TOGGLES:
|
||||
debug(RECEIVE_FEATURE_TOGGLES, action);
|
||||
return new List(action.featureToggles.map($Map));
|
||||
case USER_LOGIN:
|
||||
case USER_LOGOUT:
|
||||
debug(USER_LOGOUT, action);
|
||||
return new List([]);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { combineReducers } from 'redux';
|
||||
import features from './feature-toggle';
|
||||
import strategies from './strategy';
|
||||
import error from './error';
|
||||
import user from './user';
|
||||
import applications from './application';
|
||||
import projects from './project';
|
||||
import apiCalls from './api-calls';
|
||||
@ -11,7 +10,6 @@ const unleashStore = combineReducers({
|
||||
features,
|
||||
strategies,
|
||||
error,
|
||||
user,
|
||||
applications,
|
||||
projects,
|
||||
apiCalls,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { List } from 'immutable';
|
||||
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 }];
|
||||
|
||||
@ -22,9 +21,6 @@ const strategies = (state = getInitState(), action) => {
|
||||
const index = state.findIndex(item => item.id === action.project.id);
|
||||
return state.set(index, action.project);
|
||||
}
|
||||
case USER_LOGOUT:
|
||||
case USER_LOGIN:
|
||||
return getInitState();
|
||||
default:
|
||||
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_BUTTON = 'LOGIN_BUTTON';
|
||||
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 */
|
||||
export const ADD_NEW_STRATEGY_ID = 'ADD_NEW_STRATEGY_ID';
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { ADMIN } from '../component/providers/AccessProvider/permissions';
|
||||
import IAuthStatus, { IPermission } from '../interfaces/user';
|
||||
import { IPermission } from '../interfaces/user';
|
||||
|
||||
type objectIdx = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export const projectFilterGenerator = (
|
||||
user: IAuthStatus,
|
||||
permissions: IPermission[] = [],
|
||||
matcherPermission: string
|
||||
) => {
|
||||
let admin = false;
|
||||
const permissionMap: objectIdx = user.permissions.reduce(
|
||||
const permissionMap: objectIdx = permissions.reduce(
|
||||
(acc: objectIdx, current: IPermission) => {
|
||||
if (current.permission === ADMIN) {
|
||||
admin = true;
|
||||
|
Loading…
Reference in New Issue
Block a user