15088: Removes navigation after second login
[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 } 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 { navigateToRootProject } from "../navigation/navigation-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     return LinkAccountPanelError.NONE;
44 }
45
46 export const switchUser = (user: UserResource, token: string) =>
47     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
48         dispatch(saveUser(user));
49         dispatch(saveApiToken(token));
50     };
51
52 export const linkFailed = () =>
53     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
54         // If the link fails, switch to the user account that originated the link operation
55         const linkState = getState().linkAccountPanel;
56         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
57             if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
58                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
59                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
60             }
61             else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
62                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
63                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
64             }
65             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
66         }
67         services.linkAccountService.removeFromSession();
68     };
69
70 export const loadLinkAccountPanel = () =>
71     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
72         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
73         const curUser = getState().auth.user;
74         const curToken = getState().auth.apiToken;
75         if (curUser && curToken) {
76             const curUserResource = await services.userService.get(curUser.uuid);
77             const linkAccountData = services.linkAccountService.getFromSession();
78
79             // If there is link account session data, then the user has logged in a second time
80             if (linkAccountData) {
81
82                 // If the window is refreshed after the second login, cancel the linking
83                 if (window.performance) {
84                     if (performance.navigation.type === PerformanceNavigation.TYPE_BACK_FORWARD ||
85                         performance.navigation.type === PerformanceNavigation.TYPE_RELOAD) {
86                         dispatch(cancelLinking());
87                         return;
88                     }
89                 }
90
91                 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
92                 // issues since a user will always be able to query the api server for their own user data.
93                 dispatch(saveApiToken(linkAccountData.token));
94                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
95                 dispatch(saveApiToken(curToken));
96
97                 let params: any;
98                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
99                     params = {
100                         originatingUser: OriginatingUser.USER_TO_LINK,
101                         targetUser: curUserResource,
102                         targetUserToken: curToken,
103                         userToLink: savedUserResource,
104                         userToLinkToken: linkAccountData.token
105                     };
106                 }
107                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
108                     params = {
109                         originatingUser: OriginatingUser.TARGET_USER,
110                         targetUser: savedUserResource,
111                         targetUserToken: linkAccountData.token,
112                         userToLink: curUserResource,
113                         userToLinkToken: curToken
114                     };
115                 }
116                 else {
117                     // This should never really happen, but just in case, switch to the user that
118                     // originated the linking operation (i.e. the user saved in session data)
119                     dispatch(switchUser(savedUserResource, linkAccountData.token));
120                     dispatch(linkAccountPanelActions.LINK_INIT({targetUser: savedUserResource}));
121                     throw new Error("Invalid link account type.");
122                 }
123
124                 dispatch(switchUser(params.targetUser, params.targetUserToken));
125                 const error = validateLink(params.userToLink, params.targetUser);
126                 if (error === LinkAccountPanelError.NONE) {
127                     dispatch(linkAccountPanelActions.LINK_LOAD(params));
128                 }
129                 else {
130                     dispatch(linkAccountPanelActions.LINK_INVALID({
131                         originatingUser: params.originatingUser,
132                         targetUser: params.targetUser,
133                         userToLink: params.userToLink,
134                         error}));
135                     return;
136                 }
137             }
138             else {
139                 // If there is no link account session data, set the state to invoke the initial UI
140                 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
141                 return;
142             }
143         }
144     };
145
146 export const saveAccountLinkData = (t: LinkAccountType) =>
147     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
148         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
149         services.linkAccountService.saveToSession(accountToLink);
150         const auth = getState().auth;
151         services.authService.login(auth.localCluster, auth.remoteHosts[auth.homeCluster]);
152     };
153
154 export const getAccountLinkData = () =>
155     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
156         return services.linkAccountService.getFromSession();
157     };
158
159 export const cancelLinking = () =>
160     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
161         let user: UserResource | undefined;
162         try {
163             // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
164             const linkAccountData = services.linkAccountService.getFromSession();
165             if (linkAccountData) {
166                 dispatch(saveApiToken(linkAccountData.token));
167                 user = await services.userService.get(linkAccountData.userUuid);
168                 dispatch(switchUser(user, linkAccountData.token));
169             }
170         }
171         finally {
172             services.linkAccountService.removeFromSession();
173             dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: user }));
174         }
175     };
176
177 export const linkAccount = () =>
178     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
179         const linkState = getState().linkAccountPanel;
180         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
181
182             // First create a project owned by the target user
183             const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
184             let newGroup: GroupResource;
185             try {
186                 newGroup = await services.projectService.create({
187                     name: projectName,
188                     ensure_unique_name: true
189                 });
190             }
191             catch (e) {
192                 dispatch(linkFailed());
193                 throw e;
194             }
195
196             try {
197                 // The merge api links the user sending the request into the user
198                 // specified in the request, so switch users for this api call
199                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
200                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
201                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
202                 dispatch(navigateToRootProject);
203                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link success!', kind: SnackbarKind.SUCCESS, hideDuration: 3000 }));
204                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
205             }
206             catch(e) {
207                 // If the link operation fails, delete the previously made project
208                 try {
209                     dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
210                     await services.projectService.delete(newGroup.uuid);
211                 }
212                 finally {
213                     dispatch(linkFailed());
214                 }
215                 throw e;
216             }
217             finally {
218                 services.linkAccountService.removeFromSession();
219             }
220         }
221     };