Merge main
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isDashboardOwner } from './dashboards.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
describe('dashboard checks', () => {
|
||||
describe('isDashboardOwner returns a function, that', () => {
|
||||
it('returns false for dashboard not found', async () => {
|
||||
const result = await isDashboardOwner({
|
||||
getDashboard: async () => null
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
dashboardId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).to.equal(false)
|
||||
})
|
||||
it('returns false if user not owner', async () => {
|
||||
const result = await isDashboardOwner({
|
||||
getDashboard: async () => ({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
ownerId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: '',
|
||||
projectIds: []
|
||||
})
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
dashboardId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).to.equal(false)
|
||||
})
|
||||
it('returns true if user is owner', async () => {
|
||||
const userId = cryptoRandomString({ length: 9 })
|
||||
const result = await isDashboardOwner({
|
||||
getDashboard: async () => ({
|
||||
id: cryptoRandomString({ length: 9 }),
|
||||
ownerId: userId,
|
||||
workspaceId: '',
|
||||
projectIds: []
|
||||
})
|
||||
})({
|
||||
userId,
|
||||
dashboardId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DashboardContext, UserContext } from '../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
import { AuthPolicyCheck } from '../domain/policies.js'
|
||||
|
||||
export const isDashboardOwner: AuthPolicyCheck<
|
||||
typeof AuthCheckContextLoaderKeys.getDashboard,
|
||||
UserContext & DashboardContext
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return false
|
||||
return dashboard.ownerId === userId
|
||||
}
|
||||
@@ -221,6 +221,31 @@ export const UngroupedSavedViewGroupLockError = defineAuthError({
|
||||
message: 'The default/ungrouped group cannot be modified.'
|
||||
})
|
||||
|
||||
export const DashboardsNotEnabledError = defineAuthError({
|
||||
code: 'DashboardsNotEnabled',
|
||||
message: 'Dashboards are not enabled for this server or workspaces.'
|
||||
})
|
||||
|
||||
export const DashboardNotFoundError = defineAuthError({
|
||||
code: 'DashboardNotFound',
|
||||
message: 'Dashboard not found'
|
||||
})
|
||||
|
||||
export const DashboardProjectsNotEnoughPermissionsError = defineAuthError<
|
||||
'DashboardProjectsNotEnoughPermissions',
|
||||
{
|
||||
projectIds: string[]
|
||||
}
|
||||
>({
|
||||
code: 'DashboardProjectsNotEnoughPermissions',
|
||||
message: 'You do not have sufficient access to some projects in this workspace.'
|
||||
})
|
||||
|
||||
export const DashboardNotOwnerError = defineAuthError({
|
||||
code: 'DashboardNotOwner',
|
||||
message: 'You must be a dashboard owner to perform this action'
|
||||
})
|
||||
|
||||
// Resolve all exported error types
|
||||
export type AllAuthErrors = ValueOf<{
|
||||
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
|
||||
|
||||
@@ -7,6 +7,8 @@ export type MaybeUserContext = { userId?: string }
|
||||
export type WorkspaceContext = { workspaceId: string }
|
||||
export type MaybeWorkspaceContext = { workspaceId?: string }
|
||||
|
||||
export type DashboardContext = { dashboardId: string }
|
||||
|
||||
export type CommentContext = { commentId: string }
|
||||
|
||||
export type ModelContext = { modelId: string }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Dashboard } from './types.js'
|
||||
|
||||
export type GetDashboard = (args: { dashboardId: string }) => Promise<Dashboard | null>
|
||||
@@ -0,0 +1,6 @@
|
||||
export type Dashboard = {
|
||||
id: string
|
||||
ownerId: string
|
||||
workspaceId: string
|
||||
projectIds: string[]
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { GetModel } from './models/operations.js'
|
||||
import { GetVersion } from './versions/operations.js'
|
||||
import { GetAutomateFunction } from './automate/operations.js'
|
||||
import { GetSavedView, GetSavedViewGroup } from './savedViews/operations.js'
|
||||
import { GetDashboard } from './dashboards/operations.js'
|
||||
|
||||
// utility type that ensures all properties functions that return promises
|
||||
type PromiseAll<T> = {
|
||||
@@ -58,6 +59,7 @@ type AuthContextLoaderMappingDefinition<
|
||||
export const AuthCheckContextLoaderKeys = StringEnum([
|
||||
'getEnv',
|
||||
'getAutomateFunction',
|
||||
'getDashboard',
|
||||
'getProject',
|
||||
'getProjectRoleCounts',
|
||||
'getProjectRole',
|
||||
@@ -92,6 +94,7 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
|
||||
getEnv: GetEnv
|
||||
getAdminOverrideEnabled: GetAdminOverrideEnabled
|
||||
getAutomateFunction: GetAutomateFunction
|
||||
getDashboard: GetDashboard
|
||||
getProject: GetProject
|
||||
getProjectRole: GetProjectRole
|
||||
getProjectRoleCounts: GetProjectRoleCounts
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
DashboardNotFoundError,
|
||||
DashboardProjectsNotEnoughPermissionsError,
|
||||
DashboardsNotEnabledError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { Loaders } from '../domain/loaders.js'
|
||||
import { AuthPolicyEnsureFragment } from '../domain/policies.js'
|
||||
import { DashboardContext, UserContext, WorkspaceContext } from '../domain/context.js'
|
||||
import {
|
||||
isWorkspaceFeatureFlagOn,
|
||||
WorkspaceFeatureFlags
|
||||
} from '../../workspaces/index.js'
|
||||
import { ensureMinimumProjectRoleFragment } from './projects.js'
|
||||
|
||||
export const ensureDashboardsEnabledFragment: AuthPolicyEnsureFragment<
|
||||
typeof Loaders.getEnv,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
{},
|
||||
InstanceType<typeof DashboardsNotEnabledError>
|
||||
> = (loaders) => async () => {
|
||||
const env = await loaders.getEnv()
|
||||
if (!env.FF_DASHBOARDS_MODULE_ENABLED) return err(new DashboardsNotEnabledError())
|
||||
return ok()
|
||||
}
|
||||
|
||||
export const ensureWorkspaceDashboardsFeatureAccessFragment: AuthPolicyEnsureFragment<
|
||||
typeof Loaders.getWorkspacePlan,
|
||||
WorkspaceContext,
|
||||
InstanceType<typeof WorkspacePlanNoFeatureAccessError>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ workspaceId }) => {
|
||||
const plan = await loaders.getWorkspacePlan({ workspaceId })
|
||||
if (!plan) return err(new WorkspacePlanNoFeatureAccessError())
|
||||
|
||||
const isFlagOn = isWorkspaceFeatureFlagOn({
|
||||
workspaceFeatureFlags: plan.featureFlags,
|
||||
feature: WorkspaceFeatureFlags.dashboards
|
||||
})
|
||||
if (!isFlagOn) return err(new WorkspacePlanNoFeatureAccessError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
export const ensureDashboardProjectsReadAccess: AuthPolicyEnsureFragment<
|
||||
| typeof Loaders.getDashboard
|
||||
| typeof Loaders.getProjectRole
|
||||
| typeof Loaders.getProject
|
||||
| typeof Loaders.getServerRole
|
||||
| typeof Loaders.getEnv
|
||||
| typeof Loaders.getWorkspaceRole
|
||||
| typeof Loaders.getWorkspace
|
||||
| typeof Loaders.getWorkspaceSsoProvider
|
||||
| typeof Loaders.getWorkspaceSsoSession,
|
||||
DashboardContext & UserContext,
|
||||
InstanceType<
|
||||
typeof DashboardNotFoundError | typeof DashboardProjectsNotEnoughPermissionsError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return err(new DashboardNotFoundError())
|
||||
|
||||
const allProjectResults: [
|
||||
string,
|
||||
Awaited<ReturnType<ReturnType<typeof ensureMinimumProjectRoleFragment>>>
|
||||
][] = await Promise.all(
|
||||
dashboard.projectIds.map(async (projectId) => {
|
||||
return [
|
||||
projectId,
|
||||
await ensureMinimumProjectRoleFragment(loaders)({ projectId, userId })
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
const projectAccessErrors = allProjectResults.filter(([, e]) => e.isErr)
|
||||
|
||||
return projectAccessErrors.length
|
||||
? err(
|
||||
new DashboardProjectsNotEnoughPermissionsError({
|
||||
payload: {
|
||||
projectIds: projectAccessErrors.map(([projectId]) => projectId)
|
||||
}
|
||||
})
|
||||
)
|
||||
: ok()
|
||||
}
|
||||
@@ -977,7 +977,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'project-id',
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
@@ -990,7 +990,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'project-id',
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -1008,7 +1008,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'project-id',
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -1026,7 +1026,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'project-id',
|
||||
feature: WorkspacePlanFeatures.SavedViews,
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
allowUnworkspaced: true
|
||||
})
|
||||
|
||||
@@ -1044,7 +1044,7 @@ describe('ensureCanUseProjectWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
projectId: 'project-id',
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
|
||||
@@ -20,8 +20,7 @@ import {
|
||||
SavedViewNoAccessError,
|
||||
SavedViewNotFoundError,
|
||||
UngroupedSavedViewGroupLockError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
WorkspaceNoAccessError
|
||||
} from '../domain/authErrors.js'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
@@ -191,11 +190,17 @@ describe('ensureCanAccessSavedViewFragment', () => {
|
||||
)
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when workspace plan is too cheap (%s)',
|
||||
'succeeds to %s even on free plan',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'team'
|
||||
name: 'free'
|
||||
}),
|
||||
getSavedView: getSavedViewFake({
|
||||
id: savedViewId,
|
||||
projectId,
|
||||
visibility: SavedViewVisibility.public,
|
||||
authorId: userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -205,9 +210,7 @@ describe('ensureCanAccessSavedViewFragment', () => {
|
||||
savedViewId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -413,27 +416,6 @@ describe('ensureCanAccessSavedViewGroupFragment', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails when workspace plan is too cheap (%s)',
|
||||
async (access) => {
|
||||
const sut = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: 'team'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await sut({
|
||||
userId,
|
||||
projectId,
|
||||
savedViewGroupId,
|
||||
access
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(<const>['read', 'write'])(
|
||||
'fails if view doesnt exist (%s)',
|
||||
async (access) => {
|
||||
|
||||
@@ -109,7 +109,7 @@ export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved views."
|
||||
"Your role on this project doesn't give you permission to update views."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
@@ -122,8 +122,8 @@ export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
|
||||
new SavedViewNoAccessError({
|
||||
message:
|
||||
access === 'write'
|
||||
? 'You do not have write access for this saved view'
|
||||
: 'You do not have read access for this saved view'
|
||||
? 'You do not have permission to edit this view'
|
||||
: 'You do not have read access for this view'
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -205,7 +205,7 @@ export const ensureCanAccessSavedViewGroupFragment: AuthPolicyEnsureFragment<
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to update saved view groups."
|
||||
"Your role on this project doesn't give you permission to update view groups."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeOKResult()
|
||||
@@ -362,7 +362,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -380,7 +380,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -395,7 +395,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
@@ -413,7 +413,7 @@ describe('ensureCanUseWorkspacePlanFeatureFragment', () => {
|
||||
|
||||
const result = await sut({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
feature: WorkspacePlanFeatures.SavedViews
|
||||
feature: WorkspacePlanFeatures.HideSpeckleBranding
|
||||
})
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { DashboardContext, MaybeUserContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardNotFoundError,
|
||||
DashboardProjectsNotEnoughPermissionsError,
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardProjectsReadAccess,
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getDashboard
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSeat
|
||||
| typeof AuthCheckContextLoaderKeys.getProjectRole
|
||||
| typeof AuthCheckContextLoaderKeys.getProject
|
||||
| typeof AuthCheckContextLoaderKeys.getServerRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspace
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoProvider
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoSession
|
||||
|
||||
type PolicyArgs = MaybeUserContext & DashboardContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceNoEditorSeatError
|
||||
| typeof DashboardNotFoundError
|
||||
| typeof DashboardProjectsNotEnoughPermissionsError
|
||||
>
|
||||
|
||||
export const canCreateDashboardTokenPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return err(new DashboardNotFoundError())
|
||||
|
||||
const { workspaceId } = dashboard
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceMember = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isWorkspaceMember) return err(new WorkspaceNotEnoughPermissionsError())
|
||||
|
||||
const isWorkspaceEditorSeat = await hasEditorSeat(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId
|
||||
})
|
||||
if (!isWorkspaceEditorSeat) return err(new WorkspaceNoEditorSeatError())
|
||||
|
||||
const ensuredProjectAccess = await ensureDashboardProjectsReadAccess(loaders)({
|
||||
userId: userId!,
|
||||
dashboardId
|
||||
})
|
||||
if (ensuredProjectAccess.isErr) return err(ensuredProjectAccess.error)
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { DashboardContext, MaybeUserContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardNotFoundError,
|
||||
DashboardNotOwnerError,
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
|
||||
import { isDashboardOwner } from '../../checks/dashboards.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getDashboard
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSeat
|
||||
|
||||
type PolicyArgs = MaybeUserContext & DashboardContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof DashboardNotOwnerError
|
||||
| typeof DashboardNotFoundError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceNoEditorSeatError
|
||||
>
|
||||
|
||||
export const canDeleteDashboardPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return err(new DashboardNotFoundError())
|
||||
|
||||
const { workspaceId } = dashboard
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceEditorSeat = await hasEditorSeat(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId
|
||||
})
|
||||
if (!isWorkspaceEditorSeat) return err(new WorkspaceNoEditorSeatError())
|
||||
|
||||
const isWorkspaceAdmin = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Admin
|
||||
})
|
||||
const isOwner = await isDashboardOwner(loaders)({ userId: userId!, dashboardId })
|
||||
|
||||
if (!isWorkspaceAdmin && !isOwner) return err(new DashboardNotOwnerError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { MaybeUserContext, DashboardContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardNotFoundError,
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getDashboard
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSeat
|
||||
|
||||
type PolicyArgs = MaybeUserContext & DashboardContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof DashboardNotFoundError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceNoEditorSeatError
|
||||
>
|
||||
|
||||
export const canEditDashboardPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return err(new DashboardNotFoundError())
|
||||
|
||||
const { workspaceId } = dashboard
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceMember = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isWorkspaceMember) return err(new WorkspaceNotEnoughPermissionsError())
|
||||
|
||||
const isWorkspaceEditorSeat = await hasEditorSeat(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId
|
||||
})
|
||||
if (!isWorkspaceEditorSeat) return err(new WorkspaceNoEditorSeatError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { DashboardContext, MaybeUserContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardNotFoundError,
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getDashboard
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
|
||||
type PolicyArgs = MaybeUserContext & DashboardContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof DashboardNotFoundError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
>
|
||||
|
||||
export const canReadDashboardPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, dashboardId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const dashboard = await loaders.getDashboard({ dashboardId })
|
||||
if (!dashboard) return err(new DashboardNotFoundError())
|
||||
|
||||
const { workspaceId } = dashboard
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceMember = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isWorkspaceMember) return err(new WorkspaceNotEnoughPermissionsError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -38,6 +38,12 @@ import { canCreateSavedViewPolicy } from './project/savedViews/canCreate.js'
|
||||
import { canUpdateSavedViewPolicy } from './project/savedViews/canUpdate.js'
|
||||
import { canUpdateSavedViewGroupPolicy } from './project/savedViews/canUpdateGroup.js'
|
||||
import { canReadSavedViewPolicy } from './project/savedViews/canRead.js'
|
||||
import { canListDashboardsPolicy } from './workspace/canListDashboards.js'
|
||||
import { canDeleteDashboardPolicy } from './dashboard/canDelete.js'
|
||||
import { canCreateDashboardsPolicy } from './workspace/canCreateDashboards.js'
|
||||
import { canCreateDashboardTokenPolicy } from './dashboard/canCreateToken.js'
|
||||
import { canEditDashboardPolicy } from './dashboard/canEdit.js'
|
||||
import { canReadDashboardPolicy } from './dashboard/canRead.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
automate: {
|
||||
@@ -45,6 +51,12 @@ export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
canRegenerateToken: canEditFunctionPolicy(loaders)
|
||||
}
|
||||
},
|
||||
dashboard: {
|
||||
canCreateToken: canCreateDashboardTokenPolicy(loaders),
|
||||
canDelete: canDeleteDashboardPolicy(loaders),
|
||||
canEdit: canEditDashboardPolicy(loaders),
|
||||
canRead: canReadDashboardPolicy(loaders)
|
||||
},
|
||||
project: {
|
||||
automation: {
|
||||
canCreate: canCreateAutomationPolicy(loaders),
|
||||
@@ -99,7 +111,9 @@ export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
canReceiveWorkspaceProjectsUpdatedMessagePolicy(loaders),
|
||||
canUseWorkspacePlanFeature: canUseWorkspacePlanFeature(loaders),
|
||||
canReadMemberEmail: canReadMemberEmailPolicy(loaders),
|
||||
canCreateWorkspace: canCreateWorkspacePolicy(loaders)
|
||||
canCreateWorkspace: canCreateWorkspacePolicy(loaders),
|
||||
canCreateDashboards: canCreateDashboardsPolicy(loaders),
|
||||
canListDashboards: canListDashboardsPolicy(loaders)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@ import {
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ServerNoAccessError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspacePlanNoFeatureAccessError,
|
||||
WorkspaceReadOnlyError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { PaidWorkspacePlans } from '../../../../workspaces/index.js'
|
||||
import { WorkspacePlans } from '../../../../workspaces/index.js'
|
||||
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canCreateSavedViewPolicy>) =>
|
||||
canCreateSavedViewPolicy({
|
||||
@@ -71,7 +70,7 @@ describe('canCreateSavedViewPolicy', () => {
|
||||
id: 'workspace-id'
|
||||
}),
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: PaidWorkspacePlans.Pro
|
||||
name: WorkspacePlans.Pro
|
||||
}),
|
||||
getWorkspaceSsoProvider: async () => ({
|
||||
providerId: 'provider-id'
|
||||
@@ -153,10 +152,10 @@ describe('canCreateSavedViewPolicy', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('fails if not on pro/business plan', async () => {
|
||||
it('succeeds even on free plan', async () => {
|
||||
const canCreate = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: PaidWorkspacePlans.Team
|
||||
name: WorkspacePlans.Free
|
||||
})
|
||||
})
|
||||
|
||||
@@ -164,15 +163,13 @@ describe('canCreateSavedViewPolicy', () => {
|
||||
userId: 'user-id',
|
||||
projectId: 'project-id'
|
||||
})
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: WorkspacePlanNoFeatureAccessError.code
|
||||
})
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
it('fails if workspace readonly', async () => {
|
||||
const canCreate = buildWorkspaceSUT({
|
||||
getWorkspacePlan: getWorkspacePlanFake({
|
||||
name: PaidWorkspacePlans.Pro,
|
||||
name: WorkspacePlans.Pro,
|
||||
status: 'canceled'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@ export const canCreateSavedViewPolicy: AuthPolicy<
|
||||
return err(
|
||||
new ProjectNotEnoughPermissionsError({
|
||||
message:
|
||||
"Your role on this project doesn't give you permission to create saved views. You need the Can edit or Project owner role."
|
||||
"Your role on this project doesn't give you permission to save views. You need the Can edit or Project owner role."
|
||||
})
|
||||
)
|
||||
return err(ensuredWriteAccess.error)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { MaybeUserContext, WorkspaceContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNoEditorSeatError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { hasEditorSeat } from '../../checks/workspaceSeat.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSeat
|
||||
|
||||
type PolicyArgs = MaybeUserContext & WorkspaceContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
| typeof WorkspaceNoEditorSeatError
|
||||
>
|
||||
|
||||
export const canCreateDashboardsPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceMember = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isWorkspaceMember) return err(new WorkspaceNotEnoughPermissionsError())
|
||||
|
||||
const isWorkspaceEditorSeat = await hasEditorSeat(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId
|
||||
})
|
||||
if (!isWorkspaceEditorSeat) return err(new WorkspaceNoEditorSeatError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../../domain/loaders.js'
|
||||
import { MaybeUserContext, WorkspaceContext } from '../../domain/context.js'
|
||||
import {
|
||||
DashboardsNotEnabledError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
WorkspacePlanNoFeatureAccessError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { AuthPolicy } from '../../domain/policies.js'
|
||||
import {
|
||||
ensureDashboardsEnabledFragment,
|
||||
ensureWorkspaceDashboardsFeatureAccessFragment
|
||||
} from '../../fragments/dashboards.js'
|
||||
import { hasMinimumWorkspaceRole } from '../../checks/workspaceRole.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
|
||||
type PolicyLoaderKeys =
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspacePlan
|
||||
|
||||
type PolicyArgs = MaybeUserContext & WorkspaceContext
|
||||
|
||||
type PolicyErrors = InstanceType<
|
||||
| typeof DashboardsNotEnabledError
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof WorkspacePlanNoFeatureAccessError
|
||||
>
|
||||
|
||||
export const canListDashboardsPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const isDashboardsEnabled = await ensureDashboardsEnabledFragment(loaders)({})
|
||||
if (isDashboardsEnabled.isErr) return err(isDashboardsEnabled.error)
|
||||
|
||||
const ensuredFeatureAccess = await ensureWorkspaceDashboardsFeatureAccessFragment(
|
||||
loaders
|
||||
)({ workspaceId })
|
||||
if (ensuredFeatureAccess.isErr) return err(ensuredFeatureAccess.error)
|
||||
|
||||
const isWorkspaceMember = await hasMinimumWorkspaceRole(loaders)({
|
||||
userId: userId!,
|
||||
workspaceId,
|
||||
role: Roles.Workspace.Member
|
||||
})
|
||||
if (!isWorkspaceMember) return err(new WorkspaceNotEnoughPermissionsError())
|
||||
|
||||
return ok()
|
||||
}
|
||||
@@ -186,3 +186,15 @@ export const StringEnum = <T extends string>(args: T[]) => {
|
||||
export type StringEnumValues<T extends Record<string, string>> = {
|
||||
[K in keyof T]: T[K] extends string ? T[K] : never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Get first non-undefined/null value, or undefined if none found
|
||||
*/
|
||||
export const firstDefinedValue = <T>(
|
||||
...args: (T | undefined | null)[]
|
||||
): T | undefined => {
|
||||
for (const arg of args) {
|
||||
if (!isNullOrUndefined(arg)) return arg
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export type FeatureFlags = {
|
||||
FF_RHINO_FILE_IMPORTER_ENABLED: boolean
|
||||
FF_LEGACY_FILE_IMPORTS_ENABLED: boolean
|
||||
FF_ACC_INTEGRATION_ENABLED: boolean
|
||||
FF_DASHBOARDS_MODULE_ENABLED: boolean
|
||||
FF_SAVED_VIEWS_ENABLED: boolean
|
||||
FF_USERS_INVITE_SCOPE_IS_PUBLIC: boolean
|
||||
}
|
||||
|
||||
@@ -135,6 +135,11 @@ export const parseFeatureFlags = (
|
||||
'Enables the integration with ACC. This synchronizes models with specified ACC assets.',
|
||||
defaults: { _: false }
|
||||
},
|
||||
FF_DASHBOARDS_MODULE_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
description: 'Enables the dashboards module.',
|
||||
defaults: { _: false }
|
||||
},
|
||||
FF_SAVED_VIEWS_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
description: 'Enables the saved views feature for project models',
|
||||
|
||||
@@ -43,7 +43,13 @@ describe('Viewer State helpers', () => {
|
||||
isOrthoProjection: false,
|
||||
zoom: 1
|
||||
},
|
||||
viewMode: 0,
|
||||
viewMode: {
|
||||
mode: 0,
|
||||
edgesColor: 0,
|
||||
edgesEnabled: true,
|
||||
outlineOpacity: 0,
|
||||
edgesWeight: 0
|
||||
},
|
||||
sectionBox: null,
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
@@ -149,7 +155,13 @@ describe('Viewer State helpers', () => {
|
||||
isOrthoProjection: false,
|
||||
zoom: 1
|
||||
},
|
||||
viewMode: 0,
|
||||
viewMode: {
|
||||
mode: 0,
|
||||
edgesColor: 0,
|
||||
edgesEnabled: true,
|
||||
outlineOpacity: 0,
|
||||
edgesWeight: 0
|
||||
},
|
||||
sectionBox: null,
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
@@ -202,7 +214,13 @@ describe('Viewer State helpers', () => {
|
||||
isOrthoProjection: false,
|
||||
zoom: 1
|
||||
},
|
||||
viewMode: 0,
|
||||
viewMode: {
|
||||
mode: 0,
|
||||
edgesColor: 0,
|
||||
edgesEnabled: true,
|
||||
outlineOpacity: 0,
|
||||
edgesWeight: 0
|
||||
},
|
||||
sectionBox: null,
|
||||
lightConfig: {},
|
||||
explodeFactor: 0,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { has, intersection, isObjectLike } from '#lodash'
|
||||
import { has, intersection, isNumber, isObjectLike } from '#lodash'
|
||||
import type { MaybeNullOrUndefined, Nullable } from '../../core/helpers/utilityTypes.js'
|
||||
import type { PartialDeep } from 'type-fest'
|
||||
import { UnformattableSerializedViewerStateError } from '../errors/index.js'
|
||||
import { coerceUndefinedValuesToNull } from '../../core/index.js'
|
||||
|
||||
export const defaultViewModeEdgeColorValue = 'DEFAULT_EDGE_COLOR'
|
||||
|
||||
/** Redefining these is unfortunate. Especially since they are not part of viewer-core */
|
||||
enum MeasurementType {
|
||||
PERPENDICULAR = 0,
|
||||
@@ -35,6 +37,9 @@ export interface SectionBoxData {
|
||||
* - ui.diff added
|
||||
* v1.2 -> v1.3
|
||||
* - ui.filters.selectedObjectIds removed in favor of ui.filters.selectedObjectApplicationIds
|
||||
* v1.3 -> 1.4
|
||||
* - ui.viewMode -> ui.viewMode.mode
|
||||
* - ui.viewMode has new keys: edgesEnabled, edgesWeight, outlineOpacity, edgesColor
|
||||
*/
|
||||
export const SERIALIZED_VIEWER_STATE_VERSION = 1.3
|
||||
|
||||
@@ -96,7 +101,13 @@ export type SerializedViewerState = {
|
||||
isOrthoProjection: boolean
|
||||
zoom: number
|
||||
}
|
||||
viewMode: number
|
||||
viewMode: {
|
||||
mode: number
|
||||
edgesEnabled: boolean
|
||||
edgesWeight: number
|
||||
outlineOpacity: number
|
||||
edgesColor: typeof defaultViewModeEdgeColorValue | number
|
||||
}
|
||||
sectionBox: Nullable<SectionBoxData>
|
||||
lightConfig: {
|
||||
intensity?: number
|
||||
@@ -182,6 +193,12 @@ const initializeMissingData = (state: UnformattedState): SerializedViewerState =
|
||||
)
|
||||
}
|
||||
|
||||
const viewModeType = isNumber(state.ui?.viewMode)
|
||||
? state.ui.viewMode
|
||||
: state.ui?.viewMode?.mode
|
||||
|
||||
const viewModeSettings = isNumber(state.ui?.viewMode) ? {} : state.ui?.viewMode
|
||||
|
||||
return {
|
||||
projectId: state.projectId || throwInvalidError('projectId'),
|
||||
sessionId: state.sessionId || `nullSessionId-${Math.random() * 1000}`,
|
||||
@@ -248,7 +265,13 @@ const initializeMissingData = (state: UnformattedState): SerializedViewerState =
|
||||
isOrthoProjection: state.ui?.camera?.isOrthoProjection || false,
|
||||
zoom: state.ui?.camera?.zoom || 1
|
||||
},
|
||||
viewMode: state.ui?.viewMode || 0,
|
||||
viewMode: {
|
||||
mode: viewModeType ?? 0,
|
||||
edgesEnabled: viewModeSettings?.edgesEnabled ?? true,
|
||||
edgesWeight: viewModeSettings?.edgesWeight ?? 1,
|
||||
outlineOpacity: viewModeSettings?.outlineOpacity ?? 0.75,
|
||||
edgesColor: viewModeSettings?.edgesColor ?? defaultViewModeEdgeColorValue
|
||||
},
|
||||
sectionBox:
|
||||
state.ui?.sectionBox?.min?.length && state.ui?.sectionBox.max?.length
|
||||
? // Complains otherwise
|
||||
|
||||
@@ -68,3 +68,8 @@ export const fileImportResultPayload = z.discriminatedUnion('status', [
|
||||
])
|
||||
|
||||
export type FileImportResultPayload = z.infer<typeof fileImportResultPayload>
|
||||
|
||||
export type FileImportJobPayloadV1 = JobPayload & {
|
||||
jobType: 'fileImport'
|
||||
payloadVersion: 1
|
||||
}
|
||||
|
||||
@@ -134,135 +134,137 @@ export const WorkspacePaidPlanConfigs: (params: {
|
||||
featureFlags: Partial<FeatureFlags> | undefined
|
||||
}) => {
|
||||
[plan in PaidWorkspacePlans]: WorkspacePlanConfig<plan>
|
||||
} = (params) => ({
|
||||
[PaidWorkspacePlans.Team]: {
|
||||
plan: PaidWorkspacePlans.Team,
|
||||
features: [...baseFeatures],
|
||||
limits: {
|
||||
projectCount: 5,
|
||||
modelCount: 25,
|
||||
versionsHistory: { value: 30, unit: 'day' },
|
||||
commentHistory: { value: 30, unit: 'day' }
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.TeamUnlimited]: {
|
||||
plan: PaidWorkspacePlans.TeamUnlimited,
|
||||
features: [...baseFeatures],
|
||||
limits: {
|
||||
projectCount: null,
|
||||
modelCount: null,
|
||||
versionsHistory: { value: 30, unit: 'day' },
|
||||
commentHistory: { value: 30, unit: 'day' }
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.Pro]: {
|
||||
plan: PaidWorkspacePlans.Pro,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
],
|
||||
limits: {
|
||||
projectCount: 10,
|
||||
modelCount: 50,
|
||||
versionsHistory: null,
|
||||
commentHistory: null
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.ProUnlimited]: {
|
||||
plan: PaidWorkspacePlans.ProUnlimited,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
],
|
||||
limits: {
|
||||
projectCount: null,
|
||||
modelCount: null,
|
||||
versionsHistory: null,
|
||||
commentHistory: null
|
||||
} = (params) => {
|
||||
const finalBaseFeatures = [
|
||||
...baseFeatures,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
]
|
||||
|
||||
return {
|
||||
[PaidWorkspacePlans.Team]: {
|
||||
plan: PaidWorkspacePlans.Team,
|
||||
features: [...finalBaseFeatures],
|
||||
limits: {
|
||||
projectCount: 5,
|
||||
modelCount: 25,
|
||||
versionsHistory: { value: 30, unit: 'day' },
|
||||
commentHistory: { value: 30, unit: 'day' }
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.TeamUnlimited]: {
|
||||
plan: PaidWorkspacePlans.TeamUnlimited,
|
||||
features: [...finalBaseFeatures],
|
||||
limits: {
|
||||
projectCount: null,
|
||||
modelCount: null,
|
||||
versionsHistory: { value: 30, unit: 'day' },
|
||||
commentHistory: { value: 30, unit: 'day' }
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.Pro]: {
|
||||
plan: PaidWorkspacePlans.Pro,
|
||||
features: [
|
||||
...finalBaseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding
|
||||
],
|
||||
limits: {
|
||||
projectCount: 10,
|
||||
modelCount: 50,
|
||||
versionsHistory: null,
|
||||
commentHistory: null
|
||||
}
|
||||
},
|
||||
[PaidWorkspacePlans.ProUnlimited]: {
|
||||
plan: PaidWorkspacePlans.ProUnlimited,
|
||||
features: [
|
||||
...finalBaseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding
|
||||
],
|
||||
limits: {
|
||||
projectCount: null,
|
||||
modelCount: null,
|
||||
versionsHistory: null,
|
||||
commentHistory: null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const WorkspaceUnpaidPlanConfigs: (params: {
|
||||
featureFlags: Partial<FeatureFlags> | undefined
|
||||
}) => {
|
||||
[plan in UnpaidWorkspacePlans]: WorkspacePlanConfig<plan>
|
||||
} = (params) => ({
|
||||
[UnpaidWorkspacePlans.Enterprise]: {
|
||||
plan: UnpaidWorkspacePlans.Enterprise,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.Unlimited]: {
|
||||
plan: UnpaidWorkspacePlans.Unlimited,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.Academia]: {
|
||||
plan: UnpaidWorkspacePlans.Academia,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs(params).teamUnlimited,
|
||||
plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.ProUnlimitedInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs(params).proUnlimited,
|
||||
plan: UnpaidWorkspacePlans.ProUnlimitedInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.Free]: {
|
||||
plan: UnpaidWorkspacePlans.Free,
|
||||
features: baseFeatures,
|
||||
limits: {
|
||||
projectCount: 1,
|
||||
modelCount: 5,
|
||||
versionsHistory: { value: 7, unit: 'day' },
|
||||
commentHistory: { value: 7, unit: 'day' }
|
||||
} = (params) => {
|
||||
const finalBaseFeatures = [
|
||||
...baseFeatures,
|
||||
...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED
|
||||
? [WorkspacePlanFeatures.SavedViews]
|
||||
: [])
|
||||
]
|
||||
return {
|
||||
[UnpaidWorkspacePlans.Enterprise]: {
|
||||
plan: UnpaidWorkspacePlans.Enterprise,
|
||||
features: [
|
||||
...finalBaseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.Unlimited]: {
|
||||
plan: UnpaidWorkspacePlans.Unlimited,
|
||||
features: [
|
||||
...finalBaseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding,
|
||||
WorkspacePlanFeatures.ExclusiveMembership
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.Academia]: {
|
||||
plan: UnpaidWorkspacePlans.Academia,
|
||||
features: [
|
||||
...finalBaseFeatures,
|
||||
WorkspacePlanFeatures.DomainSecurity,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
WorkspacePlanFeatures.HideSpeckleBranding
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs(params).teamUnlimited,
|
||||
plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.ProUnlimitedInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs(params).proUnlimited,
|
||||
plan: UnpaidWorkspacePlans.ProUnlimitedInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.Free]: {
|
||||
plan: UnpaidWorkspacePlans.Free,
|
||||
features: finalBaseFeatures,
|
||||
limits: {
|
||||
projectCount: 1,
|
||||
modelCount: 5,
|
||||
versionsHistory: { value: 7, unit: 'day' },
|
||||
commentHistory: { value: 7, unit: 'day' }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const WorkspacePlanConfigs = (params: {
|
||||
featureFlags: Partial<FeatureFlags> | undefined
|
||||
|
||||
Reference in New Issue
Block a user