15088: Adds invalid states and UI and improves action error handling
[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 { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
12 import { unionize, ofType, UnionOf } from '~/common/unionize';
13 import { UserResource, User } from "~/models/user";
14 import { navigateToRootProject } from "~/store/navigation/navigation-action";
15 import { GroupResource } from "~/models/group";
16 import { LinkAccountPanelError } from "./link-account-panel-reducer";
17
18 export const linkAccountPanelActions = unionize({
19     INIT: ofType<{ user: UserResource | undefined }>(),
20     LOAD: ofType<{
21         user: UserResource | undefined,
22         userToken: string | undefined,
23         userToLink: UserResource | undefined,
24         userToLinkToken: string | undefined }>(),
25     RESET: {},
26     INVALID: ofType<{
27         user: UserResource | undefined,
28         userToLink: UserResource | undefined,
29         error: LinkAccountPanelError }>(),
30 });
31
32 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
33
34 function validateLink(user: UserResource, userToLink: UserResource) {
35     if (user.uuid === userToLink.uuid) {
36         return LinkAccountPanelError.SAME_USER;
37     }
38     else if (!user.isAdmin && userToLink.isAdmin) {
39         return LinkAccountPanelError.NON_ADMIN;
40     }
41     return LinkAccountPanelError.NONE;
42 }
43
44 export const loadLinkAccountPanel = () =>
45     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
46         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
47
48         const curUser = getState().auth.user;
49         const curToken = getState().auth.apiToken;
50         if (curUser && curToken) {
51             const curUserResource = await services.userService.get(curUser.uuid);
52             const linkAccountData = services.linkAccountService.getFromSession();
53
54             // If there is link account session data, then the user has logged in a second time
55             if (linkAccountData) {
56                 dispatch<any>(saveApiToken(linkAccountData.token));
57                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
58                 dispatch<any>(saveApiToken(curToken));
59
60                 let params: any;
61                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
62                     params = {
63                         user: savedUserResource,
64                         userToken: linkAccountData.token,
65                         userToLink: curUserResource,
66                         userToLinkToken: curToken
67                     };
68                 }
69                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
70                     params = {
71                         user: curUserResource,
72                         userToken: curToken,
73                         userToLink: savedUserResource,
74                         userToLinkToken: linkAccountData.token
75                     };
76                 }
77                 else {
78                     throw new Error("Invalid link account type.");
79                 }
80
81                 const error = validateLink(params.user, params.userToLink);
82                 if (error === LinkAccountPanelError.NONE) {
83                     dispatch<any>(linkAccountPanelActions.LOAD(params));
84                 }
85                 else {
86                     dispatch<any>(linkAccountPanelActions.INVALID({
87                         user: params.user,
88                         userToLink: params.userToLink,
89                         error}));
90                     return;
91                 }
92             }
93             else {
94                 // If there is no link account session data, set the state to invoke the initial UI
95                 dispatch<any>(linkAccountPanelActions.INIT({
96                     user: curUserResource }
97                 ));
98             }
99         }
100     };
101
102 export const saveAccountLinkData = (t: LinkAccountType) =>
103     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
104         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
105         services.linkAccountService.saveToSession(accountToLink);
106         dispatch(logout());
107     };
108
109 export const getAccountLinkData = () =>
110     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
111         return services.linkAccountService.getFromSession();
112     };
113
114 export const removeAccountLinkData = () =>
115     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
116         try {
117             const linkAccountData = services.linkAccountService.getFromSession();
118             if (linkAccountData) {
119                 const savedUser = await services.userService.get(linkAccountData.userUuid);
120                 dispatch<any>(saveUser(savedUser));
121                 dispatch<any>(saveApiToken(linkAccountData.token));
122             }
123         }
124         finally {
125             dispatch<any>(navigateToRootProject);
126             dispatch(linkAccountPanelActions.RESET());
127             services.linkAccountService.removeFromSession();
128         }
129     };
130
131 export const linkAccount = () =>
132     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
133         const linkState = getState().linkAccountPanel;
134         const currentToken = getState().auth.apiToken;
135         if (linkState.userToLink && linkState.userToLinkToken && linkState.user && linkState.userToken && currentToken) {
136
137             // First create a project owned by the "userToLink" to accept everything from the current user
138             const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
139             let newGroup: GroupResource;
140             try {
141                 dispatch<any>(saveApiToken(linkState.userToLinkToken));
142                 newGroup = await services.projectService.create({
143                     name: projectName,
144                     ensure_unique_name: true
145                 });
146             }
147             catch (e) {
148                 dispatch<any>(saveApiToken(currentToken));
149                 dispatch(snackbarActions.OPEN_SNACKBAR({
150                     message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
151                 }));
152                 throw e;
153             }
154
155             try {
156                 // Use the token of the account that is getting merged to call the merge api
157                 dispatch<any>(saveApiToken(linkState.userToken));
158                 await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
159
160                 // If the link was successful, switch to the account that was merged with
161                 dispatch<any>(saveUser(linkState.userToLink));
162                 dispatch<any>(saveApiToken(linkState.userToLinkToken));
163             }
164             catch(e) {
165                 // If the link operation fails, delete the previously made project
166                 // and stay logged in to the current account.
167                 try {
168                     dispatch<any>(saveApiToken(linkState.userToLinkToken));
169                     await services.projectService.delete(newGroup.uuid);
170                 }
171                 finally {
172                     dispatch<any>(saveApiToken(currentToken));
173                     dispatch(snackbarActions.OPEN_SNACKBAR({
174                         message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
175                     }));
176                 }
177                 throw e;
178             }
179             finally {
180                 dispatch<any>(navigateToRootProject);
181                 services.linkAccountService.removeFromSession();
182                 dispatch(linkAccountPanelActions.RESET());
183             }
184         }
185     };