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