15088: Adds account linking functionality
[arvados.git] / src / store / link-account-panel / link-account-panel-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from "redux";
6 import { RootState } from "~/store/store";
7 import { ServiceRepository } from "~/services/services";
8 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
9 import { LinkAccountType, AccountToLink } from "~/models/link-account";
10 import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
11 import { unionize, ofType, UnionOf } from '~/common/unionize';
12 import { UserResource } from "~/models/user";
13 import { navigateToRootProject } from "~/store/navigation/navigation-action";
14
15 export const linkAccountPanelActions = unionize({
16     LOAD_LINKING: ofType<{
17         user: UserResource | undefined,
18         userToken: string | undefined,
19         userToLink: UserResource | undefined,
20         userToLinkToken: string | undefined }>(),
21     RESET_LINKING: {}
22 });
23
24 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
25
26 export const loadLinkAccountPanel = () =>
27     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
28         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
29
30         const curUser = getState().auth.user;
31         const curToken = getState().auth.apiToken;
32         if (curUser && curToken) {
33             const curUserResource = await services.userService.get(curUser.uuid);
34             const linkAccountData = services.linkAccountService.getFromSession();
35
36             // If there is link account data, then the user has logged in a second time
37             if (linkAccountData) {
38                 // Use the saved token to make the api call to override the current users permissions
39                 dispatch<any>(saveApiToken(linkAccountData.token));
40                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
41                 dispatch<any>(saveApiToken(curToken));
42                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
43                     const params = {
44                         user: savedUserResource,
45                         userToken: linkAccountData.token,
46                         userToLink: curUserResource,
47                         userToLinkToken: curToken
48                     };
49                     dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
50                 }
51                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
52                     const params = {
53                         user: curUserResource,
54                         userToken: curToken,
55                         userToLink: savedUserResource,
56                         userToLinkToken: linkAccountData.token
57                     };
58                     dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
59                 }
60                 else {
61                     throw new Error("Invalid link account type.");
62                 }
63             }
64             else {
65                 // If there is no link account session data, set the state to invoke the initial UI
66                 dispatch<any>(linkAccountPanelActions.LOAD_LINKING({
67                     user: curUserResource,
68                     userToken: curToken,
69                     userToLink: undefined,
70                     userToLinkToken: undefined }
71                 ));
72             }
73         }
74     };
75
76 export const saveAccountLinkData = (t: LinkAccountType) =>
77     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
78         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
79         services.linkAccountService.saveToSession(accountToLink);
80         dispatch(logout());
81     };
82
83 export const getAccountLinkData = () =>
84     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
85         return services.linkAccountService.getFromSession();
86     };
87
88 export const removeAccountLinkData = () =>
89     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
90         const linkAccountData = services.linkAccountService.getFromSession();
91         services.linkAccountService.removeFromSession();
92         dispatch(linkAccountPanelActions.RESET_LINKING());
93         if (linkAccountData) {
94             services.userService.get(linkAccountData.userUuid).then(savedUser => {
95                 dispatch(setBreadcrumbs([{ label: ''}]));
96                 dispatch<any>(saveUser(savedUser));
97                 dispatch<any>(saveApiToken(linkAccountData.token));
98                 dispatch<any>(navigateToRootProject);
99             });
100         }
101     };
102
103 export const linkAccount = () =>
104     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
105         const linkState = getState().linkAccountPanel;
106         const currentToken = getState().auth.apiToken;
107         if (linkState.userToLink && linkState.userToLinkToken && linkState.user && linkState.userToken && currentToken) {
108
109             // First create a project owned by the "userToLink" to accept everything from the current user
110             const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
111             dispatch<any>(saveApiToken(linkState.userToLinkToken));
112             const newGroup = await services.projectService.create({
113                 name: projectName,
114                 ensure_unique_name: true
115             });
116             dispatch<any>(saveApiToken(currentToken));
117
118             try {
119                 dispatch<any>(saveApiToken(linkState.userToken));
120                 await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
121                 dispatch<any>(saveApiToken(currentToken));
122
123                 // If the link was successful, switch to the account that was merged with
124                 if (linkState.userToLink && linkState.userToLinkToken) {
125                     dispatch<any>(saveUser(linkState.userToLink));
126                     dispatch<any>(saveApiToken(linkState.userToLinkToken));
127                     dispatch<any>(navigateToRootProject);
128                 }
129                 services.linkAccountService.removeFromSession();
130                 dispatch(linkAccountPanelActions.RESET_LINKING());
131             }
132             catch(e) {
133                 // If the account link operation fails, delete the previously made project
134                 // and reset the link account state. The user will have to restart the process.
135                 dispatch<any>(saveApiToken(linkState.userToLinkToken));
136                 services.projectService.delete(newGroup.uuid);
137                 dispatch<any>(saveApiToken(currentToken));
138                 services.linkAccountService.removeFromSession();
139                 dispatch(linkAccountPanelActions.RESET_LINKING());
140                 throw e;
141             }
142         }
143     };