From 2b22123ad358b8c6667e42779f6e52e9a14d9289 Mon Sep 17 00:00:00 2001 From: Eric Biagiotti Date: Tue, 30 Apr 2019 15:44:59 -0400 Subject: [PATCH] 15088: Adds account linking functionality Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti --- src/models/group.ts | 1 + src/models/test-utils.ts | 1 + .../link-account-service.ts | 23 +++- .../link-account-panel-actions.ts | 116 ++++++++++++++---- .../link-account-panel-reducer.ts | 10 +- .../link-account-panel-root.tsx | 12 +- 6 files changed, 124 insertions(+), 39 deletions(-) diff --git a/src/models/group.ts b/src/models/group.ts index e13fbcbf..f34ede0a 100644 --- a/src/models/group.ts +++ b/src/models/group.ts @@ -11,6 +11,7 @@ export interface GroupResource extends TrashableResource { description: string; properties: any; writeableBy: string[]; + ensure_unique_name: boolean; } export enum GroupClass { diff --git a/src/models/test-utils.ts b/src/models/test-utils.ts index b08ce5a0..22a94f16 100644 --- a/src/models/test-utils.ts +++ b/src/models/test-utils.ts @@ -24,6 +24,7 @@ export const mockGroupResource = (data: Partial = {}): GroupResou trashAt: "", uuid: "", writeableBy: [], + ensure_unique_name: true, ...data }); diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts index fe78d484..cc7c62eb 100644 --- a/src/services/link-account-service/link-account-service.ts +++ b/src/services/link-account-service/link-account-service.ts @@ -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 diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts index 42fd6c52..4e505284 100644 --- a/src/store/link-account-panel/link-account-panel-actions.ts +++ b/src/store/link-account-panel/link-account-panel-actions.ts @@ -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; export const loadLinkAccountPanel = () => - (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + async (dispatch: Dispatch, 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(linkAccountPanelActions.LOAD_LINKING({ userToLink: curUserResource, user: savedUserResource })); - } - else if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) { - dispatch(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(saveApiToken(linkAccountData.token)); + const savedUserResource = await services.userService.get(linkAccountData.userUuid); + dispatch(saveApiToken(curToken)); + if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) { + const params = { + user: savedUserResource, + userToken: linkAccountData.token, + userToLink: curUserResource, + userToLinkToken: curToken + }; + dispatch(linkAccountPanelActions.LOAD_LINKING(params)); + } + else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) { + const params = { + user: curUserResource, + userToken: curToken, + userToLink: savedUserResource, + userToLinkToken: linkAccountData.token + }; + dispatch(linkAccountPanelActions.LOAD_LINKING(params)); } else { - dispatch(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(linkAccountPanelActions.LOAD_LINKING({ + user: curUserResource, + userToken: curToken, + userToLink: undefined, + userToLinkToken: undefined } + )); + } } }; export const saveAccountLinkData = (t: LinkAccountType) => (dispatch: Dispatch, 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, getState: () => RootState, services: ServiceRepository) => { - return services.linkAccountService.getLinkAccount(); + return services.linkAccountService.getFromSession(); }; export const removeAccountLinkData = () => (dispatch: Dispatch, 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, getState: () => RootState, services: ServiceRepository) => { + async (dispatch: Dispatch, 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(saveApiToken(linkState.userToLinkToken)); + const newGroup = await services.projectService.create({ + name: projectName, + ensure_unique_name: true + }); + dispatch(saveApiToken(currentToken)); + + try { + dispatch(saveApiToken(linkState.userToken)); + await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid); + dispatch(saveApiToken(currentToken)); + + // If the link was successful, switch to the account that was merged with + if (linkState.userToLink && linkState.userToLinkToken) { + dispatch(saveUser(linkState.userToLink)); + dispatch(saveApiToken(linkState.userToLinkToken)); + dispatch(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(saveApiToken(linkState.userToLinkToken)); + services.projectService.delete(newGroup.uuid); + dispatch(saveApiToken(currentToken)); + services.linkAccountService.removeFromSession(); + dispatch(linkAccountPanelActions.RESET_LINKING()); + throw e; + } + } }; \ No newline at end of file diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts index cbfb315b..fabef94e 100644 --- a/src/store/link-account-panel/link-account-panel-reducer.ts +++ b/src/store/link-account-panel/link-account-panel-reducer.ts @@ -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 diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx index 8b3fb45a..f4570de2 100644 --- a/src/views/link-account-panel/link-account-panel-root.tsx +++ b/src/views/link-account-panel/link-account-panel-root.tsx @@ -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 && - Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(user, true)}. + Clicking 'Link accounts' will link {displayUser(user, true)} to {displayUser(userToLink, true)}. - 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)}. - Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(user)}. + Any object owned by {displayUser(user)} will be transfered to {displayUser(userToLink)}. -- 2.30.2