15856: Merge branch 'master' into 15856-illegal-chars-warning 15856-illegal-chars-warning
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 3 Dec 2019 18:09:52 +0000 (15:09 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 3 Dec 2019 18:34:59 +0000 (15:34 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

46 files changed:
package.json
src/common/getuser.ts [new file with mode: 0644]
src/index.tsx
src/models/tree.ts
src/models/user.ts
src/services/auth-service/auth-service.ts
src/services/services.ts
src/services/user-service/user-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action-ssh.ts
src/store/auth/auth-action.test.ts
src/store/auth/auth-action.ts
src/store/auth/auth-middleware.ts [new file with mode: 0644]
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
src/store/collections/collection-create-actions.ts
src/store/config/config-action.ts [deleted file]
src/store/config/config-reducer.ts [deleted file]
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/favorites/favorites-actions.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/store/my-account/my-account-panel-actions.ts
src/store/navigation/navigation-action.ts
src/store/project-tree-picker/project-tree-picker-actions.ts
src/store/projects/project-create-actions.ts
src/store/projects/project-move-actions.ts
src/store/public-favorites-panel/public-favorites-middleware-service.ts
src/store/public-favorites/public-favorites-actions.ts
src/store/repositories/repositories-actions.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/search-bar/search-bar-tree-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/store.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/users/users-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/api-token/api-token.tsx
src/views-components/main-app-bar/account-menu.tsx
src/views/inactive-panel/inactive-panel.tsx
src/views/link-account-panel/link-account-panel.tsx
src/views/login-panel/login-panel.tsx
src/views/main-panel/main-panel.tsx
yarn.lock

index a4ea3b1980dcbcda9c9a9da604a39a89ead0e8a7..1059a07d3585904d7eda83d1ca414d66e2e42496 100644 (file)
     "redux-form": "7.4.2",
     "redux-thunk": "2.3.0",
     "reselect": "4.0.0",
+    "set-value": "2.0.1",
     "shell-quote": "1.6.1",
+    "sinon": "7.3",
+    "ts-mock-imports": "1.2.6",
     "tslint-etc": "1.6.0",
     "unionize": "2.1.2",
     "uuid": "3.3.2",
@@ -82,6 +85,7 @@
     "@types/react-router-dom": "4.3.1",
     "@types/react-router-redux": "5.0.16",
     "@types/redux-devtools": "3.0.44",
+    "@types/sinon": "7.5",
     "@types/uuid": "3.4.4",
     "axios-mock-adapter": "1.15.0",
     "enzyme": "3.6.0",
diff --git a/src/common/getuser.ts b/src/common/getuser.ts
new file mode 100644 (file)
index 0000000..dc96f92
--- /dev/null
@@ -0,0 +1,10 @@
+import { RootState } from '~/store/store';
+
+export const getUserUuid = (state: RootState) => {
+    const user = state.auth.user;
+    if (user) {
+        return user.uuid;
+    } else {
+        return undefined;
+    }
+};
index a8fa809ca70c728637a0c10484cfa5a5a3b6e2aa..aa372fa5081c0eb2c4d05ea1b22babed765ece8a 100644 (file)
@@ -61,6 +61,7 @@ import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions
 import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set';
 import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
 import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -99,8 +100,20 @@ fetchConfig()
                 store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
             },
             errorFn: (id, error) => {
-                // console.error("Backend error:", error);
-                // store.dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Backend error", kind: SnackbarKind.ERROR }));
+                console.error("Backend error:", error);
+                if (error.errors) {
+                    store.dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: `${error.errors[0]}`,
+                        kind: SnackbarKind.ERROR,
+                        hideDuration: 8000
+                    }));
+                } else {
+                    store.dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: `${error.message}`,
+                        kind: SnackbarKind.ERROR,
+                        hideDuration: 8000
+                    }));
+                }
             }
         });
         const store = configureStore(history, services);
index 60a95342eedbbe6b8337f16c84ea896e6f978dcd..de2f7b71a1b6861ccd6af42e262f652d8e7d67b0 100644 (file)
@@ -238,6 +238,9 @@ const getRootNodeChildrenIds = <T>(tree: Tree<T>) =>
 
 
 const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+    if (childId === "") {
+        return tree;
+    }
     const node = getNode(parentId)(tree);
     if (node) {
         const children = node.children.some(id => id === childId)
index 2497864507787cef09d6d3914780b79136090d77..87a97dfcd1934b057369cee39bcfb82f389c05e0 100644 (file)
@@ -30,15 +30,8 @@ export const getUserFullname = (user?: User) => {
     return user ? `${user.firstName} ${user.lastName}` : "";
 };
 
-export interface UserResource extends Resource {
+export interface UserResource extends Resource, User {
     kind: ResourceKind.USER;
-    email: string;
-    username: string;
-    firstName: string;
-    lastName: string;
-    isAdmin: boolean;
-    prefs: UserPrefs;
     defaultOwnerUuid: string;
-    isActive: boolean;
     writableBy: string[];
 }
index d5cb4ec205c36cdfc9d0545a167218a454b3103a..c6e93a8fe777210e45d8f8a4a1bc9f2abb7fec33 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getUserFullname, User, UserPrefs, UserResource } from '~/models/user';
+import { getUserFullname, User, UserPrefs } from '~/models/user';
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import * as uuid from "uuid/v4";
@@ -43,7 +43,10 @@ export class AuthService {
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
-        localStorage.setItem(HOME_CLUSTER, token.split('/')[1].substr(0, 5));
+        const sp = token.split('/');
+        if (sp.length === 3) {
+            localStorage.setItem(HOME_CLUSTER, sp[1].substr(0, 5));
+        }
     }
 
     public removeApiToken() {
@@ -58,50 +61,6 @@ export class AuthService {
         return localStorage.getItem(HOME_CLUSTER) || undefined;
     }
 
-    public getUuid() {
-        return localStorage.getItem(USER_UUID_KEY) || undefined;
-    }
-
-    public getOwnerUuid() {
-        return localStorage.getItem(USER_OWNER_UUID_KEY) || undefined;
-    }
-
-    public getIsAdmin(): boolean {
-        return localStorage.getItem(USER_IS_ADMIN) === 'true';
-    }
-
-    public getIsActive(): boolean {
-        return localStorage.getItem(USER_IS_ACTIVE) === 'true';
-    }
-
-    public getUser(): User | undefined {
-        const email = localStorage.getItem(USER_EMAIL_KEY);
-        const firstName = localStorage.getItem(USER_FIRST_NAME_KEY);
-        const lastName = localStorage.getItem(USER_LAST_NAME_KEY);
-        const uuid = this.getUuid();
-        const ownerUuid = this.getOwnerUuid();
-        const isAdmin = this.getIsAdmin();
-        const isActive = this.getIsActive();
-        const username = localStorage.getItem(USER_USERNAME);
-        const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
-
-        return email && firstName && lastName && uuid && ownerUuid && username && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
-            : undefined;
-    }
-
-    public saveUser(user: User | UserResource) {
-        localStorage.setItem(USER_EMAIL_KEY, user.email);
-        localStorage.setItem(USER_FIRST_NAME_KEY, user.firstName);
-        localStorage.setItem(USER_LAST_NAME_KEY, user.lastName);
-        localStorage.setItem(USER_UUID_KEY, user.uuid);
-        localStorage.setItem(USER_OWNER_UUID_KEY, user.ownerUuid);
-        localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
-        localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(user.isActive));
-        localStorage.setItem(USER_USERNAME, user.username);
-        localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
-    }
-
     public removeUser() {
         localStorage.removeItem(USER_EMAIL_KEY);
         localStorage.removeItem(USER_FIRST_NAME_KEY);
@@ -152,12 +111,6 @@ export class AuthService {
             });
     }
 
-    public getRootUuid() {
-        const uuid = this.getOwnerUuid();
-        const uuidParts = uuid ? uuid.split('-') : [];
-        return uuidParts.length > 1 ? `${uuidParts[0]}-${uuidParts[1]}` : undefined;
-    }
-
     public getSessions(): Session[] {
         try {
             const sessions = JSON.parse(localStorage.getItem("sessions") || '');
index dd3178790a05bdca84c7456418d85562286f1a66..af547deccfd81c8f8a00af0a3d2a35cbd0c81b82 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import Axios from "axios";
+import { AxiosInstance } from "axios";
 import { ApiClientAuthorizationService } from '~/services/api-client-authorization-service/api-client-authorization-service';
 import { AuthService } from "./auth-service/auth-service";
 import { GroupsService } from "./groups-service/groups-service";
@@ -35,8 +36,26 @@ import { LinkAccountService } from "./link-account-service/link-account-service"
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
-export const createServices = (config: Config, actions: ApiActions) => {
-    const apiClient = Axios.create();
+export function setAuthorizationHeader(services: ServiceRepository, token: string) {
+    services.apiClient.defaults.headers.common = {
+        Authorization: `Bearer ${token}`
+    };
+    services.webdavClient.defaults.headers = {
+        Authorization: `Bearer ${token}`
+    };
+}
+
+export function removeAuthorizationHeader(services: ServiceRepository) {
+    delete services.apiClient.defaults.headers.common;
+    delete services.webdavClient.defaults.headers.common;
+}
+
+export const createServices = (config: Config, actions: ApiActions, useApiClient?: AxiosInstance) => {
+    // Need to give empty 'headers' object or it will create an
+    // instance with a reference to the global default headers object,
+    // which is very bad because that means setAuthorizationHeader
+    // would update the global default instead of the instance default.
+    const apiClient = useApiClient || Axios.create({ headers: {} });
     apiClient.defaults.baseURL = config.baseUrl;
 
     const webdavClient = new WebDAV();
index a69203dc5bece0c4c3e1f29aceba92c3de998849..ddb9a3646b33e11717e89b0380e17d37711e9b04 100644 (file)
@@ -11,4 +11,20 @@ export class UserService extends CommonResourceService<UserResource> {
     constructor(serverApi: AxiosInstance, actions: ApiActions) {
         super(serverApi, "users", actions);
     }
+
+    activate(uuid: string) {
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + `${uuid}/activate`),
+            this.actions
+        );
+    }
+
+    unsetup(uuid: string) {
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + uuid + '/unsetup'),
+            this.actions
+        );
+    }
 }
index 5b8acf9aeb061651b656590cd496c4a2d9e8bad8..c1b97adc3ea0faa1e9b685832150cfe59bf119b8 100644 (file)
@@ -5,7 +5,7 @@
 import { Dispatch } from "redux";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
 import Axios from "axios";
 import { getUserFullname, User } from "~/models/user";
 import { authActions } from "~/store/auth/auth-action";
@@ -16,7 +16,7 @@ import {
 import { normalizeURLPath } from "~/common/url";
 import { Session, SessionStatus } from "~/models/session";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
-import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
+import { AuthService } from "~/services/auth-service/auth-service";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import * as jsSHA from "jssha";
 
@@ -81,15 +81,6 @@ const getRemoteHostConfig = async (remoteHost: string): Promise<Config | null> =
     return null;
 };
 
-const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
-    const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
-        headers: {
-            Authorization: `OAuth2 ${token}`
-        }
-    });
-    return resp.data;
-};
-
 const invalidV2Token = "Must be a v2 token";
 
 export const getSaltedToken = (clusterId: string, token: string) => {
@@ -113,19 +104,13 @@ export const validateCluster = async (config: Config, useToken: string):
     Promise<{ user: User; token: string }> => {
 
     const saltedToken = getSaltedToken(config.uuidPrefix, useToken);
-    const user = await getUserDetails(config.baseUrl, saltedToken);
+
+    const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+    setAuthorizationHeader(svc, saltedToken);
+
+    const user = await svc.authService.getUserDetails();
     return {
-        user: {
-            firstName: user.first_name,
-            lastName: user.last_name,
-            uuid: user.uuid,
-            ownerUuid: user.owner_uuid,
-            email: user.email,
-            isAdmin: user.is_admin,
-            isActive: user.is_active,
-            username: user.username,
-            prefs: user.prefs
-        },
+        user,
         token: saltedToken,
     };
 };
index 1fbdfdfa63a1b5d49aeb1ff056f3aa22b3c4fe6d..6e9ddaab3278894909b5139c4440bf4fbcff60c4 100644 (file)
@@ -5,8 +5,9 @@
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from "~/services/services";
-import {snackbarActions, SnackbarKind} from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { FormErrors, reset, startSubmit, stopSubmit } from "redux-form";
 import { KeyType } from "~/models/ssh-key";
 import {
@@ -14,9 +15,7 @@ import {
     getAuthorizedKeysServiceError
 } from "~/services/authorized-keys-service/authorized-keys-service";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
-import {
-    authActions,
-} from "~/store/auth/auth-action";
+import { authActions } from "~/store/auth/auth-action";
 
 export const SSH_KEY_CREATE_FORM_NAME = 'sshKeyCreateFormName';
 export const SSH_KEY_PUBLIC_KEY_DIALOG = 'sshKeyPublicKeyDialog';
@@ -62,7 +61,8 @@ export const removeSshKey = (uuid: string) =>
 
 export const createSshKey = (data: SshKeyCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = getState().auth.user!.uuid;
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         const { name, publicKey } = data;
         dispatch(startSubmit(SSH_KEY_CREATE_FORM_NAME));
         try {
@@ -93,11 +93,10 @@ export const createSshKey = (data: SshKeyCreateFormDialogData) =>
 export const loadSshKeysPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
-            dispatch(setBreadcrumbs([{ label: 'SSH Keys'}]));
+            dispatch(setBreadcrumbs([{ label: 'SSH Keys' }]));
             const response = await services.authorizedKeysService.list();
             dispatch(authActions.SET_SSH_KEYS(response.items));
         } catch (e) {
             return;
         }
     };
-
index a543fc18dccadd57c31043d8ecd1f99c84fe0330..f7aa5c4c9f630abcddbbe1355cb90368f8005b0b 100644 (file)
@@ -2,57 +2,66 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { authReducer, AuthState } from "./auth-reducer";
-import { AuthAction, initAuth } from "./auth-action";
-import {
-    API_TOKEN_KEY,
-    USER_EMAIL_KEY,
-    USER_FIRST_NAME_KEY,
-    USER_LAST_NAME_KEY,
-    USER_OWNER_UUID_KEY,
-    USER_UUID_KEY,
-    USER_IS_ADMIN,
-    USER_IS_ACTIVE,
-    USER_USERNAME,
-    USER_PREFS
-} from "~/services/auth-service/auth-service";
+import { initAuth } from "./auth-action";
+import { API_TOKEN_KEY } from "~/services/auth-service/auth-service";
 
 import 'jest-localstorage-mock';
-import { createServices } from "~/services/services";
+import { ServiceRepository, createServices } from "~/services/services";
 import { configureStore, RootStore } from "../store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { mockConfig } from '~/common/config';
 import { ApiActions } from "~/services/api/api-actions";
 import { ACCOUNT_LINK_STATUS_KEY } from '~/services/link-account-service/link-account-service';
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { ImportMock } from 'ts-mock-imports';
+import * as servicesModule from "~/services/services";
 
 describe('auth-actions', () => {
-    let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+    const axiosInst = Axios.create({ headers: {} });
+    const axiosMock = new MockAdapter(axiosInst);
+
     let store: RootStore;
+    let services: ServiceRepository;
     const actions: ApiActions = {
         progressFn: (id: string, working: boolean) => { },
         errorFn: (id: string, message: string) => { }
     };
+    let importMocks: any[];
 
     beforeEach(() => {
-        store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
+        axiosMock.reset();
+        services = createServices(mockConfig({}), actions, axiosInst);
+        store = configureStore(createBrowserHistory(), services);
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({}), actions));
+        importMocks = [];
+    });
+
+    afterEach(() => {
+        importMocks.map(m => m.restore());
     });
 
-    it('should initialise state with user and api token from local storage', () => {
+    it('should initialise state with user and api token from local storage', (done) => {
+
+        axiosMock
+            .onGet("/users/current")
+            .reply(200, {
+                email: "test@test.com",
+                first_name: "John",
+                last_name: "Doe",
+                uuid: "zzzzz-tpzed-abcefg",
+                owner_uuid: "ownerUuid",
+                is_admin: false,
+                is_active: true,
+                username: "jdoe",
+                prefs: {}
+            });
+
+        importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
 
         // Only test the case when a link account operation is not being cancelled
         sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
         localStorage.setItem(API_TOKEN_KEY, "token");
-        localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
-        localStorage.setItem(USER_FIRST_NAME_KEY, "John");
-        localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
-        localStorage.setItem(USER_UUID_KEY, "zzzzz-tpzed-abcefg");
-        localStorage.setItem(USER_USERNAME, "username");
-        localStorage.setItem(USER_PREFS, JSON.stringify({}));
-        localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-        localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false));
-        localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(true));
 
         const config: any = {
             rootUrl: "https://zzzzz.arvadosapi.com",
@@ -62,62 +71,84 @@ describe('auth-actions', () => {
 
         store.dispatch(initAuth(config));
 
-        expect(store.getState().auth).toEqual({
-            apiToken: "token",
-            sshKeys: [],
-            homeCluster: "zzzzz",
-            localCluster: "zzzzz",
-            loginCluster: undefined,
-            remoteHostsConfig: {
-                "zzzzz": {
-                    "remoteHosts": {
-                        "xc59z": "xc59z.arvadosapi.com",
-                    },
-                    "rootUrl": "https://zzzzz.arvadosapi.com",
-                    "uuidPrefix": "zzzzz",
-                },
-            },
-            remoteHosts: {
-                zzzzz: "zzzzz.arvadosapi.com",
-                xc59z: "xc59z.arvadosapi.com"
-            },
-            sessions: [{
-                "active": true,
-                "baseUrl": undefined,
-                "clusterId": "zzzzz",
-                "email": "test@test.com",
-                "loggedIn": true,
-                "remoteHost": "https://zzzzz.arvadosapi.com",
-                "status": 2,
-                "token": "token",
-                "name": "John Doe"
-               "uuid": "zzzzz-tpzed-abcefg",
-            }, {
-                "active": false,
-                "baseUrl": "",
-                "clusterId": "xc59z",
-                "email": "",
-                "loggedIn": false,
-                "remoteHost": "xc59z.arvadosapi.com",
-                "status": 1,
-                "token": "",
-                "name": "",
-                "uuid": "",
-            }],
-            user: {
-                email: "test@test.com",
-                firstName: "John",
-                lastName: "Doe",
-                uuid: "zzzzz-tpzed-abcefg",
-                ownerUuid: "ownerUuid",
-                username: "username",
-                prefs: {},
-                isAdmin: false,
-                isActive: true
+        store.subscribe(() => {
+            const auth = store.getState().auth;
+            if (auth.apiToken === "token" &&
+                auth.sessions.length === 2 &&
+                auth.sessions[0].status === 2 &&
+                auth.sessions[1].status === 2
+            ) {
+                try {
+                    expect(auth).toEqual({
+                        apiToken: "token",
+                        config: {
+                            remoteHosts: {
+                                "xc59z": "xc59z.arvadosapi.com",
+                            },
+                            rootUrl: "https://zzzzz.arvadosapi.com",
+                            uuidPrefix: "zzzzz",
+                        },
+                        sshKeys: [],
+                        homeCluster: "zzzzz",
+                        localCluster: "zzzzz",
+                        loginCluster: undefined,
+                        remoteHostsConfig: {
+                            "zzzzz": {
+                                "remoteHosts": {
+                                    "xc59z": "xc59z.arvadosapi.com",
+                                },
+                                "rootUrl": "https://zzzzz.arvadosapi.com",
+                                "uuidPrefix": "zzzzz",
+                            },
+                        },
+                        remoteHosts: {
+                            zzzzz: "zzzzz.arvadosapi.com",
+                            xc59z: "xc59z.arvadosapi.com"
+                        },
+                        sessions: [{
+                            "active": true,
+                            "baseUrl": undefined,
+                            "clusterId": "zzzzz",
+                            "email": "test@test.com",
+                            "loggedIn": true,
+                            "remoteHost": "https://zzzzz.arvadosapi.com",
+                            "status": 2,
+                            "token": "token",
+                            "name": "John Doe"
+                   "uuid": "zzzzz-tpzed-abcefg",
+                        }, {
+                            "active": false,
+                            "baseUrl": "",
+                            "clusterId": "xc59z",
+                            "email": "",
+                            "loggedIn": false,
+                            "remoteHost": "xc59z.arvadosapi.com",
+                            "status": 2,
+                            "token": "",
+                            "name": "",
+                            "uuid": "",
+                        }],
+                        user: {
+                            email: "test@test.com",
+                            firstName: "John",
+                            lastName: "Doe",
+                            uuid: "zzzzz-tpzed-abcefg",
+                            ownerUuid: "ownerUuid",
+                            username: "jdoe",
+                            prefs: { profile: {} },
+                            isAdmin: false,
+                            isActive: true
+                        }
+                    });
+                    done();
+                } catch (e) {
+                    console.log(e);
+                }
             }
         });
     });
 
+
     // TODO: Add remaining action tests
     /*
     it('should fire external url to login', () => {
index 9f18b5b029ea30e153fb9b59ec911630d1f5d722..1d8a01c6f61ef1ee6562012cc184d17a0ea732eb 100644 (file)
@@ -4,25 +4,23 @@
 
 import { ofType, unionize, UnionOf } from '~/common/unionize';
 import { Dispatch } from "redux";
-import { AxiosInstance } from "axios";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
 import { Session } from "~/models/session";
 import { Config } from '~/common/config';
-import { initSessions } from "~/store/auth/auth-action-session";
-import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
 import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
-import { AxiosError } from "axios";
+import { createServices, setAuthorizationHeader } from "~/services/services";
+import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
 export const authActions = unionize({
-    SAVE_API_TOKEN: ofType<string>(),
-    SAVE_USER: ofType<UserResource>(),
     LOGIN: {},
-    LOGOUT: {},
-    CONFIG: ofType<{ config: Config }>(),
-    INIT: ofType<{ user: User, token: string }>(),
+    LOGOUT: ofType<{ deleteLinkData: boolean }>(),
+    SET_CONFIG: ofType<{ config: Config }>(),
+    INIT_USER: ofType<{ user: User, token: string }>(),
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>(),
     SET_SSH_KEYS: ofType<SshKeyResource[]>(),
@@ -36,19 +34,6 @@ export const authActions = unionize({
     REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
 });
 
-export function setAuthorizationHeader(services: ServiceRepository, token: string) {
-    services.apiClient.defaults.headers.common = {
-        Authorization: `OAuth2 ${token}`
-    };
-    services.webdavClient.defaults.headers = {
-        Authorization: `OAuth2 ${token}`
-    };
-}
-
-function removeAuthorizationHeader(client: AxiosInstance) {
-    delete client.defaults.headers.common.Authorization;
-}
-
 export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     // Cancel any link account ops in progress unless the user has
     // just logged in or there has been a successful link operation
@@ -64,43 +49,36 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
 };
 
 const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    const user = services.authService.getUser();
     const token = services.authService.getApiToken();
     let homeCluster = services.authService.getHomeCluster();
-    if (token) {
-        setAuthorizationHeader(services, token);
-    }
     if (homeCluster && !config.remoteHosts[homeCluster]) {
         homeCluster = undefined;
     }
-    dispatch(authActions.CONFIG({ config }));
+    dispatch(authActions.SET_CONFIG({ config }));
     dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
-    document.title = `Arvados Workbench (${config.uuidPrefix})`;
-    if (token && user) {
-        dispatch(authActions.INIT({ user, token }));
-        dispatch<any>(initSessions(services.authService, config, user));
-        dispatch<any>(getUserDetails()).then((user: User) => {
-            dispatch(authActions.INIT({ user, token }));
-        }).catch((err: AxiosError) => {
-            if (err.response) {
-                // Bad token
-                if (err.response.status === 401) {
-                    dispatch<any>(logout());
-                }
-            }
+
+    if (token && token !== "undefined") {
+        dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+        dispatch<any>(saveApiToken(token)).then(() => {
+            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+        }).catch(() => {
+            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
         });
     }
 };
 
-export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    services.authService.saveApiToken(token);
-    setAuthorizationHeader(services, token);
-    dispatch(authActions.SAVE_API_TOKEN(token));
+export const getConfig = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Config => {
+    const state = getState().auth;
+    return state.remoteHostsConfig[state.localCluster];
 };
 
-export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    services.authService.saveUser(user);
-    dispatch(authActions.SAVE_USER(user));
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+    const config = dispatch<any>(getConfig);
+    const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+    setAuthorizationHeader(svc, token);
+    return svc.authService.getUserDetails().then((user: User) => {
+        dispatch(authActions.INIT_USER({ user, token }));
+    });
 };
 
 export const login = (uuidPrefix: string, homeCluster: string, loginCluster: string,
@@ -110,23 +88,7 @@ export const login = (uuidPrefix: string, homeCluster: string, loginCluster: str
     };
 
 export const logout = (deleteLinkData: boolean = false) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    if (deleteLinkData) {
-        services.linkAccountService.removeAccountToLink();
-    }
-    services.authService.removeApiToken();
-    services.authService.removeUser();
-    removeAuthorizationHeader(services.apiClient);
-    services.authService.logout();
-    dispatch(authActions.LOGOUT());
-};
-
-export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
-    dispatch(authActions.USER_DETAILS_REQUEST());
-    return services.authService.getUserDetails().then(user => {
-        services.authService.saveUser(user);
-        dispatch(authActions.USER_DETAILS_SUCCESS(user));
-        return user;
-    });
+    dispatch(authActions.LOGOUT({ deleteLinkData }));
 };
 
 export type AuthAction = UnionOf<typeof authActions>;
diff --git a/src/store/auth/auth-middleware.ts b/src/store/auth/auth-middleware.ts
new file mode 100644 (file)
index 0000000..76f8598
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import { authActions, } from "./auth-action";
+import { ServiceRepository, setAuthorizationHeader, removeAuthorizationHeader } from "~/services/services";
+import { initSessions } from "~/store/auth/auth-action-session";
+import { User } from "~/models/user";
+import { RootState } from '~/store/store';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
+
+export const authMiddleware = (services: ServiceRepository): Middleware => store => next => action => {
+    // Middleware to update external state (local storage, window
+    // title) to ensure that they stay in sync with redux state.
+
+    authActions.match(action, {
+        INIT_USER: ({ user, token }) => {
+            // The "next" method passes the action to the next
+            // middleware in the chain, or the reducer.  That means
+            // after next() returns, the action has (presumably) been
+            // applied by the reducer to update the state.
+            next(action);
+
+            const state: RootState = store.getState();
+
+            if (state.auth.apiToken) {
+                services.authService.saveApiToken(state.auth.apiToken);
+                setAuthorizationHeader(services, state.auth.apiToken);
+            } else {
+                services.authService.removeApiToken();
+                removeAuthorizationHeader(services);
+            }
+
+            store.dispatch<any>(initSessions(services.authService, state.auth.remoteHostsConfig[state.auth.localCluster], user));
+            if (!user.isActive) {
+                // As a special case, if the user is inactive, they
+                // may be able to self-activate using the "activate"
+                // method.  Note, for this to work there can't be any
+                // unsigned user agreements, we assume the API server is just going to
+                // rubber-stamp our activation request.  At some point in the future we'll
+                // want to either add support for displaying/signing user
+                // agreements or get rid of self-activation.
+                // For more details, see:
+                // https://doc.arvados.org/master/admin/user-management.html
+
+                store.dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+                services.userService.activate(user.uuid).then((user: User) => {
+                    store.dispatch(authActions.INIT_USER({ user, token }));
+                    store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+                }).catch(() => {
+                    store.dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+                });
+            }
+        },
+        SET_CONFIG: ({ config }) => {
+            document.title = `Arvados Workbench (${config.uuidPrefix})`;
+            next(action);
+        },
+        LOGOUT: ({ deleteLinkData }) => {
+            next(action);
+            if (deleteLinkData) {
+                services.linkAccountService.removeAccountToLink();
+            }
+            services.authService.removeApiToken();
+            services.authService.removeUser();
+            removeAuthorizationHeader(services);
+            services.authService.logout();
+        },
+        default: () => next(action)
+    });
+};
index 8311e861ed67e647879a5f2ebf1b09b98a305b13..756feeeb6b835d7c3c845c88752a0aa6726dd6db 100644 (file)
@@ -35,9 +35,10 @@ describe('auth-reducer', () => {
             isAdmin: false,
             isActive: true
         };
-        const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
+        const state = reducer(initialState, authActions.INIT_USER({ user, token: "token" }));
         expect(state).toEqual({
             apiToken: "token",
+            config: mockConfig({}),
             user,
             sshKeys: [],
             sessions: [],
@@ -49,23 +50,6 @@ describe('auth-reducer', () => {
         });
     });
 
-    it('should save api token', () => {
-        const initialState = undefined;
-
-        const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
-        expect(state).toEqual({
-            apiToken: "token",
-            user: undefined,
-            sshKeys: [],
-            sessions: [],
-            homeCluster: "",
-            localCluster: "",
-            loginCluster: "",
-            remoteHosts: {},
-            remoteHostsConfig: {}
-        });
-    });
-
     it('should set user details on success fetch', () => {
         const initialState = undefined;
 
@@ -84,9 +68,10 @@ describe('auth-reducer', () => {
         const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
         expect(state).toEqual({
             apiToken: undefined,
+            config: mockConfig({}),
             sshKeys: [],
             sessions: [],
-            homeCluster: "",
+            homeCluster: "uuid",
             localCluster: "",
             loginCluster: "",
             remoteHosts: {},
index e932b97dd1920f3f6f75a2fd0fb81707e1208956..946407fe24172610fbc3aaf9cff7b95052a43af8 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { authActions, AuthAction } from "./auth-action";
-import { User, UserResource } from "~/models/user";
+import { User } from "~/models/user";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { Session } from "~/models/session";
-import { Config } from '~/common/config';
+import { Config, mockConfig } from '~/common/config';
 
 export interface AuthState {
     user?: User;
@@ -19,6 +19,7 @@ export interface AuthState {
     loginCluster: string;
     remoteHosts: { [key: string]: string };
     remoteHostsConfig: { [key: string]: Config };
+    config: Config;
 }
 
 const initialState: AuthState = {
@@ -30,20 +31,16 @@ const initialState: AuthState = {
     homeCluster: "",
     loginCluster: "",
     remoteHosts: {},
-    remoteHostsConfig: {}
+    remoteHostsConfig: {},
+    config: mockConfig({})
 };
 
 export const authReducer = (services: ServiceRepository) => (state = initialState, action: AuthAction) => {
     return authActions.match(action, {
-        SAVE_API_TOKEN: (token: string) => {
-            return { ...state, apiToken: token };
-        },
-        SAVE_USER: (user: UserResource) => {
-            return { ...state, user };
-        },
-        CONFIG: ({ config }) => {
+        SET_CONFIG: ({ config }) => {
             return {
                 ...state,
+                config,
                 localCluster: config.uuidPrefix,
                 remoteHosts: { ...config.remoteHosts, [config.uuidPrefix]: new URL(config.rootUrl).host },
                 homeCluster: config.loginCluster || config.uuidPrefix,
@@ -57,7 +54,7 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
                 remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
             };
         },
-        INIT: ({ user, token }) => {
+        INIT_USER: ({ user, token }) => {
             return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
         },
         LOGIN: () => {
@@ -67,7 +64,7 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
             return { ...state, apiToken: undefined };
         },
         USER_DETAILS_SUCCESS: (user: User) => {
-            return { ...state, user };
+            return { ...state, user, homeCluster: user.uuid.substr(0, 5) };
         },
         SET_SSH_KEYS: (sshKeys: SshKeyResource[]) => {
             return { ...state, sshKeys };
index 04b5689c5e7aaaf7d24c7ee5e165ad3b4150b630..90af2c2fb42c55cda81fad596cef9d857d76f213 100644 (file)
@@ -4,6 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
 import { getResource } from '~/store/resources/resources';
 import { TreePicker } from '../tree-picker/tree-picker';
@@ -47,7 +48,7 @@ export const setSidePanelBreadcrumbs = (uuid: string) =>
         const path = getState().router.location!.pathname;
         const currentUuid = path.split('/')[2];
         const uuidKind = extractUuidKind(currentUuid);
-        
+
         if (uuidKind === ResourceKind.COLLECTION) {
             const collectionItem = await services.collectionService.get(currentUuid);
             dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
@@ -92,7 +93,7 @@ export const setCategoryBreadcrumbs = (uuid: string, category: SidePanelTreeCate
 export const setProjectBreadcrumbs = (uuid: string) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const ancestors = getSidePanelTreeNodeAncestorsIds(uuid)(getState().treePicker);
-        const rootUuid = services.authService.getUuid();
+        const rootUuid = getUserUuid(getState());
         if (uuid === rootUuid || ancestors.find(uuid => uuid === rootUuid)) {
             dispatch(setSidePanelBreadcrumbs(uuid));
         } else {
index 642e7b82666c176de9d805de927dc7a6846275cc..57d09bf4be1c90f552a1f9c592b84ba5e5bdf8c9 100644 (file)
@@ -6,6 +6,7 @@ import { ServiceRepository } from '~/services/services';
 import { MiddlewareAPI, Dispatch } from 'redux';
 import { DataExplorerMiddlewareService } from '~/store/data-explorer/data-explorer-middleware-service';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
 import { resourcesActions } from '~/store/resources/resources-actions';
@@ -48,7 +49,7 @@ export class CollectionsWithSameContentAddressMiddlewareService extends DataExpl
             }
             try {
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-                const userUuid = api.getState().auth.user!.uuid;
+                const userUuid = getUserUuid(api.getState());
                 const pathname = api.getState().router.location!.pathname;
                 const contentAddress = pathname.split('/')[2];
                 const response = await this.services.collectionService.list({
@@ -88,11 +89,11 @@ export class CollectionsWithSameContentAddressMiddlewareService extends DataExpl
                         .addIn('uuid', groupUuids)
                         .getFilters()
                 });
-                responseUsers.items.map(it=>{
-                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid}));
+                responseUsers.items.map(it => {
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid }));
                 });
-                responseGroups.items.map(it=>{
-                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({name: `Project: ${it.name}`, uuid: it.uuid}));
+                responseGroups.items.map(it => {
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: `Project: ${it.name}`, uuid: it.uuid }));
                 });
                 api.dispatch<any>(setBreadcrumbs([{ label: 'Projects', uuid: userUuid }]));
                 api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
@@ -134,4 +135,4 @@ const couldNotFetchCollections = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch collection with this content address.',
         kind: SnackbarKind.ERROR
-    });
\ No newline at end of file
+    });
index a9534c6bb132c18a811a991285574a18ab9f88dc..e077b2a5b33c47b717630baf51c2f83b7bb600a7 100644 (file)
@@ -5,6 +5,7 @@
 import { Dispatch } from "redux";
 import { reset, startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
@@ -28,7 +29,8 @@ export const openCollectionCreateDialog = (ownerUuid: string) =>
         const router = getState();
         const properties = getState().properties;
         if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
-            const userUuid = getState().auth.user!.uuid;
+            const userUuid = getUserUuid(getState());
+            if (!userUuid) { return; }
             dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { userUuid }));
         } else {
             dispatch(initialize(COLLECTION_CREATE_FORM_NAME, { ownerUuid }));
diff --git a/src/store/config/config-action.ts b/src/store/config/config-action.ts
deleted file mode 100644 (file)
index fd79294..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { ofType, unionize, UnionOf } from '~/common/unionize';
-import { Config } from '~/common/config';
-
-export const configActions = unionize({
-    CONFIG: ofType<{ config: Config }>(),
-});
-
-export type ConfigAction = UnionOf<typeof configActions>;
diff --git a/src/store/config/config-reducer.ts b/src/store/config/config-reducer.ts
deleted file mode 100644 (file)
index f0b76b1..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { configActions, ConfigAction } from "./config-action";
-import { mockConfig } from '~/common/config';
-
-export const configReducer = (state = mockConfig({}), action: ConfigAction) => {
-    return configActions.match(action, {
-        CONFIG: ({ config }) => {
-            return {
-                ...state, ...config
-            };
-        },
-        default: () => state
-    });
-};
index 868d7b05753d0f0b7d9dc0dc006a1f1dd70a639a..b242366d648c6e3af9a6a57d2831d5752f8be0a2 100644 (file)
@@ -5,6 +5,7 @@
 import { DataExplorerMiddlewareService } from "~/store/data-explorer/data-explorer-middleware-service";
 import { FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel";
 import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
@@ -59,7 +60,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                 const responseLinks = await this.services.linkService.list({
                     filters: new FilterBuilder()
                         .addEqual("linkClass", 'star')
-                        .addEqual('tailUuid', this.services.authService.getUuid()!)
+                        .addEqual('tailUuid', getUserUuid(api.getState()))
                         .addEqual('tailKind', ResourceKind.USER)
                         .getFilters()
                 }).then(results => results);
@@ -94,7 +95,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                     response.itemsAvailable++;
                     response.items.push(it);
                 });
-                
+
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
index dfa2740ecfdc836f9ce396f950399c7c80622373..3a16c562423122f58830770cc9fd4d30ea0e83b1 100644 (file)
@@ -5,6 +5,7 @@
 import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { Dispatch } from "redux";
 import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
 import { checkFavorite } from "./favorites-reducer";
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { ServiceRepository } from "~/services/services";
@@ -20,8 +21,11 @@ export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
 export const toggleFavorite = (resource: { uuid: string; name: string }) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) {
+            return Promise.reject("No user");
+        }
         dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
-        const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
         dispatch(snackbarActions.OPEN_SNACKBAR({
@@ -56,7 +60,8 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
 
 export const updateFavorites = (resourceUuids: string[]) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = getState().auth.user!.uuid;
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
         services.favoriteService
             .checkPresenceInFavorites(userUuid, resourceUuids)
index 88d2a4ec2911ed10ab7876de7b823acb2964bcd0..1e94fcfacaf46d44469a72b4f616c54fb432e541 100644 (file)
@@ -4,16 +4,17 @@
 
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
-import { ServiceRepository } from "~/services/services";
+import { getUserUuid } from "~/common/getuser";
+import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account";
-import { saveApiToken, saveUser } from "~/store/auth/auth-action";
+import { authActions, getConfig } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
-import { login, logout, setAuthorizationHeader } from "~/store/auth/auth-action";
+import { login, logout } from "~/store/auth/auth-action";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
@@ -58,6 +59,13 @@ function validateLink(userToLink: UserResource, targetUser: UserResource) {
     return LinkAccountPanelError.NONE;
 }
 
+const newServices = (dispatch: Dispatch<any>, token: string) => {
+    const config = dispatch<any>(getConfig);
+    const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
+    setAuthorizationHeader(svc, token);
+    return svc;
+};
+
 export const checkForLinkStatus = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const status = services.linkAccountService.getLinkOpStatus();
@@ -83,8 +91,7 @@ export const checkForLinkStatus = () =>
 
 export const switchUser = (user: UserResource, token: string) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(saveUser(user));
-        dispatch(saveApiToken(token));
+        dispatch(authActions.INIT_USER({ user, token }));
     };
 
 export const linkFailed = () =>
@@ -138,9 +145,8 @@ export const loadLinkAccountPanel = () =>
 
                     // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
                     // issues since a user will always be able to query the api server for their own user data.
-                    setAuthorizationHeader(services, linkAccountData.token);
-                    const savedUserResource = await services.userService.get(linkAccountData.userUuid);
-                    setAuthorizationHeader(services, curToken);
+                    const svc = newServices(dispatch, linkAccountData.token);
+                    const savedUserResource = await svc.userService.get(linkAccountData.userUuid);
 
                     let params: any;
                     if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
@@ -198,7 +204,9 @@ export const loadLinkAccountPanel = () =>
 
 export const startLinking = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const accountToLink = { type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken() } as AccountToLink;
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
+        const accountToLink = { type: t, userUuid, token: services.authService.getApiToken() } as AccountToLink;
         services.linkAccountService.saveAccountToLink(accountToLink);
 
         const auth = getState().auth;
@@ -226,8 +234,8 @@ export const cancelLinking = (reload: boolean = false) =>
             const linkAccountData = services.linkAccountService.getAccountToLink();
             if (linkAccountData) {
                 services.linkAccountService.removeAccountToLink();
-                setAuthorizationHeader(services, linkAccountData.token);
-                user = await services.userService.get(linkAccountData.userUuid);
+                const svc = newServices(dispatch, linkAccountData.token);
+                user = await svc.userService.get(linkAccountData.userUuid);
                 dispatch(switchUser(user, linkAccountData.token));
                 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
             }
@@ -264,8 +272,8 @@ export const linkAccount = () =>
             try {
                 // The merge api links the user sending the request into the user
                 // specified in the request, so change the authorization header accordingly
-                setAuthorizationHeader(services, linkState.userToLinkToken);
-                await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
+                const svc = newServices(dispatch, linkState.userToLinkToken);
+                await svc.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
                 services.linkAccountService.removeAccountToLink();
                 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
@@ -274,8 +282,8 @@ export const linkAccount = () =>
             catch (e) {
                 // If the link operation fails, delete the previously made project
                 try {
-                    setAuthorizationHeader(services, linkState.targetUserToken);
-                    await services.projectService.delete(newGroup.uuid);
+                    const svc = newServices(dispatch, linkState.targetUserToken);
+                    await svc.projectService.delete(newGroup.uuid);
                 }
                 finally {
                     dispatch(linkFailed());
index 34bb2693dbb7b55796767cce66efdd93a5b93403..d36430a23228ec940b2fe57c62509ea023889981 100644 (file)
@@ -14,18 +14,17 @@ export const MY_ACCOUNT_FORM = 'myAccountForm';
 
 export const loadMyAccountPanel = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(setBreadcrumbs([{ label: 'User profile'}]));
+        dispatch(setBreadcrumbs([{ label: 'User profile' }]));
     };
 
 export const saveEditedUser = (resource: any) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
             await services.userService.update(resource.uuid, resource);
-            services.authService.saveUser(resource);
             dispatch(authActions.USER_DETAILS_SUCCESS(resource));
             dispatch(initialize(MY_ACCOUNT_FORM, resource));
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch(e) {
+        } catch (e) {
             return;
         }
     };
index f60f37f93420158ea7d13e4a9f8dfe88e0149c9a..5ece1abaa1488cb989a2a9e306624dfa8c693206 100644 (file)
@@ -59,9 +59,9 @@ export const pushOrGoto = (url: string): AnyAction => {
 export const navigateToProcessLogs = compose(push, getProcessLogUrl);
 
 export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    const rootProjectUuid = services.authService.getUuid();
-    if (rootProjectUuid) {
-        dispatch<any>(navigateTo(rootProjectUuid));
+    const usr = getState().auth.user;
+    if (usr) {
+        dispatch<any>(navigateTo(usr.uuid));
     }
 };
 
index 31110175412f32c8dc1363677a223c60e2306e2b..d05e2dc7a1c73e10eff89b3420248836132f913e 100644 (file)
@@ -4,22 +4,23 @@
 
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from "~/services/services";
 import { mockProjectResource } from "~/models/test-utils";
 import { treePickerActions, receiveTreePickerProjectsData } from "~/store/tree-picker/tree-picker-actions";
 import { TreePickerId } from '~/models/tree';
 
 export const resetPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.PROJECTS}));
-    dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.SHARED_WITH_ME}));
-    dispatch<any>(treePickerActions.RESET_TREE_PICKER({pickerId: TreePickerId.FAVORITES}));
+    dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.PROJECTS }));
+    dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.SHARED_WITH_ME }));
+    dispatch<any>(treePickerActions.RESET_TREE_PICKER({ pickerId: TreePickerId.FAVORITES }));
 
     dispatch<any>(initPickerProjectTree());
 };
 
 export const initPickerProjectTree = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    const uuid = services.authService.getUuid();
-
+    const uuid = getUserUuid(getState());
+    if (!uuid) { return; }
     dispatch<any>(getPickerTreeProjects(uuid));
     dispatch<any>(getSharedWithMeProjectsPickerTree(uuid));
     dispatch<any>(getFavoritesProjectsPickerTree(uuid));
@@ -43,4 +44,4 @@ const getProjectsPickerTree = (uuid: string, kind: string) => {
         [mockProjectResource({ uuid, name: kind })],
         kind
     );
-};
\ No newline at end of file
+};
index 1796fcf81614687ee63c47f0eb187cc0ced48785..a303b5518dc7a5b43ceb28cc7028c89764dea4c7 100644 (file)
@@ -5,6 +5,7 @@
 import { Dispatch } from "redux";
 import { reset, startSubmit, stopSubmit, initialize, FormErrors, formValueSelector, change } from 'redux-form';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { ProjectResource } from '~/models/project';
@@ -38,7 +39,7 @@ export const isItemNotInProject = (properties: any) => {
     if (properties.breadcrumbs) {
         return Boolean(properties.breadcrumbs[0].label !== 'Projects');
     } else {
-        return ;
+        return;
     }
 };
 
@@ -47,7 +48,8 @@ export const openProjectCreateDialog = (ownerUuid: string) =>
         const router = getState();
         const properties = getState().properties;
         if (isItemNotInProject(properties) || !isProjectOrRunProcessRoute(router)) {
-            const userUuid = getState().auth.user!.uuid;
+            const userUuid = getUserUuid(getState());
+            if (!userUuid) { return; }
             dispatch(initialize(PROJECT_CREATE_FORM_NAME, { userUuid }));
         } else {
             dispatch(initialize(PROJECT_CREATE_FORM_NAME, { ownerUuid }));
@@ -76,12 +78,12 @@ export const addPropertyToCreateProjectForm = (data: ResourcePropertiesFormData)
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
         properties[data.key] = data.value;
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
+        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
     };
 
 export const removePropertyFromCreateProjectForm = (key: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const properties = { ...PROJECT_CREATE_FORM_SELECTOR(getState(), 'properties') };
         delete properties[key];
-        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties ));
-    };
\ No newline at end of file
+        dispatch(change(PROJECT_CREATE_FORM_NAME, 'properties', properties));
+    };
index a018f3d39de691d985e802b1eb5395eacd7eb3db..4dcaf2f514c914a17c6525fb88f2abb9fc9f1cd3 100644 (file)
@@ -7,6 +7,7 @@ import { dialogActions } from "~/store/dialog/dialog-actions";
 import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
 import { ServiceRepository } from '~/services/services';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
@@ -26,7 +27,8 @@ export const openMoveProjectDialog = (resource: { name: string, uuid: string })
 
 export const moveProject = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = getState().auth.user!.uuid;
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
         try {
             const newProject = await services.projectService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
index be7f5285953df1fcf0e2feb36b01792a95cd3174..a15fe97542713b9bdec1c80729641d9db05f5e4b 100644 (file)
@@ -53,7 +53,7 @@ export class PublicFavoritesMiddlewareService extends DataExplorerMiddlewareServ
             }
             try {
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-                const uuidPrefix = api.getState().config.uuidPrefix;
+                const uuidPrefix = api.getState().auth.config.uuidPrefix;
                 const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
                 const responseLinks = await this.services.linkService.list({
                     limit: dataExplorer.rowsPerPage,
@@ -129,4 +129,4 @@ const couldNotFetchPublicFavorites = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch public favorites contents.',
         kind: SnackbarKind.ERROR
-    });
\ No newline at end of file
+    });
index 50b9070baf0987ab6ec64c4624cf301dee96d02d..d5a5cd46264778a82913adeb88866aa72dc08c04 100644 (file)
@@ -21,7 +21,7 @@ export type PublicFavoritesAction = UnionOf<typeof publicFavoritesActions>;
 export const togglePublicFavorite = (resource: { uuid: string; name: string }) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
         dispatch(progressIndicatorActions.START_WORKING("togglePublicFavorite"));
-        const uuidPrefix = getState().config.uuidPrefix;
+        const uuidPrefix = getState().auth.config.uuidPrefix;
         const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
         dispatch(publicFavoritesActions.TOGGLE_PUBLIC_FAVORITE({ resourceUuid: resource.uuid }));
         const isPublicFavorite = checkPublicFavorite(resource.uuid, getState().publicFavorites);
@@ -57,7 +57,7 @@ export const togglePublicFavorite = (resource: { uuid: string; name: string }) =
 
 export const updatePublicFavorites = (resourceUuids: string[]) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const uuidPrefix = getState().config.uuidPrefix;
+        const uuidPrefix = getState().auth.config.uuidPrefix;
         const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
         dispatch(publicFavoritesActions.CHECK_PRESENCE_IN_PUBLIC_FAVORITES(resourceUuids));
         services.favoriteService
index 3b77defe1a3f45730df3686b0d8eeeb9159568c3..239b531c61e939b5ea512fe9d0fd23e17e257b7a 100644 (file)
@@ -5,6 +5,7 @@
 import { Dispatch } from "redux";
 import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from "~/services/services";
 import { navigateToRepositories } from "~/store/navigation/navigation-action";
 import { unionize, ofType, UnionOf } from "~/common/unionize";
@@ -40,7 +41,8 @@ export const openRepositoryAttributes = (uuid: string) =>
 
 export const openRepositoryCreateDialog = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = await services.authService.getUuid();
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         const user = await services.userService.get(userUuid!);
         dispatch(reset(REPOSITORY_CREATE_FORM_NAME));
         dispatch(dialogActions.OPEN_DIALOG({ id: REPOSITORY_CREATE_FORM_NAME, data: { user } }));
@@ -48,7 +50,8 @@ export const openRepositoryCreateDialog = () =>
 
 export const createRepository = (repository: RepositoryResource) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = await services.authService.getUuid();
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         const user = await services.userService.get(userUuid!);
         dispatch(startSubmit(REPOSITORY_CREATE_FORM_NAME));
         try {
index b543b160704bae5b8aa4ab60b23c62692e322b48..66c784f944460dbc5f20b11926e27045baea1589 100644 (file)
@@ -6,6 +6,7 @@ import { Dispatch } from 'redux';
 import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { ServiceRepository } from "~/services/services";
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { WorkflowResource, getWorkflowInputs, parseWorkflowDefinition } from '~/models/workflow';
 import { getFormValues, initialize } from 'redux-form';
 import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from '~/views/run-process-panel/run-process-basic-form';
@@ -118,7 +119,8 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
     const basicForm = getFormValues(RUN_PROCESS_BASIC_FORM)(state) as RunProcessBasicFormData;
     const inputsForm = getFormValues(RUN_PROCESS_INPUTS_FORM)(state) as WorkflowInputsData;
     const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || DEFAULT_ADVANCED_FORM_VALUES;
-    const userUuid = getState().auth.user!.uuid;
+    const userUuid = getUserUuid(getState());
+    if (!userUuid) { return; }
     const pathname = getState().runProcessPanel.processPathname;
     const { processOwnerUuid, selectedWorkflow } = state.runProcessPanel;
     if (selectedWorkflow) {
index 5dc769a6e48e0f7c61262c83029a7d75e6356730..c97d77b35c4ce2308c1f2698893c5ac5fab9733e 100644 (file)
@@ -6,6 +6,7 @@ import { getTreePicker, TreePicker } from "~/store/tree-picker/tree-picker";
 import { getNode, getNodeAncestorsIds, initTreeNode, TreeNodeStatus } from "~/models/tree";
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from "~/services/services";
 import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
 import { FilterBuilder } from "~/services/api/filter-builder";
@@ -38,8 +39,10 @@ export const getSearchBarTreeNodeAncestorsIds = (id: string) => (treePicker: Tre
 };
 
 export const activateSearchBarTreeBranch = (id: string) =>
-    async (dispatch: Dispatch, _: void, services: ServiceRepository) => {
-        const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || '');
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
+        const ancestors = await services.ancestorsService.ancestors(id, userUuid);
 
         for (const ancestor of ancestors) {
             await dispatch<any>(loadSearchBarTreeProjects(ancestor.uuid));
@@ -98,4 +101,3 @@ const loadSearchBarProject = (projectUuid: string) =>
         }));
         dispatch(resourcesActions.SET_RESOURCES(items));
     };
-
index 6ad71391db199f8cd8d504be0892c96293b24cb8..1bb1624df7cdd58935c561493bb18e89ce529301 100644 (file)
@@ -5,6 +5,7 @@
 import { Dispatch } from 'redux';
 import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { resourcesActions } from '~/store/resources/resources-actions';
@@ -52,8 +53,9 @@ const SIDE_PANEL_CATEGORIES = [
 export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
 
 export const initSidePanelTree = () =>
-    (dispatch: Dispatch, _: () => RootState, { authService }: ServiceRepository) => {
-        const rootProjectUuid = authService.getUuid() || '';
+    (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+        const rootProjectUuid = getUserUuid(getState());
+        if (!rootProjectUuid) { return; }
         const nodes = SIDE_PANEL_CATEGORIES.map(id => initTreeNode({ id, value: id }));
         const projectsNode = initTreeNode({ id: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
         const sharedNode = initTreeNode({ id: SidePanelTreeCategory.SHARED_WITH_ME, value: SidePanelTreeCategory.SHARED_WITH_ME });
@@ -155,9 +157,11 @@ export const activateSidePanelTreeProject = (id: string) =>
     };
 
 export const activateSidePanelTreeBranch = (id: string) =>
-    async (dispatch: Dispatch, _: void, services: ServiceRepository) => {
-        const ancestors = await services.ancestorsService.ancestors(id, services.authService.getUuid() || '');
-        const isShared = ancestors.every(({ uuid }) => uuid !== services.authService.getUuid());
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
+        const ancestors = await services.ancestorsService.ancestors(id, userUuid);
+        const isShared = ancestors.every(({ uuid }) => uuid !== userUuid);
         if (isShared) {
             await dispatch<any>(loadSidePanelTreeProjects(SidePanelTreeCategory.SHARED_WITH_ME));
         }
index 2e27a56f74ce1c09cb607a449d313532fee12731..76a3b7e4398e3d38a84dfc3623b82bfd9eb8f0ed 100644 (file)
@@ -8,7 +8,7 @@ import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
 
 import { authReducer } from "./auth/auth-reducer";
-import { configReducer } from "./config/config-reducer";
+import { authMiddleware } from "./auth/auth-middleware";
 import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
 import { detailsPanelReducer } from './details-panel/details-panel-reducer';
 import { contextMenuReducer } from './context-menu/context-menu-reducer';
@@ -27,7 +27,6 @@ import { ServiceRepository } from "~/services/services";
 import { treePickerReducer } from './tree-picker/tree-picker-reducer';
 import { resourcesReducer } from '~/store/resources/resources-reducer';
 import { propertiesReducer } from './properties/properties-reducer';
-import { RootState } from './store';
 import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
 import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
 import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
@@ -125,6 +124,7 @@ export function configureStore(history: History, services: ServiceRepository): R
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
+        authMiddleware(services),
         projectPanelMiddleware,
         favoritePanelMiddleware,
         trashPanelMiddleware,
@@ -146,7 +146,6 @@ export function configureStore(history: History, services: ServiceRepository): R
 
 const createRootReducer = (services: ServiceRepository) => combineReducers({
     auth: authReducer(services),
-    config: configReducer,
     collectionPanel: collectionPanelReducer,
     collectionPanelFiles: collectionPanelFilesReducer,
     contextMenu: contextMenuReducer,
index f5173fc98cceea69eedaadfa80a822e97a5e3e21..bf7fae56ece038391cc7bde875ce7a8baf590a26 100644 (file)
@@ -7,6 +7,7 @@ import {
     listResultsToDataExplorerItemsMeta
 } from "../data-explorer/data-explorer-middleware-service";
 import { RootState } from "../store";
+import { getUserUuid } from "~/common/getuser";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
@@ -64,9 +65,10 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
                 .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
         }
 
+        const userUuid = getUserUuid(api.getState());
+        if (!userUuid) { return; }
         try {
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
-            const userUuid = this.services.authService.getUuid()!;
             const listResults = await this.services.groupsService
                 .contents(userUuid, {
                     ...dataExplorerToListParams(dataExplorer),
@@ -104,4 +106,3 @@ const couldNotFetchTrashContents = () =>
         message: 'Could not fetch trash contents.',
         kind: SnackbarKind.ERROR
     });
-
index 1ec49141e36b8043d2c607ef510132a0dc85ddbe..ff36e3e68b855d37a0b670d8331521fec7f2d801 100644 (file)
@@ -7,6 +7,7 @@ import { TreeNode, initTreeNode, getNodeDescendants, TreeNodeStatus, getNode, Tr
 import { createCollectionFilesTree } from "~/models/collection-file";
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { pipe, values } from 'lodash/fp';
@@ -162,7 +163,7 @@ export const loadCollection = (id: string, pickerId: string) =>
 
 export const initUserProject = (pickerId: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const uuid = services.authService.getUuid();
+        const uuid = getUserUuid(getState());
         if (uuid) {
             dispatch(receiveTreePickerData({
                 id: '',
@@ -178,7 +179,7 @@ export const initUserProject = (pickerId: string) =>
     };
 export const loadUserProject = (pickerId: string, includeCollections = false, includeFiles = false) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const uuid = services.authService.getUuid();
+        const uuid = getUserUuid(getState());
         if (uuid) {
             dispatch(loadProject({ id: uuid, pickerId, includeCollections, includeFiles }));
         }
@@ -238,9 +239,8 @@ interface LoadFavoritesProjectParams {
 export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const { pickerId, includeCollections = false, includeFiles = false } = params;
-        const uuid = services.authService.getUuid();
+        const uuid = getUserUuid(getState());
         if (uuid) {
-
             const filters = pipe(
                 (fb: FilterBuilder) => includeCollections
                     ? fb.addIsA('headUuid', [ResourceKind.PROJECT, ResourceKind.COLLECTION])
@@ -270,7 +270,7 @@ export const loadFavoritesProject = (params: LoadFavoritesProjectParams) =>
 export const loadPublicFavoritesProject = (params: LoadFavoritesProjectParams) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const { pickerId, includeCollections = false, includeFiles = false } = params;
-        const uuidPrefix = getState().config.uuidPrefix;
+        const uuidPrefix = getState().auth.config.uuidPrefix;
         const uuid = `${uuidPrefix}-j7d0g-fffffffffffffff`;
         if (uuid) {
 
@@ -319,7 +319,8 @@ export const loadProjectTreePickerProjects = (id: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId: TreePickerId.PROJECTS }));
 
-        const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
+
+        const ownerUuid = id.length === 0 ? getUserUuid(getState()) || '' : id;
         const { items } = await services.projectService.list(buildParams(ownerUuid));
 
         dispatch<any>(receiveTreePickerProjectsData(id, items, TreePickerId.PROJECTS));
@@ -327,7 +328,7 @@ export const loadProjectTreePickerProjects = (id: string) =>
 
 export const loadFavoriteTreePickerProjects = (id: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const parentId = services.authService.getUuid() || '';
+        const parentId = getUserUuid(getState()) || '';
 
         if (id === '') {
             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.FAVORITES }));
@@ -343,7 +344,7 @@ export const loadFavoriteTreePickerProjects = (id: string) =>
 
 export const loadPublicFavoriteTreePickerProjects = (id: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const parentId = services.authService.getUuid() || '';
+        const parentId = getUserUuid(getState()) || '';
 
         if (id === '') {
             dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: parentId, pickerId: TreePickerId.PUBLIC_FAVORITES }));
@@ -366,4 +367,4 @@ const buildParams = (ownerUuid: string) => {
             .addAsc('name')
             .getOrder()
     };
-};
\ No newline at end of file
+};
index 44b2bad68d92cf1ac0a77d0f55ad90a3575e61a5..f6287260d67e816603ea9ef47ad139932ca0118d 100644 (file)
@@ -5,6 +5,7 @@
 import { Dispatch } from "redux";
 import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
 import { RootState } from '~/store/store';
+import { getUserUuid } from "~/common/getuser";
 import { ServiceRepository } from "~/services/services";
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { startSubmit, reset } from "redux-form";
@@ -12,7 +13,7 @@ import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions
 import { UserResource } from "~/models/user";
 import { getResource } from '~/store/resources/resources';
 import { navigateTo, navigateToUsers, navigateToRootProject } from "~/store/navigation/navigation-action";
-import { saveApiToken } from '~/store/auth/auth-action';
+import { authActions } from '~/store/auth/auth-action';
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
@@ -53,18 +54,18 @@ export const loginAs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
         const data = getResource<UserResource>(uuid)(resources);
+        const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
         if (data) {
-            services.authService.saveUser(data);
+            dispatch<any>(authActions.INIT_USER({ user: data, token: `v2/${client.uuid}/${client.apiToken}` }));
+            location.reload();
+            dispatch<any>(navigateToRootProject);
         }
-        const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
-        dispatch<any>(saveApiToken(`v2/${client.uuid}/${client.apiToken}`));
-        location.reload();
-        dispatch<any>(navigateToRootProject);
     };
 
 export const openUserCreateDialog = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const userUuid = await services.authService.getUuid();
+        const userUuid = getUserUuid(getState());
+        if (!userUuid) { return; }
         const user = await services.userService.get(userUuid!);
         const virtualMachines = await services.virtualMachineService.list();
         dispatch(reset(USER_CREATE_FORM_NAME));
@@ -109,7 +110,12 @@ export const toggleIsActive = (uuid: string) =>
         const { resources } = getState();
         const data = getResource<UserResource>(uuid)(resources);
         const isActive = data!.isActive;
-        const newActivity = await services.userService.update(uuid, { isActive: !isActive });
+        let newActivity;
+        if (isActive) {
+            newActivity = await services.userService.unsetup(uuid);
+        } else {
+            newActivity = await services.userService.update(uuid, { isActive: true });
+        }
         dispatch<any>(loadUsersPanel());
         return newActivity;
     };
index f514d2b38ac4cea073c92bcfaeef78109fa6b04a..81e84ac52951542b2b5647e538a7ec637a5ac342 100644 (file)
@@ -4,6 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { RootState } from "~/store/store";
+import { getUserUuid } from "~/common/getuser"; 
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { favoritePanelActions, loadFavoritePanel } from '~/store/favorite-panel/favorite-panel-action';
@@ -177,7 +178,7 @@ export const loadTrash = () =>
 export const loadProject = (uuid: string) =>
     handleFirstTimeLoad(
         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-            const userUuid = services.authService.getUuid();
+            const userUuid = getUserUuid(getState());
             dispatch(setIsProjectPanelTrashed(false));
             if (userUuid) {
                 if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
@@ -260,7 +261,7 @@ export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialog
 export const loadCollection = (uuid: string) =>
     handleFirstTimeLoad(
         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-            const userUuid = services.authService.getUuid();
+            const userUuid = getUserUuid(getState());
             if (userUuid) {
                 const match = await loadGroupContentsResource({ uuid, userUuid, services });
                 match({
index 0e90b720c378670e4c7291c8db04c9f59689f094..e11afa7bf3395b587e23c312bf331b69151c8a97 100644 (file)
@@ -5,13 +5,11 @@
 import { RouteProps } from "react-router";
 import * as React from "react";
 import { connect, DispatchProp } from "react-redux";
-import { getUserDetails, saveApiToken } from "~/store/auth/auth-action";
+import { saveApiToken } from "~/store/auth/auth-action";
 import { getUrlParameter } from "~/common/url";
 import { AuthService } from "~/services/auth-service/auth-service";
 import { navigateToRootProject, navigateToLinkAccount } from "~/store/navigation/navigation-action";
-import { User } from "~/models/user";
 import { Config } from "~/common/config";
-import { initSessions } from "~/store/auth/auth-action-session";
 import { getAccountLinkData } from "~/store/link-account-panel/link-account-panel-actions";
 
 interface ApiTokenProps {
@@ -26,10 +24,7 @@ export const ApiToken = connect()(
             const search = this.props.location ? this.props.location.search : "";
             const apiToken = getUrlParameter(search, 'api_token');
             const loadMainApp = this.props.loadMainApp;
-            this.props.dispatch(saveApiToken(apiToken));
-            this.props.dispatch<any>(getUserDetails()).then((user: User) => {
-                this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
-            }).finally(() => {
+            this.props.dispatch<any>(saveApiToken(apiToken)).finally(() => {
                 if (loadMainApp) {
                     if (this.props.dispatch(getAccountLinkData())) {
                         this.props.dispatch(navigateToLinkAccount);
index 722180534469b1eebcc779b40d491408251ac4d7..346a9ef02ea529db2fa33d4e5664ed7a567ddf40 100644 (file)
@@ -32,7 +32,7 @@ interface AccountMenuProps {
 const mapStateToProps = (state: RootState): AccountMenuProps => ({
     user: state.auth.user,
     currentRoute: state.router.location ? state.router.location.pathname : '',
-    workbenchURL: state.config.workbenchUrl,
+    workbenchURL: state.auth.config.workbenchUrl,
     apiToken: state.auth.apiToken,
     localCluster: state.auth.localCluster
 });
index 91b4a51df8c41b150613077c277d7406b3226f7a..42262deb06beb67da2e3caa550300e3e026ef7c5 100644 (file)
@@ -54,7 +54,7 @@ export interface InactivePanelStateProps {
 type InactivePanelProps = WithStyles<CssRules> & InactivePanelActionProps & InactivePanelStateProps;
 
 export const InactivePanel = connect((state: RootState) => ({
-    inactivePageText: state.config.clusterConfig.Workbench.InactivePageHTML
+    inactivePageText: state.auth.config.clusterConfig.Workbench.InactivePageHTML
 }), mapDispatchToProps)(withStyles(styles)((({ classes, startLinking, inactivePageText }: InactivePanelProps) =>
     <Grid container justify="center" alignItems="center" direction="column" spacing={24}
         className={classes.root}
index 60166cda853d7b196b902ad62c34a88fe2f8d7c7..78b7efd21afc892d4609d0b1959ec986fec07ca5 100644 (file)
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { startLinking, cancelLinking, linkAccount, linkAccountPanelActions } from '~/store/link-account-panel/link-account-panel-actions';
+import { startLinking, linkAccount, linkAccountPanelActions, cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
index 293026c3d6733f19ad86f84b00006d8c143b03f9..6fe3eee2aeb6a5425615ae9dec713ec6d4b57bb7 100644 (file)
@@ -61,7 +61,7 @@ export const LoginPanel = withStyles(styles)(
         homeCluster: state.auth.homeCluster,
         uuidPrefix: state.auth.localCluster,
         loginCluster: state.auth.loginCluster,
-        welcomePage: state.config.clusterConfig.Workbench.WelcomePageHTML
+        welcomePage: state.auth.config.clusterConfig.Workbench.WelcomePageHTML
     }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix, loginCluster, welcomePage }: LoginPanelProps) =>
         <Grid container justify="center" alignItems="center"
             className={classes.root}
index dab4533fa306107b89bd6f28aabb22565e826c6b..5828c6db9054e8fc980a7029b174f7f6816c5893 100644 (file)
@@ -19,7 +19,7 @@ const mapStateToProps = (state: RootState): MainPanelRootDataProps => {
         uuidPrefix: state.auth.localCluster,
         isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.NONE || state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL,
         isLinkingPath: state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false,
-        siteBanner: state.config.clusterConfig.Workbench.SiteName
+        siteBanner: state.auth.config.clusterConfig.Workbench.SiteName
     };
 };
 
index 1e718dab1cb1cd177c1b66aaac1b9ead88b23aee..d33d165d762b2565222696933b8d5282345012d2 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     esquery "^1.0.1"
 
+"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0":
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393"
+  integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg==
+  dependencies:
+    type-detect "4.0.8"
+
+"@sinonjs/formatio@^3.2.1":
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
+  integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
+  dependencies:
+    "@sinonjs/commons" "^1"
+    "@sinonjs/samsam" "^3.1.0"
+
+"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.1":
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
+  integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
+  dependencies:
+    "@sinonjs/commons" "^1.3.0"
+    array-from "^2.1.1"
+    lodash "^4.17.15"
+
+"@sinonjs/text-encoding@^0.7.1":
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
+  integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+
 "@types/cheerio@*":
   version "0.22.9"
   resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.9.tgz#b5990152604c2ada749b7f88cab3476f21f39d7b"
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/@types/shell-quote/-/shell-quote-1.6.0.tgz#537b2949a2ebdcb0d353e448fee45b081021963f"
 
+"@types/sinon@7.5":
+  version "7.5.1"
+  resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c"
+  integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==
+
 "@types/uuid@3.4.4":
   version "3.4.4"
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
@@ -474,6 +508,11 @@ array-flatten@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
 
+array-from@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
+  integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
+
 array-includes@^3.0.3:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
@@ -2416,7 +2455,7 @@ detect-port-alt@1.1.6:
     address "^1.0.1"
     debug "^2.6.0"
 
-diff@^3.2.0:
+diff@^3.2.0, diff@^3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
 
@@ -4924,6 +4963,11 @@ jszip@3.1.5:
     pako "~1.0.2"
     readable-stream "~2.0.6"
 
+just-extend@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
+  integrity sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==
+
 keycode@^2.1.9:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
@@ -5154,6 +5198,11 @@ lodash@4.17.13:
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
 
+lodash@^4.17.15:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
 log-symbols@^2.1.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
@@ -5173,6 +5222,11 @@ loglevelnext@^1.0.1:
     es6-symbol "^3.1.1"
     object.assign "^4.1.0"
 
+lolex@^4.0.1, lolex@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7"
+  integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==
+
 longest@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -5546,6 +5600,17 @@ next-tick@1:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
 
+nise@^1.4.10:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652"
+  integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA==
+  dependencies:
+    "@sinonjs/formatio" "^3.2.1"
+    "@sinonjs/text-encoding" "^0.7.1"
+    just-extend "^4.0.2"
+    lolex "^4.1.0"
+    path-to-regexp "^1.7.0"
+
 no-case@^2.2.0:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
@@ -7536,6 +7601,16 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
 
+set-value@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+  integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+  dependencies:
+    extend-shallow "^2.0.1"
+    is-extendable "^0.1.1"
+    is-plain-object "^2.0.3"
+    split-string "^3.0.1"
+
 set-value@^0.4.3:
   version "0.4.3"
   resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
@@ -7600,6 +7675,19 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
+sinon@7.3:
+  version "7.3.2"
+  resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.3.2.tgz#82dba3a6d85f6d2181e1eca2c10d8657c2161f28"
+  integrity sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==
+  dependencies:
+    "@sinonjs/commons" "^1.4.0"
+    "@sinonjs/formatio" "^3.2.1"
+    "@sinonjs/samsam" "^3.3.1"
+    diff "^3.5.0"
+    lolex "^4.0.1"
+    nise "^1.4.10"
+    supports-color "^5.5.0"
+
 slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -7959,7 +8047,7 @@ supports-color@^4.2.1:
   dependencies:
     has-flag "^2.0.0"
 
-supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
   dependencies:
@@ -8203,6 +8291,11 @@ ts-loader@^2.3.7:
     loader-utils "^1.0.2"
     semver "^5.0.1"
 
+ts-mock-imports@1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.2.6.tgz#5a98a398c3eadb7f75b6904984bb0ba5f3fbb912"
+  integrity sha512-rZjsIEBWx9a3RGUo4Rhj/hzEGB4GPWJx46fls9EJf4UBsf5SxS2qiozf6dQp0Ym/9LC5MArlXZbZ+93wJzAmjA==
+
 tsconfig-paths-webpack-plugin@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-2.0.0.tgz#7652dc684bb3206c8e7e446831ca01cbf4d11772"
@@ -8299,6 +8392,11 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
+type-detect@4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
 type-is@~1.6.16:
   version "1.6.16"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"