15088: Adds reload after link account operation
[arvados-workbench2.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 { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
10 import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account";
11 import { saveApiToken, saveUser } from "~/store/auth/auth-action";
12 import { unionize, ofType, UnionOf } from '~/common/unionize';
13 import { UserResource } from "~/models/user";
14 import { GroupResource } from "~/models/group";
15 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
16 import { login, logout } from "~/store/auth/auth-action";
17
18 export const linkAccountPanelActions = unionize({
19     LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
20     LINK_LOAD: ofType<{
21         originatingUser: OriginatingUser | undefined,
22         targetUser: UserResource | undefined,
23         targetUserToken: string | undefined,
24         userToLink: UserResource | undefined,
25         userToLinkToken: string | undefined }>(),
26     LINK_INVALID: ofType<{
27         originatingUser: OriginatingUser | undefined,
28         targetUser: UserResource | undefined,
29         userToLink: UserResource | undefined,
30         error: LinkAccountPanelError }>(),
31     HAS_SESSION_DATA: {}
32 });
33
34 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
35
36 function validateLink(userToLink: UserResource, targetUser: UserResource) {
37     if (userToLink.uuid === targetUser.uuid) {
38         return LinkAccountPanelError.SAME_USER;
39     }
40     else if (userToLink.isAdmin && !targetUser.isAdmin) {
41         return LinkAccountPanelError.NON_ADMIN;
42     }
43     else if (!targetUser.isActive) {
44         return LinkAccountPanelError.INACTIVE;
45     }
46     return LinkAccountPanelError.NONE;
47 }
48
49 export const checkForLinkStatus = () =>
50     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
51         const status = services.linkAccountService.getLinkOpStatus();
52         if (status !== undefined) {
53             let msg: string;
54             let msgKind: SnackbarKind;
55             if (status.valueOf() === LinkAccountStatus.CANCELLED) {
56                 msg = "Account link cancelled!", msgKind = SnackbarKind.INFO;
57             }
58             else if (status.valueOf() === LinkAccountStatus.FAILED) {
59                 msg = "Account link failed!", msgKind = SnackbarKind.ERROR;
60             }
61             else if (status.valueOf() === LinkAccountStatus.SUCCESS) {
62                 msg = "Account link success!", msgKind = SnackbarKind.SUCCESS;
63             }
64             else {
65                 msg = "Unknown Error!", msgKind = SnackbarKind.ERROR;
66             }
67             dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 }));
68             services.linkAccountService.removeLinkOpStatus();
69         }
70     };
71
72 export const finishLinking = (status: LinkAccountStatus) =>
73     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
74         services.linkAccountService.removeFromSession();
75         services.linkAccountService.saveLinkOpStatus(status);
76         location.reload();
77     };
78
79 export const switchUser = (user: UserResource, token: string) =>
80     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
81         dispatch(saveUser(user));
82         dispatch(saveApiToken(token));
83     };
84
85 export const linkFailed = () =>
86     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
87         // If the link fails, switch to the user account that originated the link operation
88         const linkState = getState().linkAccountPanel;
89         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
90             if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
91                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
92                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
93             }
94             else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
95                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
96                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
97             }
98             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
99         }
100         dispatch(finishLinking(LinkAccountStatus.FAILED));
101     };
102
103 export const loadLinkAccountPanel = () =>
104     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
105
106         // First check if an account link operation has completed
107         dispatch(checkForLinkStatus());
108
109         // Continue loading the link account panel
110         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
111         const curUser = getState().auth.user;
112         const curToken = getState().auth.apiToken;
113         if (curUser && curToken) {
114             const curUserResource = await services.userService.get(curUser.uuid);
115             const linkAccountData = services.linkAccountService.getFromSession();
116
117             // If there is link account session data, then the user has logged in a second time
118             if (linkAccountData) {
119
120                 // If the window is refreshed after the second login, cancel the linking
121                 if (window.performance) {
122                     if (performance.navigation.type === PerformanceNavigation.TYPE_BACK_FORWARD ||
123                         performance.navigation.type === PerformanceNavigation.TYPE_RELOAD) {
124                         dispatch(cancelLinking());
125                         return;
126                     }
127                 }
128
129                 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
130                 // issues since a user will always be able to query the api server for their own user data.
131                 dispatch(saveApiToken(linkAccountData.token));
132                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
133                 dispatch(saveApiToken(curToken));
134
135                 let params: any;
136                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
137                     params = {
138                         originatingUser: OriginatingUser.USER_TO_LINK,
139                         targetUser: curUserResource,
140                         targetUserToken: curToken,
141                         userToLink: savedUserResource,
142                         userToLinkToken: linkAccountData.token
143                     };
144                 }
145                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
146                     params = {
147                         originatingUser: OriginatingUser.TARGET_USER,
148                         targetUser: savedUserResource,
149                         targetUserToken: linkAccountData.token,
150                         userToLink: curUserResource,
151                         userToLinkToken: curToken
152                     };
153                 }
154                 else {
155                     // This should never really happen, but just in case, switch to the user that
156                     // originated the linking operation (i.e. the user saved in session data)
157                     dispatch(switchUser(savedUserResource, linkAccountData.token));
158                     dispatch(finishLinking(LinkAccountStatus.FAILED));
159                 }
160
161                 dispatch(switchUser(params.targetUser, params.targetUserToken));
162                 const error = validateLink(params.userToLink, params.targetUser);
163                 if (error === LinkAccountPanelError.NONE) {
164                     dispatch(linkAccountPanelActions.LINK_LOAD(params));
165                 }
166                 else {
167                     dispatch(linkAccountPanelActions.LINK_INVALID({
168                         originatingUser: params.originatingUser,
169                         targetUser: params.targetUser,
170                         userToLink: params.userToLink,
171                         error}));
172                     return;
173                 }
174             }
175             else {
176                 // If there is no link account session data, set the state to invoke the initial UI
177                 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
178                 return;
179             }
180         }
181     };
182
183 export const startLinking = (t: LinkAccountType) =>
184     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
185         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
186         services.linkAccountService.saveToSession(accountToLink);
187         const auth = getState().auth;
188         dispatch(logout());
189         dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
190     };
191
192 export const getAccountLinkData = () =>
193     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
194         return services.linkAccountService.getFromSession();
195     };
196
197 export const cancelLinking = () =>
198     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
199         let user: UserResource | undefined;
200         try {
201             // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
202             const linkAccountData = services.linkAccountService.getFromSession();
203             if (linkAccountData) {
204                 dispatch(saveApiToken(linkAccountData.token));
205                 user = await services.userService.get(linkAccountData.userUuid);
206                 dispatch(switchUser(user, linkAccountData.token));
207             }
208         }
209         finally {
210             dispatch(finishLinking(LinkAccountStatus.CANCELLED));
211         }
212     };
213
214 export const linkAccount = () =>
215     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
216         const linkState = getState().linkAccountPanel;
217         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
218
219             // First create a project owned by the target user
220             const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
221             let newGroup: GroupResource;
222             try {
223                 newGroup = await services.projectService.create({
224                     name: projectName,
225                     ensure_unique_name: true
226                 });
227             }
228             catch (e) {
229                 dispatch(linkFailed());
230                 throw e;
231             }
232
233             try {
234                 // The merge api links the user sending the request into the user
235                 // specified in the request, so switch users for this api call
236                 dispatch(saveApiToken(linkState.userToLinkToken));
237                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
238                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
239             }
240             catch(e) {
241                 // If the link operation fails, delete the previously made project
242                 try {
243                     dispatch(saveApiToken(linkState.targetUserToken));
244                     await services.projectService.delete(newGroup.uuid);
245                 }
246                 finally {
247                     dispatch(linkFailed());
248                 }
249                 throw e;
250             }
251             finally {
252                 dispatch(finishLinking(LinkAccountStatus.SUCCESS));
253             }
254         }
255     };