feat(server): workspace opt-out of root-domain directory listing#22423
feat(server): workspace opt-out of root-domain directory listing#22423FelixMalfait wants to merge 8 commits into
Conversation
Adds an isDirectoryListingEnabled flag (default true) on the workspace. When disabled, the workspace is excluded from approved-access-domain discovery in findAvailableWorkspacesByEmail, so it no longer appears in the multi-workspace root-domain picker for users who match its email domain. Existing members and explicitly invited users are unaffected. The flag is a SECURITY-permissioned field on updateWorkspace and is added via a fast instance command.
|
🚀 Preview Environment Ready! Your preview environment is available at: https://identify-supervisors-disturbed-scope.trycloudflare.com This environment will automatically shut down after 5 hours. |
| @Column({ default: true }) | ||
| isPublicInviteLinkEnabled: boolean; | ||
|
|
||
| // When false, the workspace is hidden from approved-access-domain discovery |
🔍 Automated Pre-Review✅ No issues detected - This PR is ready for human review. Automated pre-review — human approval still required. |
…ypes Adds a 'List in workspace directory' toggle under Settings > Security (shown only in multi-workspace mode) that flips isDirectoryListingEnabled via updateWorkspace, and threads the field through the current-user fragment, CurrentWorkspace type and mocks. Regenerates the metadata and client-sdk GraphQL types for the new field.
There was a problem hiding this comment.
1 issue found across 10 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx">
<violation number="1" location="packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySettings.tsx:157">
P2: If the mutation fails (network error, validation, etc.), the toggle will stay in the changed position despite the server rejecting it — the local state is optimistically updated but never rolled back on error. This is the same pattern as handleSyncInternalEmailsChange in this file, so it's consistent, but adding a rollback in the .catch() would make the UI more reliable and avoid confusing the user.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The new isDirectoryListingEnabled column on WorkspaceEntity is a cached field, so it is required on FlatWorkspace. Add the missing mapping to fix the tsgo typecheck error. Claude-Session: https://claude.ai/code/session_01GwCx8XsGh7g4yx6C9ctbYx
🔍 Visual Regression Review —
|
| Story | Verdict | Confidence | Explained by | |
|---|---|---|---|---|
| 🟡 | modules-settings-playground-graphqlplayground--default |
uncertain | 62% | — |
| 🟡 | modules-settings-accounts-blocklist-settingsaccountsblocklistinput--default |
uncertain | 65% | — |
| 🟡 | ui-feedback-snackbarmanager-snackbar--default |
uncertain | 70% | — |
Changed stories
| Story | Diff % |
|---|---|
| modules-settings-playground-graphqlplayground--default | 3% |
| modules-settings-accounts-blocklist-settingsaccountsblocklistinput--default | 1% |
| ui-feedback-snackbarmanager-snackbar--default | 0% |
| modules-objectrecord-recordcalendar-month--default | 0% |
View run details · advisory mode
…tation The Settings > Security directory-listing toggle updates the field via updateWorkspace but the mutation did not select it back, so the Apollo cache kept a stale value (unlike its mirror isInternalMessagesImportEnabled). Select the field in the mutation and regenerate the metadata GraphQL types. Claude-Session: https://claude.ai/code/session_01GwCx8XsGh7g4yx6C9ctbYx
Address review feedback. Claude-Session: https://claude.ai/code/session_01GwCx8XsGh7g4yx6C9ctbYx
| <SettingsOptionCardContentToggle | ||
| Icon={IconList} | ||
| title={t`List in workspace directory`} | ||
| description={t`Let people whose email domain matches an approved access domain find this workspace on the sign-in screen. Members and invited users are unaffected.`} |
There was a problem hiding this comment.
Is this linked to approved domain?
I feel like this explanation is wrong let's be more explicit, says that's it's about app.Twenty.com (or replace with whatever url it is). Move to the authentication section. Make the test very clear by also maybe mentioning current workspace url (not sure if a good idea because it might be too long)
Address review feedback: relocate the 'List in workspace directory' toggle from a standalone Discovery section into the Authentication section (grouped with Invite by Link), and rewrite the description to be explicit about the root multi-workspace sign-in domain and its link to approved access domains. The domain is read from client config so it is correct for self-hosted instances. Optimistically updates with rollback on error, matching the sibling auth toggles. Claude-Session: https://claude.ai/code/session_01GwCx8XsGh7g4yx6C9ctbYx
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Address review feedback: read the latest currentWorkspace value in both the optimistic update and the error rollback via Jotai's functional updater, so a concurrent workspace-state change during an in-flight request isn't overwritten by a stale closure snapshot. Claude-Session: https://claude.ai/code/session_01GwCx8XsGh7g4yx6C9ctbYx
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx">
<violation number="1" location="packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx:143">
P2: The optimistic rollback in `handleDirectoryListingChange` unconditionally applies `!value` for any failed request. Because `value` is captured in the closure, if a user toggles rapidly and two requests overlap, the later rollback can overwrite the correct state restored by the earlier one. The `Toggle` component has no debounce and the handler never disables it during a mutation, so this overlap is possible.
A simple guard is to only rollback when the current atom value still matches the optimistic value that was applied (`isDirectoryListingEnabled === value`). This prevents a stale-failure handler from corrupting state that has since been updated by another toggle.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| return; | ||
| } | ||
|
|
||
| setCurrentWorkspace((currentWorkspaceValue) => |
There was a problem hiding this comment.
P2: The optimistic rollback in handleDirectoryListingChange unconditionally applies !value for any failed request. Because value is captured in the closure, if a user toggles rapidly and two requests overlap, the later rollback can overwrite the correct state restored by the earlier one. The Toggle component has no debounce and the handler never disables it during a mutation, so this overlap is possible.
A simple guard is to only rollback when the current atom value still matches the optimistic value that was applied (isDirectoryListingEnabled === value). This prevents a stale-failure handler from corrupting state that has since been updated by another toggle.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/settings/security/components/SettingsSecurityAuthProvidersOptionsList.tsx, line 143:
<comment>The optimistic rollback in `handleDirectoryListingChange` unconditionally applies `!value` for any failed request. Because `value` is captured in the closure, if a user toggles rapidly and two requests overlap, the later rollback can overwrite the correct state restored by the earlier one. The `Toggle` component has no debounce and the handler never disables it during a mutation, so this overlap is possible.
A simple guard is to only rollback when the current atom value still matches the optimistic value that was applied (`isDirectoryListingEnabled === value`). This prevents a stale-failure handler from corrupting state that has since been updated by another toggle.</comment>
<file context>
@@ -140,10 +140,11 @@ export const SettingsSecurityAuthProvidersOptionsList = () => {
- ...currentWorkspace,
- isDirectoryListingEnabled: value,
- });
+ setCurrentWorkspace((currentWorkspaceValue) =>
+ currentWorkspaceValue
+ ? { ...currentWorkspaceValue, isDirectoryListingEnabled: value }
</file context>
--> I think we should revisit this and cover both. Not sure if it should be a dropdown, two toggles, etc? UX need to be clear for user. @claude please look at this in depth and describe all options (with text) + make recommandations |
What
Lets a workspace opt out of being surfaced in the multi-workspace root-domain (app.twenty.com) picker via email-domain discovery.
Adds
isDirectoryListingEnabled(defaulttrue) on the workspace. Whenfalse, the workspace is filtered out of the approved-access-domain branch offindAvailableWorkspacesByEmail, so a user whose email domain matches an approved access domain no longer sees the workspace in the sign-up picker.Scope of the opt-out (deliberately narrow)
The filter is applied only to the approved-access-domain discovery source:
availableWorkspacesForSignIn) — never filtered; they keep access.A hidden workspace stays fully reachable by members and invited users via the direct workspace subdomain; it just isn't advertised in the global picker.
Changes
Backend
workspace.entity.ts— newisDirectoryListingEnabledcolumn (@Field, defaulttrue).user-workspace.service.ts— filter the approved-access-domain branch on the flag.update-workspace-input.ts— expose the field onupdateWorkspace.workspace.service.ts—PermissionFlagType.SECURITY(same as the other discovery/security toggles).true, so no existing workspace is hidden).Frontend
updateWorkspace, mirroring the existingisInternalMessagesImportEnabledtoggle.CurrentWorkspacetype, and mock data.generated-metadata,twenty-client-sdk/.../generated) — generated against a server booted from this branch.Verification
tsgotypecheck: 0 errors.oxlint: 0/0.oxfmt: clean.