16115: Further code cleaning and permission handling improvements.
[arvados-workbench2.git] / src / store / sharing-dialog / sharing-dialog-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { dialogActions } from "store/dialog/dialog-actions";
6 import { withDialog } from "store/dialog/with-dialog";
7 import {
8     SHARING_DIALOG_NAME,
9     SHARING_INVITATION_FORM_NAME,
10     SharingManagementFormData,
11     SharingInvitationFormData,
12     getSharingMangementFormData,
13 } from './sharing-dialog-types';
14 import { Dispatch } from 'redux';
15 import { ServiceRepository } from "services/services";
16 import { FilterBuilder } from 'services/api/filter-builder';
17 import { initialize, getFormValues, reset } from 'redux-form';
18 import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
19 import { RootState } from 'store/store';
20 import { getDialog } from 'store/dialog/dialog-reducer';
21 import { PermissionLevel } from 'models/permission';
22 import { PermissionResource } from 'models/permission';
23 import { differenceWith } from "lodash";
24 import { withProgress } from "store/progress-indicator/with-progress";
25 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
26 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
27 import {
28     extractUuidObjectType,
29     ResourceObjectType
30 } from "models/resource";
31 import { resourcesActions } from "store/resources/resources-actions";
32
33 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
34     (dispatch: Dispatch) => {
35         dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: {resourceUuid, refresh} }));
36         dispatch<any>(loadSharingDialog);
37     };
38
39 export const closeSharingDialog = () =>
40     dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME });
41
42 export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME);
43 export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
44
45
46 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
47     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
48     await dispatch<any>(saveManagementChanges);
49     await dispatch<any>(sendInvitations);
50     dispatch(reset(SHARING_INVITATION_FORM_NAME));
51     await dispatch<any>(loadSharingDialog);
52     dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
53
54     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
55     if (dialog && dialog.data.refresh) {
56         dialog.data.refresh();
57     }
58 };
59
60 export interface SharingDialogData {
61     resourceUuid: string;
62     refresh: () => void;
63 }
64
65 export const createSharingToken = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
66     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
67     if (dialog) {
68         const resourceUuid = dialog.data.resourceUuid;
69         if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
70             dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
71             try {
72                 const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid);
73                 dispatch(resourcesActions.SET_RESOURCES([sharingToken]));
74                 dispatch(snackbarActions.OPEN_SNACKBAR({
75                     message: 'Sharing URL created',
76                     hideDuration: 2000,
77                     kind: SnackbarKind.SUCCESS,
78                 }));
79             } catch (e) {
80                 dispatch(snackbarActions.OPEN_SNACKBAR({
81                     message: 'Failed to create sharing URL',
82                     hideDuration: 2000,
83                     kind: SnackbarKind.ERROR,
84                 }));
85             } finally {
86                 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
87             }
88         }
89     }
90 };
91
92 export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
93     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
94     try {
95         await apiClientAuthorizationService.delete(uuid);
96         dispatch(resourcesActions.DELETE_RESOURCES([uuid]));
97         dispatch(snackbarActions.OPEN_SNACKBAR({
98             message: 'Sharing URL removed',
99             hideDuration: 2000,
100             kind: SnackbarKind.SUCCESS,
101         }));
102     } catch (e) {
103         dispatch(snackbarActions.OPEN_SNACKBAR({
104             message: 'Failed to remove sharing URL',
105             hideDuration: 2000,
106             kind: SnackbarKind.ERROR,
107         }));
108     } finally {
109         dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
110     }
111 };
112
113 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService, apiClientAuthorizationService }: ServiceRepository) => {
114
115     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
116     if (dialog) {
117         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
118         try {
119             const resourceUuid = dialog.data.resourceUuid;
120             const { items } = await permissionService.listResourcePermissions(resourceUuid);
121             await dispatch<any>(initializeManagementForm(items));
122             // For collections, we need to load the public sharing tokens
123             if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
124                 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
125                 dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
126             }
127         } catch (e) {
128             dispatch(snackbarActions.OPEN_SNACKBAR({
129                 message: 'You do not have access to share this item',
130                 hideDuration: 2000,
131                 kind: SnackbarKind.ERROR }));
132             dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
133         } finally {
134             dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
135         }
136     }
137 };
138
139 const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
140     async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService }: ServiceRepository) => {
141
142         const filters = new FilterBuilder()
143             .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
144             .getFilters();
145
146         const { items: users } = await userService.list({ filters, count: "none" });
147         const { items: groups } = await groupsService.list({ filters, count: "none" });
148
149         const getEmail = (tailUuid: string) => {
150             const user = users.find(({ uuid }) => uuid === tailUuid);
151             const group = groups.find(({ uuid }) => uuid === tailUuid);
152             return user
153                 ? user.email
154                 : group
155                     ? group.name
156                     : tailUuid;
157         };
158
159         const managementPermissions = permissionLinks
160             .map(({ tailUuid, name, uuid }) => ({
161                 email: getEmail(tailUuid),
162                 permissions: name as PermissionLevel,
163                 permissionUuid: uuid,
164             }));
165
166         const managementFormData: SharingManagementFormData = {
167             permissions: managementPermissions,
168             initialPermissions: managementPermissions,
169         };
170
171         dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
172     };
173
174 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
175     const state = getState();
176     const { user } = state.auth;
177     const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
178     if (dialog && user) {
179         const { initialPermissions, permissions } = getSharingMangementFormData(state);
180         const cancelledPermissions = differenceWith(
181             initialPermissions,
182             permissions,
183             (a, b) => a.permissionUuid === b.permissionUuid
184         );
185
186         const deletions = cancelledPermissions.map(({ permissionUuid }) =>
187             permissionService.delete(permissionUuid));
188         const updates = permissions.map(update =>
189             permissionService.update(update.permissionUuid, { name: update.permissions }));
190         await Promise.all([...deletions, ...updates]);
191     }
192 };
193
194 const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
195     const state = getState();
196     const { user } = state.auth;
197     const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
198     if (dialog && user) {
199         const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
200         const data = invitations.invitedPeople.map(invitee => ({
201             ownerUuid: user.uuid,
202             headUuid: dialog.data.resourceUuid,
203             tailUuid: invitee.uuid,
204             name: invitations.permissions
205         }));
206         const changes = data.map( invitation => permissionService.create(invitation));
207         await Promise.all(changes);
208     }
209 };