15088: Adds account linking functionality
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Tue, 30 Apr 2019 19:44:59 +0000 (15:44 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Tue, 30 Apr 2019 19:44:59 +0000 (15:44 -0400)
Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

src/models/group.ts
src/models/test-utils.ts
src/services/link-account-service/link-account-service.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/store/link-account-panel/link-account-panel-reducer.ts
src/views/link-account-panel/link-account-panel-root.tsx

index e13fbcbf05da2cb9584e21149020d3447350c0cd..f34ede0afcddaaa5bdbd2e9279b881deb8bb8c8a 100644 (file)
@@ -11,6 +11,7 @@ export interface GroupResource extends TrashableResource {
     description: string;
     properties: any;
     writeableBy: string[];
+    ensure_unique_name: boolean;
 }
 
 export enum GroupClass {
index b08ce5a0525bd913b4d774262f3548b664e8e9ac..22a94f166403d4b1c54c7b1e242c5f684d6e155c 100644 (file)
@@ -24,6 +24,7 @@ export const mockGroupResource = (data: Partial<GroupResource> = {}): GroupResou
     trashAt: "",
     uuid: "",
     writeableBy: [],
+    ensure_unique_name: true,
     ...data
 });
 
index fe78d484ed6d66c4c60b16f28598c1898551c9b7..cc7c62ebdbfd285510261d1f0a36dae8f97d67ca 100644 (file)
@@ -5,25 +5,40 @@
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import { AccountToLink } from "~/models/link-account";
+import { CommonService } from "~/services/common-service/common-service";
+import { AuthService } from "../auth-service/auth-service";
 
 export const USER_LINK_ACCOUNT_KEY = 'accountToLink';
 
 export class LinkAccountService {
 
     constructor(
-        protected apiClient: AxiosInstance,
+        protected serverApi: AxiosInstance,
         protected actions: ApiActions) { }
 
-    public saveLinkAccount(account: AccountToLink) {
+    public saveToSession(account: AccountToLink) {
         sessionStorage.setItem(USER_LINK_ACCOUNT_KEY, JSON.stringify(account));
     }
 
-    public removeLinkAccount() {
+    public removeFromSession() {
         sessionStorage.removeItem(USER_LINK_ACCOUNT_KEY);
     }
 
-    public getLinkAccount() {
+    public getFromSession() {
         const data = sessionStorage.getItem(USER_LINK_ACCOUNT_KEY);
         return data ? JSON.parse(data) as AccountToLink : undefined;
     }
+
+    public linkAccounts(newUserToken: string, newGroupUuid: string) {
+        const params = {
+            new_user_token: newUserToken,
+            new_owner_uuid: newGroupUuid,
+            redirect_to_new_user: true
+        };
+        return CommonService.defaultResponse(
+            this.serverApi.post('/users/merge/', params),
+            this.actions,
+            false
+        );
+    }
 }
\ No newline at end of file
index 42fd6c52481d0b286d412f5f10411f95c74b684b..4e5052845ad68e28e4fe98fe1db158f4e5b3ca02 100644 (file)
@@ -13,57 +13,83 @@ import { UserResource } from "~/models/user";
 import { navigateToRootProject } from "~/store/navigation/navigation-action";
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<{ user: UserResource | undefined, userToLink: UserResource | undefined }>(),
-    REMOVE_LINKING: {}
+    LOAD_LINKING: ofType<{
+        user: UserResource | undefined,
+        userToken: string | undefined,
+        userToLink: UserResource | undefined,
+        userToLinkToken: string | undefined }>(),
+    RESET_LINKING: {}
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
 
 export const loadLinkAccountPanel = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
 
         const curUser = getState().auth.user;
-        if (curUser) {
-            services.userService.get(curUser.uuid).then(curUserResource => {
-                const linkAccountData = services.linkAccountService.getLinkAccount();
-                if (linkAccountData) {
-                    services.userService.get(linkAccountData.userUuid).then(savedUserResource => {
-                        if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
-                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: curUserResource, user: savedUserResource }));
-                        }
-                        else if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
-                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: savedUserResource, user: curUserResource }));
-                        }
-                        else {
-                            throw new Error('Invalid link account type.');
-                        }
-                    });
+        const curToken = getState().auth.apiToken;
+        if (curUser && curToken) {
+            const curUserResource = await services.userService.get(curUser.uuid);
+            const linkAccountData = services.linkAccountService.getFromSession();
+
+            // If there is link account data, then the user has logged in a second time
+            if (linkAccountData) {
+                // Use the saved token to make the api call to override the current users permissions
+                dispatch<any>(saveApiToken(linkAccountData.token));
+                const savedUserResource = await services.userService.get(linkAccountData.userUuid);
+                dispatch<any>(saveApiToken(curToken));
+                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                    const params = {
+                        user: savedUserResource,
+                        userToken: linkAccountData.token,
+                        userToLink: curUserResource,
+                        userToLinkToken: curToken
+                    };
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
+                }
+                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                    const params = {
+                        user: curUserResource,
+                        userToken: curToken,
+                        userToLink: savedUserResource,
+                        userToLinkToken: linkAccountData.token
+                    };
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
                 }
                 else {
-                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: undefined, user: curUserResource }));
+                    throw new Error("Invalid link account type.");
                 }
-            });
+            }
+            else {
+                // If there is no link account session data, set the state to invoke the initial UI
+                dispatch<any>(linkAccountPanelActions.LOAD_LINKING({
+                    user: curUserResource,
+                    userToken: curToken,
+                    userToLink: undefined,
+                    userToLinkToken: undefined }
+                ));
+            }
         }
     };
 
 export const saveAccountLinkData = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
-        services.linkAccountService.saveLinkAccount(accountToLink);
+        services.linkAccountService.saveToSession(accountToLink);
         dispatch(logout());
     };
 
 export const getAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        return services.linkAccountService.getLinkAccount();
+        return services.linkAccountService.getFromSession();
     };
 
 export const removeAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const linkAccountData = services.linkAccountService.getLinkAccount();
-        services.linkAccountService.removeLinkAccount();
-        dispatch(linkAccountPanelActions.REMOVE_LINKING());
+        const linkAccountData = services.linkAccountService.getFromSession();
+        services.linkAccountService.removeFromSession();
+        dispatch(linkAccountPanelActions.RESET_LINKING());
         if (linkAccountData) {
             services.userService.get(linkAccountData.userUuid).then(savedUser => {
                 dispatch(setBreadcrumbs([{ label: ''}]));
@@ -75,5 +101,43 @@ export const removeAccountLinkData = () =>
     };
 
 export const linkAccount = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const linkState = getState().linkAccountPanel;
+        const currentToken = getState().auth.apiToken;
+        if (linkState.userToLink && linkState.userToLinkToken && linkState.user && linkState.userToken && currentToken) {
+
+            // First create a project owned by the "userToLink" to accept everything from the current user
+            const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
+            dispatch<any>(saveApiToken(linkState.userToLinkToken));
+            const newGroup = await services.projectService.create({
+                name: projectName,
+                ensure_unique_name: true
+            });
+            dispatch<any>(saveApiToken(currentToken));
+
+            try {
+                dispatch<any>(saveApiToken(linkState.userToken));
+                await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
+                dispatch<any>(saveApiToken(currentToken));
+
+                // If the link was successful, switch to the account that was merged with
+                if (linkState.userToLink && linkState.userToLinkToken) {
+                    dispatch<any>(saveUser(linkState.userToLink));
+                    dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                    dispatch<any>(navigateToRootProject);
+                }
+                services.linkAccountService.removeFromSession();
+                dispatch(linkAccountPanelActions.RESET_LINKING());
+            }
+            catch(e) {
+                // If the account link operation fails, delete the previously made project
+                // and reset the link account state. The user will have to restart the process.
+                dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                services.projectService.delete(newGroup.uuid);
+                dispatch<any>(saveApiToken(currentToken));
+                services.linkAccountService.removeFromSession();
+                dispatch(linkAccountPanelActions.RESET_LINKING());
+                throw e;
+            }
+        }
     };
\ No newline at end of file
index cbfb315b200c193342fe0a01bd0de9879b896c67..fabef94ed270bcd3c2569f3d5724824cab2a6cd2 100644 (file)
@@ -7,17 +7,21 @@ import { UserResource, User } from "~/models/user";
 
 export interface LinkAccountPanelState {
     user: UserResource | undefined;
+    userToken: string | undefined;
     userToLink: UserResource | undefined;
+    userToLinkToken: string | undefined;
 }
 
 const initialState = {
     user: undefined,
-    userToLink: undefined
+    userToken: undefined,
+    userToLink: undefined,
+    userToLinkToken: undefined
 };
 
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        LOAD_LINKING: ({ userToLink, user }) => ({ ...state, user, userToLink }),
-        REMOVE_LINKING: () => ({ ...state, user: undefined, userToLink: undefined })
+        LOAD_LINKING: ({ userToLink, user, userToken, userToLinkToken}) => ({ ...state, user, userToken, userToLink, userToLinkToken }),
+        RESET_LINKING: () => ({ ...state, user: undefined, userToken: undefined, userToLink: undefined, userToLinkToken: undefined })
     });
\ No newline at end of file
index 8b3fb45a607209b512339d7f3f6c5d7de65a8527..f4570de2bc924d23847f3014271c0a49b0dcd516 100644 (file)
@@ -14,9 +14,9 @@ import {
     Grid,
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { User, UserResource } from "~/models/user";
-import { LinkAccountType, AccountToLink } from "~/models/link-account";
-import { formatDate }from "~/common/formatters";
+import { UserResource } from "~/models/user";
+import { LinkAccountType } from "~/models/link-account";
+import { formatDate } from "~/common/formatters";
 
 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
 
@@ -78,13 +78,13 @@ export const LinkAccountPanelRoot = withStyles(styles) (
             { userToLink && user && <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(user, true)}.
+                        Clicking 'Link accounts' will link {displayUser(user, true)} to {displayUser(userToLink, true)}.
                     </Grid>
                     <Grid item>
-                        After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(user)}.
+                        After linking, logging in as {displayUser(user)} will log you into the same account as {displayUser(userToLink)}.
                     </Grid>
                     <Grid item>
-                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(user)}.
+                       Any object owned by {displayUser(user)} will be transfered to {displayUser(userToLink)}.
                     </Grid>
                 </Grid>
                 <Grid container item direction="row" spacing={24}>