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 } 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 import { login, logout } from "~/store/auth/auth-action";
19 export const linkAccountPanelActions = unionize({
20 LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
22 originatingUser: OriginatingUser | undefined,
23 targetUser: UserResource | undefined,
24 targetUserToken: string | undefined,
25 userToLink: UserResource | undefined,
26 userToLinkToken: string | undefined }>(),
27 LINK_INVALID: ofType<{
28 originatingUser: OriginatingUser | undefined,
29 targetUser: UserResource | undefined,
30 userToLink: UserResource | undefined,
31 error: LinkAccountPanelError }>(),
35 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
37 function validateLink(userToLink: UserResource, targetUser: UserResource) {
38 if (userToLink.uuid === targetUser.uuid) {
39 return LinkAccountPanelError.SAME_USER;
41 else if (userToLink.isAdmin && !targetUser.isAdmin) {
42 return LinkAccountPanelError.NON_ADMIN;
44 else if (!targetUser.isActive) {
45 return LinkAccountPanelError.INACTIVE;
47 return LinkAccountPanelError.NONE;
50 export const switchUser = (user: UserResource, token: string) =>
51 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
52 dispatch(saveUser(user));
53 dispatch(saveApiToken(token));
56 export const linkFailed = () =>
57 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
58 // If the link fails, switch to the user account that originated the link operation
59 const linkState = getState().linkAccountPanel;
60 if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
61 if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
62 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
63 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
65 else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
66 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
67 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
69 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
71 services.linkAccountService.removeFromSession();
74 export const loadLinkAccountPanel = () =>
75 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
76 dispatch(setBreadcrumbs([{ label: 'Link account'}]));
77 const curUser = getState().auth.user;
78 const curToken = getState().auth.apiToken;
79 if (curUser && curToken) {
80 const curUserResource = await services.userService.get(curUser.uuid);
81 const linkAccountData = services.linkAccountService.getFromSession();
83 // If there is link account session data, then the user has logged in a second time
84 if (linkAccountData) {
86 // If the window is refreshed after the second login, cancel the linking
87 if (window.performance) {
88 if (performance.navigation.type === PerformanceNavigation.TYPE_BACK_FORWARD ||
89 performance.navigation.type === PerformanceNavigation.TYPE_RELOAD) {
90 dispatch(cancelLinking());
95 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
96 // issues since a user will always be able to query the api server for their own user data.
97 dispatch(saveApiToken(linkAccountData.token));
98 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
99 dispatch(saveApiToken(curToken));
102 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
104 originatingUser: OriginatingUser.USER_TO_LINK,
105 targetUser: curUserResource,
106 targetUserToken: curToken,
107 userToLink: savedUserResource,
108 userToLinkToken: linkAccountData.token
111 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
113 originatingUser: OriginatingUser.TARGET_USER,
114 targetUser: savedUserResource,
115 targetUserToken: linkAccountData.token,
116 userToLink: curUserResource,
117 userToLinkToken: curToken
121 // This should never really happen, but just in case, switch to the user that
122 // originated the linking operation (i.e. the user saved in session data)
123 dispatch(switchUser(savedUserResource, linkAccountData.token));
124 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: savedUserResource}));
125 throw new Error("Invalid link account type.");
128 dispatch(switchUser(params.targetUser, params.targetUserToken));
129 const error = validateLink(params.userToLink, params.targetUser);
130 if (error === LinkAccountPanelError.NONE) {
131 dispatch(linkAccountPanelActions.LINK_LOAD(params));
134 dispatch(linkAccountPanelActions.LINK_INVALID({
135 originatingUser: params.originatingUser,
136 targetUser: params.targetUser,
137 userToLink: params.userToLink,
143 // If there is no link account session data, set the state to invoke the initial UI
144 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
150 export const saveAccountLinkData = (t: LinkAccountType) =>
151 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
152 const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
153 services.linkAccountService.saveToSession(accountToLink);
154 const auth = getState().auth;
156 dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
159 export const getAccountLinkData = () =>
160 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
161 return services.linkAccountService.getFromSession();
164 export const cancelLinking = () =>
165 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
166 let user: UserResource | undefined;
168 // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
169 const linkAccountData = services.linkAccountService.getFromSession();
170 if (linkAccountData) {
171 dispatch(saveApiToken(linkAccountData.token));
172 user = await services.userService.get(linkAccountData.userUuid);
173 dispatch(switchUser(user, linkAccountData.token));
177 services.linkAccountService.removeFromSession();
178 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: user }));
182 export const linkAccount = () =>
183 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
184 const linkState = getState().linkAccountPanel;
185 if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
187 // First create a project owned by the target user
188 const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
189 let newGroup: GroupResource;
191 newGroup = await services.projectService.create({
193 ensure_unique_name: true
197 dispatch(linkFailed());
202 // The merge api links the user sending the request into the user
203 // specified in the request, so switch users for this api call
204 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
205 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
206 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
207 dispatch(navigateToRootProject);
208 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link success!', kind: SnackbarKind.SUCCESS, hideDuration: 3000 }));
209 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
212 // If the link operation fails, delete the previously made project
214 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
215 await services.projectService.delete(newGroup.uuid);
218 dispatch(linkFailed());
223 services.linkAccountService.removeFromSession();