From 507690fedfad961b613e866a5818b2d220e12a0c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 11 Oct 2020 00:08:51 +0200 Subject: [PATCH] feat: register pages routes guards. feat: retrieve all organizations details to authenticated user. feat: redux organization reducers and actions. --- client/package.json | 2 + client/src/components/App.js | 2 +- client/src/components/Dashboard/Dashboard.js | 71 +++++++++++++------ .../Dashboard/DashboardLoadingIndicator.js | 25 +++++++ .../Dashboard/EnsureOrganizationIsReady.js | 16 +++++ client/src/containers/Authentication/Login.js | 3 + .../Register/RegisterLeftSection.js | 47 ++++++++---- .../Register/RegisterRightSection.js | 59 +++++++++++---- .../Register/RegisterUserForm.js | 8 +-- .../Organization/withOrganizationActions.js | 10 +++ .../Organization/withOrganizationByOrgId.js | 16 +++++ .../Organization/withOrganizationByTenId.js | 16 +++++ .../authentication/authentication.reducer.js | 8 +++ .../organizations/organizations.actions.js | 16 +++++ .../organizations/organizations.reducers.js | 25 +++++++ .../organizations/organizations.selectors.js | 18 +++++ .../organizations/organizations.types.js | 5 ++ client/src/store/reducers.js | 2 + client/src/store/types.js | 2 + server/src/api/controllers/Organization.ts | 29 +++++++- server/src/services/Organization/index.ts | 20 +++++- .../system/repositories/TenantRepository.ts | 14 +++- 22 files changed, 348 insertions(+), 66 deletions(-) create mode 100644 client/src/components/Dashboard/DashboardLoadingIndicator.js create mode 100644 client/src/components/Dashboard/EnsureOrganizationIsReady.js create mode 100644 client/src/containers/Organization/withOrganizationActions.js create mode 100644 client/src/containers/Organization/withOrganizationByOrgId.js create mode 100644 client/src/containers/Organization/withOrganizationByTenId.js create mode 100644 client/src/store/organizations/organizations.actions.js create mode 100644 client/src/store/organizations/organizations.reducers.js create mode 100644 client/src/store/organizations/organizations.selectors.js create mode 100644 client/src/store/organizations/organizations.types.js diff --git a/client/package.json b/client/package.json index 0fdd0e4ce..e88487f62 100644 --- a/client/package.json +++ b/client/package.json @@ -64,6 +64,7 @@ "postcss-preset-env": "6.7.0", "postcss-safe-parser": "4.0.1", "react": "^16.12.0", + "react-albus": "^2.0.0", "react-app-polyfill": "^1.0.6", "react-body-classname": "^1.3.1", "react-dev-utils": "^10.2.0", @@ -83,6 +84,7 @@ "react-split-pane": "^0.1.91", "react-table": "^7.0.0", "react-table-sticky": "^1.1.2", + "react-transition-group": "^4.4.1", "react-use": "^13.26.1", "react-window": "^1.8.5", "redux": "^4.0.5", diff --git a/client/src/components/App.js b/client/src/components/App.js index 51c3a8acb..aa7695535 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -20,7 +20,7 @@ function App({ locale }) { const queryConfig = { queries: { refetchOnWindowFocus: false, - } + }, }; return ( diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js index 7be031c16..4b8dda00a 100644 --- a/client/src/components/Dashboard/Dashboard.js +++ b/client/src/components/Dashboard/Dashboard.js @@ -1,6 +1,8 @@ import React from 'react'; import { Switch, Route } from 'react-router'; -import classNames from 'classnames'; +import { useQuery } from 'react-query'; + +import DashboardLoadingIndicator from './DashboardLoadingIndicator'; import Sidebar from 'components/Sidebar/Sidebar'; import DashboardContent from 'components/Dashboard/DashboardContent'; @@ -9,29 +11,56 @@ import PreferencesContent from 'components/Preferences/PreferencesContent'; import PreferencesSidebar from 'components/Preferences/PreferencesSidebar'; import Search from 'containers/GeneralSearch/Search'; import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane'; +import EnsureOrganizationIsReady from './EnsureOrganizationIsReady'; -export default function Dashboard() { +import withSettingsActions from 'containers/Settings/withSettingsActions'; +import withOrganizationsActions from 'containers/Organization/withOrganizationActions'; + +import { compose } from 'utils'; + + +function Dashboard({ + // #withSettings + requestFetchOptions, + + // #withOrganizations + requestOrganizationsList, +}) { + const fetchOrganizations = useQuery( + ['organizations'], + (key) => requestOrganizationsList(), + ); return ( -
- - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + + - - -
+ + + + ); } + +export default compose( + withSettingsActions, + withOrganizationsActions, +)(Dashboard); \ No newline at end of file diff --git a/client/src/components/Dashboard/DashboardLoadingIndicator.js b/client/src/components/Dashboard/DashboardLoadingIndicator.js new file mode 100644 index 000000000..b07f92dd5 --- /dev/null +++ b/client/src/components/Dashboard/DashboardLoadingIndicator.js @@ -0,0 +1,25 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Choose, Icon } from 'components'; + +export default function Dashboard({ + isLoading = false, + children, +}) { + return ( +
+ + +
+ + Please wait while resources loading... +
+
+ + + { children } + +
+
+ ); +} diff --git a/client/src/components/Dashboard/EnsureOrganizationIsReady.js b/client/src/components/Dashboard/EnsureOrganizationIsReady.js new file mode 100644 index 000000000..a0b7e8929 --- /dev/null +++ b/client/src/components/Dashboard/EnsureOrganizationIsReady.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Redirect } from 'react-router-dom'; + +export default function EnsureOrganizationIsReady({ + children, +}) { + const isOrganizationReady = false; + + return (isOrganizationReady) ? children : ( + + ); +} \ No newline at end of file diff --git a/client/src/containers/Authentication/Login.js b/client/src/containers/Authentication/Login.js index 9dc911ab9..fa73a5aab 100644 --- a/client/src/containers/Authentication/Login.js +++ b/client/src/containers/Authentication/Login.js @@ -19,6 +19,7 @@ import Icon from 'components/Icon'; import { If } from 'components'; import withAuthenticationActions from './withAuthenticationActions'; +import withOrganizationsActions from 'containers/Organization/withOrganizationActions'; import { compose } from 'utils'; @@ -29,6 +30,7 @@ const ERRORS_TYPES = { }; function Login({ requestLogin, + requestOrganizationsList, }) { const { formatMessage } = useIntl(); const history = useHistory(); @@ -168,4 +170,5 @@ function Login({ export default compose( withAuthenticationActions, + withOrganizationsActions, )(Login); \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterLeftSection.js b/client/src/containers/Authentication/Register/RegisterLeftSection.js index 3524a385e..aeb8ad0a0 100644 --- a/client/src/containers/Authentication/Register/RegisterLeftSection.js +++ b/client/src/containers/Authentication/Register/RegisterLeftSection.js @@ -1,12 +1,23 @@ -import React, { useState } from 'react'; -import { Icon } from 'components'; +import React, { useState, useCallback } from 'react'; +import { Icon, If } from 'components'; import { FormattedMessage as T } from 'react-intl'; -export default function RegisterLeftSection({ +import withAuthentication from 'containers/Authentication/withAuthentication'; +import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions'; +import { compose } from 'utils'; + + +function RegisterLeftSection({ + requestLogout, + isAuthorized }) { const [org] = useState('LibyanSpider'); + const onClickLogout = useCallback(() => { + requestLogout(); + }, [requestLogout]); + return (
@@ -27,17 +38,18 @@ export default function RegisterLeftSection({

-
- - - {org}, - - - - - - -
+ + +
+ + + {org}, + + + + +
+
) -} \ No newline at end of file +} + +export default compose( + withAuthentication(({ isAuthorized }) => ({ isAuthorized })), + withAuthenticationActions, +)(RegisterLeftSection); diff --git a/client/src/containers/Authentication/Register/RegisterRightSection.js b/client/src/containers/Authentication/Register/RegisterRightSection.js index 3f3231e26..ac628ac0b 100644 --- a/client/src/containers/Authentication/Register/RegisterRightSection.js +++ b/client/src/containers/Authentication/Register/RegisterRightSection.js @@ -1,26 +1,55 @@ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { Wizard, Steps, Step } from 'react-albus'; +import { useHistory } from "react-router-dom"; import RegisterWizardSteps from './RegisterWizardSteps'; -import registerRoutes from 'routes/register'; +import PrivateRoute from 'components/PrivateRoute'; + +import RegisterUserForm from 'containers/Authentication/Register/RegisterUserForm'; +import RegisterSubscriptionForm from 'containers/Authentication/Register/RegisterSubscriptionForm'; +import RegisterOrganizationForm from 'containers/Authentication/Register/RegisterOrganizationForm'; export default function RegisterRightSection () { + const history = useHistory(); return (
- + ( +
+ -
- - { registerRoutes.map((route, index) => ( - - )) } - -
+ + +
+ + + + + + + + + + + + + + +

Ice King

+
+
+
+
+
+
+ )} />
) } \ No newline at end of file diff --git a/client/src/containers/Authentication/Register/RegisterUserForm.js b/client/src/containers/Authentication/Register/RegisterUserForm.js index d2d05892b..7dd592543 100644 --- a/client/src/containers/Authentication/Register/RegisterUserForm.js +++ b/client/src/containers/Authentication/Register/RegisterUserForm.js @@ -77,17 +77,12 @@ function RegisterUserForm({ requestRegister, requestLogin }) { onSubmit: (values, { setSubmitting, setErrors }) => { requestRegister(values) .then((response) => { - // AppToaster.show({ - // message: formatMessage({ - // id: 'welcome_organization_account_has_been_created', - // }), - // intent: Intent.SUCCESS, - // }); requestLogin({ crediential: values.email, password: values.password, }) .then(() => { + history.push('/register/subscription'); setSubmitting(false); }) .catch((errors) => { @@ -98,7 +93,6 @@ function RegisterUserForm({ requestRegister, requestLogin }) { intent: Intent.SUCCESS, }); }); - // history.push('/auth/login'); }) .catch((errors) => { if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) { diff --git a/client/src/containers/Organization/withOrganizationActions.js b/client/src/containers/Organization/withOrganizationActions.js new file mode 100644 index 000000000..2a23bb6bb --- /dev/null +++ b/client/src/containers/Organization/withOrganizationActions.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { + fetchOrganizations, +} from 'store/organizations/organizations.actions'; + +export const mapDispatchToProps = (dispatch) => ({ + requestOrganizationsList: () => dispatch(fetchOrganizations()), +}); + +export default connect(null, mapDispatchToProps); \ No newline at end of file diff --git a/client/src/containers/Organization/withOrganizationByOrgId.js b/client/src/containers/Organization/withOrganizationByOrgId.js new file mode 100644 index 000000000..b07c89167 --- /dev/null +++ b/client/src/containers/Organization/withOrganizationByOrgId.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { + getOrganizationByOrgIdFactory, +} from 'store/organizations/organizations.selector'; + +export default (mapState) => { + const getOrganizationByOrgId = getOrganizationByOrgIdFactory(); + + const mapStateToProps = (state, props) => { + const mapped = { + organization: getOrganizationByOrgId(state, props), + }; + return mapState ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; \ No newline at end of file diff --git a/client/src/containers/Organization/withOrganizationByTenId.js b/client/src/containers/Organization/withOrganizationByTenId.js new file mode 100644 index 000000000..998308914 --- /dev/null +++ b/client/src/containers/Organization/withOrganizationByTenId.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { + getOrganizationByTenantIdFactory, +} from 'store/organizations/organizations.selector'; + +export default (mapState) => { + const getOrgByTenId = getOrganizationByTenantIdFactory(); + + const mapStateToProps = (state, props) => { + const mapped = { + organization: getOrgByTenId(state, props), + }; + return mapState ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; \ No newline at end of file diff --git a/client/src/store/authentication/authentication.reducer.js b/client/src/store/authentication/authentication.reducer.js index 15a4d281f..4cd2f0adc 100644 --- a/client/src/store/authentication/authentication.reducer.js +++ b/client/src/store/authentication/authentication.reducer.js @@ -5,6 +5,7 @@ const initialState = { token: '', organization: '', user: '', + tenant: {}, locale: '', errors: [], }; @@ -15,6 +16,7 @@ export default createReducer(initialState, { state.token = token; state.user = user; state.organization = tenant.organization_id; + state.tenant = tenant; }, [t.LOGIN_FAILURE]: (state, action) => { @@ -36,3 +38,9 @@ export const isAuthenticated = (state) => !!state.authentication.token; export const hasErrorType = (state, errorType) => { return state.authentication.errors.find((e) => e.type === errorType); }; + +export const isTenantSeeded = (state) => !!state.tenant.seeded_at; +export const isTenantBuilt = (state) => !!state.tenant.initialized_at; + +export const isTenantHasSubscription = () => false; +export const isTenantSubscriptionExpired = () => false; \ No newline at end of file diff --git a/client/src/store/organizations/organizations.actions.js b/client/src/store/organizations/organizations.actions.js new file mode 100644 index 000000000..fec7dee04 --- /dev/null +++ b/client/src/store/organizations/organizations.actions.js @@ -0,0 +1,16 @@ +import ApiService from 'services/ApiService'; +import t from 'store/types'; + +export const fetchOrganizations = () => { + return (dispatch) => new Promise((resolve, reject) => { + ApiService.get('organization/all').then((response) => { + dispatch({ + type: t.ORGANIZATIONS_LIST_SET, + payload: { + organizations: response.data.organizations, + }, + }); + resolve(response) + }).catch(error => { reject(error); }); + }); +}; \ No newline at end of file diff --git a/client/src/store/organizations/organizations.reducers.js b/client/src/store/organizations/organizations.reducers.js new file mode 100644 index 000000000..b8407108d --- /dev/null +++ b/client/src/store/organizations/organizations.reducers.js @@ -0,0 +1,25 @@ +import { createReducer } from '@reduxjs/toolkit'; +import t from 'store/types'; + +const initialState = { + data: {}, + byOrganizationId: {}, +}; + +const reducer = createReducer(initialState, { + + [t.ORGANIZATIONS_LIST_SET]: (state, action) => { + const { organizations } = action.payload; + const _data = {}; + const _dataByOrganizationId = {}; + + organizations.forEach((organization) => { + _data[organization.id] = organization; + _dataByOrganizationId[organization.organization_id] = organization.id; + }); + state.data = _data; + state.byOrganizationId = _dataByOrganizationId; + }, +}) + +export default reducer; \ No newline at end of file diff --git a/client/src/store/organizations/organizations.selectors.js b/client/src/store/organizations/organizations.selectors.js new file mode 100644 index 000000000..4d5368b93 --- /dev/null +++ b/client/src/store/organizations/organizations.selectors.js @@ -0,0 +1,18 @@ +import { createSelector } from '@reduxjs/toolkit'; + +const oragnizationByTenantIdSelector = (state, props) => state.organizations[props.tenantId]; +const organizationByIdSelector = (state, props) => state.organizations.byOrganizationId[props.organizationId]; + +export const getOrganizationByOrgIdFactory = () => createSelector( + organizationByIdSelector, + (organization) => { + return organization; + }, +); + +export const getOrganizationByTenantIdFactory = () => createSelector( + oragnizationByTenantIdSelector, + (organization) => { + return organization; + } +) \ No newline at end of file diff --git a/client/src/store/organizations/organizations.types.js b/client/src/store/organizations/organizations.types.js new file mode 100644 index 000000000..1b9d47692 --- /dev/null +++ b/client/src/store/organizations/organizations.types.js @@ -0,0 +1,5 @@ + + +export default { + ORGANIZATIONS_LIST_SET: 'ORGANIZATIONS_LIST_SET', +}; \ No newline at end of file diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js index 2e6e5f46e..2827e1eed 100644 --- a/client/src/store/reducers.js +++ b/client/src/store/reducers.js @@ -25,9 +25,11 @@ import bills from './Bills/bills.reducer'; import vendors from './vendors/vendors.reducer'; import paymentReceives from './PaymentReceive/paymentReceive.reducer'; import paymentMades from './PaymentMades/paymentMade.reducer'; +import organizations from './organizations/organizations.reducers'; export default combineReducers({ authentication, + organizations, dashboard, users, accounts, diff --git a/client/src/store/types.js b/client/src/store/types.js index b0adaa6cb..755bad8dc 100644 --- a/client/src/store/types.js +++ b/client/src/store/types.js @@ -24,6 +24,7 @@ import bills from './Bills/bills.type'; import vendors from './vendors/vendors.types'; import paymentReceives from './PaymentReceive/paymentReceive.type'; import paymentMades from './PaymentMades/paymentMade.type'; +import organizations from './organizations/organizations.types'; export default { ...authentication, @@ -52,4 +53,5 @@ export default { ...bills, ...paymentReceives, ...paymentMades, + ...organizations, }; diff --git a/server/src/api/controllers/Organization.ts b/server/src/api/controllers/Organization.ts index 9d2351e05..5c298d0c1 100644 --- a/server/src/api/controllers/Organization.ts +++ b/server/src/api/controllers/Organization.ts @@ -1,5 +1,5 @@ import { Inject, Service } from 'typedi'; -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import asyncMiddleware from "api/middleware/asyncMiddleware"; import JWTAuth from 'api/middleware/jwtAuth'; import TenancyMiddleware from 'api/middleware/TenancyMiddleware'; @@ -27,12 +27,14 @@ export default class OrganizationController extends BaseController{ router.use(JWTAuth); router.use(AttachCurrentTenantUser); router.use(TenancyMiddleware); - router.use(SubscriptionMiddleware('main')); - + // Should to seed organization tenant be configured. + router.use('/seed', SubscriptionMiddleware('main')); router.use('/seed', SettingsMiddleware); router.use('/seed', EnsureConfiguredMiddleware); + router.use('/build', SubscriptionMiddleware('main')); + router.post( '/build', asyncMiddleware(this.build.bind(this)) @@ -41,6 +43,10 @@ export default class OrganizationController extends BaseController{ '/seed', asyncMiddleware(this.seed.bind(this)), ); + router.get( + '/all', + asyncMiddleware(this.allOrganizations.bind(this)), + ); return router; } @@ -116,4 +122,21 @@ export default class OrganizationController extends BaseController{ next(error); } } + + /** + * Listing all organizations that assocaited to the authorized user. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async allOrganizations(req: Request, res: Response, next: NextFunction) { + const { user } = req; + + try { + const organizations = await this.organizationService.listOrganizations(user); + return res.status(200).send({ organizations }); + } catch (error) { + next(error); + } + } } \ No newline at end of file diff --git a/server/src/services/Organization/index.ts b/server/src/services/Organization/index.ts index 88ef7515a..48fde80b3 100644 --- a/server/src/services/Organization/index.ts +++ b/server/src/services/Organization/index.ts @@ -1,6 +1,6 @@ import { Service, Inject } from 'typedi'; import { ServiceError } from 'exceptions'; -import { ITenant } from 'interfaces'; +import { ISystemService, ISystemUser, ITenant } from 'interfaces'; import { EventDispatcher, EventDispatcherInterface, @@ -38,7 +38,7 @@ export default class OrganizationService { * @param {srting} organizationId * @return {Promise} */ - async build(organizationId: string): Promise { + public async build(organizationId: string): Promise { const tenant = await this.getTenantByOrgIdOrThrowError(organizationId); this.throwIfTenantInitizalized(tenant); @@ -69,7 +69,7 @@ export default class OrganizationService { * @param {number} organizationId * @return {Promise} */ - async seed(organizationId: string): Promise { + public async seed(organizationId: string): Promise { const tenant = await this.getTenantByOrgIdOrThrowError(organizationId); this.throwIfTenantSeeded(tenant); @@ -91,6 +91,20 @@ export default class OrganizationService { } } + /** + * Listing all associated organizations to the given user. + * @param {ISystemUser} user - + * @return {Promise} + */ + public async listOrganizations(user: ISystemUser): Promise { + this.logger.info('[organization] trying to list all organizations.', { user }); + + const { tenantRepository } = this.sysRepositories; + const tenant = await tenantRepository.getByIdWithSubscriptions(user.tenantId); + + return [tenant]; + } + /** * Throws error in case the given tenant is undefined. * @param {ITenant} tenant diff --git a/server/src/system/repositories/TenantRepository.ts b/server/src/system/repositories/TenantRepository.ts index 23de76a6b..b5392740d 100644 --- a/server/src/system/repositories/TenantRepository.ts +++ b/server/src/system/repositories/TenantRepository.ts @@ -63,11 +63,23 @@ export default class TenantRepository extends SystemRepository { /** * Retrieve tenant details by the given tenant id. - * @param {string} tenantId + * @param {string} tenantId - Tenant id. */ getById(tenantId: number) { return this.cache.get(`tenant.id.${tenantId}`, () => { return Tenant.query().findById(tenantId); }); } + + /** + * Retrieve tenant details with associated subscriptions + * and plans by the given tenant id. + * @param {number} tenantId - Tenant id. + */ + getByIdWithSubscriptions(tenantId: number) { + return this.cache.get(`tenant.id.${tenantId}.subscriptions`, () => { + return Tenant.query().findById(tenantId) + .withGraphFetched('subscriptions.plan'); + }); + } } \ No newline at end of file