From 213e8950d3d303357ad0220de71255a7fd072710 Mon Sep 17 00:00:00 2001 From: olav Date: Thu, 10 Feb 2022 17:04:10 +0100 Subject: [PATCH] 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 --- frontend/.env | 1 + .../workflows/{e2e.yml => e2e.auth.yml} | 3 +- frontend/.github/workflows/e2e.feature.yml | 25 +++ .../cypress/integration/auth/auth.spec.js | 204 ++++++++++++++++++ frontend/src/accessStoreFake.js | 13 +- frontend/src/component/App.tsx | 98 +++------ frontend/src/component/AppContainer.tsx | 13 +- frontend/src/component/admin/api/index.js | 8 +- ... application-edit-component-test.jsx.snap} | 0 ...js => application-edit-component-test.jsx} | 0 .../common/BreadcrumbNav/BreadcrumbNav.tsx | 13 +- .../component/common/Feedback/Feedback.tsx | 9 +- .../src/component/common/Splash/Splash.tsx | 2 +- .../feature/FeatureForm/FeatureForm.tsx | 7 +- .../__tests__/list-component-test.jsx | 15 +- .../FeatureSettingsProject.tsx | 7 +- frontend/src/component/menu/Header/Header.tsx | 6 +- frontend/src/component/menu/routes.js | 2 +- .../Project/CreateProject/CreateProject.tsx | 6 +- .../AccessProvider/AccessProvider.tsx | 169 ++++++++------- .../providers/SWRProvider/SWRProvider.tsx | 8 +- .../__tests__/list-component-test.jsx | 9 +- .../strategy-details-component-test.jsx | 3 +- .../TagTypeList/__tests__/TagTypeList.test.js | 13 +- .../__snapshots__/TagTypeList.test.js.snap | 57 ++++- .../user/Authentication/Authentication.tsx | 51 +---- .../component/user/Authentication/index.js | 15 -- .../src/component/user/DemoAuth/DemoAuth.jsx | 32 +-- .../src/component/user/DemoAuth/index.jsx | 3 - .../ForgottenPassword/ForgottenPassword.tsx | 2 + .../component/user/HostedAuth/HostedAuth.jsx | 19 +- frontend/src/component/user/Login/Login.tsx | 16 +- .../src/component/user/NewUser/NewUser.tsx | 23 +- frontend/src/component/user/NewUser/index.js | 8 - .../user/PasswordAuth/PasswordAuth.jsx | 21 +- .../component/user/SimpleAuth/SimpleAuth.jsx | 34 +-- .../src/component/user/SimpleAuth/index.jsx | 3 - .../src/component/user/UserProfile/index.tsx | 10 +- .../user/common/AuthOptions/AuthOptions.tsx | 4 +- frontend/src/contexts/AccessContext.js | 5 - frontend/src/contexts/AccessContext.ts | 21 ++ .../useAdminUsersApi/useAdminUsersApi.ts | 8 +- .../api/actions/useAuthApi/useAuthApi.tsx | 51 +++++ .../api/getters/useAuth/useAuthDetails.ts | 24 +++ .../api/getters/useAuth/useAuthEndpoint.ts | 84 ++++++++ .../api/getters/useAuth/useAuthFeedback.ts | 21 ++ .../api/getters/useAuth/useAuthPermissions.ts | 24 +++ .../api/getters/useAuth/useAuthSplash.ts | 21 ++ .../hooks/api/getters/useAuth/useAuthUser.ts | 21 ++ .../src/hooks/api/getters/useUser/useUser.ts | 58 ----- frontend/src/hooks/usePermissions.ts | 8 - frontend/src/index.tsx | 2 +- frontend/src/interfaces/user.ts | 45 +--- frontend/src/store/application/index.js | 4 - frontend/src/store/feature-toggle/index.js | 6 - frontend/src/store/index.js | 2 - frontend/src/store/project/index.js | 4 - frontend/src/store/user/actions.js | 62 ------ frontend/src/store/user/api.js | 52 ----- frontend/src/store/user/index.js | 28 --- frontend/src/testIds.js | 2 + .../src/utils/project-filter-generator.ts | 6 +- 62 files changed, 839 insertions(+), 652 deletions(-) create mode 100644 frontend/.env rename frontend/.github/workflows/{e2e.yml => e2e.auth.yml} (92%) create mode 100644 frontend/.github/workflows/e2e.feature.yml create mode 100644 frontend/cypress/integration/auth/auth.spec.js rename frontend/src/component/application/__tests__/__snapshots__/{application-edit-component-test.js.snap => application-edit-component-test.jsx.snap} (100%) rename frontend/src/component/application/__tests__/{application-edit-component-test.js => application-edit-component-test.jsx} (100%) delete mode 100644 frontend/src/component/user/Authentication/index.js delete mode 100644 frontend/src/component/user/DemoAuth/index.jsx delete mode 100644 frontend/src/component/user/NewUser/index.js delete mode 100644 frontend/src/component/user/SimpleAuth/index.jsx delete mode 100644 frontend/src/contexts/AccessContext.js create mode 100644 frontend/src/contexts/AccessContext.ts create mode 100644 frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts create mode 100644 frontend/src/hooks/api/getters/useAuth/useAuthUser.ts delete mode 100644 frontend/src/hooks/api/getters/useUser/useUser.ts delete mode 100644 frontend/src/hooks/usePermissions.ts delete mode 100644 frontend/src/store/user/actions.js delete mode 100644 frontend/src/store/user/api.js delete mode 100644 frontend/src/store/user/index.js diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000000..991f007c42 --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +TSC_COMPILE_ON_ERROR=true diff --git a/frontend/.github/workflows/e2e.yml b/frontend/.github/workflows/e2e.auth.yml similarity index 92% rename from frontend/.github/workflows/e2e.yml rename to frontend/.github/workflows/e2e.auth.yml index dab1d944ca..6270d196b9 100644 --- a/frontend/.github/workflows/e2e.yml +++ b/frontend/.github/workflows/e2e.auth.yml @@ -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 }} \ No newline at end of file diff --git a/frontend/.github/workflows/e2e.feature.yml b/frontend/.github/workflows/e2e.feature.yml new file mode 100644 index 0000000000..cfa3af2438 --- /dev/null +++ b/frontend/.github/workflows/e2e.feature.yml @@ -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 }} \ No newline at end of file diff --git a/frontend/cypress/integration/auth/auth.spec.js b/frontend/cypress/integration/auth/auth.spec.js new file mode 100644 index 0000000000..63500c543d --- /dev/null +++ b/frontend/cypress/integration/auth/auth.spec.js @@ -0,0 +1,204 @@ +/* eslint-disable jest/no-conditional-expect */ +/// +// 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(); + }); +}); diff --git a/frontend/src/accessStoreFake.js b/frontend/src/accessStoreFake.js index c0069ca1b0..fb39464765 100644 --- a/frontend/src/accessStoreFake.js +++ b/frontend/src/accessStoreFake.js @@ -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, + }), }), - } -} \ No newline at end of file + }; +}; diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index e1a1fcef85..62ff747de1 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -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) => { )} /> @@ -99,21 +77,18 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { }; return ( - + } elseShow={
+ } elseShow={ @@ -133,9 +108,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { /> - + } /> @@ -145,10 +118,3 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => { ); }; - -// 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); diff --git a/frontend/src/component/AppContainer.tsx b/frontend/src/component/AppContainer.tsx index 00823c17fa..7a0958fa6e 100644 --- a/frontend/src/component/AppContainer.tsx +++ b/frontend/src/component/AppContainer.tsx @@ -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); diff --git a/frontend/src/component/admin/api/index.js b/frontend/src/component/admin/api/index.js index d60078d65e..0cc902332c 100644 --- a/frontend/src/component/admin/api/index.js +++ b/frontend/src/component/admin/api/index.js @@ -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 (
} /> diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.jsx.snap similarity index 100% rename from frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap rename to frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.jsx.snap diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.jsx similarity index 100% rename from frontend/src/component/application/__tests__/application-edit-component-test.js rename to frontend/src/component/application/__tests__/application-edit-component-test.jsx diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx index 380410945f..b08c182b97 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx @@ -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 ( { 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); diff --git a/frontend/src/component/common/Splash/Splash.tsx b/frontend/src/component/common/Splash/Splash.tsx index 1517709b36..18990afe60 100644 --- a/frontend/src/component/common/Splash/Splash.tsx +++ b/frontend/src/component/common/Splash/Splash.tsx @@ -64,7 +64,7 @@ const Splash: React.FC = ({ ); } - return ; + return ; }); }; diff --git a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx b/frontend/src/component/feature/FeatureForm/FeatureForm.tsx index 8affd14eda..17d915401b 100644 --- a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx +++ b/frontend/src/component/feature/FeatureForm/FeatureForm.tsx @@ -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 = ({ 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 = ({ }} enabled={editable} filter={projectFilterGenerator( - { permissions }, + permissions, CREATE_FEATURE )} IconComponent={KeyboardArrowDownOutlined} diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx index 516f20da64..b7d5742dfb 100644 --- a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx @@ -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( - + { const tree = renderer.create( - + { 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; diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index 46d337caa4..516233ddca 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -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 ); diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js index 22d66f43a1..564708142a 100644 --- a/frontend/src/component/menu/routes.js +++ b/frontend/src/component/menu/routes.js @@ -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'; diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx index eed75ece87..a847e890ee 100644 --- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx +++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx @@ -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', diff --git a/frontend/src/component/providers/AccessProvider/AccessProvider.tsx b/frontend/src/component/providers/AccessProvider/AccessProvider.tsx index f03d01607a..c77880ade2 100644 --- a/frontend/src/component/providers/AccessProvider/AccessProvider.tsx +++ b/frontend/src/component/providers/AccessProvider/AccessProvider.tsx @@ -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 = ({ 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 ( - - {children} + + {props.children} ); }; +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; diff --git a/frontend/src/component/providers/SWRProvider/SWRProvider.tsx b/frontend/src/component/providers/SWRProvider/SWRProvider.tsx index 51541c51c6..008c217804 100644 --- a/frontend/src/component/providers/SWRProvider/SWRProvider.tsx +++ b/frontend/src/component/providers/SWRProvider/SWRProvider.tsx @@ -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>; isUnauthorized: () => boolean; } @@ -14,20 +14,18 @@ const INVALID_TOKEN_ERROR = 'InvalidTokenError'; const SWRProvider: React.FC = ({ 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 ( diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx index bc9abb9a02..ce3afed807 100644 --- a/frontend/src/component/strategies/__tests__/list-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -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( - + { const tree = renderer.create( - + { @@ -35,7 +34,7 @@ test('renders correctly with one strategy', () => { ]; const tree = renderer.create( - + { - + {
+ > + +
+ > + +
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 = ( <> - + } @@ -53,29 +34,13 @@ const Authentication = ({ ); } else if (authDetails.type === SIMPLE_TYPE) { - content = ( - - ); + content = ; } else if (authDetails.type === DEMO_TYPE) { - content = ( - - ); + content = ; } else if (authDetails.type === HOSTED_TYPE) { content = ( <> - + } diff --git a/frontend/src/component/user/Authentication/index.js b/frontend/src/component/user/Authentication/index.js deleted file mode 100644 index ad12103f8b..0000000000 --- a/frontend/src/component/user/Authentication/index.js +++ /dev/null @@ -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); diff --git a/frontend/src/component/user/DemoAuth/DemoAuth.jsx b/frontend/src/component/user/DemoAuth/DemoAuth.jsx index d27b83285e..8fa4dd880c 100644 --- a/frontend/src/component/user/DemoAuth/DemoAuth.jsx +++ b/frontend/src/component/user/DemoAuth/DemoAuth.jsx @@ -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 ( -
+

Access the Unleash demo instance

@@ -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; diff --git a/frontend/src/component/user/DemoAuth/index.jsx b/frontend/src/component/user/DemoAuth/index.jsx deleted file mode 100644 index 4985c198a9..0000000000 --- a/frontend/src/component/user/DemoAuth/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import DemoAuth from './DemoAuth'; - -export default DemoAuth; diff --git a/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx b/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx index 2043e1b120..d7163ff18f 100644 --- a/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx +++ b/frontend/src/component/user/ForgottenPassword/ForgottenPassword.tsx @@ -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); diff --git a/frontend/src/component/user/HostedAuth/HostedAuth.jsx b/frontend/src/component/user/HostedAuth/HostedAuth.jsx index 64893d72a5..921c19225f 100644 --- a/frontend/src/component/user/HostedAuth/HostedAuth.jsx +++ b/frontend/src/component/user/HostedAuth/HostedAuth.jsx @@ -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 }) => { + { HostedAuth.propTypes = { authDetails: PropTypes.object.isRequired, - passwordLogin: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, }; export default HostedAuth; diff --git a/frontend/src/component/user/Login/Login.tsx b/frontend/src/component/user/Login/Login.tsx index c98a86508c..33869d1e43 100644 --- a/frontend/src/component/user/Login/Login.tsx +++ b/frontend/src/component/user/Login/Login.tsx @@ -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={} /> - +
); diff --git a/frontend/src/component/user/NewUser/NewUser.tsx b/frontend/src/component/user/NewUser/NewUser.tsx index 8629b10760..0c72905ba2 100644 --- a/frontend/src/component/user/NewUser/NewUser.tsx +++ b/frontend/src/component/user/NewUser/NewUser.tsx @@ -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) => { />
- 0 - } + condition={Boolean( + authDetails?.options?.length + )} show={ <> { {
); }; - -export default NewUser; diff --git a/frontend/src/component/user/NewUser/index.js b/frontend/src/component/user/NewUser/index.js deleted file mode 100644 index d2d6eb8f8e..0000000000 --- a/frontend/src/component/user/NewUser/index.js +++ /dev/null @@ -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); diff --git a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx index b0071c6096..3c2fbcc965 100644 --- a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx +++ b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx @@ -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 }) => { + { PasswordAuth.propTypes = { authDetails: PropTypes.object.isRequired, - passwordLogin: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, }; export default PasswordAuth; diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx index 0687d64540..ede999d2ca 100644 --- a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx @@ -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 ( - +

{authDetails.message}

@@ -50,6 +57,7 @@ const SimpleAuth = ({ insecureLogin, history, authDetails }) => { name="email" required type="email" + data-test={LOGIN_EMAIL_ID} />
@@ -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 @@ -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; diff --git a/frontend/src/component/user/SimpleAuth/index.jsx b/frontend/src/component/user/SimpleAuth/index.jsx deleted file mode 100644 index 5d169efa90..0000000000 --- a/frontend/src/component/user/SimpleAuth/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import SimpleAuth from './SimpleAuth'; - -export default SimpleAuth; diff --git a/frontend/src/component/user/UserProfile/index.tsx b/frontend/src/component/user/UserProfile/index.tsx index 3ece468a26..5868eccd02 100644 --- a/frontend/src/component/user/UserProfile/index.tsx +++ b/frontend/src/component/user/UserProfile/index.tsx @@ -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 ( ); }; diff --git a/frontend/src/component/user/common/AuthOptions/AuthOptions.tsx b/frontend/src/component/user/common/AuthOptions/AuthOptions.tsx index 6787685c20..e6008c644b 100644 --- a/frontend/src/component/user/common/AuthOptions/AuthOptions.tsx +++ b/frontend/src/component/user/common/AuthOptions/AuthOptions.tsx @@ -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={ boolean; +} + +const hasAccessPlaceholder = () => { + throw new Error('hasAccess called outside AccessContext'); +}; + +const AccessContext = React.createContext({ + isAdmin: false, + hasAccess: hasAccessPlaceholder, +}); + +export default AccessContext; diff --git a/frontend/src/hooks/api/actions/useAdminUsersApi/useAdminUsersApi.ts b/frontend/src/hooks/api/actions/useAdminUsersApi/useAdminUsersApi.ts index 5fa6f9cde1..379d017bcb 100644 --- a/frontend/src/hooks/api/actions/useAdminUsersApi/useAdminUsersApi.ts +++ b/frontend/src/hooks/api/actions/useAdminUsersApi/useAdminUsersApi.ts @@ -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'; diff --git a/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx b/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx new file mode 100644 index 0000000000..9e5e2bb907 --- /dev/null +++ b/frontend/src/hooks/api/actions/useAuthApi/useAuthApi.tsx @@ -0,0 +1,51 @@ +import useAPI from '../useApi/useApi'; + +type PasswordLogin = ( + path: string, + username: string, + password: string +) => Promise; + +type EmailLogin = (path: string, email: string) => Promise; + +interface IUseAuthApiOutput { + passwordAuth: PasswordLogin; + emailAuth: EmailLogin; + errors: Record; + 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(/^\//, ''); +}; diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts b/frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts new file mode 100644 index 0000000000..33dbc02707 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthDetails.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts b/frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts new file mode 100644 index 0000000000..37eeb86932 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthEndpoint.ts @@ -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( + 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 => { + 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`); diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts b/frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts new file mode 100644 index 0000000000..3f9d6bda89 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthFeedback.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts b/frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts new file mode 100644 index 0000000000..3cb4d2fd2e --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthPermissions.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts b/frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts new file mode 100644 index 0000000000..38956e7875 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthSplash.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useAuth/useAuthUser.ts b/frontend/src/hooks/api/getters/useAuth/useAuthUser.ts new file mode 100644 index 0000000000..2a142a55e1 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAuth/useAuthUser.ts @@ -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, + }; +}; diff --git a/frontend/src/hooks/api/getters/useUser/useUser.ts b/frontend/src/hooks/api/getters/useUser/useUser.ts deleted file mode 100644 index 6297eeec48..0000000000 --- a/frontend/src/hooks/api/getters/useUser/useUser.ts +++ /dev/null @@ -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; diff --git a/frontend/src/hooks/usePermissions.ts b/frontend/src/hooks/usePermissions.ts deleted file mode 100644 index e9fbe80fc2..0000000000 --- a/frontend/src/hooks/usePermissions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useContext } from 'react'; -import AccessContext from '../contexts/AccessContext'; - -const usePermissions = () => { - return useContext(AccessContext); -}; - -export default usePermissions; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 028bac1bcb..02ae50648c 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -43,7 +43,7 @@ ReactDOM.render( - + diff --git a/frontend/src/interfaces/user.ts b/frontend/src/interfaces/user.ts index ae48d444e1..3c19e970bd 100644 --- a/frontend/src/interfaces/user.ts +++ b/frontend/src/interfaces/user.ts @@ -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; diff --git a/frontend/src/store/application/index.js b/frontend/src/store/application/index.js index 06da890586..a5f57e2afa 100644 --- a/frontend/src/store/application/index.js +++ b/frontend/src/store/application/index.js @@ -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; } diff --git a/frontend/src/store/feature-toggle/index.js b/frontend/src/store/feature-toggle/index.js index d7a305a5b3..2ae1dbb69a 100644 --- a/frontend/src/store/feature-toggle/index.js +++ b/frontend/src/store/feature-toggle/index.js @@ -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; } diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 13aea0e75a..d1496447ad 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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, diff --git a/frontend/src/store/project/index.js b/frontend/src/store/project/index.js index c191f76cd5..abf6a82d6d 100644 --- a/frontend/src/store/project/index.js +++ b/frontend/src/store/project/index.js @@ -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; } diff --git a/frontend/src/store/user/actions.js b/frontend/src/store/user/actions.js deleted file mode 100644 index c9687dc8c9..0000000000 --- a/frontend/src/store/user/actions.js +++ /dev/null @@ -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 })); - }; -} diff --git a/frontend/src/store/user/api.js b/frontend/src/store/user/api.js deleted file mode 100644 index 1e54c701ee..0000000000 --- a/frontend/src/store/user/api.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/user/index.js b/frontend/src/store/user/index.js deleted file mode 100644 index 957cb93e99..0000000000 --- a/frontend/src/store/user/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/testIds.js b/frontend/src/testIds.js index ba6984e647..e72fb77e77 100644 --- a/frontend/src/testIds.js +++ b/frontend/src/testIds.js @@ -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'; diff --git a/frontend/src/utils/project-filter-generator.ts b/frontend/src/utils/project-filter-generator.ts index 3e271fd478..88cf2d8e9b 100644 --- a/frontend/src/utils/project-filter-generator.ts +++ b/frontend/src/utils/project-filter-generator.ts @@ -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;