1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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";
18 export const linkAccountPanelActions = unionize({
19 LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
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 }>(),
34 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
36 function validateLink(userToLink: UserResource, targetUser: UserResource) {
37 if (userToLink.uuid === targetUser.uuid) {
38 return LinkAccountPanelError.SAME_USER;
40 else if (userToLink.isAdmin && !targetUser.isAdmin) {
41 return LinkAccountPanelError.NON_ADMIN;
43 else if (!targetUser.isActive) {
44 return LinkAccountPanelError.INACTIVE;
46 return LinkAccountPanelError.NONE;
49 export const checkForLinkStatus = () =>
50 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
51 const status = services.linkAccountService.getLinkOpStatus();
52 if (status !== undefined) {
54 let msgKind: SnackbarKind;
55 if (status.valueOf() === LinkAccountStatus.CANCELLED) {
56 msg = "Account link cancelled!", msgKind = SnackbarKind.INFO;
58 else if (status.valueOf() === LinkAccountStatus.FAILED) {
59 msg = "Account link failed!", msgKind = SnackbarKind.ERROR;
61 else if (status.valueOf() === LinkAccountStatus.SUCCESS) {
62 msg = "Account link success!", msgKind = SnackbarKind.SUCCESS;
65 msg = "Unknown Error!", msgKind = SnackbarKind.ERROR;
67 dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 }));
68 services.linkAccountService.removeLinkOpStatus();
72 export const finishLinking = (status: LinkAccountStatus) =>
73 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
74 services.linkAccountService.removeFromSession();
75 services.linkAccountService.saveLinkOpStatus(status);
79 export const switchUser = (user: UserResource, token: string) =>
80 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
81 dispatch(saveUser(user));
82 dispatch(saveApiToken(token));
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}));
94 else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
95 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
96 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
98 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
100 dispatch(finishLinking(LinkAccountStatus.FAILED));
103 export const loadLinkAccountPanel = () =>
104 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
106 // First check if an account link operation has completed
107 dispatch(checkForLinkStatus());
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();
117 // If there is link account session data, then the user has logged in a second time
118 if (linkAccountData) {
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());
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));
136 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
138 originatingUser: OriginatingUser.USER_TO_LINK,
139 targetUser: curUserResource,
140 targetUserToken: curToken,
141 userToLink: savedUserResource,
142 userToLinkToken: linkAccountData.token
145 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
147 originatingUser: OriginatingUser.TARGET_USER,
148 targetUser: savedUserResource,
149 targetUserToken: linkAccountData.token,
150 userToLink: curUserResource,
151 userToLinkToken: curToken
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));
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));
167 dispatch(linkAccountPanelActions.LINK_INVALID({
168 originatingUser: params.originatingUser,
169 targetUser: params.targetUser,
170 userToLink: params.userToLink,
176 // If there is no link account session data, set the state to invoke the initial UI
177 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
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;
189 dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
192 export const getAccountLinkData = () =>
193 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
194 return services.linkAccountService.getFromSession();
197 export const cancelLinking = () =>
198 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
199 let user: UserResource | undefined;
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));
210 dispatch(finishLinking(LinkAccountStatus.CANCELLED));
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) {
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;
223 newGroup = await services.projectService.create({
225 ensure_unique_name: true
229 dispatch(linkFailed());
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));
241 // If the link operation fails, delete the previously made project
243 dispatch(saveApiToken(linkState.targetUserToken));
244 await services.projectService.delete(newGroup.uuid);
247 dispatch(linkFailed());
252 dispatch(finishLinking(LinkAccountStatus.SUCCESS));