From 23c1fd444339ec8f6b391a0ab5a9e0a51e9b0c75 Mon Sep 17 00:00:00 2001 From: Csaky Date: Mon, 10 Jun 2024 16:22:03 -0700 Subject: [PATCH 1/4] Minor changes to Invite component --- frontend/src/assets/variables.scss | 2 +- frontend/src/components/common/Invite.vue | 16 ++++++++-------- frontend/src/components/common/ShareButton.vue | 1 - frontend/src/services/inviteService.ts | 5 ++--- frontend/src/utils/emailTemplates.ts | 9 ++++----- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/frontend/src/assets/variables.scss b/frontend/src/assets/variables.scss index ce744318..11a25097 100644 --- a/frontend/src/assets/variables.scss +++ b/frontend/src/assets/variables.scss @@ -9,7 +9,7 @@ // buttons, links $bcbox-primary: #036; $bcbox-link-text: #1a5a96; -$bcbox-link-text-hover: #00f; +$bcbox-link-text-hover: #2378c7; $bcbox-outline-on-primary: #fff; $bcbox-error: #d8292f; diff --git a/frontend/src/components/common/Invite.vue b/frontend/src/components/common/Invite.vue index 8a687f14..4156f39d 100644 --- a/frontend/src/components/common/Invite.vue +++ b/frontend/src/components/common/Invite.vue @@ -17,7 +17,6 @@ import type { Ref } from 'vue'; // Types type Props = { - label: string; resourceType: string; resource: any; }; @@ -142,7 +141,7 @@ const onSubmit = handleSubmit(async (values: any) => { diff --git a/frontend/src/components/common/ShareButton.vue b/frontend/src/components/common/ShareButton.vue index 68b70a77..4005db1f 100644 --- a/frontend/src/components/common/ShareButton.vue +++ b/frontend/src/components/common/ShareButton.vue @@ -129,7 +129,6 @@ const displayShareDialog = ref(false); :disabled="!hasManagePermission" > diff --git a/frontend/src/services/inviteService.ts b/frontend/src/services/inviteService.ts index 7bb5bd3c..16f7f9bc 100644 --- a/frontend/src/services/inviteService.ts +++ b/frontend/src/services/inviteService.ts @@ -49,8 +49,7 @@ export default { }) ); // send invite email notifications - await this.emailInvites(resourceType, resource, currentUser, invites); - return invites; + return await this.emailInvites(resourceType, resource, currentUser, invites); }, /** @@ -94,7 +93,7 @@ export default { }; return appAxios().post('email', emailData); } catch (err) { - return 'Failed to send Invite notification'; + return Promise.reject(err); } }, diff --git a/frontend/src/utils/emailTemplates.ts b/frontend/src/utils/emailTemplates.ts index 4dcfcf04..a515f494 100644 --- a/frontend/src/utils/emailTemplates.ts +++ b/frontend/src/utils/emailTemplates.ts @@ -5,7 +5,7 @@ * @param {User | null} currentUser current user sending the invite * @returns {string} the template html */ -export function invite(resourceType: string, resourceName: string, currentUser: any): string{ +export function invite(resourceType: string, resourceName: string, currentUser: any): string { let html = ''; // eslint-disable-next-line max-len const currentUserEmail = `${currentUser.email}`; @@ -15,16 +15,15 @@ export function invite(resourceType: string, resourceName: string, currentUser: html += `

${currentUserEmail} invited you to access a file on BCBox

`; html += '

'; html += `Here's a link to access the file that ${currentUserEmail} shared with you:

`; - } - else if (resourceType === 'bucket') { + } else if (resourceType === 'bucket') { html += '
'; html += `

${currentUserEmail} invited you to access a folder on BCBox

\n`; html += '

'; html += `Here's a link to access the folder that ${currentUserEmail} shared with you:

`; } - html += '

'; + html += '

'; html += ``; - html += `${resourceName }


`; + html += `${resourceName}


`; html += ` This invite will only work for you and people with existing access. If you do not recognize the sender, do not click on the link above.
From 93afe87bb517de97e80018fa35690fda12f7cbfd Mon Sep 17 00:00:00 2001 From: Csaky Date: Mon, 10 Jun 2024 16:41:27 -0700 Subject: [PATCH 2/4] Bulk Invite: Add perms to existing users and build results --- frontend/src/components/common/Invite.vue | 95 ++++++++++++++++++----- frontend/src/utils/constants.ts | 8 ++ frontend/src/utils/formatters.ts | 30 +++++++ 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/common/Invite.vue b/frontend/src/components/common/Invite.vue index 4156f39d..557aaf61 100644 --- a/frontend/src/components/common/Invite.vue +++ b/frontend/src/components/common/Invite.vue @@ -9,8 +9,9 @@ import TextInput from '@/components/form/TextInput.vue'; import { Spinner } from '@/components/layout'; import { Regex } from '@/utils/constants'; +import { toBulkResult } from '@/utils/formatters'; -import { inviteService } from '@/services'; +import { inviteService, permissionService, userService } from '@/services'; import { useAuthStore } from '@/store'; import type { Ref } from 'vue'; @@ -20,7 +21,6 @@ type Props = { resourceType: string; resource: any; }; - // Props const props = withDefaults(defineProps(), {}); @@ -29,12 +29,22 @@ const { getUser } = storeToRefs(useAuthStore()); const toast = useToast(); // State -const inviteLoading: Ref = ref(false); +const loading: Ref = ref(false); const permHelpLink: Ref = computed(() => { return props.resourceType === 'bucket' ? 'https://github.com/bcgov/bcbox/wiki/My-Files#folder-permissions' : 'https://github.com/bcgov/bcbox/wiki/Files#file-permissions'; }); +const resourceId: Ref = computed(() => + props.resourceType === 'object' ? props.resource.id : props.resource.bucketId +); +const results: Ref> = ref([ + { + resourceType: props.resourceType, + resourceId: resourceId.value, + results: [] + } +]); const timeFrames: Record = { '1 Hour': 3600, @@ -111,32 +121,75 @@ const isDisabled = (optionValue: string) => { }; // Invite form is submitted const onSubmit = handleSubmit(async (values: any) => { - inviteLoading.value = true; + loading.value = true; try { - // set expiry date - const expiresAt = Math.floor(Date.now() / 1000) + values.expiresAt; - // put email(s) into an array + // put email(s) into an array, delimit, de-dupe and remove empty let emailArray; if (values.emailType === 'single') emailArray = [values.email]; - // for list of emails, delimit, de-dupe and remove empty else emailArray = [...new Set(values.multiEmail.split(/[\r\n ,;]+/).filter((item: string) => item))]; - // TODO: add perms to users already in the system - // generate invites (for emails not already in the system) - await inviteService.createInvites( - props.resourceType, - props.resource, - getUser.value?.profile, - emailArray, - expiresAt, - values.permCodes + // for each email, if user exists in db then give permissions, otherwise send invite + let permData: Array<{ userId: string; permCode: string }> = []; + let newUsers: Array = []; + const result = await Promise.all( + emailArray.map(async (email) => { + const user = (await userService.searchForUsers({ email: email })).data; + // if an exact match on one account + if (user.length > 0 && user[0].email === email) { + values.permCodes.forEach((pc: string) => { + permData.push({ userId: user[0].userId, permCode: pc }); + }); + return { + email: email, + userId: user[0].userId, + permissions: [] + }; + } else { + newUsers.push(email); + return { + email: email + }; + } + }) ); - // TODO: output report (list of invites sent, CHES trx ID (?)) + // add to results array + results.value[0].result = result; + + // give permissions to users already in the system + if (permData.length > 0) { + const permResponse = await permissionService.bucketAddPermissions(props.resource.bucketId, permData); + permResponse.data.forEach((p: any) => { + const el = results.value[0].result.find((r: any) => r.userId === p.userId); + el.permissions.push({ + createdAt: p.createdAt, + permCode: p.permCode + }); + }); + } + + // generate invites (for emails not already in the system) + if (newUsers.length > 0) { + const expiresAt = Math.floor(Date.now() / 1000) + values.expiresAt; + const emailResponse = await inviteService.createInvites( + props.resourceType, + props.resource, + getUser.value?.profile, + newUsers, + expiresAt, + values.permCodes + ); + // add to results + emailResponse.data.messages.forEach((msg: { msgId: string; to: Array }) => { + results.value[0].result.find((r: any) => r.email === msg.to[0]).chesMsgId = msg.msgId; + }); + } + results.value[0].result = toBulkResult(results.value[0].result); + // console.log('results', JSON.stringify(results.value)); toast.success('', 'Invite notifications sent.', { life: 5000 }); } catch (error: any) { toast.error('Creating Invite', error.response?.data.detail, { life: 0 }); } - inviteLoading.value = false; + loading.value = false; }); @@ -268,7 +321,7 @@ const onSubmit = handleSubmit(async (values: any) => {
diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index dffd1b13..8d1ab0ad 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -22,6 +22,14 @@ export const Permissions = Object.freeze({ MANAGE: 'MANAGE' }); +export const Permissionlabels = Object.freeze({ + CREATE: 'Upload', + READ: 'Read', + UPDATE: 'Update', + DELETE: 'Delete', + MANAGE: 'manage' +}); + export const Regex = Object.freeze({ // https://emailregex.com/ // HTML5 - Modified to require domain of at least 2 characters diff --git a/frontend/src/utils/formatters.ts b/frontend/src/utils/formatters.ts index ad06587a..157af3ec 100644 --- a/frontend/src/utils/formatters.ts +++ b/frontend/src/utils/formatters.ts @@ -1,4 +1,5 @@ import { format, parseJSON } from 'date-fns'; +import { Permissionlabels } from '@/utils/constants'; function _dateFnsFormat(value: string, formatter: string) { const formatted = ''; @@ -37,3 +38,32 @@ export function toKebabCase(str: string | null) { const strs = str && str.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g); return strs ? strs.join('-').toLocaleLowerCase() : ''; } + +/** + * @function toBulkResult + * transforms an array of invite/add/remove data into an array of human-readable descriptions + * @param {object[]} data results invite/add/remove + * @returns {object[]} an array of human-readable descriptions + */ +export function toBulkResult( + data: Array<{ email: string; chesMsgId: string; permissions: Array<{ permCode: string }> | undefined }> +) { + const result = data.map((r) => { + let description = ''; + + if (r.chesMsgId) description = 'Success: Invite emailed'; + else if (r.permissions && r.permissions.length > 0) { + const perms = r.permissions.map((p) => { + return Permissionlabels[p.permCode as keyof typeof Permissionlabels]; + }); + description = `Success: Permissions applied ( ${perms.join(' and ')})`; + } else if (r.permissions && r.permissions.length == 0) { + description = 'Success: Permissions already existed'; + } + return new Object({ + email: r.email, + description: description + }); + }); + return result; +} From 6a2da140a386c48d00b2dbadb44e76c7a1bab66a Mon Sep 17 00:00:00 2001 From: Csaky Date: Tue, 11 Jun 2024 16:19:51 -0700 Subject: [PATCH 3/4] Bulk add/remove permissions --- .../components/bucket/BucketPermission.vue | 287 +++++++------ .../src/components/common/BulkPermission.vue | 396 ++++++++++++++++++ frontend/src/components/common/Invite.vue | 62 ++- frontend/src/components/common/index.ts | 1 + .../components/object/ObjectPermission.vue | 302 ++++++------- .../options/BucketDeletePermissionsOptions.ts | 2 +- frontend/src/utils/formatters.ts | 23 +- frontend/src/views/HomeView.vue | 14 +- .../bucket/BucketPermission.spec.ts | 2 +- 9 files changed, 761 insertions(+), 328 deletions(-) create mode 100644 frontend/src/components/common/BulkPermission.vue diff --git a/frontend/src/components/bucket/BucketPermission.vue b/frontend/src/components/bucket/BucketPermission.vue index 903cf100..da760e70 100644 --- a/frontend/src/components/bucket/BucketPermission.vue +++ b/frontend/src/components/bucket/BucketPermission.vue @@ -1,16 +1,18 @@