feat: comment read/write auth policies in BE & FE (#4368)
* webhooks perm minor fix * tryna get fileimport service to work * new comment policies - shared * BE done? * checks implemented in FE * lint fix * tests fix * readme fix
This commit is contained in:
committed by
GitHub
parent
41e7daa60d
commit
b6c21fd506
@@ -68,6 +68,8 @@ minio-data/
|
||||
postgres-data/
|
||||
redis-data/
|
||||
|
||||
packages/fileimport-service/src/ifc-dotnet/output
|
||||
|
||||
.tshy-build
|
||||
obj/
|
||||
bin/
|
||||
|
||||
@@ -11,3 +11,35 @@ The File Import service can parse either STL, OBJ, or IFC files using external p
|
||||
The parsers are responsible for extracting the necessary data from the files and storing it in the database. They are also responsible for creating a new Speckle model if necessary.
|
||||
|
||||
The service is then responsible for updating the status of the `file_uploads` table, and for posting a Postgres notification.
|
||||
|
||||
## Dev setup
|
||||
|
||||
### Building/Running the .NET importer
|
||||
|
||||
Requirements:
|
||||
|
||||
- Ubuntu 24+
|
||||
|
||||
Do this on Ubuntu/OSX to install dotnet:
|
||||
|
||||
```bash
|
||||
# Add microsoft package repo
|
||||
sudo apt update && sudo apt install -y wget apt-transport-https
|
||||
wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
# Install dotnet sdk 8
|
||||
sudo apt update
|
||||
sudo apt install -y dotnet-sdk-8.0
|
||||
|
||||
# Verify version
|
||||
dotnet --version
|
||||
```
|
||||
|
||||
Do this to build:
|
||||
|
||||
```bash
|
||||
cd ./packages/fileimport-service/src/ifc-dotnet
|
||||
|
||||
dotnet publish ifc-converter.csproj -c Release -o output/
|
||||
```
|
||||
|
||||
@@ -16,6 +16,7 @@ import { logger } from '@/observability/logging.js'
|
||||
import { Nullable, Scopes, wait } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { Logger } from 'pino'
|
||||
import { getIfcDllPath, useLegacyIfcImporter } from '@/controller/helpers/env.js'
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
@@ -153,7 +154,10 @@ async function doTask(
|
||||
taskLogger.info('Triggering importer for {fileType}')
|
||||
|
||||
if (info.fileType.toLowerCase() === 'ifc') {
|
||||
if (info.fileName.toLowerCase().endsWith('.legacyimporter.ifc')) {
|
||||
if (
|
||||
info.fileName.toLowerCase().endsWith('.legacyimporter.ifc') ||
|
||||
useLegacyIfcImporter()
|
||||
) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['NODE_BINARY_PATH'] || 'node',
|
||||
@@ -181,8 +185,7 @@ async function doTask(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
[
|
||||
process.env['IFC_DOTNET_DLL_PATH'] ||
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
|
||||
getIfcDllPath(),
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
import file from 'node:fs'
|
||||
|
||||
export const isDevEnv = () => {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
}
|
||||
|
||||
export const isTestEnv = () => {
|
||||
return process.env.NODE_ENV === 'test'
|
||||
}
|
||||
|
||||
export const isDevOrTestEnv = () => isDevEnv() || isTestEnv()
|
||||
|
||||
export const useLegacyIfcImporter = () => {
|
||||
return ['true', '1'].includes(process.env.USE_LEGACY_IFC_IMPORTER || 'false')
|
||||
}
|
||||
|
||||
export const getPackageRootDirPath = () => {
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
let root = path.resolve(__dirname, '../../../')
|
||||
if (root.endsWith('dist')) {
|
||||
// Resolved path may differ depending on whether running from dist or src (w/ ts-node)
|
||||
root = path.resolve(root, '../')
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
let cachedIfcDllPath: string | undefined = undefined
|
||||
export const getIfcDllPath = () => {
|
||||
if (cachedIfcDllPath) return cachedIfcDllPath
|
||||
|
||||
const absolutePath = process.env['IFC_DOTNET_DLL_PATH']
|
||||
if (absolutePath && file.existsSync(absolutePath)) {
|
||||
cachedIfcDllPath = absolutePath
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
if (isDevOrTestEnv()) {
|
||||
const possiblePath = path.resolve(
|
||||
getPackageRootDirPath(),
|
||||
'./src/ifc-dotnet/output/ifc-converter.dll'
|
||||
)
|
||||
if (file.existsSync(possiblePath)) {
|
||||
cachedIfcDllPath = absolutePath
|
||||
return possiblePath
|
||||
}
|
||||
}
|
||||
|
||||
const fallback =
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll'
|
||||
if (file.existsSync(fallback)) {
|
||||
cachedIfcDllPath = fallback
|
||||
return fallback
|
||||
}
|
||||
|
||||
throw new Error('Could not resolve .NET IFC DLL')
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Observability from '@speckle/shared/dist/commonjs/observability/index.js'
|
||||
export const logger = Observability.extendLoggerComponent(
|
||||
Observability.getLogger(
|
||||
process.env.LOG_LEVEL || 'info',
|
||||
process.env.LOG_PRETTY === 'true'
|
||||
process.env.LOG_PRETTY === 'true' && !process.env.FORCE_NO_PRETTY
|
||||
),
|
||||
'fileimport-service'
|
||||
)
|
||||
|
||||
@@ -195,7 +195,7 @@ import {
|
||||
ArrowLeftIcon,
|
||||
ArrowUpRightIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { ensureError, Roles } from '@speckle/shared'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import { onKeyDown, useClipboard, useDraggable, onClickOutside } from '@vueuse/core'
|
||||
import { scrollToBottom } from '~~/lib/common/helpers/dom'
|
||||
@@ -216,6 +216,18 @@ import { useDisableGlobalTextSelection } from '~~/lib/common/composables/window'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useThreadUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
graphql(`
|
||||
fragment ViewerCommentThreadData on Comment {
|
||||
id
|
||||
permissions {
|
||||
canArchive {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: CommentBubbleModel): void
|
||||
@@ -235,14 +247,9 @@ const { isEmbedEnabled } = useEmbed()
|
||||
|
||||
const threadId = computed(() => props.modelValue.id)
|
||||
const { copy } = useClipboard()
|
||||
const { activeUser, isLoggedIn } = useActiveUser()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const archiveComment = useArchiveComment()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const {
|
||||
resources: {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const { projectId } = useInjectedViewerState()
|
||||
const canReply = useCheckViewerCommentingAccess()
|
||||
@@ -401,10 +408,7 @@ const changeExpanded = async (newVal: boolean) => {
|
||||
}
|
||||
|
||||
const canArchiveOrUnarchive = computed(
|
||||
() =>
|
||||
activeUser.value &&
|
||||
(props.modelValue.author.id === activeUser.value.id ||
|
||||
project.value?.role === Roles.Stream.Owner)
|
||||
() => props.modelValue.permissions.canArchive.authorized
|
||||
)
|
||||
|
||||
const toggleCommentResolvedStatus = async () => {
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
size="sm"
|
||||
:icon-left="includeArchived ? CheckCircleIcon : CheckCircleIconOutlined"
|
||||
text
|
||||
:disabled="commentThreadsMetadata?.totalArchivedCount === 0"
|
||||
class="!text-foreground"
|
||||
@click="includeArchived = includeArchived ? undefined : 'includeArchived'"
|
||||
>
|
||||
|
||||
@@ -133,6 +133,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": typeof types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n": typeof types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": typeof types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": typeof types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": typeof types.ViewerModelVersionCardItemFragmentDoc,
|
||||
@@ -329,8 +330,10 @@ type Documents = {
|
||||
"\n mutation verifyEmail($input: VerifyUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n verify(input: $input)\n }\n }\n }\n": typeof types.VerifyEmailDocument,
|
||||
"\n fragment EmailFields on UserEmail {\n id\n email\n verified\n primary\n userId\n }\n": typeof types.EmailFieldsFragmentDoc,
|
||||
"\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n": typeof types.UserEmailsDocument,
|
||||
"\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseViewerUserActivityBroadcasting_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": typeof types.ViewerCommentBubblesDataFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": typeof types.BroadcastViewerUserActivityDocument,
|
||||
"\n mutation MarkCommentViewed($input: MarkCommentViewedInput!) {\n commentMutations {\n markViewed(input: $input)\n }\n }\n": typeof types.MarkCommentViewedDocument,
|
||||
@@ -338,7 +341,7 @@ type Documents = {
|
||||
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": typeof types.CreateCommentReplyDocument,
|
||||
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": typeof types.ArchiveCommentDocument,
|
||||
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": typeof types.ViewerModelVersionsDocument,
|
||||
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": typeof types.ViewerDiffVersionsDocument,
|
||||
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": typeof types.ViewerLoadedThreadsDocument,
|
||||
@@ -407,10 +410,10 @@ type Documents = {
|
||||
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": typeof types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": typeof types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": typeof types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": typeof types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsServerProjects_UserFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
|
||||
@@ -542,6 +545,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
|
||||
@@ -738,8 +742,10 @@ const documents: Documents = {
|
||||
"\n mutation verifyEmail($input: VerifyUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n verify(input: $input)\n }\n }\n }\n": types.VerifyEmailDocument,
|
||||
"\n fragment EmailFields on UserEmail {\n id\n email\n verified\n primary\n userId\n }\n": types.EmailFieldsFragmentDoc,
|
||||
"\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n": types.UserEmailsDocument,
|
||||
"\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseViewerUserActivityBroadcasting_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": types.ViewerCommentBubblesDataFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n": types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": types.BroadcastViewerUserActivityDocument,
|
||||
"\n mutation MarkCommentViewed($input: MarkCommentViewedInput!) {\n commentMutations {\n markViewed(input: $input)\n }\n }\n": types.MarkCommentViewedDocument,
|
||||
@@ -747,7 +753,7 @@ const documents: Documents = {
|
||||
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
|
||||
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": types.ArchiveCommentDocument,
|
||||
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
|
||||
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument,
|
||||
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
|
||||
@@ -816,10 +822,10 @@ const documents: Documents = {
|
||||
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsServerProjects_UserFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
|
||||
@@ -1322,6 +1328,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesSecuritySsoWrapp
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n"): (typeof documents)["\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2106,6 +2116,10 @@ export function graphql(source: "\n fragment EmailFields on UserEmail {\n id
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n"): (typeof documents)["\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2113,7 +2127,11 @@ export function graphql(source: "\n fragment ViewerCommentBubblesData on Commen
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n"): (typeof documents)["\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n"];
|
||||
export function graphql(source: "\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n"): (typeof documents)["\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2145,7 +2163,7 @@ export function graphql(source: "\n query ProjectViewerResources($projectId: St
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n"];
|
||||
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2421,7 +2439,7 @@ export function graphql(source: "\n query AutomateFunctionPageWorkspace($worksp
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2433,7 +2451,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationPage_Project
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -306,6 +306,9 @@ function createCache(): InMemoryCache {
|
||||
},
|
||||
subscription: {
|
||||
merge: mergeAsObjectsFunction
|
||||
},
|
||||
creationState: {
|
||||
merge: mergeAsObjectsFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
useStateSerialization
|
||||
} from '~~/lib/viewer/composables/serialization'
|
||||
import type { Merge } from 'type-fest'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
/**
|
||||
* How often we send out an "activity" message even if user hasn't made any clicks (just to keep him active)
|
||||
@@ -66,6 +67,17 @@ function useCollectMainMetadata() {
|
||||
})
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment UseViewerUserActivityBroadcasting_Project on Project {
|
||||
id
|
||||
permissions {
|
||||
canBroadcastActivity {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export function useViewerUserActivityBroadcasting(
|
||||
options?: Partial<{
|
||||
state: InjectableViewerState
|
||||
@@ -74,14 +86,18 @@ export function useViewerUserActivityBroadcasting(
|
||||
const {
|
||||
projectId,
|
||||
resources: {
|
||||
request: { resourceIdString }
|
||||
request: { resourceIdString },
|
||||
response: { project }
|
||||
}
|
||||
} = options?.state || useInjectedViewerState()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const getMainMetadata = useCollectMainMetadata()
|
||||
const apollo = useApolloClient().client
|
||||
const { isEnabled: isEmbedEnabled } = useEmbed()
|
||||
|
||||
const canBroadcast = computed(
|
||||
() => project.value?.permissions.canBroadcastActivity.authorized
|
||||
)
|
||||
|
||||
const isSameMessage = (
|
||||
previousSerializedMessage: Optional<string>,
|
||||
newMessage: ViewerUserActivityMessageInput
|
||||
@@ -118,7 +134,7 @@ export function useViewerUserActivityBroadcasting(
|
||||
}
|
||||
|
||||
const invoke = async (message: ViewerUserActivityMessageInput) => {
|
||||
if (!isLoggedIn.value || isEmbedEnabled.value) return false
|
||||
if (!canBroadcast.value || isEmbedEnabled.value) return false
|
||||
return await Promise.all([
|
||||
invokeMutation(message),
|
||||
invokeObservabilityEvent(message)
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
StateApplyMode
|
||||
} from '~~/lib/viewer/composables/serialization'
|
||||
import type { CommentBubbleModel } from '~/lib/viewer/composables/commentBubbles'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
export function useViewerCommentUpdateTracking(
|
||||
params: {
|
||||
@@ -229,21 +230,26 @@ export function useArchiveComment() {
|
||||
}
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment UseCheckViewerCommentingAccess_Project on Project {
|
||||
id
|
||||
permissions {
|
||||
canCreateComment {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export function useCheckViewerCommentingAccess() {
|
||||
const {
|
||||
resources: {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const { activeUser } = useActiveUser()
|
||||
|
||||
return computed(() => {
|
||||
if (!activeUser.value) return false
|
||||
|
||||
const hasRole = !!project.value?.role
|
||||
const allowPublicComments = !!project.value?.allowPublicComments
|
||||
|
||||
return hasRole || allowPublicComments
|
||||
return project.value?.permissions.canCreateComment.authorized
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,7 @@ import {
|
||||
SpeckleLoader
|
||||
} from '@speckle/viewer'
|
||||
import { useAuthCookie } from '~~/lib/auth/composables/auth'
|
||||
import type {
|
||||
Project,
|
||||
ProjectCommentThreadsArgs,
|
||||
ViewerResourceItem
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import type { ViewerResourceItem } from '~~/lib/common/generated/gql/graphql'
|
||||
import { ProjectCommentsUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
useInjectedViewer,
|
||||
@@ -39,11 +35,7 @@ import {
|
||||
useViewerEventListener
|
||||
} from '~~/lib/viewer/composables/viewer'
|
||||
import { useViewerCommentUpdateTracking } from '~~/lib/viewer/composables/commentManagement'
|
||||
import {
|
||||
getCacheId,
|
||||
getObjectReference,
|
||||
modifyObjectFields
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { getCacheId } from '~~/lib/common/helpers/graphql'
|
||||
import {
|
||||
useViewerOpenedThreadUpdateEmitter,
|
||||
useViewerThreadTracking
|
||||
@@ -264,21 +256,19 @@ function useViewerSubscriptionEventTracker() {
|
||||
})
|
||||
|
||||
// Remove from project.commentThreads
|
||||
modifyObjectFields<ProjectCommentThreadsArgs, Project['commentThreads']>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Project', projectId.value),
|
||||
(fieldName, variables, data) => {
|
||||
if (fieldName !== 'commentThreads') return
|
||||
if (variables.filter?.includeArchived) return
|
||||
'commentThreads',
|
||||
({ variables, helpers: { createUpdatedValue, readField } }) => {
|
||||
if (variables.filter?.includeArchived) return // we want it in that list
|
||||
|
||||
const newItems = (data.items || []).filter(
|
||||
(i) => i.__ref !== getObjectReference('Comment', event.id).__ref
|
||||
)
|
||||
return {
|
||||
...data,
|
||||
...(data.items ? { items: newItems } : {}),
|
||||
...(data.totalCount ? { totalCount: data.totalCount - 1 } : {})
|
||||
}
|
||||
return createUpdatedValue(({ update }) => {
|
||||
update('totalCount', (totalCount) => totalCount - 1)
|
||||
update('items', (items) =>
|
||||
items.filter((i) => readField(i, 'id') !== event.id)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
} else if (isNew && comment) {
|
||||
@@ -298,21 +288,22 @@ function useViewerSubscriptionEventTracker() {
|
||||
)
|
||||
} else {
|
||||
// Add comment thread
|
||||
modifyObjectFields<ProjectCommentThreadsArgs, Project['commentThreads']>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Project', projectId.value),
|
||||
(fieldName, _variables, data) => {
|
||||
if (fieldName !== 'commentThreads') return
|
||||
'commentThreads',
|
||||
({ helpers: { ref, createUpdatedValue, readField }, value }) => {
|
||||
// In case this is actually an unarchived comment, we only want to add it if it doesnt
|
||||
// exist in the includesArchived list already
|
||||
const includesItem = value.items?.find(
|
||||
(i) => readField(i, 'id') === comment.id
|
||||
)
|
||||
if (includesItem) return
|
||||
|
||||
const newItems = [
|
||||
getObjectReference('Comment', comment.id),
|
||||
...(data.items || [])
|
||||
]
|
||||
return {
|
||||
...data,
|
||||
...(data.items ? { items: newItems } : {}),
|
||||
...(data.totalCount ? { totalCount: data.totalCount + 1 } : {})
|
||||
}
|
||||
return createUpdatedValue(({ update }) => {
|
||||
update('totalCount', (totalCount) => totalCount + 1)
|
||||
update('items', (items) => [ref('Comment', comment.id), ...items])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const viewerCommentThreadFragment = graphql(`
|
||||
...ViewerCommentsListItem
|
||||
...ViewerCommentBubblesData
|
||||
...ViewerCommentsReplyItem
|
||||
...ViewerCommentThreadData
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export const viewerLoadedResourcesQuery = graphql(`
|
||||
...ProjectPageLatestItemsModels
|
||||
...ModelPageProject
|
||||
...HeaderNavShare_Project
|
||||
...UseCheckViewerCommentingAccess_Project
|
||||
...UseViewerUserActivityBroadcasting_Project
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -105,6 +105,7 @@ graphql(`
|
||||
...ProjectPageProjectHeader
|
||||
...ProjectPageTeamDialog
|
||||
...ProjectsMoveToWorkspaceDialog_Project
|
||||
...ProjectPageSettingsTab_Project
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { LayoutTabsVertical, type LayoutPageTabItem } from '@speckle/ui-components'
|
||||
import { projectSettingsRoute, projectWebhooksRoute } from '~~/lib/common/helpers/route'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import type { ProjectPageSettingsTab_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['can-view-settings']
|
||||
@@ -22,8 +22,9 @@ definePageMeta({
|
||||
graphql(`
|
||||
fragment ProjectPageSettingsTab_Project on Project {
|
||||
id
|
||||
name
|
||||
permissions {
|
||||
canUpdate {
|
||||
canReadWebhooks {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
@@ -31,12 +32,12 @@ graphql(`
|
||||
`)
|
||||
|
||||
const attrs = useAttrs() as {
|
||||
project: ProjectPageProjectFragment
|
||||
project: ProjectPageSettingsTab_ProjectFragment
|
||||
}
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const canUpdate = computed(() => attrs.project.permissions.canUpdate)
|
||||
const canReadWebhooks = computed(() => attrs.project.permissions.canReadWebhooks)
|
||||
const projectName = computed(() =>
|
||||
attrs.project.name.length ? attrs.project.name : ''
|
||||
)
|
||||
@@ -53,8 +54,8 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
|
||||
{
|
||||
title: 'Webhooks',
|
||||
id: 'webhooks',
|
||||
disabled: !canUpdate.value.authorized,
|
||||
disabledMessage: canUpdate.value.message
|
||||
disabled: !canReadWebhooks.value.authorized,
|
||||
disabledMessage: canReadWebhooks.value.message
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
type CommentPermissionChecks {
|
||||
canArchive: PermissionCheckResult!
|
||||
}
|
||||
|
||||
extend type Comment {
|
||||
permissions: CommentPermissionChecks!
|
||||
}
|
||||
|
||||
extend type ProjectPermissionChecks {
|
||||
canCreateComment: PermissionCheckResult!
|
||||
canBroadcastActivity: PermissionCheckResult!
|
||||
}
|
||||
@@ -38,6 +38,7 @@ generates:
|
||||
ServerStats: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn'
|
||||
CommentReplyAuthorCollection: '@/modules/comments/helpers/graphTypes#CommentReplyAuthorCollectionGraphQLReturn'
|
||||
Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn'
|
||||
CommentPermissionChecks: '@/modules/comments/helpers/graphTypes#CommentPermissionChecksGraphQLReturn'
|
||||
PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn'
|
||||
StreamCollaborator: '@/modules/core/helpers/graphTypes#StreamCollaboratorGraphQLReturn'
|
||||
ProjectCollaborator: '@/modules/core/helpers/graphTypes#ProjectCollaboratorGraphQLReturn'
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineModuleLoaders } from '@/modules/loaders'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
|
||||
export default defineModuleLoaders(async () => {
|
||||
return {
|
||||
getComment: async ({ commentId, projectId }, { dataLoaders }) => {
|
||||
const db = await getProjectDbClient({ projectId })
|
||||
const comment = await dataLoaders
|
||||
.forRegion({ db })
|
||||
.comments.getComment.load(commentId)
|
||||
if (!comment) return null
|
||||
|
||||
return {
|
||||
...comment,
|
||||
projectId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -57,6 +57,8 @@ export type GetComment = (params: {
|
||||
userId?: string
|
||||
}) => Promise<Optional<ExtendedComment>>
|
||||
|
||||
export type GetComments = (params: { ids: string[] }) => Promise<CommentRecord[]>
|
||||
|
||||
export type CheckStreamResourceAccess = (
|
||||
res: ResourceIdentifier,
|
||||
streamId: string
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { keyBy } from 'lodash'
|
||||
import { Nullable } from '@/modules/shared/helpers/typeHelper'
|
||||
import { ResourceIdentifier } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
getCommentParentsFactory,
|
||||
getCommentReplyAuthorIdsFactory,
|
||||
getCommentReplyCountsFactory,
|
||||
getCommentsFactory,
|
||||
getCommentsResourcesFactory,
|
||||
getCommentsViewedAtFactory
|
||||
} from '@/modules/comments/repositories/comments'
|
||||
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
|
||||
declare module '@/modules/core/loaders' {
|
||||
interface ModularizedDataLoaders extends ReturnType<typeof dataLoadersDefinition> {}
|
||||
}
|
||||
|
||||
const dataLoadersDefinition = defineRequestDataloaders(
|
||||
({ ctx, createLoader, deps: { db } }) => {
|
||||
const userId = ctx.userId
|
||||
|
||||
const getCommentsResources = getCommentsResourcesFactory({ db })
|
||||
const getCommentsViewedAt = getCommentsViewedAtFactory({ db })
|
||||
const getCommentReplyCounts = getCommentReplyCountsFactory({ db })
|
||||
const getCommentReplyAuthorIds = getCommentReplyAuthorIdsFactory({ db })
|
||||
const getCommentParents = getCommentParentsFactory({ db })
|
||||
const getComments = getCommentsFactory({ db })
|
||||
|
||||
return {
|
||||
comments: {
|
||||
getComment: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (commentIds) => {
|
||||
const results = keyBy(await getComments({ ids: commentIds.slice() }), 'id')
|
||||
return commentIds.map((id) => results[id] || null)
|
||||
}
|
||||
),
|
||||
getViewedAt: createLoader<string, Nullable<Date>>(async (commentIds) => {
|
||||
if (!userId) return commentIds.slice().map(() => null)
|
||||
|
||||
const results = keyBy(
|
||||
await getCommentsViewedAt(commentIds.slice(), userId),
|
||||
'commentId'
|
||||
)
|
||||
return commentIds.map((id) => results[id]?.viewedAt || null)
|
||||
}),
|
||||
getResources: createLoader<string, ResourceIdentifier[]>(async (commentIds) => {
|
||||
const results = await getCommentsResources(commentIds.slice())
|
||||
return commentIds.map((id) => results[id]?.resources || [])
|
||||
}),
|
||||
getReplyCount: createLoader<string, number>(async (threadIds) => {
|
||||
const results = keyBy(
|
||||
await getCommentReplyCounts(threadIds.slice()),
|
||||
'threadId'
|
||||
)
|
||||
return threadIds.map((id) => results[id]?.count || 0)
|
||||
}),
|
||||
getReplyAuthorIds: createLoader<string, string[]>(async (threadIds) => {
|
||||
const results = await getCommentReplyAuthorIds(threadIds.slice())
|
||||
return threadIds.map((id) => results[id] || [])
|
||||
}),
|
||||
getReplyParent: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (replyIds) => {
|
||||
const results = keyBy(await getCommentParents(replyIds.slice()), 'replyId')
|
||||
return replyIds.map((id) => results[id] || null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default dataLoadersDefinition
|
||||
@@ -58,8 +58,6 @@ import {
|
||||
getViewerResourceGroupsFactory
|
||||
} from '@/modules/core/services/commit/viewerResources'
|
||||
import {
|
||||
authorizeProjectCommentsAccessFactory,
|
||||
authorizeCommentAccessFactory,
|
||||
createCommentThreadAndNotifyFactory,
|
||||
createCommentReplyAndNotifyFactory,
|
||||
editCommentAndNotifyFactory,
|
||||
@@ -83,7 +81,6 @@ import {
|
||||
getCommitsAndTheirBranchIdsFactory,
|
||||
getSpecificBranchCommitsFactory
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
getBranchLatestCommitsFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
@@ -93,20 +90,10 @@ import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { Knex } from 'knex'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
|
||||
// We can use the main DB for these
|
||||
const getStream = getStreamFactory({ db })
|
||||
const authorizeProjectCommentsAccess = authorizeProjectCommentsAccessFactory({
|
||||
getStream,
|
||||
adminOverrideEnabled
|
||||
})
|
||||
|
||||
const buildAuthorizeCommentAccess = (deps: { db: Knex; mainDb: Knex }) =>
|
||||
authorizeCommentAccessFactory({
|
||||
getStream: getStreamFactory({ db: deps.mainDb }),
|
||||
adminOverrideEnabled,
|
||||
getComment: getCommentFactory(deps)
|
||||
})
|
||||
|
||||
const buildGetViewerResourcesFromLegacyIdentifiers = (deps: { db: Knex }) => {
|
||||
const getViewerResourcesFromLegacyIdentifiers =
|
||||
@@ -133,20 +120,17 @@ const buildGetViewerResourceItemsUngrouped = (deps: { db: Knex }) =>
|
||||
})
|
||||
})
|
||||
|
||||
const getStreamCommentFactory =
|
||||
const getAuthorizedStreamCommentFactory =
|
||||
(deps: { db: Knex; mainDb: Knex }) =>
|
||||
async (
|
||||
{ streamId, commentId }: { streamId: string; commentId: string },
|
||||
ctx: GraphQLContext
|
||||
) => {
|
||||
const authorizeProjectCommentsAccess = authorizeProjectCommentsAccessFactory({
|
||||
getStream: getStreamFactory(deps),
|
||||
adminOverrideEnabled
|
||||
})
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: streamId,
|
||||
authCtx: ctx
|
||||
const canReadProject = await ctx.authPolicies.project.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const getComment = getCommentFactory(deps)
|
||||
const comment = await getComment({ id: commentId, userId: ctx.userId })
|
||||
@@ -161,7 +145,10 @@ export = {
|
||||
async comment(_parent, args, context) {
|
||||
const projectId = args.streamId
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getStreamComment = getStreamCommentFactory({ db: projectDb, mainDb })
|
||||
const getStreamComment = getAuthorizedStreamCommentFactory({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
|
||||
return await getStreamComment(
|
||||
{ streamId: args.streamId, commentId: args.id },
|
||||
@@ -171,12 +158,13 @@ export = {
|
||||
|
||||
async comments(_parent, args, context) {
|
||||
const projectId = args.streamId
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getComments = getCommentsLegacyFactory({ db: projectDb })
|
||||
return {
|
||||
...(await getComments({
|
||||
@@ -356,12 +344,7 @@ export = {
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
async commentThreads(parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: parent.id,
|
||||
authCtx: context
|
||||
})
|
||||
|
||||
async commentThreads(parent, args) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
const getPaginatedProjectComments = getPaginatedProjectCommentsFactory({
|
||||
resolvePaginatedProjectCommentsLatestModelResources:
|
||||
@@ -388,7 +371,10 @@ export = {
|
||||
async comment(parent, args, context) {
|
||||
const projectId = parent.id
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getStreamComment = getStreamCommentFactory({ db: projectDb, mainDb })
|
||||
const getStreamComment = getAuthorizedStreamCommentFactory({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
return await getStreamComment(
|
||||
{ streamId: parent.id, commentId: args.id },
|
||||
context
|
||||
@@ -396,13 +382,8 @@ export = {
|
||||
}
|
||||
},
|
||||
Version: {
|
||||
async commentThreads(parent, args, context) {
|
||||
async commentThreads(parent, args) {
|
||||
const projectId = parent.streamId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getPaginatedCommitComments = getPaginatedCommitCommentsFactory({
|
||||
getPaginatedCommitCommentsPage: getPaginatedCommitCommentsPageFactory({
|
||||
@@ -425,12 +406,8 @@ export = {
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
async commentThreads(parent, args, context) {
|
||||
async commentThreads(parent, args) {
|
||||
const projectId = parent.streamId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
const getPaginatedBranchComments = getPaginatedBranchCommentsFactory({
|
||||
@@ -498,16 +475,13 @@ export = {
|
||||
},
|
||||
CommentMutations: {
|
||||
async markViewed(_parent, args, ctx) {
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId
|
||||
const canReadProject = await ctx.authPolicies.project.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const markCommentViewed = markCommentViewedFactory({ db: projectDb })
|
||||
await markCommentViewed(args.input.commentId, ctx.userId!)
|
||||
|
||||
@@ -515,12 +489,11 @@ export = {
|
||||
},
|
||||
async create(_parent, args, ctx) {
|
||||
const projectId = args.input.projectId
|
||||
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
const canCreate = await ctx.authPolicies.project.comment.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
@@ -547,17 +520,13 @@ export = {
|
||||
return await createCommentThreadAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async reply(_parent, args, ctx) {
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
commentId: args.input.threadId,
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
const canCreateComment = await ctx.authPolicies.project.comment.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canCreateComment)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const validateInputAttachments = validateInputAttachmentsFactory({
|
||||
getBlobs: getBlobsFactory({ db: projectDb })
|
||||
@@ -583,19 +552,16 @@ export = {
|
||||
return await createCommentReplyAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async edit(_parent, args, ctx) {
|
||||
const canEditComment = await ctx.authPolicies.project.comment.canEdit({
|
||||
projectId: args.input.projectId,
|
||||
userId: ctx.userId,
|
||||
commentId: args.input.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canEditComment)
|
||||
|
||||
const projectDb = await getProjectDbClient({
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const validateInputAttachments = validateInputAttachmentsFactory({
|
||||
getBlobs: getBlobsFactory({ db: projectDb })
|
||||
@@ -612,19 +578,16 @@ export = {
|
||||
return await editCommentAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async archive(_parent, args, ctx) {
|
||||
const canArchive = await ctx.authPolicies.project.comment.canArchive({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId,
|
||||
commentId: args.input.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canArchive)
|
||||
|
||||
const projectDb = await getProjectDbClient({
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const getStream = getStreamFactory({ db: projectDb })
|
||||
const updateComment = updateCommentFactory({ db: projectDb })
|
||||
@@ -654,10 +617,12 @@ export = {
|
||||
commentMutations: () => ({}),
|
||||
async broadcastViewerUserActivity(_parent, args, context) {
|
||||
const projectId = args.projectId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
const canBroadcastActivity =
|
||||
await context.authPolicies.project.canBroadcastActivity({
|
||||
projectId,
|
||||
userId: context.userId
|
||||
})
|
||||
throwIfAuthNotOk(canBroadcastActivity)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
@@ -674,10 +639,13 @@ export = {
|
||||
},
|
||||
|
||||
async userViewerActivityBroadcast(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
})
|
||||
const projectId = args.streamId
|
||||
const canBroadcastActivity =
|
||||
await context.authPolicies.project.canBroadcastActivity({
|
||||
projectId,
|
||||
userId: context.userId
|
||||
})
|
||||
throwIfAuthNotOk(canBroadcastActivity)
|
||||
|
||||
await pubsub.publish(CommentSubscriptions.ViewerActivity, {
|
||||
userViewerActivity: args.data,
|
||||
@@ -707,16 +675,11 @@ export = {
|
||||
},
|
||||
|
||||
async commentCreate(_parent, args, context) {
|
||||
if (!context.userId)
|
||||
throw new ForbiddenError('Only registered users can comment.')
|
||||
|
||||
const stream = await getStream({
|
||||
streamId: args.input.streamId,
|
||||
userId: context.userId
|
||||
const canCreate = await context.authPolicies.project.comment.canCreate({
|
||||
userId: context.userId,
|
||||
projectId: args.input.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.streamId })
|
||||
const getViewerResourcesFromLegacyIdentifiers =
|
||||
@@ -737,7 +700,7 @@ export = {
|
||||
getViewerResourcesFromLegacyIdentifiers
|
||||
})
|
||||
const comment = await createComment({
|
||||
userId: context.userId,
|
||||
userId: context.userId!,
|
||||
input: args.input
|
||||
})
|
||||
|
||||
@@ -745,13 +708,12 @@ export = {
|
||||
},
|
||||
|
||||
async commentEdit(_parent, args, context) {
|
||||
// NOTE: This is NOT in use anywhere
|
||||
const stream = await authorizeProjectCommentsAccess({
|
||||
const canEdit = await context.authPolicies.project.comment.canEdit({
|
||||
userId: context.userId,
|
||||
projectId: args.input.streamId,
|
||||
authCtx: context,
|
||||
requireProjectRole: true
|
||||
commentId: args.input.id
|
||||
})
|
||||
const matchUser = !stream.role
|
||||
throwIfAuthNotOk(canEdit)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.streamId })
|
||||
const editComment = editCommentFactory({
|
||||
@@ -763,16 +725,17 @@ export = {
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
await editComment({ userId: context.userId!, input: args.input, matchUser })
|
||||
await editComment({ userId: context.userId!, input: args.input })
|
||||
return true
|
||||
},
|
||||
|
||||
// used for flagging a comment as viewed
|
||||
async commentView(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: args.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.streamId })
|
||||
const markCommentViewed = markCommentViewedFactory({ db: projectDb })
|
||||
@@ -782,11 +745,12 @@ export = {
|
||||
},
|
||||
|
||||
async commentArchive(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
const canArchive = await context.authPolicies.project.comment.canArchive({
|
||||
userId: context.userId,
|
||||
projectId: args.streamId,
|
||||
authCtx: context,
|
||||
requireProjectRole: true
|
||||
commentId: args.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canArchive)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.streamId })
|
||||
const archiveComment = archiveCommentFactory({
|
||||
@@ -849,15 +813,13 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.ViewerActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
|
||||
// dont report users activity to himself
|
||||
// dont report user's activity to themselves
|
||||
if (context.userId && context.userId === payload.authorId) {
|
||||
return false
|
||||
}
|
||||
@@ -873,13 +835,11 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
// if we're listening for a stream's root comments events
|
||||
if (!variables.resourceIds) {
|
||||
@@ -929,13 +889,11 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentThreadActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
return (
|
||||
payload.streamId === variables.streamId &&
|
||||
@@ -955,21 +913,17 @@ export = {
|
||||
if (!target.resourceIdString.trim().length) return false
|
||||
if (payload.projectId !== target.projectId) return false
|
||||
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: payload.projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
db: projectDb
|
||||
})
|
||||
|
||||
const [stream, requestedResourceItems] = await Promise.all([
|
||||
getStream({
|
||||
streamId: payload.projectId,
|
||||
userId: context.userId
|
||||
}),
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!stream?.isPublic && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
const requestedResourceItems = await getViewerResourceItemsUngrouped(target)
|
||||
|
||||
// dont report users activity to himself
|
||||
if (
|
||||
@@ -995,21 +949,18 @@ export = {
|
||||
const target = variables.target
|
||||
if (payload.projectId !== target.projectId) return false
|
||||
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: payload.projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
db: projectDb
|
||||
})
|
||||
|
||||
const [stream, requestedResourceItems] = await Promise.all([
|
||||
getStream({
|
||||
streamId: payload.projectId,
|
||||
userId: context.userId
|
||||
}),
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!(stream?.isDiscoverable || stream?.isPublic) && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
const requestedResourceItems = await getViewerResourceItemsUngrouped(target)
|
||||
|
||||
if (!target.resourceIdString) {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
export default {
|
||||
Comment: {
|
||||
permissions: async (parent) => ({
|
||||
commentId: parent.id,
|
||||
projectId: parent.streamId
|
||||
})
|
||||
},
|
||||
CommentPermissionChecks: {
|
||||
canArchive: async (parent, _args, ctx) => {
|
||||
const canArchive = await ctx.authPolicies.project.comment.canArchive({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canArchive)
|
||||
}
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateComment: async (parent, _args, ctx) => {
|
||||
const canCreateComment = await ctx.authPolicies.project.comment.canCreate({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateComment)
|
||||
},
|
||||
canBroadcastActivity: async (parent, _args, ctx) => {
|
||||
const canBroadcastActivity = await ctx.authPolicies.project.canBroadcastActivity({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canBroadcastActivity)
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
@@ -14,3 +14,8 @@ export type CommentReplyAuthorCollectionGraphQLReturn = {
|
||||
}
|
||||
|
||||
export type CommentGraphQLReturn = CommentRecord
|
||||
|
||||
export type CommentPermissionChecksGraphQLReturn = {
|
||||
commentId: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
GetCommentParents,
|
||||
GetCommentReplyAuthorIds,
|
||||
GetCommentReplyCounts,
|
||||
GetComments,
|
||||
GetCommentsResources,
|
||||
GetCommitCommentCounts,
|
||||
GetPaginatedBranchCommentsPage,
|
||||
@@ -106,6 +107,18 @@ export const getCommentFactory =
|
||||
return await query
|
||||
}
|
||||
|
||||
export const getCommentsFactory =
|
||||
(deps: { db: Knex }): GetComments =>
|
||||
async (params) => {
|
||||
const { ids } = params
|
||||
if (!ids.length) return []
|
||||
|
||||
const query = tables.comments(deps.db).select<CommentRecord[]>('*')
|
||||
query.whereIn(Comments.col.id, ids)
|
||||
|
||||
return await query
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resources array for the specified comments. Results object is keyed by comment ID.
|
||||
*/
|
||||
|
||||
@@ -247,7 +247,7 @@ export const editCommentFactory =
|
||||
}: {
|
||||
userId: string
|
||||
input: CommentEditInput
|
||||
matchUser: boolean
|
||||
matchUser?: boolean
|
||||
}) => {
|
||||
const editedComment = await deps.getComment({ id: input.id })
|
||||
if (!editedComment) throw new CommentNotFoundError("The comment doesn't exist")
|
||||
|
||||
@@ -90,33 +90,6 @@ export const authorizeProjectCommentsAccessFactory =
|
||||
return project
|
||||
}
|
||||
|
||||
export const authorizeCommentAccessFactory =
|
||||
(
|
||||
deps: {
|
||||
getComment: GetComment
|
||||
} & AuthorizeProjectCommentsAccessDeps
|
||||
) =>
|
||||
async (params: {
|
||||
authCtx: AuthContext
|
||||
commentId: string
|
||||
requireProjectRole?: boolean
|
||||
}) => {
|
||||
const { authCtx, commentId, requireProjectRole } = params
|
||||
const comment = await deps.getComment({
|
||||
id: commentId,
|
||||
userId: authCtx.userId
|
||||
})
|
||||
if (!comment) {
|
||||
throw new StreamInvalidAccessError('Attempting to access a nonexistant comment')
|
||||
}
|
||||
|
||||
return authorizeProjectCommentsAccessFactory(deps)({
|
||||
projectId: comment.streamId,
|
||||
authCtx,
|
||||
requireProjectRole
|
||||
})
|
||||
}
|
||||
|
||||
export const createCommentThreadAndNotifyFactory =
|
||||
(deps: {
|
||||
getViewerResourceItemsUngrouped: GetViewerResourceItemsUngrouped
|
||||
@@ -311,10 +284,7 @@ export const archiveCommentAndNotifyFactory =
|
||||
}
|
||||
|
||||
const stream = await deps.getStream({ streamId: comment.streamId, userId })
|
||||
if (
|
||||
!stream ||
|
||||
(comment.authorId !== userId && stream.role !== Roles.Stream.Owner)
|
||||
) {
|
||||
if (!stream) {
|
||||
throw new CommentUpdateError(
|
||||
'You do not have permissions to archive this comment'
|
||||
)
|
||||
|
||||
@@ -855,7 +855,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, true],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -875,7 +875,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, false],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -895,7 +895,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, false],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -978,8 +978,8 @@ describe('Graphql @comments', () => {
|
||||
[archiveOthersComment, false],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, false],
|
||||
[queryComment, false],
|
||||
[queryComments, false],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
[queryStreamCommentCount, false],
|
||||
[queryObjectCommentCount, false],
|
||||
[queryCommitCommentCount, false],
|
||||
@@ -996,8 +996,8 @@ describe('Graphql @comments', () => {
|
||||
[archiveOthersComment, false],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, false],
|
||||
[queryComment, false],
|
||||
[queryComments, false],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
[queryStreamCommentCount, false],
|
||||
[queryObjectCommentCount, false],
|
||||
[queryCommitCommentCount, false],
|
||||
@@ -1164,13 +1164,13 @@ describe('Graphql @comments', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe(`testing ${streamContext.cases.length} cases of acting on ${
|
||||
describe(`testing ${streamContext.cases.length} cases of acting on "${
|
||||
stream.name
|
||||
} stream where I'm a ${
|
||||
user && stream.role ? stream.role : 'trouble:maker'
|
||||
}" stream where I ${
|
||||
user && stream.role ? 'have the role ' + stream.role : 'have no role'
|
||||
}`, () => {
|
||||
streamContext.cases.forEach(([testCase, shouldSucceed]) => {
|
||||
it(`${shouldSucceed ? 'can' : 'am not allowed to'} ${
|
||||
it(`${shouldSucceed ? 'should' : 'should not be allowed to'} ${
|
||||
testCase.name
|
||||
}`, async () => {
|
||||
await testCase({ apollo, streamId: stream.id, resources, shouldSucceed })
|
||||
|
||||
@@ -33,14 +33,9 @@ import {
|
||||
getUserAuthoredCommitCountsFactory,
|
||||
getUserStreamCommitCountsFactory
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { ResourceIdentifier, Scope } from '@/modules/core/graph/generated/graphql'
|
||||
import { Scope } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
getBranchCommentCountsFactory,
|
||||
getCommentParentsFactory,
|
||||
getCommentReplyAuthorIdsFactory,
|
||||
getCommentReplyCountsFactory,
|
||||
getCommentsResourcesFactory,
|
||||
getCommentsViewedAtFactory,
|
||||
getCommitCommentCountsFactory,
|
||||
getStreamCommentCountsFactory
|
||||
} from '@/modules/comments/repositories/comments'
|
||||
@@ -51,7 +46,6 @@ import {
|
||||
getStreamBranchCountsFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
} from '@/modules/core/repositories/branches'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { metaHelpers } from '@/modules/core/helpers/meta'
|
||||
import { Users } from '@/modules/core/dbSchema'
|
||||
import { getStreamPendingModelsFactory } from '@/modules/fileuploads/repositories/fileUploads'
|
||||
@@ -119,13 +113,8 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
const getRevisionsFunctions = getRevisionsFunctionsFactory({ db })
|
||||
const getStreamCommentCounts = getStreamCommentCountsFactory({ db })
|
||||
const getAutomationRunsTriggers = getAutomationRunsTriggersFactory({ db })
|
||||
const getCommentsResources = getCommentsResourcesFactory({ db })
|
||||
const getCommentsViewedAt = getCommentsViewedAtFactory({ db })
|
||||
const getCommitCommentCounts = getCommitCommentCountsFactory({ db })
|
||||
const getBranchCommentCounts = getBranchCommentCountsFactory({ db })
|
||||
const getCommentReplyCounts = getCommentReplyCountsFactory({ db })
|
||||
const getCommentReplyAuthorIds = getCommentReplyAuthorIdsFactory({ db })
|
||||
const getCommentParents = getCommentParentsFactory({ db })
|
||||
const getBranchesByIds = getBranchesByIdsFactory({ db })
|
||||
const getStreamBranchesByName = getStreamBranchesByNameFactory({ db })
|
||||
const getBranchLatestCommits = getBranchLatestCommitsFactory({ db })
|
||||
@@ -469,38 +458,6 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
return commitIds.map((i) => results[i] || null)
|
||||
})
|
||||
},
|
||||
comments: {
|
||||
getViewedAt: createLoader<string, Nullable<Date>>(async (commentIds) => {
|
||||
if (!userId) return commentIds.slice().map(() => null)
|
||||
|
||||
const results = keyBy(
|
||||
await getCommentsViewedAt(commentIds.slice(), userId),
|
||||
'commentId'
|
||||
)
|
||||
return commentIds.map((id) => results[id]?.viewedAt || null)
|
||||
}),
|
||||
getResources: createLoader<string, ResourceIdentifier[]>(async (commentIds) => {
|
||||
const results = await getCommentsResources(commentIds.slice())
|
||||
return commentIds.map((id) => results[id]?.resources || [])
|
||||
}),
|
||||
getReplyCount: createLoader<string, number>(async (threadIds) => {
|
||||
const results = keyBy(
|
||||
await getCommentReplyCounts(threadIds.slice()),
|
||||
'threadId'
|
||||
)
|
||||
return threadIds.map((id) => results[id]?.count || 0)
|
||||
}),
|
||||
getReplyAuthorIds: createLoader<string, string[]>(async (threadIds) => {
|
||||
const results = await getCommentReplyAuthorIds(threadIds.slice())
|
||||
return threadIds.map((id) => results[id] || [])
|
||||
}),
|
||||
getReplyParent: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (replyIds) => {
|
||||
const results = keyBy(await getCommentParents(replyIds.slice()), 'replyId')
|
||||
return replyIds.map((id) => results[id] || null)
|
||||
}
|
||||
)
|
||||
},
|
||||
users: {
|
||||
/**
|
||||
* Get user from DB
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
|
||||
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, ProjectPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
|
||||
import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, CommentPermissionChecksGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
|
||||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
@@ -631,6 +631,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -764,6 +765,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -2560,6 +2566,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
@@ -5189,6 +5197,7 @@ export type ResolversTypes = {
|
||||
CommentDataFiltersInput: CommentDataFiltersInput;
|
||||
CommentEditInput: CommentEditInput;
|
||||
CommentMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
CommentPermissionChecks: ResolverTypeWrapper<CommentPermissionChecksGraphQLReturn>;
|
||||
CommentReplyAuthorCollection: ResolverTypeWrapper<CommentReplyAuthorCollectionGraphQLReturn>;
|
||||
CommentThreadActivityMessage: ResolverTypeWrapper<Omit<CommentThreadActivityMessage, 'reply'> & { reply?: Maybe<ResolversTypes['Comment']> }>;
|
||||
Commit: ResolverTypeWrapper<CommitGraphQLReturn>;
|
||||
@@ -5516,6 +5525,7 @@ export type ResolversParentTypes = {
|
||||
CommentDataFiltersInput: CommentDataFiltersInput;
|
||||
CommentEditInput: CommentEditInput;
|
||||
CommentMutations: MutationsObjectGraphQLReturn;
|
||||
CommentPermissionChecks: CommentPermissionChecksGraphQLReturn;
|
||||
CommentReplyAuthorCollection: CommentReplyAuthorCollectionGraphQLReturn;
|
||||
CommentThreadActivityMessage: Omit<CommentThreadActivityMessage, 'reply'> & { reply?: Maybe<ResolversParentTypes['Comment']> };
|
||||
Commit: CommitGraphQLReturn;
|
||||
@@ -6094,6 +6104,7 @@ export type CommentResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
hasParent?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
parent?: Resolver<Maybe<ResolversTypes['Comment']>, ParentType, ContextType>;
|
||||
permissions?: Resolver<ResolversTypes['CommentPermissionChecks'], ParentType, ContextType>;
|
||||
rawText?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
reactions?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
|
||||
replies?: Resolver<ResolversTypes['CommentCollection'], ParentType, ContextType, RequireFields<CommentRepliesArgs, 'limit'>>;
|
||||
@@ -6140,6 +6151,11 @@ export type CommentMutationsResolvers<ContextType = GraphQLContext, ParentType e
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommentPermissionChecks'] = ResolversParentTypes['CommentPermissionChecks']> = {
|
||||
canArchive?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommentReplyAuthorCollection'] = ResolversParentTypes['CommentReplyAuthorCollection']> = {
|
||||
items?: Resolver<Array<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
|
||||
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
@@ -6659,6 +6675,8 @@ export type ProjectPendingVersionsUpdatedMessageResolvers<ContextType = GraphQLC
|
||||
};
|
||||
|
||||
export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectPermissionChecks'] = ResolversParentTypes['ProjectPermissionChecks']> = {
|
||||
canBroadcastActivity?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canLeave?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<ProjectPermissionChecksCanMoveToWorkspaceArgs>>;
|
||||
@@ -7529,6 +7547,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
CommentCollection?: CommentCollectionResolvers<ContextType>;
|
||||
CommentDataFilters?: CommentDataFiltersResolvers<ContextType>;
|
||||
CommentMutations?: CommentMutationsResolvers<ContextType>;
|
||||
CommentPermissionChecks?: CommentPermissionChecksResolvers<ContextType>;
|
||||
CommentReplyAuthorCollection?: CommentReplyAuthorCollectionResolvers<ContextType>;
|
||||
CommentThreadActivityMessage?: CommentThreadActivityMessageResolvers<ContextType>;
|
||||
Commit?: CommitResolvers<ContextType>;
|
||||
|
||||
@@ -81,7 +81,7 @@ export = {
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
|
||||
const canCreate = await context.authPolicies.project.canCreateModel({
|
||||
const canCreate = await context.authPolicies.project.model.canCreate({
|
||||
userId: context.userId,
|
||||
projectId: args.branch.streamId
|
||||
})
|
||||
|
||||
@@ -297,7 +297,7 @@ export = {
|
||||
},
|
||||
ModelMutations: {
|
||||
async create(_parent, args, ctx) {
|
||||
const canCreate = await ctx.authPolicies.project.canCreateModel({
|
||||
const canCreate = await ctx.authPolicies.project.model.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateModel: async (parent, _args, ctx) => {
|
||||
const canCreateModel = await ctx.authPolicies.project.canCreateModel({
|
||||
const canCreateModel = await ctx.authPolicies.project.model.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
|
||||
@@ -611,6 +611,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -744,6 +745,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -2540,6 +2546,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces'
|
||||
import { BadRequestError, BaseError, ForbiddenError } from '@/modules/shared/errors'
|
||||
import {
|
||||
BadRequestError,
|
||||
BaseError,
|
||||
ForbiddenError,
|
||||
NotFoundError
|
||||
} from '@/modules/shared/errors'
|
||||
import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors'
|
||||
import { Authz, ensureError, throwUncoveredError } from '@speckle/shared'
|
||||
import { VError } from 'verror'
|
||||
@@ -34,6 +39,7 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.WorkspaceLimitsReachedError.code:
|
||||
case Authz.WorkspaceNoEditorSeatError.code:
|
||||
case Authz.WorkspaceProjectMoveInvalidError.code:
|
||||
case Authz.CommentNoAccessError.code:
|
||||
return new ForbiddenError(e.message)
|
||||
case Authz.WorkspaceSsoSessionNoAccessError.code:
|
||||
throw new SsoSessionMissingOrExpiredError(e.message, {
|
||||
@@ -48,6 +54,8 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
return new WorkspacesModuleDisabledError()
|
||||
case Authz.ProjectLastOwnerError.code:
|
||||
return new BadRequestError(e.message)
|
||||
case Authz.CommentNotFoundError.code:
|
||||
return new NotFoundError(e.message)
|
||||
default:
|
||||
throwUncoveredError(e)
|
||||
}
|
||||
|
||||
@@ -612,6 +612,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -745,6 +746,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -2541,6 +2547,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
|
||||
@@ -127,6 +127,16 @@ export const ServerNoSessionError = defineAuthError({
|
||||
message: 'You are not logged in to this server'
|
||||
})
|
||||
|
||||
export const CommentNotFoundError = defineAuthError({
|
||||
code: 'CommentNotFound',
|
||||
message: 'Comment not found'
|
||||
})
|
||||
|
||||
export const CommentNoAccessError = defineAuthError({
|
||||
code: 'CommentNoAccess',
|
||||
message: 'You do not have access to this comment'
|
||||
})
|
||||
|
||||
// Resolve all exported error types
|
||||
export type AllAuthErrors = ValueOf<{
|
||||
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Comment } from './types.js'
|
||||
|
||||
export type GetComment = (args: {
|
||||
commentId: string
|
||||
projectId: string
|
||||
}) => Promise<Comment | null>
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Comment = {
|
||||
id: string
|
||||
authorId: string
|
||||
projectId: string
|
||||
}
|
||||
@@ -3,4 +3,5 @@ export type MaybeProjectContext = { projectId?: string }
|
||||
export type UserContext = { userId: string }
|
||||
export type MaybeUserContext = { userId?: string }
|
||||
export type WorkspaceContext = { workspaceId: string }
|
||||
export type CommentContext = { commentId: string }
|
||||
export type MaybeWorkspaceContext = { workspaceId?: string }
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
GetWorkspaceSsoProvider,
|
||||
GetWorkspaceSsoSession
|
||||
} from './workspaces/operations.js'
|
||||
import { GetComment } from './comments/operations.js'
|
||||
|
||||
// utility type that ensures all properties functions that return promises
|
||||
type PromiseAll<T> = {
|
||||
@@ -63,7 +64,8 @@ export const AuthCheckContextLoaderKeys = <const>{
|
||||
getWorkspaceLimits: 'getWorkspaceLimits',
|
||||
getWorkspaceSsoProvider: 'getWorkspaceSsoProvider',
|
||||
getWorkspaceSsoSession: 'getWorkspaceSsoSession',
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled'
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled',
|
||||
getComment: 'getComment'
|
||||
}
|
||||
export const Loaders = AuthCheckContextLoaderKeys // shorter alias
|
||||
/* v8 ignore end */
|
||||
@@ -87,6 +89,7 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
|
||||
getWorkspaceModelCount: GetWorkspaceModelCount
|
||||
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
|
||||
getWorkspaceSsoSession: GetWorkspaceSsoSession
|
||||
getComment: GetComment
|
||||
}>
|
||||
|
||||
export type AuthCheckContextLoaders<
|
||||
|
||||
@@ -6,6 +6,7 @@ export type Project = {
|
||||
isDiscoverable: boolean
|
||||
isPublic: boolean
|
||||
workspaceId: string | null
|
||||
allowPublicComments: boolean
|
||||
}
|
||||
|
||||
export type ProjectVisibility = 'public' | 'linkShareable' | 'private'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
checkIfPubliclyReadableProjectFragment,
|
||||
ensureImplicitProjectMemberWithReadAccessFragment,
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment,
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from './projects.js'
|
||||
@@ -16,11 +17,12 @@ import {
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { OverridesOf } from '../../tests/helpers/types.js'
|
||||
import { getProjectFake } from '../../tests/fakes.js'
|
||||
|
||||
describe('ensureMinimumProjectRoleFragment', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof ensureMinimumProjectRoleFragment>) =>
|
||||
ensureMinimumProjectRoleFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -40,7 +42,7 @@ describe('ensureMinimumProjectRoleFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureMinimumProjectRoleFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -160,7 +162,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
overrides?: OverridesOf<typeof checkIfPubliclyReadableProjectFragment>
|
||||
) =>
|
||||
checkIfPubliclyReadableProjectFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -192,7 +194,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
|
||||
it('returns true if project is public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -209,7 +211,7 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
|
||||
it('returns false if project is not public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -228,7 +230,7 @@ describe('ensureProjectWorkspaceAccessFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureProjectWorkspaceAccessFragment>
|
||||
) =>
|
||||
ensureProjectWorkspaceAccessFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -246,7 +248,7 @@ describe('ensureProjectWorkspaceAccessFragment', () => {
|
||||
overrides?: OverridesOf<typeof ensureProjectWorkspaceAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -384,7 +386,7 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithReadAccessFragment>
|
||||
) =>
|
||||
ensureImplicitProjectMemberWithReadAccessFragment({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -405,7 +407,7 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithReadAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
@@ -599,3 +601,275 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureImplicitProjectMemberWithWriteAccessFragment', () => {
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithWriteAccessFragment>
|
||||
) =>
|
||||
ensureImplicitProjectMemberWithWriteAccessFragment({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof ensureImplicitProjectMemberWithWriteAccessFragment>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'projectId',
|
||||
workspaceId: 'workspaceId',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspaceId',
|
||||
slug: 'workspaceSlug'
|
||||
}),
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'ssoProviderId'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
providerId: 'ssoSessionId',
|
||||
userId: 'userId',
|
||||
validUntil: new Date(Date.now() + 1000 * 60 * 60 * 24)
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds with explicit member role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if user not specified', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user is guest and asking for owner', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => Roles.Server.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o role even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails without project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds with reviewer role, if permitted', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails with a too restrictive project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds with implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if workspace role not permissive enough', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/ low workspace role if allowed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId',
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o sso session, if workspace guest w/ explicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o sso session, if not configured', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if no sso session, but required', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if sso session expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
providerId: 'ssoSessionId',
|
||||
userId: 'userId',
|
||||
validUntil: new Date(Date.now() - 1000 * 60 * 60 * 24)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'userId',
|
||||
projectId: 'projectId'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,15 +187,6 @@ export const ensureImplicitProjectMemberWithReadAccessFragment: AuthPolicyEnsure
|
||||
}
|
||||
if (isAdminOverrideEnabled.value) return ok()
|
||||
|
||||
// No god mode, ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
// And ensure (implicit/explicit) project role
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
@@ -206,5 +197,82 @@ export const ensureImplicitProjectMemberWithReadAccessFragment: AuthPolicyEnsure
|
||||
return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
// No god mode, ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user has implicit/explicit project membership and write access
|
||||
*/
|
||||
export const ensureImplicitProjectMemberWithWriteAccessFragment: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext &
|
||||
ProjectContext & {
|
||||
/**
|
||||
* By default assumes Contributor+ for any writes, but some operations
|
||||
* may allow for lower roles (e.g. comments)
|
||||
*/
|
||||
role?: StreamRoles
|
||||
},
|
||||
InstanceType<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, role }) => {
|
||||
const requiredProjectRole = role || Roles.Stream.Contributor
|
||||
const requiredServerRole =
|
||||
requiredProjectRole === Roles.Stream.Owner
|
||||
? Roles.Server.User
|
||||
: Roles.Server.Guest
|
||||
|
||||
// Ensure user is authed
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId,
|
||||
role: requiredServerRole
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// And ensure (implicit/explicit) project role
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: requiredProjectRole
|
||||
})
|
||||
if (ensuredProjectRole.isErr) {
|
||||
return err(ensuredProjectRole.error)
|
||||
}
|
||||
|
||||
// Ensure workspace access
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AllAuthCheckContextLoaders } from '../domain/loaders.js'
|
||||
import { canCreateWorkspaceProjectPolicy } from './workspace/canCreateWorkspaceProject.js'
|
||||
import { canReadProjectPolicy } from './project/canReadProject.js'
|
||||
import { canCreateModelPolicy } from './project/canCreateModel.js'
|
||||
import { canCreateModelPolicy } from './project/model/canCreateModel.js'
|
||||
import { canMoveToWorkspacePolicy } from './project/canMoveToWorkspace.js'
|
||||
import { canCreatePersonalProjectPolicy } from './project/canCreatePersonal.js'
|
||||
import { canUpdateProjectPolicy } from './project/canUpdate.js'
|
||||
@@ -9,11 +9,23 @@ import { canReadProjectSettingsPolicy } from './project/canReadSettings.js'
|
||||
import { canReadProjectWebhooksPolicy } from './project/canReadWebhooks.js'
|
||||
import { canUpdateProjectAllowPublicCommentsPolicy } from './project/canUpdateAllowPublicComments.js'
|
||||
import { canLeaveProjectPolicy } from './project/canLeave.js'
|
||||
import { canBroadcastProjectActivityPolicy } from './project/canBroadcastActivity.js'
|
||||
import { canCreateProjectCommentPolicy } from './project/comment/canCreate.js'
|
||||
import { canArchiveProjectCommentPolicy } from './project/comment/canArchive.js'
|
||||
import { canEditProjectCommentPolicy } from './project/comment/canEdit.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
model: {
|
||||
canCreate: canCreateModelPolicy(loaders)
|
||||
},
|
||||
comment: {
|
||||
canCreate: canCreateProjectCommentPolicy(loaders),
|
||||
canArchive: canArchiveProjectCommentPolicy(loaders),
|
||||
canEdit: canEditProjectCommentPolicy(loaders)
|
||||
},
|
||||
canBroadcastActivity: canBroadcastProjectActivityPolicy(loaders),
|
||||
canRead: canReadProjectPolicy(loaders),
|
||||
canCreateModel: canCreateModelPolicy(loaders),
|
||||
canMoveToWorkspace: canMoveToWorkspacePolicy(loaders),
|
||||
canCreatePersonal: canCreatePersonalProjectPolicy(loaders),
|
||||
canUpdate: canUpdateProjectPolicy(loaders),
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../tests/helpers/types.js'
|
||||
import { canBroadcastProjectActivityPolicy } from './canBroadcastActivity.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
|
||||
describe('canBroadcastProjectActivityPolicy', () => {
|
||||
const buildSUT = (
|
||||
overrides?: OverridesOf<typeof canBroadcastProjectActivityPolicy>
|
||||
) =>
|
||||
canBroadcastProjectActivityPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getAdminOverrideEnabled: async () => false,
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canBroadcastProjectActivityPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user has no project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/ admin override, even w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getAdminOverrideEnabled: async () => true,
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
describe('with workspace project', async () => {
|
||||
it('succeeds w/ workspace role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project & workspace role if public', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => null,
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: true
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if user has no workspace role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o sso, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o sso, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if sso expired', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
checkIfPubliclyReadableProjectFragment,
|
||||
ensureImplicitProjectMemberWithReadAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
|
||||
export const canBroadcastProjectActivityPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole
|
||||
| typeof Loaders.getAdminOverrideEnabled,
|
||||
MaybeUserContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Ensure logged in
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// If publicly readable - any authed user can broadcast
|
||||
const isPubliclyReadable = await checkIfPubliclyReadableProjectFragment(loaders)({
|
||||
projectId
|
||||
})
|
||||
if (isPubliclyReadable.isErr) {
|
||||
return err(isPubliclyReadable.error)
|
||||
}
|
||||
if (isPubliclyReadable.value) return ok()
|
||||
|
||||
// Not public. Ensure user has at least implicit membership & read access
|
||||
const hasReadAccess = await ensureImplicitProjectMemberWithReadAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (hasReadAccess.isErr) {
|
||||
return err(hasReadAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canLeaveProjectPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canLeaveProjectPolicy>) =>
|
||||
canLeaveProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -33,7 +34,7 @@ describe('canLeaveProjectPolicy', () => {
|
||||
|
||||
const buildWorkspaceSUT = (overrides?: OverridesOf<typeof canLeaveProjectPolicy>) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canReadProjectSettingsPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canReadProjectSettingsPolicy>) =>
|
||||
canReadProjectSettingsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -35,7 +36,7 @@ describe('canReadProjectSettingsPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canReadProjectSettingsPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { canReadProjectWebhooksPolicy } from './canReadWebhooks.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canReadProjectWebhooksPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canReadProjectWebhooksPolicy>) =>
|
||||
canReadProjectWebhooksPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -35,7 +36,7 @@ describe('canReadProjectWebhooksPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canReadProjectWebhooksPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
// Default deps allow test to succeed, this makes it so that we need to override less of them
|
||||
const buildSUT = (overrides?: Partial<Parameters<typeof canUpdateProjectPolicy>[0]>) =>
|
||||
canUpdateProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -33,7 +34,7 @@ const buildWorkspaceSUT = (
|
||||
overrides?: Partial<Parameters<typeof canUpdateProjectPolicy>[0]>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -2,11 +2,7 @@ import { err, ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import {
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../fragments/projects.js'
|
||||
import { Loaders } from '../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
@@ -38,29 +34,16 @@ export const canUpdateProjectPolicy: AuthPolicy<
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
// Ensure proper project owner level write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
role: Roles.Server.User
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
const ensuredWorkspaceAccess = await ensureProjectWorkspaceAccessFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId
|
||||
})
|
||||
if (ensuredWorkspaceAccess.isErr) {
|
||||
return err(ensuredWorkspaceAccess.error)
|
||||
}
|
||||
|
||||
const ensuredProjectRole = await ensureMinimumProjectRoleFragment(loaders)({
|
||||
userId: userId!,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredProjectRole.isErr) {
|
||||
return err(ensuredProjectRole.error)
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
|
||||
@@ -4,6 +4,7 @@ import { canUpdateProjectAllowPublicCommentsPolicy } from './canUpdateAllowPubli
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { ProjectNoAccessError } from '../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
|
||||
describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
const buildSUT = (
|
||||
@@ -11,7 +12,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
) =>
|
||||
canUpdateProjectAllowPublicCommentsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
@@ -39,7 +40,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
|
||||
it('succeeds if discoverable project', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: true,
|
||||
@@ -72,7 +73,7 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
|
||||
it('fails if project is neither public nor discoverable', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => ({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canArchiveProjectCommentPolicy } from './canArchive.js'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getCommentFake, getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
describe('canArchiveProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canArchiveProjectCommentPolicy>) =>
|
||||
canArchiveProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canArchiveProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('can archive own comment', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive own comment w/o project roles", async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can archive others' comments if owner", async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive others' comments if not owner", async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Contributor
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not defined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if comment not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'aaaaaaaaaaaa',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('can archive own comment', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can archive others' comments if workspace admin", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can archive others' comments as admin w/o sso, if not needed", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can arhive others' comments if explicit project owner", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it("can't archive others' comments if not owner", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can't archive others' comments as owner, if no sso session", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it("can't archive others' comments as owner, if sso session expired", async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
authorId: 'other-user-id',
|
||||
projectId: 'project-id'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin,
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(0)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
commentId: 'comment-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
CommentContext,
|
||||
MaybeUserContext,
|
||||
ProjectContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
|
||||
export const canArchiveProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getComment
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & CommentContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof CommentNotFoundError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, commentId, projectId }) => {
|
||||
// Includes canCreate check (checks general comment write access,
|
||||
// cause just owning a comment is not enough, if you've been banned from it)
|
||||
const canCreate = await canCreateProjectCommentPolicy(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (canCreate.isErr) {
|
||||
return err(canCreate.error)
|
||||
}
|
||||
|
||||
// Check that comment exists
|
||||
const comment = await loaders.getComment({ commentId, projectId })
|
||||
if (!comment) return err(new CommentNotFoundError())
|
||||
|
||||
// If user is owner, no extra checks necessary
|
||||
if (comment.authorId === userId) return ok()
|
||||
|
||||
// Otherwise Ensure proper project owner level write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
|
||||
describe('canCreateProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canCreateProjectCommentPolicy>) =>
|
||||
canCreateProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canCreateProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public and public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role if public, but no public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: false
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds w/ implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role, if guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o project role, if only guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if guest w/ explicit role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o session, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/ expired session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../../fragments/server.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { ensureImplicitProjectMemberWithWriteAccessFragment } from '../../../fragments/projects.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
|
||||
export const canCreateProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
// Ensure server access
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
userId
|
||||
})
|
||||
if (ensuredServerRole.isErr) {
|
||||
return err(ensuredServerRole.error)
|
||||
}
|
||||
|
||||
// Check if public commenting enabled
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project) return err(new ProjectNotFoundError())
|
||||
const allowPublicCommenting =
|
||||
(project.isPublic || project.isDiscoverable) && project.allowPublicComments
|
||||
if (allowPublicCommenting) return ok()
|
||||
|
||||
// Not public, ensure proper project write access
|
||||
const ensuredWriteAccess = await ensureImplicitProjectMemberWithWriteAccessFragment(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
projectId,
|
||||
role: Roles.Stream.Reviewer
|
||||
})
|
||||
if (ensuredWriteAccess.isErr) {
|
||||
return err(ensuredWriteAccess.error)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../../tests/helpers/types.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { getCommentFake, getProjectFake } from '../../../../tests/fakes.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import {
|
||||
CommentNoAccessError,
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { canEditProjectCommentPolicy } from './canEdit.js'
|
||||
|
||||
describe('canEditProjectCommentPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canEditProjectCommentPolicy>) =>
|
||||
canEditProjectCommentPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'user-id'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
const buildWorkspaceSUT = (
|
||||
overrides?: OverridesOf<typeof canEditProjectCommentPolicy>
|
||||
) =>
|
||||
buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: 'workspace-id',
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
allowPublicComments: false
|
||||
}),
|
||||
getProjectRole: async () => null,
|
||||
getWorkspace: async () => ({
|
||||
id: 'workspace-id',
|
||||
slug: 'workspace-slug'
|
||||
}),
|
||||
getWorkspaceRole: async () => Roles.Workspace.Member,
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
}),
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date()
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o project role if public and public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: true
|
||||
}),
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if not the author, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => Roles.Stream.Owner
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if comment not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role if public, but no public comments allowed', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
isDiscoverable: false,
|
||||
isPublic: true,
|
||||
allowPublicComments: false
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user undefined', async () => {
|
||||
const sut = buildSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: undefined,
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoSessionError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if user not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getServerRole: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ServerNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if project not found', async () => {
|
||||
const sut = buildSUT({
|
||||
getProject: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNotFoundError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getProjectRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
describe('with workspace project', () => {
|
||||
it('succeeds w/ implicit project role', async () => {
|
||||
const sut = buildWorkspaceSUT()
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/ explicit project role, if guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if not the author, even if admin', async () => {
|
||||
const sut = buildSUT({
|
||||
getComment: getCommentFake({
|
||||
id: 'comment-id',
|
||||
projectId: 'project-id',
|
||||
authorId: 'other-user-id'
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.Admin,
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Admin
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: CommentNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/o project role, if only guest', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => null,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if guest w/ explicit role', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getProjectRole: async () => Roles.Stream.Reviewer,
|
||||
getWorkspaceRole: async () => Roles.Workspace.Guest,
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('succeeds w/o session, if not needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails w/o session, if needed', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => null
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('fails w/ expired session', async () => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspaceSsoSession: async () => ({
|
||||
userId: 'user-id',
|
||||
providerId: 'provider-id',
|
||||
validUntil: new Date(Date.now() - 1000)
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id',
|
||||
commentId: 'comment-id'
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspaceSsoSessionNoAccessError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import {
|
||||
CommentContext,
|
||||
MaybeUserContext,
|
||||
ProjectContext
|
||||
} from '../../../domain/context.js'
|
||||
import { Loaders } from '../../../domain/loaders.js'
|
||||
import {
|
||||
CommentNoAccessError,
|
||||
CommentNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionNoAccessError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { canCreateProjectCommentPolicy } from './canCreate.js'
|
||||
|
||||
export const canEditProjectCommentPolicy: AuthPolicy<
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getComment
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession
|
||||
| typeof Loaders.getProjectRole,
|
||||
MaybeUserContext & CommentContext & ProjectContext,
|
||||
InstanceType<
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof WorkspaceSsoSessionNoAccessError
|
||||
| typeof CommentNotFoundError
|
||||
| typeof CommentNoAccessError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, commentId, projectId }) => {
|
||||
// Includes canCreate check
|
||||
const canCreate = await canCreateProjectCommentPolicy(loaders)({
|
||||
userId,
|
||||
projectId
|
||||
})
|
||||
if (canCreate.isErr) {
|
||||
return err(canCreate.error)
|
||||
}
|
||||
|
||||
// Check that comment exists
|
||||
const comment = await loaders.getComment({ commentId, projectId })
|
||||
if (!comment) return err(new CommentNotFoundError())
|
||||
|
||||
// Disallow if user is not the author
|
||||
if (comment.authorId !== userId) {
|
||||
return err(
|
||||
new CommentNoAccessError('You do not have access to edit this comment')
|
||||
)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
+13
-14
@@ -1,32 +1,31 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { assert, describe, expect, it } from 'vitest'
|
||||
import { canCreateModelPolicy } from './canCreateModel.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { Workspace } from '../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../workspaces/index.js'
|
||||
import { Project } from '../../domain/projects/types.js'
|
||||
import { parseFeatureFlags } from '../../../../environment/index.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { Workspace } from '../../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../../workspaces/index.js'
|
||||
import { Project } from '../../../domain/projects/types.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceLimitsReachedError,
|
||||
WorkspaceNoAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../../../tests/fakes.js'
|
||||
|
||||
const buildCanCreateModelPolicy = (
|
||||
overrides?: Partial<Parameters<typeof canCreateModelPolicy>[0]>
|
||||
) =>
|
||||
canCreateModelPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => {
|
||||
return {
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
}
|
||||
},
|
||||
getProject: getProjectFake({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
}),
|
||||
getProjectRole: async () => {
|
||||
return Roles.Stream.Contributor
|
||||
},
|
||||
+8
-8
@@ -8,17 +8,17 @@ import {
|
||||
ServerNoSessionError,
|
||||
ServerNoAccessError,
|
||||
WorkspaceReadOnlyError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { isWorkspacePlanStatusReadOnly } from '../../../workspaces/index.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../../../domain/loaders.js'
|
||||
import { AuthPolicy } from '../../../domain/policies.js'
|
||||
import { Roles } from '../../../../core/constants.js'
|
||||
import { isWorkspacePlanStatusReadOnly } from '../../../../workspaces/index.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../../fragments/server.js'
|
||||
import {
|
||||
ensureMinimumProjectRoleFragment,
|
||||
ensureProjectWorkspaceAccessFragment
|
||||
} from '../../fragments/projects.js'
|
||||
} from '../../../fragments/projects.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
@@ -1,5 +1,6 @@
|
||||
import { merge } from 'lodash'
|
||||
import { Project } from '../authz/domain/projects/types.js'
|
||||
import { Comment } from '../authz/domain/comments/types.js'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export const fakeGetFactory =
|
||||
@@ -16,5 +17,12 @@ export const getProjectFake = fakeGetFactory<Project>({
|
||||
id: nanoid(10),
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: null
|
||||
workspaceId: null,
|
||||
allowPublicComments: false
|
||||
})
|
||||
|
||||
export const getCommentFake = fakeGetFactory<Comment>({
|
||||
id: nanoid(10),
|
||||
authorId: nanoid(10),
|
||||
projectId: nanoid(10)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user