X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/13700efea8cd742fbb4888252f3d06788f5fd845..1d4e548cf6c8f11d939712ea5f4df06786a89d64:/src/store/link-account-panel/link-account-panel-actions.ts 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 47e27f7d94..1e94fcfaca 100644 --- a/src/store/link-account-panel/link-account-panel-actions.ts +++ b/src/store/link-account-panel/link-account-panel-actions.ts @@ -4,182 +4,291 @@ 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 } from "~/models/link-account"; -import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action"; +import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account"; +import { authActions, getConfig } from "~/store/auth/auth-action"; import { unionize, ofType, UnionOf } from '~/common/unionize'; -import { UserResource, User } from "~/models/user"; -import { navigateToRootProject } from "~/store/navigation/navigation-action"; +import { UserResource } from "~/models/user"; import { GroupResource } from "~/models/group"; -import { LinkAccountPanelError } from "./link-account-panel-reducer"; +import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer"; +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'; export const linkAccountPanelActions = unionize({ - INIT: ofType<{ user: UserResource | undefined }>(), - LOAD: ofType<{ - user: UserResource | undefined, - userToken: string | undefined, + LINK_INIT: ofType<{ + targetUser: UserResource | undefined + }>(), + LINK_LOAD: ofType<{ + originatingUser: OriginatingUser | undefined, + targetUser: UserResource | undefined, + targetUserToken: string | undefined, userToLink: UserResource | undefined, - userToLinkToken: string | undefined }>(), - RESET: {}, - INVALID: ofType<{ - user: UserResource | undefined, + userToLinkToken: string | undefined + }>(), + LINK_INVALID: ofType<{ + originatingUser: OriginatingUser | undefined, + targetUser: UserResource | undefined, userToLink: UserResource | undefined, - error: LinkAccountPanelError }>(), + error: LinkAccountPanelError + }>(), + SET_SELECTED_CLUSTER: ofType<{ + selectedCluster: string + }>(), + SET_IS_PROCESSING: ofType<{ + isProcessing: boolean + }>(), + HAS_SESSION_DATA: {} }); export type LinkAccountPanelAction = UnionOf; -function validateLink(user: UserResource, userToLink: UserResource) { - if (user.uuid === userToLink.uuid) { +function validateLink(userToLink: UserResource, targetUser: UserResource) { + if (userToLink.uuid === targetUser.uuid) { return LinkAccountPanelError.SAME_USER; } - else if (!user.isAdmin && userToLink.isAdmin) { + else if (userToLink.isAdmin && !targetUser.isAdmin) { return LinkAccountPanelError.NON_ADMIN; } + else if (!targetUser.isActive) { + return LinkAccountPanelError.INACTIVE; + } return LinkAccountPanelError.NONE; } +const newServices = (dispatch: Dispatch, token: string) => { + const config = dispatch(getConfig); + const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } }); + setAuthorizationHeader(svc, token); + return svc; +}; + +export const checkForLinkStatus = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + const status = services.linkAccountService.getLinkOpStatus(); + if (status !== undefined) { + let msg: string; + let msgKind: SnackbarKind; + if (status.valueOf() === LinkAccountStatus.CANCELLED) { + msg = "Account link cancelled!", msgKind = SnackbarKind.INFO; + } + else if (status.valueOf() === LinkAccountStatus.FAILED) { + msg = "Account link failed!", msgKind = SnackbarKind.ERROR; + } + else if (status.valueOf() === LinkAccountStatus.SUCCESS) { + msg = "Account link success!", msgKind = SnackbarKind.SUCCESS; + } + else { + msg = "Unknown Error!", msgKind = SnackbarKind.ERROR; + } + dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 })); + services.linkAccountService.removeLinkOpStatus(); + } + }; + +export const switchUser = (user: UserResource, token: string) => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(authActions.INIT_USER({ user, token })); + }; + +export const linkFailed = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + // If the link fails, switch to the user account that originated the link operation + const linkState = getState().linkAccountPanel; + if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) { + if (linkState.originatingUser === OriginatingUser.TARGET_USER) { + dispatch(switchUser(linkState.targetUser, linkState.targetUserToken)); + } + else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) { + dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken)); + } + } + services.linkAccountService.removeAccountToLink(); + services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.FAILED); + location.reload(); + }; + export const loadLinkAccountPanel = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(setBreadcrumbs([{ label: 'Link account'}])); + try { + // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster + if (getState().linkAccountPanel.selectedCluster === undefined) { + const localCluster = getState().auth.localCluster; + let selectedCluster = localCluster; + for (const key in getState().auth.remoteHosts) { + if (key !== localCluster) { + selectedCluster = key; + break; + } + } + dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster })); + } - const curUser = getState().auth.user; - const curToken = getState().auth.apiToken; - if (curUser && curToken) { - const curUserResource = await services.userService.get(curUser.uuid); - const linkAccountData = services.linkAccountService.getFromSession(); + // First check if an account link operation has completed + dispatch(checkForLinkStatus()); - // If there is link account session data, then the user has logged in a second time - if (linkAccountData) { - dispatch(saveApiToken(linkAccountData.token)); - const savedUserResource = await services.userService.get(linkAccountData.userUuid); - dispatch(saveApiToken(curToken)); - - let params: any; - if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) { - params = { - user: savedUserResource, - userToken: linkAccountData.token, - userToLink: curUserResource, - userToLinkToken: curToken - }; - } - else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) { - params = { - user: curUserResource, - userToken: curToken, - userToLink: savedUserResource, - userToLinkToken: linkAccountData.token - }; - } - else { - throw new Error("Invalid link account type."); - } + // Continue loading the link account panel + dispatch(setBreadcrumbs([{ label: 'Link account' }])); + const curUser = getState().auth.user; + const curToken = getState().auth.apiToken; + if (curUser && curToken) { + + // If there is link account session data, then the user has logged in a second time + const linkAccountData = services.linkAccountService.getAccountToLink(); + if (linkAccountData) { - const error = validateLink(params.user, params.userToLink); - if (error === LinkAccountPanelError.NONE) { - dispatch(linkAccountPanelActions.LOAD(params)); + dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: true })); + const curUserResource = await services.userService.get(curUser.uuid); + + // 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. + 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) { + params = { + originatingUser: OriginatingUser.USER_TO_LINK, + targetUser: curUserResource, + targetUserToken: curToken, + userToLink: savedUserResource, + userToLinkToken: linkAccountData.token + }; + } + else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) { + params = { + originatingUser: OriginatingUser.TARGET_USER, + targetUser: savedUserResource, + targetUserToken: linkAccountData.token, + userToLink: curUserResource, + userToLinkToken: curToken + }; + } + else { + throw new Error("Unknown link account type"); + } + + dispatch(switchUser(params.targetUser, params.targetUserToken)); + const error = validateLink(params.userToLink, params.targetUser); + if (error === LinkAccountPanelError.NONE) { + dispatch(linkAccountPanelActions.LINK_LOAD(params)); + } + else { + dispatch(linkAccountPanelActions.LINK_INVALID({ + originatingUser: params.originatingUser, + targetUser: params.targetUser, + userToLink: params.userToLink, + error + })); + return; + } } else { - dispatch(linkAccountPanelActions.INVALID({ - user: params.user, - userToLink: params.userToLink, - error})); + // If there is no link account session data, set the state to invoke the initial UI + const curUserResource = await services.userService.get(curUser.uuid); + dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource })); return; } } - else { - // If there is no link account session data, set the state to invoke the initial UI - dispatch(linkAccountPanelActions.INIT({ - user: curUserResource } - )); - } + } + catch (e) { + dispatch(linkFailed()); + } + finally { + dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: false })); } }; -export const saveAccountLinkData = (t: LinkAccountType) => +export const startLinking = (t: LinkAccountType) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink; - services.linkAccountService.saveToSession(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; + const isLocalUser = auth.user!.uuid.substring(0, 5) === auth.localCluster; + let homeCluster = auth.localCluster; + if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) { + homeCluster = getState().linkAccountPanel.selectedCluster!; + } + dispatch(logout()); + dispatch(login(auth.localCluster, homeCluster, auth.loginCluster, auth.remoteHosts)); }; export const getAccountLinkData = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - return services.linkAccountService.getFromSession(); + return services.linkAccountService.getAccountToLink(); }; -export const removeAccountLinkData = () => +export const cancelLinking = (reload: boolean = false) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + let user: UserResource | undefined; try { - const linkAccountData = services.linkAccountService.getFromSession(); + // When linking is cancelled switch to the originating user (i.e. the user saved in session data) + dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN)); + const linkAccountData = services.linkAccountService.getAccountToLink(); if (linkAccountData) { - const savedUser = await services.userService.get(linkAccountData.userUuid); - dispatch(saveUser(savedUser)); - dispatch(saveApiToken(linkAccountData.token)); + services.linkAccountService.removeAccountToLink(); + const svc = newServices(dispatch, linkAccountData.token); + user = await svc.userService.get(linkAccountData.userUuid); + dispatch(switchUser(user, linkAccountData.token)); + services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED); } } finally { - dispatch(navigateToRootProject); - dispatch(linkAccountPanelActions.RESET()); - services.linkAccountService.removeFromSession(); + if (reload) { + location.reload(); + } + else { + dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN)); + } } }; export const linkAccount = () => 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) { + if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) { - // 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})`; + // First create a project owned by the target user + const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`; let newGroup: GroupResource; try { - dispatch(saveApiToken(linkState.userToLinkToken)); newGroup = await services.projectService.create({ name: projectName, ensure_unique_name: true }); } catch (e) { - dispatch(saveApiToken(currentToken)); - dispatch(snackbarActions.OPEN_SNACKBAR({ - message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 - })); + dispatch(linkFailed()); throw e; } try { - // Use the token of the account that is getting merged to call the merge api - dispatch(saveApiToken(linkState.userToken)); - await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid); - - // If the link was successful, switch to the account that was merged with - dispatch(saveUser(linkState.userToLink)); - dispatch(saveApiToken(linkState.userToLinkToken)); + // The merge api links the user sending the request into the user + // specified in the request, so change the authorization header accordingly + 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); + location.reload(); } - catch(e) { + catch (e) { // If the link operation fails, delete the previously made project - // and stay logged in to the current account. try { - dispatch(saveApiToken(linkState.userToLinkToken)); - await services.projectService.delete(newGroup.uuid); + const svc = newServices(dispatch, linkState.targetUserToken); + await svc.projectService.delete(newGroup.uuid); } finally { - dispatch(saveApiToken(currentToken)); - dispatch(snackbarActions.OPEN_SNACKBAR({ - message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 - })); + dispatch(linkFailed()); } throw e; } - finally { - dispatch(navigateToRootProject); - services.linkAccountService.removeFromSession(); - dispatch(linkAccountPanelActions.RESET()); - } } - }; \ No newline at end of file + };