Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e80899c
add 'pending' permission to role enums
lcampbell2 May 1, 2023
479bd48
init request invite mut
lcampbell2 May 1, 2023
0af50a9
add option to only fetch pending users
lcampbell2 May 2, 2023
e24c742
add gql to frontend
lcampbell2 May 2, 2023
3c2b4e9
add btn to request invite on org cards and page
lcampbell2 May 2, 2023
d3cad54
show pending users on admin page
lcampbell2 May 2, 2023
61d926a
refetch affiliations on mut
lcampbell2 May 3, 2023
0424531
only show request invite btn when logged in
lcampbell2 May 3, 2023
9b36423
add notify email for invite requests
lcampbell2 May 4, 2023
52141da
change filterPending to includePending
lcampbell2 May 4, 2023
afa7d9e
change filterPending to includePending
lcampbell2 May 4, 2023
15c229d
skip notify emails if no admins found
lcampbell2 May 4, 2023
9957833
add modal to confirm invite request
lcampbell2 May 4, 2023
7c3a39d
update translations
lcampbell2 May 4, 2023
ded1d4b
give pending role green colour
lcampbell2 May 4, 2023
35e1c69
fix var in fe trans
lcampbell2 May 4, 2023
7693840
use state for passing org values to invite request modal
lcampbell2 May 4, 2023
7468852
fix fetching list of admins for emailing
lcampbell2 May 4, 2023
8a9c240
undo dev changes
lcampbell2 May 4, 2023
cf6386c
update faked schema
lcampbell2 May 4, 2023
c2e6855
update UserList test
lcampbell2 May 4, 2023
db9ecd0
fix AdminPage tests
lcampbell2 May 4, 2023
1edae82
Merge branch 'master' into request-invites-to-orgs
lcampbell2 May 4, 2023
22995c1
simplify affiliation check in request-org-affiliation
lcampbell2 May 8, 2023
56305b0
new mut tests
lcampbell2 May 8, 2023
18a7d56
new modal tests
lcampbell2 May 8, 2023
83af3ed
change isLoggedIn props to use userVar
lcampbell2 May 10, 2023
47cf027
use reveiver displayName in invite request email
lcampbell2 May 10, 2023
836a06e
add pending to possible user roles in modal
lcampbell2 May 10, 2023
e1a1d07
add new env vars to deployment secrets
lcampbell2 May 10, 2023
946556b
put request buttons in B stream
lcampbell2 May 11, 2023
0ab76ed
Merge branch 'master' into request-invites-to-orgs
lcampbell2 May 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 172 additions & 180 deletions api/src/affiliation/loaders/load-affiliation-connections-by-org-id.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/src/affiliation/mutations/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './invite-user-to-org'
export * from './leave-organization'
export * from './remove-user-from-org'
export * from './request-org-affiliation'
export * from './transfer-org-ownership'
export * from './update-user-role'
78 changes: 24 additions & 54 deletions api/src/affiliation/mutations/remove-user-from-org.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {GraphQLNonNull, GraphQLID} from 'graphql'
import {mutationWithClientMutationId, fromGlobalId} from 'graphql-relay'
import {t} from '@lingui/macro'
import { GraphQLNonNull, GraphQLID } from 'graphql'
import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay'
import { t } from '@lingui/macro'

import {removeUserFromOrgUnion} from '../unions'
import { removeUserFromOrgUnion } from '../unions'
import { logActivity } from '../../audit-logs/mutations/log-activity'

export const removeUserFromOrg = new mutationWithClientMutationId({
name: 'RemoveUserFromOrg',
description:
'This mutation allows admins or higher to remove users from any organizations they belong to.',
description: 'This mutation allows admins or higher to remove users from any organizations they belong to.',
inputFields: () => ({
userId: {
type: GraphQLNonNull(GraphQLID),
Expand All @@ -35,20 +34,20 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
collections,
transaction,
userKey,
auth: {checkPermission, userRequired, verifiedRequired, tfaRequired},
loaders: {loadOrgByKey, loadUserByKey},
validators: {cleanseInput},
auth: { checkPermission, userRequired, verifiedRequired, tfaRequired },
loaders: { loadOrgByKey, loadUserByKey },
validators: { cleanseInput },
},
) => {
// Cleanse Input
const {id: requestedUserKey} = fromGlobalId(cleanseInput(args.userId))
const {id: requestedOrgKey} = fromGlobalId(cleanseInput(args.orgId))
const { id: requestedUserKey } = fromGlobalId(cleanseInput(args.userId))
const { id: requestedOrgKey } = fromGlobalId(cleanseInput(args.orgId))

// Get requesting user
const user = await userRequired()

verifiedRequired({user})
tfaRequired({user})
verifiedRequired({ user })
tfaRequired({ user })

// Get requested org
const requestedOrg = await loadOrgByKey.load(requestedOrgKey)
Expand All @@ -59,14 +58,12 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
return {
_type: 'error',
code: 400,
description: i18n._(
t`Unable to remove user from unknown organization.`,
),
description: i18n._(t`Unable to remove user from unknown organization.`),
}
}

// Check requesting users permission
const permission = await checkPermission({orgId: requestedOrg._id})
const permission = await checkPermission({ orgId: requestedOrg._id })
if (permission === 'user' || typeof permission === 'undefined') {
console.warn(
`User: ${userKey} attempted to remove user: ${requestedUserKey} from org: ${requestedOrg._key}, however they do not have the permission to remove users.`,
Expand All @@ -87,9 +84,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
return {
_type: 'error',
code: 400,
description: i18n._(
t`Unable to remove unknown user from organization.`,
),
description: i18n._(t`Unable to remove unknown user from organization.`),
}
}

Expand All @@ -106,11 +101,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
console.error(
`Database error occurred when user: ${userKey} attempted to check the current permission of user: ${requestedUser._key} to see if they could be removed: ${err}`,
)
throw new Error(
i18n._(
t`Unable to remove user from this organization. Please try again.`,
),
)
throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`))
}

if (affiliationCursor.count < 1) {
Expand All @@ -120,9 +111,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
return {
_type: 'error',
code: 400,
description: i18n._(
t`Unable to remove a user that already does not belong to this organization.`,
),
description: i18n._(t`Unable to remove a user that already does not belong to this organization.`),
}
}

Expand All @@ -133,20 +122,13 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
console.error(
`Cursor error occurred when user: ${userKey} attempted to check the current permission of user: ${requestedUser._key} to see if they could be removed: ${err}`,
)
throw new Error(
i18n._(
t`Unable to remove user from this organization. Please try again.`,
),
)
throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`))
}

let canRemove
if (
permission === 'super_admin' &&
(affiliation.permission === 'admin' || affiliation.permission === 'user')
) {
if (permission === 'super_admin' && ['pending', 'user', 'admin'].includes(affiliation.permission)) {
canRemove = true
} else if (permission === 'admin' && affiliation.permission === 'user') {
} else if (permission === 'admin' && ['pending', 'user'].includes(affiliation.permission)) {
canRemove = true
} else {
canRemove = false
Expand All @@ -171,11 +153,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
console.error(
`Trx step error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`,
)
throw new Error(
i18n._(
t`Unable to remove user from this organization. Please try again.`,
),
)
throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`))
}

try {
Expand All @@ -184,16 +162,10 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
console.error(
`Trx commit error occurred when user: ${userKey} attempted to remove user: ${requestedUser._key} from org: ${requestedOrg._key}, error: ${err}`,
)
throw new Error(
i18n._(
t`Unable to remove user from this organization. Please try again.`,
),
)
throw new Error(i18n._(t`Unable to remove user from this organization. Please try again.`))
}

console.info(
`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`,
)
console.info(`User: ${userKey} successfully removed user: ${requestedUser._key} from org: ${requestedOrg._key}.`)
await logActivity({
transaction,
collections,
Expand Down Expand Up @@ -229,9 +201,7 @@ export const removeUserFromOrg = new mutationWithClientMutationId({
return {
_type: 'error',
code: 400,
description: i18n._(
t`Permission Denied: Please contact organization admin for help with removing users.`,
),
description: i18n._(t`Permission Denied: Please contact organization admin for help with removing users.`),
}
}
},
Expand Down
201 changes: 201 additions & 0 deletions api/src/affiliation/mutations/request-org-affiliation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { GraphQLNonNull, GraphQLID } from 'graphql'
import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay'
import { t } from '@lingui/macro'

import { inviteUserToOrgUnion } from '../unions'
import { logActivity } from '../../audit-logs/mutations/log-activity'

export const requestOrgAffiliation = new mutationWithClientMutationId({
name: 'RequestOrgAffiliation',
description: `This mutation allows users to request to join an organization.`,
inputFields: () => ({
orgId: {
type: GraphQLNonNull(GraphQLID),
description: 'The organization you wish to invite the user to.',
},
}),
outputFields: () => ({
result: {
type: inviteUserToOrgUnion,
description:
'`InviteUserToOrgUnion` returning either a `InviteUserToOrgResult`, or `InviteUserToOrgError` object.',
resolve: (payload) => payload,
},
}),
mutateAndGetPayload: async (
args,
{
i18n,
query,
request,
collections,
transaction,
userKey,
auth: { userRequired, verifiedRequired },
loaders: { loadOrgByKey, loadUserByKey },
notify: { sendInviteRequestEmail },
validators: { cleanseInput },
},
) => {
const { id: orgId } = fromGlobalId(cleanseInput(args.orgId))

// Get requesting user
const user = await userRequired()
verifiedRequired({ user })

// Check to see if requested org exists
const org = await loadOrgByKey.load(orgId)

if (typeof org === 'undefined') {
console.warn(
`User: ${userKey} attempted to request invite to org: ${orgId} however there is no org associated with that id.`,
)
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to request invite to unknown organization.`),
}
}

// Check to see if user is already a member of the org
let affiliationCursor
try {
affiliationCursor = await query`
FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations
FILTER e._to == ${user._id}
RETURN e
`
} catch (err) {
console.error(
`Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`,
)
throw new Error(i18n._(t`Unable to request invite. Please try again.`))
}

if (affiliationCursor.count > 0) {
const requestedAffiliation = await affiliationCursor.next()
if (requestedAffiliation.permission === 'pending') {
console.warn(
`User: ${userKey} attempted to request invite to org: ${orgId} however they have already requested to join that org.`,
)
return {
_type: 'error',
code: 400,
description: i18n._(
t`Unable to request invite to organization with which you have already requested to join.`,
),
}
} else {
console.warn(
`User: ${userKey} attempted to request invite to org: ${orgId} however they are already affiliated with that org.`,
)
return {
_type: 'error',
code: 400,
description: i18n._(t`Unable to request invite to organization with which you are already affiliated.`),
}
}
}

// Setup Transaction
const trx = await transaction(collections)

// Create pending affiliation
try {
await trx.step(
() =>
query`
WITH affiliations, organizations, users
INSERT {
_from: ${org._id},
_to: ${user._id},
permission: "pending",
owner: false
} INTO affiliations
`,
)
} catch (err) {
console.error(
`Transaction step error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`,
)
throw new Error(i18n._(t`Unable to request invite. Please try again.`))
}

// get all org admins
let orgAdminsCursor
try {
orgAdminsCursor = await query`
WITH affiliations, organizations, users
FOR v, e IN 1..1 OUTBOUND ${org._id} affiliations
FILTER e.permission == "admin"
RETURN v._key
`
} catch (err) {
console.error(
`Database error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`,
)
throw new Error(i18n._(t`Unable to request invite. Please try again.`))
}

let orgAdmins
try {
orgAdmins = await orgAdminsCursor.all()
} catch (err) {
console.error(
`Cursor error occurred when user: ${userKey} attempted to request invite to ${orgId}, error: ${err}`,
)
throw new Error(i18n._(t`Unable to request invite. Please try again.`))
}

if (orgAdmins.length > 0) {
const adminLink = `https://${request.get('host')}/admin/organizations`
// send notification to org admins
for (const userKey of orgAdmins) {
const adminUser = await loadUserByKey.load(userKey)
await sendInviteRequestEmail({ user: adminUser, orgName: org.name, adminLink })
}
}

// Commit Transaction
try {
await trx.commit()
} catch (err) {
console.error(
`Transaction commit error occurred while user: ${userKey} attempted to request invite to org: ${org.slug}, error: ${err}`,
)
throw new Error(i18n._(t`Unable to request invite. Please try again.`))
}

console.info(`User: ${userKey} successfully requested invite to the org: ${org.slug}.`)
await logActivity({
transaction,
collections,
query,
initiatedBy: {
id: user._key,
userName: user.userName,
},
action: 'add',
target: {
resource: user.userName,
organization: {
id: org._key,
name: org.name,
}, // name of resource being acted upon
updatedProperties: [
{
name: 'permission',
oldValue: null,
newValue: 'pending',
},
],
resourceType: 'user', // user, org, domain
},
})

return {
_type: 'regular',
status: i18n._(t`Successfully requested invite to organization, and sent notification email.`),
}
},
})
Loading