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!
|
||||
|
||||
Reference in New Issue
Block a user