Merge branch 'main' of github.com:specklesystems/speckle-server into gergo/web-1968-add-features-list
This commit is contained in:
@@ -8,9 +8,17 @@ import {
|
||||
WorkspaceWithOptionalRole
|
||||
} from '@/modules/workspacesCore/domain/types'
|
||||
import { EventBusPayloads } from '@/modules/shared/services/eventBus'
|
||||
import { PartialNullable, StreamRoles, WorkspaceRoles } from '@speckle/shared'
|
||||
import {
|
||||
MaybeNullOrUndefined,
|
||||
Nullable,
|
||||
PartialNullable,
|
||||
StreamRoles,
|
||||
WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { WorkspaceRoleToDefaultProjectRoleMapping } from '@/modules/workspaces/domain/types'
|
||||
import { WorkspaceTeam } from '@/modules/workspaces/domain/types'
|
||||
import { Stream } from '@/modules/core/domain/streams/types'
|
||||
import { TokenResourceIdentifier } from '@/modules/core/domain/tokens/types'
|
||||
|
||||
/** Workspace */
|
||||
|
||||
@@ -79,9 +87,9 @@ export type GetWorkspaceCollaboratorsArgs = {
|
||||
cursor?: string
|
||||
filter?: {
|
||||
/**
|
||||
* Optionally filter by workspace role
|
||||
* Optionally filter by workspace role(s)
|
||||
*/
|
||||
role?: string
|
||||
roles?: string[]
|
||||
/**
|
||||
* Optionally filter by user name or email
|
||||
*/
|
||||
@@ -184,6 +192,23 @@ export type GrantWorkspaceProjectRoles = (
|
||||
args: GrantWorkspaceProjectRolesArgs
|
||||
) => Promise<void>
|
||||
|
||||
type UpdateWorkspaceProjectRoleArgs = {
|
||||
role: {
|
||||
projectId: string
|
||||
userId: string
|
||||
// Undefined or null role means delete role
|
||||
role?: Nullable<string>
|
||||
}
|
||||
updater: {
|
||||
userId: string
|
||||
resourceAccessRules: MaybeNullOrUndefined<TokenResourceIdentifier[]>
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateWorkspaceProjectRole = (
|
||||
args: UpdateWorkspaceProjectRoleArgs
|
||||
) => Promise<Stream | undefined>
|
||||
|
||||
/** Events */
|
||||
|
||||
export type EmitWorkspaceEvent = <TEvent extends WorkspaceEvents>(args: {
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
} from '@/modules/core/events/projectsEmitter'
|
||||
import {
|
||||
deleteProjectRoleFactory,
|
||||
getStream,
|
||||
getStreamFactory,
|
||||
legacyGetStreamsFactory,
|
||||
upsertProjectRoleFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
@@ -42,9 +43,9 @@ import {
|
||||
queryAllWorkspaceProjectsFactory,
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory
|
||||
} from '@/modules/workspaces/services/projects'
|
||||
import { getStreams } from '@/modules/core/services/streams'
|
||||
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
|
||||
import { findVerifiedEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
|
||||
import { GetStream } from '@/modules/core/domain/streams/operations'
|
||||
|
||||
export const onProjectCreatedFactory =
|
||||
({
|
||||
@@ -89,7 +90,7 @@ export const onProjectCreatedFactory =
|
||||
|
||||
export const onInviteFinalizedFactory =
|
||||
(deps: {
|
||||
getStream: typeof getStream
|
||||
getStream: GetStream
|
||||
logger: typeof logger
|
||||
updateWorkspaceRole: ReturnType<typeof updateWorkspaceRoleFactory>
|
||||
}) =>
|
||||
@@ -216,6 +217,7 @@ export const initializeEventListenersFactory =
|
||||
({ db }: { db: Knex }) =>
|
||||
() => {
|
||||
const eventBus = getEventBus()
|
||||
const getStreams = legacyGetStreamsFactory({ db })
|
||||
const quitCbs = [
|
||||
ProjectsEmitter.listen(ProjectEvents.Created, async (payload) => {
|
||||
const onProjectCreated = onProjectCreatedFactory({
|
||||
@@ -230,7 +232,7 @@ export const initializeEventListenersFactory =
|
||||
}),
|
||||
eventBus.listen(ServerInvitesEvents.Finalized, async ({ payload }) => {
|
||||
const onInviteFinalized = onInviteFinalizedFactory({
|
||||
getStream,
|
||||
getStream: getStreamFactory({ db }),
|
||||
logger: moduleLogger,
|
||||
updateWorkspaceRole: updateWorkspaceRoleFactory({
|
||||
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
|
||||
|
||||
@@ -4,15 +4,17 @@ import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import {
|
||||
getProjectCollaboratorsFactory,
|
||||
getProjectFactory,
|
||||
getStream,
|
||||
getUserStreams,
|
||||
getUserStreamsCount,
|
||||
updateProjectFactory,
|
||||
upsertProjectRoleFactory,
|
||||
getRolesByUserIdFactory
|
||||
getRolesByUserIdFactory,
|
||||
getStreamFactory,
|
||||
deleteStreamFactory,
|
||||
revokeStreamPermissionsFactory,
|
||||
grantStreamPermissionsFactory,
|
||||
legacyGetStreamsFactory,
|
||||
getUserStreamsPageFactory,
|
||||
getUserStreamsCountFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUser, getUsers } from '@/modules/core/repositories/users'
|
||||
import { getStreams } from '@/modules/core/services/streams'
|
||||
import { InviteCreateValidationError } from '@/modules/serverinvites/errors'
|
||||
import {
|
||||
deleteAllResourceInvitesFactory,
|
||||
@@ -45,8 +47,6 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
|
||||
import {
|
||||
WorkspaceAdminError,
|
||||
WorkspaceInvalidProjectError,
|
||||
WorkspaceInvalidRoleError,
|
||||
WorkspaceJoinNotAllowedError,
|
||||
WorkspaceNotFoundError,
|
||||
@@ -100,7 +100,8 @@ import {
|
||||
getWorkspaceProjectsFactory,
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory,
|
||||
moveProjectToWorkspaceFactory,
|
||||
queryAllWorkspaceProjectsFactory
|
||||
queryAllWorkspaceProjectsFactory,
|
||||
updateWorkspaceProjectRoleFactory
|
||||
} from '@/modules/workspaces/services/projects'
|
||||
import {
|
||||
getDiscoverableWorkspacesForUserFactory,
|
||||
@@ -109,7 +110,6 @@ import {
|
||||
} from '@/modules/workspaces/services/retrieval'
|
||||
import { Roles, WorkspaceRoles, removeNullOrUndefinedKeys } from '@speckle/shared'
|
||||
import { chunk } from 'lodash'
|
||||
import { deleteStream } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
findEmailsByUserIdFactory,
|
||||
findVerifiedEmailsByUserIdFactory,
|
||||
@@ -129,13 +129,31 @@ import {
|
||||
deleteWorkspaceDomainFactory,
|
||||
isUserWorkspaceDomainPolicyCompliantFactory
|
||||
} from '@/modules/workspaces/services/domains'
|
||||
import { getServerInfo } from '@/modules/core/services/generic'
|
||||
import { updateStreamRoleAndNotify } from '@/modules/core/services/streams/management'
|
||||
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import { parseDefaultProjectRole } from '@/modules/workspaces/domain/logic'
|
||||
import { saveActivityFactory } from '@/modules/activitystream/repositories'
|
||||
import {
|
||||
addOrUpdateStreamCollaboratorFactory,
|
||||
isStreamCollaboratorFactory,
|
||||
removeStreamCollaboratorFactory,
|
||||
validateStreamAccessFactory
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import {
|
||||
addStreamInviteAcceptedActivityFactory,
|
||||
addStreamPermissionsAddedActivityFactory,
|
||||
addStreamPermissionsRevokedActivityFactory
|
||||
} from '@/modules/activitystream/services/streamActivity'
|
||||
import { publish } from '@/modules/shared/utils/subscriptions'
|
||||
import { updateStreamRoleAndNotifyFactory } from '@/modules/core/services/streams/management'
|
||||
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUser = getUserFactory({ db })
|
||||
const getUsers = getUsersFactory({ db })
|
||||
const getStream = getStreamFactory({ db })
|
||||
const requestNewEmailVerification = requestNewEmailVerificationFactory({
|
||||
findEmail: findEmailFactory({ db }),
|
||||
getUser,
|
||||
@@ -155,7 +173,7 @@ const buildCollectAndValidateResourceTargets = () =>
|
||||
|
||||
const buildCreateAndSendServerOrProjectInvite = () =>
|
||||
createAndSendInviteFactory({
|
||||
findUserByTarget: findUserByTargetFactory(),
|
||||
findUserByTarget: findUserByTargetFactory({ db }),
|
||||
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
||||
collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(),
|
||||
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
|
||||
@@ -165,12 +183,14 @@ const buildCreateAndSendServerOrProjectInvite = () =>
|
||||
getEventBus().emit({
|
||||
eventName,
|
||||
payload
|
||||
})
|
||||
}),
|
||||
getUser,
|
||||
getServerInfo
|
||||
})
|
||||
|
||||
const buildCreateAndSendWorkspaceInvite = () =>
|
||||
createAndSendInviteFactory({
|
||||
findUserByTarget: findUserByTargetFactory(),
|
||||
findUserByTarget: findUserByTargetFactory({ db }),
|
||||
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
||||
collectAndValidateResourceTargets: buildCollectAndValidateResourceTargets(),
|
||||
buildInviteEmailContents: buildWorkspaceInviteEmailContentsFactory({
|
||||
@@ -181,8 +201,44 @@ const buildCreateAndSendWorkspaceInvite = () =>
|
||||
getEventBus().emit({
|
||||
eventName,
|
||||
payload
|
||||
})
|
||||
}),
|
||||
getUser,
|
||||
getServerInfo
|
||||
})
|
||||
const deleteStream = deleteStreamFactory({ db })
|
||||
const saveActivity = saveActivityFactory({ db })
|
||||
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
||||
const isStreamCollaborator = isStreamCollaboratorFactory({
|
||||
getStream
|
||||
})
|
||||
const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
addStreamPermissionsRevokedActivity: addStreamPermissionsRevokedActivityFactory({
|
||||
saveActivity,
|
||||
publish
|
||||
})
|
||||
})
|
||||
const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
|
||||
isStreamCollaborator,
|
||||
addOrUpdateStreamCollaborator: addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
addStreamInviteAcceptedActivity: addStreamInviteAcceptedActivityFactory({
|
||||
saveActivity,
|
||||
publish
|
||||
}),
|
||||
addStreamPermissionsAddedActivity: addStreamPermissionsAddedActivityFactory({
|
||||
saveActivity,
|
||||
publish
|
||||
})
|
||||
}),
|
||||
removeStreamCollaborator
|
||||
})
|
||||
const getUserStreams = getUserStreamsPageFactory({ db })
|
||||
const getUserStreamsCount = getUserStreamsCountFactory({ db })
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
@@ -348,6 +404,7 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
)
|
||||
|
||||
// Delete workspace and associated resources (i.e. invites)
|
||||
const getStreams = legacyGetStreamsFactory({ db })
|
||||
const deleteWorkspace = deleteWorkspaceFactory({
|
||||
deleteWorkspace: repoDeleteWorkspaceFactory({ db }),
|
||||
deleteProject: deleteStream,
|
||||
@@ -538,12 +595,14 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
getStream,
|
||||
getWorkspace: getWorkspaceFactory({ db })
|
||||
}),
|
||||
findUserByTarget: findUserByTargetFactory(),
|
||||
findUserByTarget: findUserByTargetFactory({ db }),
|
||||
findInvite: findInviteFactory({
|
||||
db,
|
||||
filterQuery: workspaceInviteValidityFilter
|
||||
}),
|
||||
markInviteUpdated: markInviteUpdatedfactory({ db })
|
||||
markInviteUpdated: markInviteUpdatedfactory({ db }),
|
||||
getUser,
|
||||
getServerInfo
|
||||
})
|
||||
|
||||
await resendInviteEmail({
|
||||
@@ -681,29 +740,19 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
},
|
||||
WorkspaceProjectMutations: {
|
||||
updateRole: async (_parent, args, context) => {
|
||||
const { projectId, userId, role } = args.input
|
||||
|
||||
const { workspaceId } = (await getStream({ streamId: projectId })) ?? {}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new WorkspaceInvalidProjectError()
|
||||
}
|
||||
|
||||
const currentRole = await getWorkspaceRoleForUserFactory({ db })({
|
||||
workspaceId,
|
||||
userId
|
||||
const updateWorkspaceProjectRole = updateWorkspaceProjectRoleFactory({
|
||||
getStream,
|
||||
updateStreamRoleAndNotify,
|
||||
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db })
|
||||
})
|
||||
|
||||
if (currentRole?.role === Roles.Workspace.Admin) {
|
||||
// User is workspace admin and cannot have their project roles changed
|
||||
throw new WorkspaceAdminError()
|
||||
}
|
||||
|
||||
return await updateStreamRoleAndNotify(
|
||||
{ projectId, userId, role },
|
||||
context.userId!,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
return await updateWorkspaceProjectRole({
|
||||
role: args.input,
|
||||
updater: {
|
||||
userId: context.userId!,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
}
|
||||
})
|
||||
},
|
||||
moveToWorkspace: async (_parent, args, context) => {
|
||||
const { projectId, workspaceId } = args
|
||||
@@ -724,10 +773,10 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
const trx = await db.transaction()
|
||||
|
||||
const moveProjectToWorkspace = moveProjectToWorkspaceFactory({
|
||||
getProject: getProjectFactory(),
|
||||
getProject: getProjectFactory({ db }),
|
||||
updateProject: updateProjectFactory({ db: trx }),
|
||||
upsertProjectRole: upsertProjectRoleFactory({ db: trx }),
|
||||
getProjectCollaborators: getProjectCollaboratorsFactory(),
|
||||
getProjectCollaborators: getProjectCollaboratorsFactory({ db }),
|
||||
getWorkspaceRoles: getWorkspaceRolesFactory({ db: trx }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
|
||||
@@ -265,7 +265,7 @@ export const getWorkspaceCollaboratorsFactory =
|
||||
.where(DbWorkspaceAcl.col.workspaceId, workspaceId)
|
||||
.orderBy('workspaceRoleCreatedAt', 'desc')
|
||||
|
||||
const { search, role } = filter || {}
|
||||
const { search, roles } = filter || {}
|
||||
|
||||
if (search) {
|
||||
query.andWhere((builder) => {
|
||||
@@ -275,8 +275,10 @@ export const getWorkspaceCollaboratorsFactory =
|
||||
})
|
||||
}
|
||||
|
||||
if (role) {
|
||||
query.andWhere(DbWorkspaceAcl.col.role, role)
|
||||
if (roles) {
|
||||
query.andWhere((builder) => {
|
||||
builder.whereIn(DbWorkspaceAcl.col.role, roles)
|
||||
})
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
|
||||
@@ -36,14 +36,14 @@ import {
|
||||
sessionMiddlewareFactory
|
||||
} from '@/modules/auth/middleware'
|
||||
import { createAuthorizationCodeFactory } from '@/modules/auth/repositories/apps'
|
||||
import { getUserById } from '@/modules/core/services/users'
|
||||
import { legacyGetUserFactory } from '@/modules/core/repositories/users'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const sessionMiddleware = sessionMiddlewareFactory()
|
||||
const finalizeAuthMiddleware = finalizeAuthMiddlewareFactory({
|
||||
createAuthorizationCode: createAuthorizationCodeFactory({ db }),
|
||||
getUserById
|
||||
getUser: legacyGetUserFactory({ db })
|
||||
})
|
||||
|
||||
const buildAuthRedirectUrl = (workspaceSlug: string): URL =>
|
||||
@@ -144,6 +144,7 @@ router.get(
|
||||
const logger = req.log.child({ workspaceSlug: req.params.workspaceSlug })
|
||||
|
||||
let provider: OIDCProvider | null = null
|
||||
|
||||
if (req.query.validate === 'true') {
|
||||
const workspace = await getWorkspaceBySlugFactory({ db })({
|
||||
workspaceSlug: req.params.workspaceSlug
|
||||
|
||||
@@ -9,7 +9,6 @@ import { getWorkspaceRoute } from '@/modules/core/helpers/routeHelper'
|
||||
import { isResourceAllowed } from '@/modules/core/helpers/token'
|
||||
import { UserRecord } from '@/modules/core/helpers/types'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import { getUser } from '@/modules/core/repositories/users'
|
||||
import {
|
||||
ProjectInviteResourceType,
|
||||
ServerInviteResourceType
|
||||
@@ -71,7 +70,8 @@ import {
|
||||
anyEmailCompliantWithWorkspaceDomains,
|
||||
userEmailsCompliantWithWorkspaceDomains
|
||||
} from '@/modules/workspaces/domain/logic'
|
||||
import { getStream } from '@/modules/core/repositories/streams'
|
||||
import { GetStream } from '@/modules/core/domain/streams/operations'
|
||||
import { GetUser } from '@/modules/core/domain/users/operations'
|
||||
|
||||
const isWorkspaceResourceTarget = (
|
||||
target: InviteResourceTarget
|
||||
@@ -124,7 +124,7 @@ type CollectAndValidateWorkspaceTargetsFactoryDeps =
|
||||
getWorkspace: GetWorkspace
|
||||
getWorkspaceDomains: GetWorkspaceDomains
|
||||
findVerifiedEmailsByUserId: FindVerifiedEmailsByUserId
|
||||
getStream: typeof getStream
|
||||
getStream: GetStream
|
||||
}
|
||||
|
||||
export const collectAndValidateWorkspaceTargetsFactory =
|
||||
@@ -381,7 +381,7 @@ function buildPendingWorkspaceCollaboratorModel(
|
||||
export const getUserPendingWorkspaceInviteFactory =
|
||||
(deps: {
|
||||
findInvite: FindInvite
|
||||
getUser: typeof getUser
|
||||
getUser: GetUser
|
||||
getWorkspaceBySlug: GetWorkspaceBySlug
|
||||
}) =>
|
||||
async (params: {
|
||||
@@ -425,10 +425,7 @@ export const getUserPendingWorkspaceInviteFactory =
|
||||
}
|
||||
|
||||
export const getUserPendingWorkspaceInvitesFactory =
|
||||
(deps: {
|
||||
getUserResourceInvites: QueryAllUserResourceInvites
|
||||
getUser: typeof getUser
|
||||
}) =>
|
||||
(deps: { getUserResourceInvites: QueryAllUserResourceInvites; getUser: GetUser }) =>
|
||||
async (userId: string): Promise<PendingWorkspaceCollaboratorGraphQLReturn[]> => {
|
||||
if (!userId) return []
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
validateWorkspaceSlug
|
||||
} from '@speckle/shared'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { deleteStream } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
DeleteWorkspaceRole,
|
||||
GetWorkspaceRoleForUser,
|
||||
@@ -64,6 +63,7 @@ import { chunk, isEmpty, omit } from 'lodash'
|
||||
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
|
||||
import { workspaceRoles as workspaceRoleDefinitions } from '@/modules/workspaces/roles'
|
||||
import { blockedDomains } from '@speckle/shared'
|
||||
import { DeleteStreamRecord } from '@/modules/core/domain/streams/operations'
|
||||
|
||||
type WorkspaceCreateArgs = {
|
||||
userId: string
|
||||
@@ -278,7 +278,7 @@ export const deleteWorkspaceFactory =
|
||||
deleteAllResourceInvites
|
||||
}: {
|
||||
deleteWorkspace: DeleteWorkspace
|
||||
deleteProject: typeof deleteStream
|
||||
deleteProject: DeleteStreamRecord
|
||||
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
|
||||
deleteAllResourceInvites: DeleteAllResourceInvites
|
||||
}) =>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { getStreams as serviceGetStreams } from '@/modules/core/services/streams'
|
||||
import { getUserStreams } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
GetWorkspace,
|
||||
GetWorkspaceRoleForUser,
|
||||
GetWorkspaceRoles,
|
||||
GetWorkspaceRoleToDefaultProjectRoleMapping,
|
||||
QueryAllWorkspaceProjects,
|
||||
UpdateWorkspaceProjectRole,
|
||||
UpdateWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
WorkspaceAdminError,
|
||||
WorkspaceInvalidProjectError,
|
||||
WorkspaceInvalidRoleError,
|
||||
WorkspaceNotFoundError,
|
||||
WorkspaceQueryError
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
@@ -23,12 +25,17 @@ import { chunk } from 'lodash'
|
||||
import { Roles, StreamRoles } from '@speckle/shared'
|
||||
import { orderByWeight } from '@/modules/shared/domain/rolesAndScopes/logic'
|
||||
import coreUserRoles from '@/modules/core/roles'
|
||||
import {
|
||||
GetStream,
|
||||
GetUserStreamsPage,
|
||||
LegacyGetStreams,
|
||||
UpdateStreamRole
|
||||
} from '@/modules/core/domain/streams/operations'
|
||||
|
||||
export const queryAllWorkspaceProjectsFactory = ({
|
||||
getStreams
|
||||
}: {
|
||||
// TODO: Core service factory functions
|
||||
getStreams: typeof serviceGetStreams
|
||||
getStreams: LegacyGetStreams
|
||||
}): QueryAllWorkspaceProjects =>
|
||||
async function* queryAllWorkspaceProjects({
|
||||
workspaceId
|
||||
@@ -75,7 +82,7 @@ type GetWorkspaceProjectsReturnValue = {
|
||||
}
|
||||
|
||||
export const getWorkspaceProjectsFactory =
|
||||
({ getStreams }: { getStreams: typeof getUserStreams }) =>
|
||||
({ getStreams }: { getStreams: GetUserStreamsPage }) =>
|
||||
async (
|
||||
args: GetWorkspaceProjectsArgs,
|
||||
opts: GetWorkspaceProjectsOptions
|
||||
@@ -196,3 +203,42 @@ export const getWorkspaceRoleToDefaultProjectRoleMappingFactory =
|
||||
[Roles.Workspace.Admin]: Roles.Stream.Owner
|
||||
}
|
||||
}
|
||||
|
||||
export const updateWorkspaceProjectRoleFactory =
|
||||
({
|
||||
getStream,
|
||||
getWorkspaceRoleForUser,
|
||||
updateStreamRoleAndNotify
|
||||
}: {
|
||||
getStream: GetStream
|
||||
getWorkspaceRoleForUser: GetWorkspaceRoleForUser
|
||||
updateStreamRoleAndNotify: UpdateStreamRole
|
||||
}): UpdateWorkspaceProjectRole =>
|
||||
async ({ role, updater }) => {
|
||||
const { workspaceId } = (await getStream({ streamId: role.projectId })) ?? {}
|
||||
if (!workspaceId) throw new WorkspaceInvalidProjectError()
|
||||
|
||||
const currentWorkspaceRole = await getWorkspaceRoleForUser({
|
||||
workspaceId,
|
||||
userId: role.userId
|
||||
})
|
||||
|
||||
if (currentWorkspaceRole?.role === Roles.Workspace.Admin) {
|
||||
// User is workspace admin and cannot have their project roles changed
|
||||
throw new WorkspaceAdminError()
|
||||
}
|
||||
|
||||
if (
|
||||
currentWorkspaceRole?.role === Roles.Workspace.Guest &&
|
||||
role.role === Roles.Stream.Owner
|
||||
) {
|
||||
// Workspace guests cannot be project owners
|
||||
throw new WorkspaceInvalidRoleError('Workspace guests cannot be project owners.')
|
||||
}
|
||||
|
||||
return await updateStreamRoleAndNotify(
|
||||
role,
|
||||
updater.userId!,
|
||||
updater.resourceAccessRules
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { getStream } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
findEmailsByUserIdFactory,
|
||||
findVerifiedEmailsByUserIdFactory
|
||||
@@ -45,6 +44,9 @@ import {
|
||||
StreamRoles,
|
||||
WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
|
||||
export type BasicTestWorkspace = {
|
||||
/**
|
||||
@@ -207,8 +209,11 @@ export const createWorkspaceInviteDirectly = async (
|
||||
args: CreateWorkspaceInviteMutationVariables,
|
||||
inviterId: string
|
||||
) => {
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getStream = getStreamFactory({ db })
|
||||
const getUser = getUserFactory({ db })
|
||||
const createAndSendInvite = createAndSendInviteFactory({
|
||||
findUserByTarget: findUserByTargetFactory(),
|
||||
findUserByTarget: findUserByTargetFactory({ db }),
|
||||
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
||||
collectAndValidateResourceTargets: collectAndValidateWorkspaceTargetsFactory({
|
||||
getStream,
|
||||
@@ -224,7 +229,9 @@ export const createWorkspaceInviteDirectly = async (
|
||||
getEventBus().emit({
|
||||
eventName,
|
||||
payload
|
||||
})
|
||||
}),
|
||||
getUser,
|
||||
getServerInfo
|
||||
})
|
||||
|
||||
const createInvite = createWorkspaceInviteFactory({
|
||||
|
||||
@@ -65,7 +65,6 @@ import {
|
||||
import type { Express } from 'express'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { getStream } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
createUserEmailFactory,
|
||||
deleteUserEmailFactory,
|
||||
@@ -75,10 +74,25 @@ import {
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
|
||||
import { createRandomPassword } from '@/modules/core/helpers/testHelpers'
|
||||
import { addOrUpdateStreamCollaborator } from '@/modules/core/services/streams/streamAccessService'
|
||||
import { WorkspaceProtectedError } from '@/modules/workspaces/errors/workspace'
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import {
|
||||
getStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { saveActivityFactory } from '@/modules/activitystream/repositories'
|
||||
import {
|
||||
addOrUpdateStreamCollaboratorFactory,
|
||||
validateStreamAccessFactory
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import {
|
||||
addStreamInviteAcceptedActivityFactory,
|
||||
addStreamPermissionsAddedActivityFactory
|
||||
} from '@/modules/activitystream/services/streamActivity'
|
||||
import { publish } from '@/modules/shared/utils/subscriptions'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
|
||||
enum InviteByTarget {
|
||||
Email = 'email',
|
||||
@@ -87,6 +101,25 @@ enum InviteByTarget {
|
||||
|
||||
type TestGraphQLOperations = ReturnType<typeof buildGraphqlOperations>
|
||||
|
||||
const getStream = getStreamFactory({ db })
|
||||
const saveActivity = saveActivityFactory({ db })
|
||||
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
||||
|
||||
const getUser = getUserFactory({ db })
|
||||
const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
addStreamInviteAcceptedActivity: addStreamInviteAcceptedActivityFactory({
|
||||
saveActivity,
|
||||
publish
|
||||
}),
|
||||
addStreamPermissionsAddedActivity: addStreamPermissionsAddedActivityFactory({
|
||||
saveActivity,
|
||||
publish
|
||||
})
|
||||
})
|
||||
|
||||
const buildGraphqlOperations = (deps: { apollo: TestApolloServer }) => {
|
||||
const { apollo } = deps
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { grantStreamPermissions } from '@/modules/core/repositories/streams'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
@@ -27,6 +28,8 @@ import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
|
||||
|
||||
describe('Workspace project GQL CRUD', () => {
|
||||
let apollo: TestApolloServer
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
import { truncateTables } from '@/test/hooks'
|
||||
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import {
|
||||
grantStreamPermissions,
|
||||
grantStreamPermissionsFactory,
|
||||
upsertProjectRoleFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { omit } from 'lodash'
|
||||
@@ -58,6 +58,7 @@ const createUserEmail = createUserEmailFactory({ db })
|
||||
const updateUserEmail = updateUserEmailFactory({ db })
|
||||
const getUserDiscoverableWorkspaces = getUserDiscoverableWorkspacesFactory({ db })
|
||||
const upsertProjectRole = upsertProjectRoleFactory({ db })
|
||||
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
|
||||
|
||||
const upsertWorkspace = upsertWorkspaceFactory({ db })
|
||||
const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { grantStreamPermissions } from '@/modules/core/repositories/streams'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
assignToWorkspace,
|
||||
BasicTestWorkspace,
|
||||
@@ -29,6 +30,8 @@ import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { isUndefined } from 'lodash'
|
||||
|
||||
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
|
||||
|
||||
describe('Workspaces Roles GQL', () => {
|
||||
let apollo: TestApolloServer
|
||||
|
||||
|
||||
@@ -49,8 +49,10 @@ import {
|
||||
createRandomString
|
||||
} from '@/modules/core/helpers/testHelpers'
|
||||
import { getBranchesByStreamId } from '@/modules/core/services/branches'
|
||||
import { grantStreamPermissions } from '@/modules/core/repositories/streams'
|
||||
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
|
||||
const grantStreamPermissions = grantStreamPermissionsFactory({ db })
|
||||
|
||||
const createProjectWithVersions =
|
||||
({ apollo }: { apollo: TestApolloServer }) =>
|
||||
@@ -325,11 +327,11 @@ describe('Workspaces GQL CRUD', () => {
|
||||
expect(res.data?.workspace.team.items[0].user.name).to.equal('John C Speckle')
|
||||
})
|
||||
|
||||
it('should respect role filters', async () => {
|
||||
it('should respect role filters with one value', async () => {
|
||||
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
|
||||
workspaceId: largeWorkspace.id,
|
||||
filter: {
|
||||
role: 'workspace:member'
|
||||
roles: ['workspace:member']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -337,6 +339,18 @@ describe('Workspaces GQL CRUD', () => {
|
||||
expect(res.data?.workspace.team.items.length).to.equal(2)
|
||||
})
|
||||
|
||||
it('should respect role filters with multiple values', async () => {
|
||||
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
|
||||
workspaceId: largeWorkspace.id,
|
||||
filter: {
|
||||
roles: ['workspace:admin', 'workspace:member']
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspace.team.items.length).to.equal(4)
|
||||
})
|
||||
|
||||
it('should respect search limits', async () => {
|
||||
const res = await largeWorkspaceApollo.execute(GetWorkspaceTeamDocument, {
|
||||
workspaceId: largeWorkspace.id,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ProjectTeamMember } from '@/modules/core/domain/projects/types'
|
||||
import { Stream } from '@/modules/core/domain/streams/types'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import {
|
||||
moveProjectToWorkspaceFactory,
|
||||
queryAllWorkspaceProjectsFactory
|
||||
queryAllWorkspaceProjectsFactory,
|
||||
updateWorkspaceProjectRoleFactory
|
||||
} from '@/modules/workspaces/services/projects'
|
||||
import { WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import { assert, expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
const getWorkspaceRoleToDefaultProjectRoleMapping = async () => ({
|
||||
@@ -393,4 +395,33 @@ describe('Project management services', () => {
|
||||
expect(updatedRoles[0].role).to.equal(Roles.Stream.Owner)
|
||||
})
|
||||
})
|
||||
describe('updateWorkspaceProjectRoleFactory returns a function, that', () => {
|
||||
it('should throw when attempting to promote a workspace guest to project owner', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 9 })
|
||||
const projectId = cryptoRandomString({ length: 9 })
|
||||
const userId = cryptoRandomString({ length: 9 })
|
||||
|
||||
const updateWorkspaceProjectRole = updateWorkspaceProjectRoleFactory({
|
||||
getStream: async () => {
|
||||
return { workspaceId } as Stream
|
||||
},
|
||||
getWorkspaceRoleForUser: async () => ({
|
||||
workspaceId,
|
||||
userId,
|
||||
role: Roles.Workspace.Guest,
|
||||
createdAt: new Date()
|
||||
}),
|
||||
updateStreamRoleAndNotify: async () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
|
||||
await expectToThrow(() =>
|
||||
updateWorkspaceProjectRole({
|
||||
role: { userId, projectId, role: Roles.Stream.Owner },
|
||||
updater: { userId: '', resourceAccessRules: [] }
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user