fix(server): workspace member permissions and profile onboarding (#19786)
## Summary Aligns **workspace member** editing and **onboarding** with how the product is actually used: profile and other “settings” fields go through **`updateWorkspaceMemberSettings`**, while **`/graphql`** record APIs follow **object-level** permissions for the `workspaceMember` object. ## Product behaviour ### Completing “Create profile” onboarding Users who must create a profile (empty name at sign-up) get `ONBOARDING_CREATE_PROFILE_PENDING` set. The onboarding UI saves the name with **`updateWorkspaceMemberSettings`**, not with a workspace record **`updateOne`**. **Before:** The server only cleared the pending flag on **`workspaceMember.updateOne`**, so the flag could stay set and onboarding appeared stuck. **After:** Clearing the profile step runs when **`updateWorkspaceMemberSettings`** persists an update that includes a **name** (same rules as before: non-empty name parts). Onboarding can advance normally after **Continue** on Create profile. ### Two ways to change workspace member data | Path | Typical use | Who can change what | |------|----------------|---------------------| | **`updateWorkspaceMemberSettings`** (metadata API) | Standard member fields the app treats as “my profile / preferences” (name, avatar-related settings, locale, time zone, etc.) | **Always** your **own** workspace member. Changing **another** member still requires **Workspace members** in role settings (`WORKSPACE_MEMBERS`). Custom fields are **not** allowed on this endpoint (unchanged). | | **`/graphql`** record mutations on **`workspaceMember`** | Custom fields, integrations, anything that goes through the generic record API | **`WorkspaceMember`** is special-cased in permissions: **read** stays **on** for everyone, but **update / create / delete** require **`WORKSPACE_MEMBERS`**, including updating **your own** row via `/graphql`. So a **Member** without that permission cannot fix their name through **`updateWorkspaceMember`**; they use **Settings** / **`updateWorkspaceMemberSettings`** instead. | This matches **`WorkspaceRolesPermissionsCacheService`**: for the workspace member object, `canReadObjectRecords` is always true; `canUpdateObjectRecords` (and delete-related flags) follow **`WORKSPACE_MEMBERS`**. ### Hooks and delete side-effects - Removed **`workspaceMember.updateOne`** pre-query hook and **`WorkspaceMemberPreQueryHookService`**: they duplicated the same rules the permission cache already enforces for `/graphql`. - **`WorkspaceMember.deleteOne`** pre-hook still tells users to remove members via the dedicated flow; the post-hook only runs the **`deleteUserWorkspace`** side-effect when a member row is actually removed—**no** extra settings-permission check there, since only callers that already passed **object** delete permission can remove the row. ## Tests - **`workspace-members.integration-spec.ts`**: clarifies and extends coverage so **`/graphql`** **`updateOne`** is denied for **own** record on a **standard** name field and on a **custom** field when the role lacks **`WORKSPACE_MEMBERS`**. ## Implementation notes - **`OnboardingService.completeOnboardingProfileStepIfNameProvided`** centralises the “clear profile pending if name present” logic; **`UserResolver.updateWorkspaceMemberSettings`** calls it after save, using the typed update payload’s **`name`** (no cast). - **`UserWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace`**: drops a redundant **`coreEntityCacheService.invalidate`**; **`updateWorkspaceMemberSettings`** still invalidates the user-workspace cache after the mutation.
This commit is contained in:
@@ -1266,6 +1266,37 @@ type Analytics {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ApprovedAccessDomain {
|
||||
id: UUID!
|
||||
domain: String!
|
||||
isValidated: Boolean!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type FileWithSignedUrl {
|
||||
id: UUID!
|
||||
path: String!
|
||||
size: Float!
|
||||
createdAt: DateTime!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type EnterpriseLicenseInfoDTO {
|
||||
isValid: Boolean!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
subscriptionId: String
|
||||
}
|
||||
|
||||
type EnterpriseSubscriptionStatusDTO {
|
||||
status: String!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
cancelAt: DateTime
|
||||
currentPeriodEnd: DateTime
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type BillingSubscriptionSchedulePhaseItem {
|
||||
price: String!
|
||||
quantity: Float
|
||||
@@ -1419,42 +1450,11 @@ type BillingUpdate {
|
||||
billingSubscriptions: [BillingSubscription!]!
|
||||
}
|
||||
|
||||
type EnterpriseLicenseInfoDTO {
|
||||
isValid: Boolean!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
subscriptionId: String
|
||||
}
|
||||
|
||||
type EnterpriseSubscriptionStatusDTO {
|
||||
status: String!
|
||||
licensee: String
|
||||
expiresAt: DateTime
|
||||
cancelAt: DateTime
|
||||
currentPeriodEnd: DateTime
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type OnboardingStepSuccess {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ApprovedAccessDomain {
|
||||
id: UUID!
|
||||
domain: String!
|
||||
isValidated: Boolean!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type FileWithSignedUrl {
|
||||
id: UUID!
|
||||
path: String!
|
||||
size: Float!
|
||||
createdAt: DateTime!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type WorkspaceInvitation {
|
||||
id: UUID!
|
||||
email: String!
|
||||
|
||||
@@ -989,6 +989,41 @@ export interface Analytics {
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomain {
|
||||
id: Scalars['UUID']
|
||||
domain: Scalars['String']
|
||||
isValidated: Scalars['Boolean']
|
||||
createdAt: Scalars['DateTime']
|
||||
__typename: 'ApprovedAccessDomain'
|
||||
}
|
||||
|
||||
export interface FileWithSignedUrl {
|
||||
id: Scalars['UUID']
|
||||
path: Scalars['String']
|
||||
size: Scalars['Float']
|
||||
createdAt: Scalars['DateTime']
|
||||
url: Scalars['String']
|
||||
__typename: 'FileWithSignedUrl'
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTO {
|
||||
isValid: Scalars['Boolean']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
subscriptionId?: Scalars['String']
|
||||
__typename: 'EnterpriseLicenseInfoDTO'
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTO {
|
||||
status: Scalars['String']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
cancelAt?: Scalars['DateTime']
|
||||
currentPeriodEnd?: Scalars['DateTime']
|
||||
isCancellationScheduled: Scalars['Boolean']
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface BillingSubscriptionSchedulePhaseItem {
|
||||
price: Scalars['String']
|
||||
quantity?: Scalars['Float']
|
||||
@@ -1134,47 +1169,12 @@ export interface BillingUpdate {
|
||||
__typename: 'BillingUpdate'
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTO {
|
||||
isValid: Scalars['Boolean']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
subscriptionId?: Scalars['String']
|
||||
__typename: 'EnterpriseLicenseInfoDTO'
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTO {
|
||||
status: Scalars['String']
|
||||
licensee?: Scalars['String']
|
||||
expiresAt?: Scalars['DateTime']
|
||||
cancelAt?: Scalars['DateTime']
|
||||
currentPeriodEnd?: Scalars['DateTime']
|
||||
isCancellationScheduled: Scalars['Boolean']
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface OnboardingStepSuccess {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'OnboardingStepSuccess'
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomain {
|
||||
id: Scalars['UUID']
|
||||
domain: Scalars['String']
|
||||
isValidated: Scalars['Boolean']
|
||||
createdAt: Scalars['DateTime']
|
||||
__typename: 'ApprovedAccessDomain'
|
||||
}
|
||||
|
||||
export interface FileWithSignedUrl {
|
||||
id: Scalars['UUID']
|
||||
path: Scalars['String']
|
||||
size: Scalars['Float']
|
||||
createdAt: Scalars['DateTime']
|
||||
url: Scalars['String']
|
||||
__typename: 'FileWithSignedUrl'
|
||||
}
|
||||
|
||||
export interface WorkspaceInvitation {
|
||||
id: Scalars['UUID']
|
||||
email: Scalars['String']
|
||||
@@ -4239,6 +4239,45 @@ export interface AnalyticsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomainGenqlSelection{
|
||||
id?: boolean | number
|
||||
domain?: boolean | number
|
||||
isValidated?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FileWithSignedUrlGenqlSelection{
|
||||
id?: boolean | number
|
||||
path?: boolean | number
|
||||
size?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
url?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTOGenqlSelection{
|
||||
isValid?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
subscriptionId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
status?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
cancelAt?: boolean | number
|
||||
currentPeriodEnd?: boolean | number
|
||||
isCancellationScheduled?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface BillingSubscriptionSchedulePhaseItemGenqlSelection{
|
||||
price?: boolean | number
|
||||
quantity?: boolean | number
|
||||
@@ -4386,26 +4425,6 @@ export interface BillingUpdateGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseLicenseInfoDTOGenqlSelection{
|
||||
isValid?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
subscriptionId?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
status?: boolean | number
|
||||
licensee?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
cancelAt?: boolean | number
|
||||
currentPeriodEnd?: boolean | number
|
||||
isCancellationScheduled?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface OnboardingStepSuccessGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
@@ -4413,25 +4432,6 @@ export interface OnboardingStepSuccessGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApprovedAccessDomainGenqlSelection{
|
||||
id?: boolean | number
|
||||
domain?: boolean | number
|
||||
isValidated?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FileWithSignedUrlGenqlSelection{
|
||||
id?: boolean | number
|
||||
path?: boolean | number
|
||||
size?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
url?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface WorkspaceInvitationGenqlSelection{
|
||||
id?: boolean | number
|
||||
email?: boolean | number
|
||||
@@ -7493,6 +7493,38 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain']
|
||||
export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"')
|
||||
return ApprovedAccessDomain_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl']
|
||||
export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"')
|
||||
return FileWithSignedUrl_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
|
||||
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
|
||||
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
|
||||
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
|
||||
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const BillingSubscriptionSchedulePhaseItem_possibleTypes: string[] = ['BillingSubscriptionSchedulePhaseItem']
|
||||
export const isBillingSubscriptionSchedulePhaseItem = (obj?: { __typename?: any } | null): obj is BillingSubscriptionSchedulePhaseItem => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isBillingSubscriptionSchedulePhaseItem"')
|
||||
@@ -7621,22 +7653,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const EnterpriseLicenseInfoDTO_possibleTypes: string[] = ['EnterpriseLicenseInfoDTO']
|
||||
export const isEnterpriseLicenseInfoDTO = (obj?: { __typename?: any } | null): obj is EnterpriseLicenseInfoDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseLicenseInfoDTO"')
|
||||
return EnterpriseLicenseInfoDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EnterpriseSubscriptionStatusDTO_possibleTypes: string[] = ['EnterpriseSubscriptionStatusDTO']
|
||||
export const isEnterpriseSubscriptionStatusDTO = (obj?: { __typename?: any } | null): obj is EnterpriseSubscriptionStatusDTO => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEnterpriseSubscriptionStatusDTO"')
|
||||
return EnterpriseSubscriptionStatusDTO_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OnboardingStepSuccess_possibleTypes: string[] = ['OnboardingStepSuccess']
|
||||
export const isOnboardingStepSuccess = (obj?: { __typename?: any } | null): obj is OnboardingStepSuccess => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isOnboardingStepSuccess"')
|
||||
@@ -7645,22 +7661,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const ApprovedAccessDomain_possibleTypes: string[] = ['ApprovedAccessDomain']
|
||||
export const isApprovedAccessDomain = (obj?: { __typename?: any } | null): obj is ApprovedAccessDomain => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApprovedAccessDomain"')
|
||||
return ApprovedAccessDomain_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FileWithSignedUrl_possibleTypes: string[] = ['FileWithSignedUrl']
|
||||
export const isFileWithSignedUrl = (obj?: { __typename?: any } | null): obj is FileWithSignedUrl => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFileWithSignedUrl"')
|
||||
return FileWithSignedUrl_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const WorkspaceInvitation_possibleTypes: string[] = ['WorkspaceInvitation']
|
||||
export const isWorkspaceInvitation = (obj?: { __typename?: any } | null): obj is WorkspaceInvitation => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isWorkspaceInvitation"')
|
||||
|
||||
@@ -39,11 +39,11 @@ export default {
|
||||
95,
|
||||
101,
|
||||
115,
|
||||
120,
|
||||
121,
|
||||
122,
|
||||
124,
|
||||
132,
|
||||
125,
|
||||
126,
|
||||
128,
|
||||
136,
|
||||
147,
|
||||
150,
|
||||
152,
|
||||
@@ -105,13 +105,13 @@ export default {
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
119
|
||||
123
|
||||
],
|
||||
"on_BillingLicensedProduct": [
|
||||
128
|
||||
132
|
||||
],
|
||||
"on_BillingMeteredProduct": [
|
||||
129
|
||||
133
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -1700,10 +1700,10 @@ export default {
|
||||
157
|
||||
],
|
||||
"billingSubscriptions": [
|
||||
131
|
||||
135
|
||||
],
|
||||
"currentBillingSubscription": [
|
||||
131
|
||||
135
|
||||
],
|
||||
"billingEntitlements": [
|
||||
253
|
||||
@@ -2621,259 +2621,36 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionSchedulePhaseItem": {
|
||||
"price": [
|
||||
1
|
||||
],
|
||||
"quantity": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionSchedulePhase": {
|
||||
"start_date": [
|
||||
11
|
||||
],
|
||||
"end_date": [
|
||||
11
|
||||
],
|
||||
"items": [
|
||||
117
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingProductMetadata": {
|
||||
"planKey": [
|
||||
120
|
||||
],
|
||||
"priceUsageBased": [
|
||||
121
|
||||
],
|
||||
"productKey": [
|
||||
122
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPlanKey": {},
|
||||
"BillingUsageType": {},
|
||||
"BillingProductKey": {},
|
||||
"BillingPriceLicensed": {
|
||||
"recurringInterval": [
|
||||
124
|
||||
],
|
||||
"unitAmount": [
|
||||
11
|
||||
],
|
||||
"stripePriceId": [
|
||||
1
|
||||
],
|
||||
"priceUsageType": [
|
||||
121
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"SubscriptionInterval": {},
|
||||
"BillingPriceTier": {
|
||||
"upTo": [
|
||||
11
|
||||
],
|
||||
"flatAmount": [
|
||||
11
|
||||
],
|
||||
"unitAmount": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPriceMetered": {
|
||||
"tiers": [
|
||||
125
|
||||
],
|
||||
"recurringInterval": [
|
||||
124
|
||||
],
|
||||
"stripePriceId": [
|
||||
1
|
||||
],
|
||||
"priceUsageType": [
|
||||
121
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
119
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingLicensedProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
119
|
||||
],
|
||||
"prices": [
|
||||
123
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingMeteredProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
119
|
||||
],
|
||||
"prices": [
|
||||
126
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionItem": {
|
||||
"ApprovedAccessDomain": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"hasReachedCurrentPeriodCap": [
|
||||
6
|
||||
],
|
||||
"quantity": [
|
||||
11
|
||||
],
|
||||
"stripePriceId": [
|
||||
"domain": [
|
||||
1
|
||||
],
|
||||
"billingProduct": [
|
||||
0
|
||||
"isValidated": [
|
||||
6
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscription": {
|
||||
"FileWithSignedUrl": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"status": [
|
||||
132
|
||||
"path": [
|
||||
1
|
||||
],
|
||||
"interval": [
|
||||
124
|
||||
"size": [
|
||||
11
|
||||
],
|
||||
"billingSubscriptionItems": [
|
||||
130
|
||||
],
|
||||
"currentPeriodEnd": [
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"metadata": [
|
||||
15
|
||||
],
|
||||
"phases": [
|
||||
118
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"SubscriptionStatus": {},
|
||||
"BillingEndTrialPeriod": {
|
||||
"status": [
|
||||
132
|
||||
],
|
||||
"hasPaymentMethod": [
|
||||
6
|
||||
],
|
||||
"billingPortalUrl": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingMeteredProductUsage": {
|
||||
"productKey": [
|
||||
122
|
||||
],
|
||||
"periodStart": [
|
||||
4
|
||||
],
|
||||
"periodEnd": [
|
||||
4
|
||||
],
|
||||
"usedCredits": [
|
||||
11
|
||||
],
|
||||
"grantedCredits": [
|
||||
11
|
||||
],
|
||||
"rolloverCredits": [
|
||||
11
|
||||
],
|
||||
"totalGrantedCredits": [
|
||||
11
|
||||
],
|
||||
"unitPriceCents": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPlan": {
|
||||
"planKey": [
|
||||
120
|
||||
],
|
||||
"licensedProducts": [
|
||||
128
|
||||
],
|
||||
"meteredProducts": [
|
||||
129
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSession": {
|
||||
"url": [
|
||||
1
|
||||
],
|
||||
@@ -2881,17 +2658,6 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingUpdate": {
|
||||
"currentBillingSubscription": [
|
||||
131
|
||||
],
|
||||
"billingSubscriptions": [
|
||||
131
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"EnterpriseLicenseInfoDTO": {
|
||||
"isValid": [
|
||||
6
|
||||
@@ -2932,6 +2698,277 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionSchedulePhaseItem": {
|
||||
"price": [
|
||||
1
|
||||
],
|
||||
"quantity": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionSchedulePhase": {
|
||||
"start_date": [
|
||||
11
|
||||
],
|
||||
"end_date": [
|
||||
11
|
||||
],
|
||||
"items": [
|
||||
121
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingProductMetadata": {
|
||||
"planKey": [
|
||||
124
|
||||
],
|
||||
"priceUsageBased": [
|
||||
125
|
||||
],
|
||||
"productKey": [
|
||||
126
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPlanKey": {},
|
||||
"BillingUsageType": {},
|
||||
"BillingProductKey": {},
|
||||
"BillingPriceLicensed": {
|
||||
"recurringInterval": [
|
||||
128
|
||||
],
|
||||
"unitAmount": [
|
||||
11
|
||||
],
|
||||
"stripePriceId": [
|
||||
1
|
||||
],
|
||||
"priceUsageType": [
|
||||
125
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"SubscriptionInterval": {},
|
||||
"BillingPriceTier": {
|
||||
"upTo": [
|
||||
11
|
||||
],
|
||||
"flatAmount": [
|
||||
11
|
||||
],
|
||||
"unitAmount": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPriceMetered": {
|
||||
"tiers": [
|
||||
129
|
||||
],
|
||||
"recurringInterval": [
|
||||
128
|
||||
],
|
||||
"stripePriceId": [
|
||||
1
|
||||
],
|
||||
"priceUsageType": [
|
||||
125
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
123
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingLicensedProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
123
|
||||
],
|
||||
"prices": [
|
||||
127
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingMeteredProduct": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"description": [
|
||||
1
|
||||
],
|
||||
"images": [
|
||||
1
|
||||
],
|
||||
"metadata": [
|
||||
123
|
||||
],
|
||||
"prices": [
|
||||
130
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscriptionItem": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"hasReachedCurrentPeriodCap": [
|
||||
6
|
||||
],
|
||||
"quantity": [
|
||||
11
|
||||
],
|
||||
"stripePriceId": [
|
||||
1
|
||||
],
|
||||
"billingProduct": [
|
||||
0
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSubscription": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"status": [
|
||||
136
|
||||
],
|
||||
"interval": [
|
||||
128
|
||||
],
|
||||
"billingSubscriptionItems": [
|
||||
134
|
||||
],
|
||||
"currentPeriodEnd": [
|
||||
4
|
||||
],
|
||||
"metadata": [
|
||||
15
|
||||
],
|
||||
"phases": [
|
||||
122
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"SubscriptionStatus": {},
|
||||
"BillingEndTrialPeriod": {
|
||||
"status": [
|
||||
136
|
||||
],
|
||||
"hasPaymentMethod": [
|
||||
6
|
||||
],
|
||||
"billingPortalUrl": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingMeteredProductUsage": {
|
||||
"productKey": [
|
||||
126
|
||||
],
|
||||
"periodStart": [
|
||||
4
|
||||
],
|
||||
"periodEnd": [
|
||||
4
|
||||
],
|
||||
"usedCredits": [
|
||||
11
|
||||
],
|
||||
"grantedCredits": [
|
||||
11
|
||||
],
|
||||
"rolloverCredits": [
|
||||
11
|
||||
],
|
||||
"totalGrantedCredits": [
|
||||
11
|
||||
],
|
||||
"unitPriceCents": [
|
||||
11
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingPlan": {
|
||||
"planKey": [
|
||||
124
|
||||
],
|
||||
"licensedProducts": [
|
||||
132
|
||||
],
|
||||
"meteredProducts": [
|
||||
133
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingSession": {
|
||||
"url": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"BillingUpdate": {
|
||||
"currentBillingSubscription": [
|
||||
135
|
||||
],
|
||||
"billingSubscriptions": [
|
||||
135
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"OnboardingStepSuccess": {
|
||||
"success": [
|
||||
6
|
||||
@@ -2940,43 +2977,6 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"ApprovedAccessDomain": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"domain": [
|
||||
1
|
||||
],
|
||||
"isValidated": [
|
||||
6
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"FileWithSignedUrl": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"path": [
|
||||
1
|
||||
],
|
||||
"size": [
|
||||
11
|
||||
],
|
||||
"createdAt": [
|
||||
4
|
||||
],
|
||||
"url": [
|
||||
1
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"WorkspaceInvitation": {
|
||||
"id": [
|
||||
3
|
||||
@@ -6481,10 +6481,10 @@ export default {
|
||||
}
|
||||
],
|
||||
"enterpriseSubscriptionStatus": [
|
||||
139
|
||||
120
|
||||
],
|
||||
"billingPortalSession": [
|
||||
136,
|
||||
140,
|
||||
{
|
||||
"returnUrlPath": [
|
||||
1
|
||||
@@ -6492,16 +6492,16 @@ export default {
|
||||
}
|
||||
],
|
||||
"listPlans": [
|
||||
135
|
||||
139
|
||||
],
|
||||
"getMeteredProductsUsage": [
|
||||
134
|
||||
138
|
||||
],
|
||||
"findWorkspaceInvitations": [
|
||||
143
|
||||
],
|
||||
"getApprovedAccessDomains": [
|
||||
141
|
||||
117
|
||||
],
|
||||
"getPageLayoutTabs": [
|
||||
113,
|
||||
@@ -7533,7 +7533,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadEmailAttachmentFile": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7542,7 +7542,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadAIChatFile": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7551,7 +7551,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadWorkflowFile": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7560,7 +7560,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadWorkspaceLogo": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7569,7 +7569,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadWorkspaceMemberProfilePicture": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7578,7 +7578,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadFilesFieldFile": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7591,7 +7591,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"uploadFilesFieldFileByUniversalIdentifier": [
|
||||
142,
|
||||
118,
|
||||
{
|
||||
"file": [
|
||||
388,
|
||||
@@ -7936,7 +7936,7 @@ export default {
|
||||
6
|
||||
],
|
||||
"setEnterpriseKey": [
|
||||
138,
|
||||
119,
|
||||
{
|
||||
"enterpriseKey": [
|
||||
1,
|
||||
@@ -7945,20 +7945,20 @@ export default {
|
||||
}
|
||||
],
|
||||
"skipSyncEmailOnboardingStep": [
|
||||
140
|
||||
142
|
||||
],
|
||||
"skipBookOnboardingStep": [
|
||||
140
|
||||
142
|
||||
],
|
||||
"checkoutSession": [
|
||||
136,
|
||||
140,
|
||||
{
|
||||
"recurringInterval": [
|
||||
124,
|
||||
128,
|
||||
"SubscriptionInterval!"
|
||||
],
|
||||
"plan": [
|
||||
120,
|
||||
124,
|
||||
"BillingPlanKey!"
|
||||
],
|
||||
"requirePaymentMethod": [
|
||||
@@ -7971,19 +7971,19 @@ export default {
|
||||
}
|
||||
],
|
||||
"switchSubscriptionInterval": [
|
||||
137
|
||||
141
|
||||
],
|
||||
"switchBillingPlan": [
|
||||
137
|
||||
141
|
||||
],
|
||||
"cancelSwitchBillingPlan": [
|
||||
137
|
||||
141
|
||||
],
|
||||
"cancelSwitchBillingInterval": [
|
||||
137
|
||||
141
|
||||
],
|
||||
"setMeteredSubscriptionPrice": [
|
||||
137,
|
||||
141,
|
||||
{
|
||||
"priceId": [
|
||||
1,
|
||||
@@ -7992,10 +7992,10 @@ export default {
|
||||
}
|
||||
],
|
||||
"endSubscriptionTrialPeriod": [
|
||||
133
|
||||
137
|
||||
],
|
||||
"cancelSwitchMeteredPrice": [
|
||||
137
|
||||
141
|
||||
],
|
||||
"deleteWorkspaceInvitation": [
|
||||
1,
|
||||
@@ -8028,7 +8028,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"createApprovedAccessDomain": [
|
||||
141,
|
||||
117,
|
||||
{
|
||||
"input": [
|
||||
420,
|
||||
@@ -8046,7 +8046,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"validateApprovedAccessDomain": [
|
||||
141,
|
||||
117,
|
||||
{
|
||||
"input": [
|
||||
422,
|
||||
|
||||
@@ -212,6 +212,35 @@ export class OnboardingService {
|
||||
);
|
||||
}
|
||||
|
||||
async completeOnboardingProfileStepIfNameProvided({
|
||||
userId,
|
||||
workspaceId,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}) {
|
||||
if (!isDefined(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasProvidedNamePart =
|
||||
(isDefined(firstName) && firstName !== '') ||
|
||||
(isDefined(lastName) && lastName !== '');
|
||||
if (!hasProvidedNamePart) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setOnboardingCreateProfilePending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
async setOnboardingBookOnboardingPending({
|
||||
workspaceId,
|
||||
value,
|
||||
|
||||
@@ -522,13 +522,20 @@ export class UserResolver {
|
||||
workspaceMemberRepository.save(workspaceMemberUpdatePayload),
|
||||
);
|
||||
|
||||
if (isDefined(input.update.locale)) {
|
||||
const targetUserWorkspace =
|
||||
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
await this.onboardingService.completeOnboardingProfileStepIfNameProvided({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
firstName: workspaceMemberUpdatePayload.name?.firstName,
|
||||
lastName: workspaceMemberUpdatePayload.name?.lastName,
|
||||
});
|
||||
|
||||
const targetUserWorkspace =
|
||||
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (isDefined(input.update.locale)) {
|
||||
await this.userWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace(
|
||||
{
|
||||
locale: input.update.locale as UserWorkspaceEntity['locale'],
|
||||
|
||||
-20
@@ -7,8 +7,6 @@ import { type WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/work
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
@@ -18,7 +16,6 @@ import {
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@WorkspaceQueryHook({
|
||||
@@ -32,7 +29,6 @@ export class WorkspaceMemberDeleteOnePostQueryHook
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
private readonly workspaceMemberPreQueryHookService: WorkspaceMemberPreQueryHookService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
@@ -52,22 +48,6 @@ export class WorkspaceMemberDeleteOnePostQueryHook
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
await this.workspaceMemberPreQueryHookService.validateWorkspaceMemberUpdatePermissionOrThrow(
|
||||
{
|
||||
userWorkspaceId: isUserAuthContext(authContext)
|
||||
? authContext.userWorkspaceId
|
||||
: undefined,
|
||||
workspaceMemberId: isUserAuthContext(authContext)
|
||||
? authContext.workspaceMemberId
|
||||
: undefined,
|
||||
targettedWorkspaceMemberId,
|
||||
workspaceId: workspace.id,
|
||||
apiKey: isApiKeyAuthContext(authContext)
|
||||
? authContext.apiKey
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMember =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMemberPreQueryHookService {
|
||||
constructor(
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
) {}
|
||||
|
||||
async validateWorkspaceMemberUpdatePermissionOrThrow({
|
||||
userWorkspaceId,
|
||||
workspaceMemberId,
|
||||
targettedWorkspaceMemberId,
|
||||
workspaceId,
|
||||
apiKey,
|
||||
}: {
|
||||
userWorkspaceId?: string;
|
||||
workspaceMemberId?: string;
|
||||
targettedWorkspaceMemberId?: string;
|
||||
workspaceId: string;
|
||||
apiKey?: FlatApiKey | null;
|
||||
}) {
|
||||
if (isDefined(apiKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userWorkspaceId) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.USER_WORKSPACE_NOT_FOUND,
|
||||
PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(targettedWorkspaceMemberId) &&
|
||||
workspaceMemberId === targettedWorkspaceMemberId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
await this.permissionsService.userHasWorkspaceSettingPermission({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
setting: PermissionFlagType.WORKSPACE_MEMBERS,
|
||||
apiKeyId: apiKey ?? undefined,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
PermissionsExceptionCode.PERMISSION_DENIED,
|
||||
);
|
||||
}
|
||||
|
||||
async completeOnboardingProfileStepIfNameProvided({
|
||||
userId,
|
||||
workspaceId,
|
||||
firstName,
|
||||
lastName,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}) {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstName === '' && lastName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(firstName) && !isDefined(lastName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onboardingService.setOnboardingCreateProfilePending({
|
||||
userId,
|
||||
workspaceId,
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
-8
@@ -3,10 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { WorkspaceMemberCreateManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-create-many.pre-query.hook';
|
||||
import { WorkspaceMemberCreateOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-create-one.pre-query.hook';
|
||||
import { WorkspaceMemberDeleteManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-many.pre-query.hook';
|
||||
@@ -14,15 +12,12 @@ import { WorkspaceMemberDeleteOnePostQueryHook } from 'src/modules/workspace-mem
|
||||
import { WorkspaceMemberDeleteOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-delete-one.pre-query.hook';
|
||||
import { WorkspaceMemberDestroyManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-destroy-many.pre-query.hook';
|
||||
import { WorkspaceMemberDestroyOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-destroy-one.pre-query.hook';
|
||||
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
|
||||
import { WorkspaceMemberRestoreManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-restore-many.pre-query.hook';
|
||||
import { WorkspaceMemberRestoreOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-restore-one.pre-query.hook';
|
||||
import { WorkspaceMemberUpdateManyPreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-update-many.pre-query.hook';
|
||||
import { WorkspaceMemberUpdateOnePreQueryHook } from 'src/modules/workspace-member/query-hooks/workspace-member-update-one.pre-query.hook';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
WorkspaceMemberPreQueryHookService,
|
||||
WorkspaceMemberCreateOnePreQueryHook,
|
||||
WorkspaceMemberCreateManyPreQueryHook,
|
||||
WorkspaceMemberDeleteOnePreQueryHook,
|
||||
@@ -32,14 +27,11 @@ import { WorkspaceMemberUpdateOnePreQueryHook } from 'src/modules/workspace-memb
|
||||
WorkspaceMemberDestroyManyPreQueryHook,
|
||||
WorkspaceMemberRestoreOnePreQueryHook,
|
||||
WorkspaceMemberRestoreManyPreQueryHook,
|
||||
WorkspaceMemberUpdateOnePreQueryHook,
|
||||
WorkspaceMemberUpdateManyPreQueryHook,
|
||||
],
|
||||
imports: [
|
||||
CoreEntityCacheModule,
|
||||
FeatureFlagModule,
|
||||
OnboardingModule,
|
||||
PermissionsModule,
|
||||
UserWorkspaceModule,
|
||||
TypeOrmModule.forFeature([UserWorkspaceEntity]),
|
||||
],
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
|
||||
import { WorkspaceMemberPreQueryHookService } from 'src/modules/workspace-member/query-hooks/workspace-member-pre-query-hook.service';
|
||||
|
||||
@WorkspaceQueryHook(`workspaceMember.updateOne`)
|
||||
export class WorkspaceMemberUpdateOnePreQueryHook
|
||||
implements WorkspacePreQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly workspaceMemberPreQueryHookService: WorkspaceMemberPreQueryHookService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: WorkspaceAuthContext,
|
||||
_objectName: string,
|
||||
payload: UpdateOneResolverArgs,
|
||||
): Promise<UpdateOneResolverArgs> {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
await this.workspaceMemberPreQueryHookService.validateWorkspaceMemberUpdatePermissionOrThrow(
|
||||
{
|
||||
userWorkspaceId: isUserAuthContext(authContext)
|
||||
? authContext.userWorkspaceId
|
||||
: undefined,
|
||||
workspaceMemberId: isUserAuthContext(authContext)
|
||||
? authContext.workspaceMemberId
|
||||
: undefined,
|
||||
targettedWorkspaceMemberId: payload.id,
|
||||
workspaceId: workspace.id,
|
||||
apiKey: isApiKeyAuthContext(authContext)
|
||||
? authContext.apiKey
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
await this.workspaceMemberPreQueryHookService.completeOnboardingProfileStepIfNameProvided(
|
||||
{
|
||||
userId: isUserAuthContext(authContext)
|
||||
? authContext.user.id
|
||||
: undefined,
|
||||
workspaceId: workspace.id,
|
||||
firstName: payload.data.name?.firstName,
|
||||
lastName: payload.data.name?.lastName,
|
||||
},
|
||||
);
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import request from 'supertest';
|
||||
import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-auth-tokens-from-login-token.util';
|
||||
import { deleteUser } from 'test/integration/graphql/utils/delete-user.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const APPLE_WORKSPACE_INVITE_HASH = 'apple.dev-invite-hash';
|
||||
|
||||
describe('updateWorkspaceMemberSettings and profile onboarding', () => {
|
||||
let newUserAccessToken: string | undefined;
|
||||
let originalIsPublicInviteLinkEnabled: boolean;
|
||||
|
||||
beforeAll(async () => {
|
||||
const currentWorkspaceQuery = gql`
|
||||
query CurrentWorkspacePublicInvite {
|
||||
currentWorkspace {
|
||||
isPublicInviteLinkEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const response = await makeMetadataAPIRequest(
|
||||
{
|
||||
query: currentWorkspaceQuery,
|
||||
variables: {},
|
||||
},
|
||||
APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
);
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.currentWorkspace).toBeDefined();
|
||||
|
||||
originalIsPublicInviteLinkEnabled =
|
||||
response.body.data.currentWorkspace.isPublicInviteLinkEnabled;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const restoreMutation = gql`
|
||||
mutation RestoreWorkspacePublicInvite($data: UpdateWorkspaceInput!) {
|
||||
updateWorkspace(data: $data) {
|
||||
id
|
||||
isPublicInviteLinkEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await makeMetadataAPIRequest(
|
||||
{
|
||||
query: restoreMutation,
|
||||
variables: {
|
||||
data: {
|
||||
isPublicInviteLinkEnabled: originalIsPublicInviteLinkEnabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!newUserAccessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteUser({
|
||||
accessToken: newUserAccessToken,
|
||||
expectToFail: false,
|
||||
});
|
||||
newUserAccessToken = undefined;
|
||||
});
|
||||
|
||||
it('should clear PROFILE_CREATION onboarding after saving first and last name via updateWorkspaceMemberSettings', async () => {
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
const enablePublicInviteLinkMutation = {
|
||||
query: `
|
||||
mutation UpdateWorkspace {
|
||||
updateWorkspace(data: { isPublicInviteLinkEnabled: true }) {
|
||||
id
|
||||
isPublicInviteLinkEnabled
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
await client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(enablePublicInviteLinkMutation)
|
||||
.expect(200)
|
||||
.expect((response) => {
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.updateWorkspace.isPublicInviteLinkEnabled).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
const uniqueEmail = `profile-onboarding-${randomUUID()}@example.com`;
|
||||
const password = 'Password123!';
|
||||
|
||||
const signUpInWorkspaceMutation = gql`
|
||||
mutation SignUpInWorkspace(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String
|
||||
$workspaceId: UUID
|
||||
) {
|
||||
signUpInWorkspace(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
workspaceId: $workspaceId
|
||||
) {
|
||||
loginToken {
|
||||
token
|
||||
}
|
||||
workspace {
|
||||
id
|
||||
workspaceUrls {
|
||||
subdomainUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const signUpResponse = await makeMetadataAPIRequest(
|
||||
{
|
||||
query: signUpInWorkspaceMutation,
|
||||
variables: {
|
||||
email: uniqueEmail,
|
||||
password,
|
||||
workspaceInviteHash: APPLE_WORKSPACE_INVITE_HASH,
|
||||
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(signUpResponse.status).toBe(200);
|
||||
expect(signUpResponse.body.errors).toBeUndefined();
|
||||
|
||||
const signUpPayload = signUpResponse.body.data.signUpInWorkspace;
|
||||
|
||||
expect(signUpPayload.loginToken.token).toBeDefined();
|
||||
|
||||
await testDataSource.query(
|
||||
'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1',
|
||||
[uniqueEmail],
|
||||
);
|
||||
|
||||
const origin =
|
||||
signUpPayload.workspace.workspaceUrls?.subdomainUrl ??
|
||||
'http://localhost:3001';
|
||||
|
||||
const {
|
||||
data: { getAuthTokensFromLoginToken: authTokensData },
|
||||
} = await getAuthTokensFromLoginToken({
|
||||
loginToken: signUpPayload.loginToken.token,
|
||||
origin,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
newUserAccessToken =
|
||||
authTokensData.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
|
||||
const currentUserWithOnboardingQuery = gql`
|
||||
query CurrentUserWithOnboarding {
|
||||
currentUser {
|
||||
id
|
||||
onboardingStatus
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const beforeNameUpdateResponse = await makeMetadataAPIRequest(
|
||||
{
|
||||
query: currentUserWithOnboardingQuery,
|
||||
variables: {},
|
||||
},
|
||||
newUserAccessToken,
|
||||
);
|
||||
|
||||
expect(beforeNameUpdateResponse.status).toBe(200);
|
||||
expect(beforeNameUpdateResponse.body.errors).toBeUndefined();
|
||||
expect(beforeNameUpdateResponse.body.data.currentUser.onboardingStatus).toBe(
|
||||
OnboardingStatus.PROFILE_CREATION,
|
||||
);
|
||||
|
||||
const workspaceMemberQuery = gql`
|
||||
query WorkspaceMemberForProfileOnboarding(
|
||||
$workspaceMemberFilter: WorkspaceMemberFilterInput!
|
||||
) {
|
||||
workspaceMember(filter: $workspaceMemberFilter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const workspaceMemberResponse = await makeGraphqlAPIRequest(
|
||||
{
|
||||
query: workspaceMemberQuery,
|
||||
variables: {
|
||||
workspaceMemberFilter: {
|
||||
userEmail: {
|
||||
eq: uniqueEmail,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
newUserAccessToken,
|
||||
);
|
||||
|
||||
expect(workspaceMemberResponse.status).toBe(200);
|
||||
expect(workspaceMemberResponse.body.errors).toBeUndefined();
|
||||
|
||||
const workspaceMemberId =
|
||||
workspaceMemberResponse.body.data.workspaceMember?.id;
|
||||
|
||||
expect(workspaceMemberId).toBeDefined();
|
||||
|
||||
const updateWorkspaceMemberSettingsMutation = gql`
|
||||
mutation UpdateWorkspaceMemberSettings(
|
||||
$input: UpdateWorkspaceMemberSettingsInput!
|
||||
) {
|
||||
updateWorkspaceMemberSettings(input: $input)
|
||||
}
|
||||
`;
|
||||
|
||||
const updateNameResponse = await makeMetadataAPIRequest(
|
||||
{
|
||||
query: updateWorkspaceMemberSettingsMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workspaceMemberId,
|
||||
update: {
|
||||
name: {
|
||||
firstName: 'Onboarding',
|
||||
lastName: 'Integration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
newUserAccessToken,
|
||||
);
|
||||
|
||||
expect(updateNameResponse.status).toBe(200);
|
||||
expect(updateNameResponse.body.errors).toBeUndefined();
|
||||
expect(updateNameResponse.body.data.updateWorkspaceMemberSettings).toBe(true);
|
||||
|
||||
const afterNameUpdateResponse = await makeMetadataAPIRequest(
|
||||
{
|
||||
query: currentUserWithOnboardingQuery,
|
||||
variables: {},
|
||||
},
|
||||
newUserAccessToken,
|
||||
);
|
||||
|
||||
expect(afterNameUpdateResponse.status).toBe(200);
|
||||
expect(afterNameUpdateResponse.body.errors).toBeUndefined();
|
||||
expect(
|
||||
afterNameUpdateResponse.body.data.currentUser.onboardingStatus,
|
||||
).not.toBe(OnboardingStatus.PROFILE_CREATION);
|
||||
expect(afterNameUpdateResponse.body.data.currentUser.onboardingStatus).toBe(
|
||||
OnboardingStatus.COMPLETED,
|
||||
);
|
||||
});
|
||||
});
|
||||
+26
-2
@@ -123,7 +123,7 @@ describe('workspace members permissions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should deny updateOne on /graphql when member updates themself', async () => {
|
||||
it('should deny /graphql updateOne on own record for standard field name.firstName when member lacks WORKSPACE_MEMBERS', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'workspaceMember',
|
||||
gqlFields: WORKSPACE_MEMBER_GQL_FIELDS,
|
||||
@@ -146,7 +146,31 @@ describe('workspace members permissions', () => {
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should deny update on custom field for another workspace member on /graphql (member role)', async () => {
|
||||
it('should deny /graphql updateOne on own record for a custom field when member lacks WORKSPACE_MEMBERS', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'workspaceMember',
|
||||
gqlFields: `
|
||||
id
|
||||
${customFieldName}
|
||||
`,
|
||||
recordId: WORKSPACE_MEMBER_DATA_SEED_IDS.JONY,
|
||||
data: {
|
||||
[customFieldName]: 'self-custom-value',
|
||||
},
|
||||
});
|
||||
|
||||
const response =
|
||||
await makeGraphqlAPIRequestWithMemberRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toStrictEqual({ updateWorkspaceMember: null });
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
PermissionsExceptionMessage.PERMISSION_DENIED,
|
||||
);
|
||||
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
|
||||
});
|
||||
|
||||
it('should deny /graphql updateOne on another workspace member for a custom field when member lacks WORKSPACE_MEMBERS', async () => {
|
||||
const customFieldValue = 'Ile-de-france';
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'workspaceMember',
|
||||
|
||||
Reference in New Issue
Block a user