Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Verified Domains let organization owners and admins on Enterprise plans prove th

## Verify a domain

Go to **Settings → Security → Verified domains** in your organization settings.
Go to **Settings → Security → Single sign-on** in your organization settings. Domains are managed in the **Verified domains** section at the top of that page, directly above the identity provider configuration.

1. Enter the domain, for example `acme.com`, and click **Add domain**.
2. Sim shows a DNS **TXT record** to publish — a host (`_sim-challenge.acme.com`) and a unique value (`sim-domain-verification=…`).
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/auth/sso/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const domainNotVerifiedResponse = () =>
NextResponse.json(
{
error: `Verify ownership of ${domain} under Settings → Verified domains before configuring SSO for it.`,
error: `Verify ownership of ${domain} under Verified domains above before configuring SSO for it.`,
code: 'SSO_DOMAIN_NOT_VERIFIED',
},
{ status: 403 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const SECTION_ALIASES: Readonly<Record<string, SettingsSection>> = {
subscription: 'billing',
team: 'organization',
'api-keys': 'apikeys',
// Verified domains moved into the SSO page; keep old links working.
domains: 'sso',
}

const TOP_LEVEL_REDIRECTS: Readonly<Record<string, (workspaceId: string) => string>> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,57 @@ export const groupIdUrlKeys = {
clearOnDefault: true,
} as const

/**
* `group-tab` is the active tab inside the deep-linked permission-group detail
* view, so a shared `group-id` link can land on the same tab (mirrors
* `server-tab` on the workflow MCP server detail).
*/
export const groupTabParam = {
key: 'group-tab',
parser: parseAsStringLiteral(['general', 'providers', 'blocks', 'platform'] as const).withDefault(
'general'
),
} as const

/** Tab view-state: clean URLs, no back-stack churn. */
export const groupTabUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const

/**
* `group-search` is the search box inside the permission-group detail view. The
* provider/block/platform tabs never render together, so they share one param
* rather than carrying three mutually-exclusive keys; the tab handler clears it
* so a query cannot bleed across tabs. Distinct from the list's shared
* `?search=` (`useSettingsSearch`), which belongs to the group list behind it.
*/
export const groupSearchParam = {
key: 'group-search',
parser: parseAsString.withDefault(''),
} as const

/** Search view-state: clean URLs, no back-stack churn. */
export const groupSearchUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const

/**
* `group-status` filters the permission-group detail's toggle lists by enabled
* state. Shared across the tabs for the same reason as `group-search`.
*/
export const groupStatusParam = {
key: 'group-status',
parser: parseAsStringLiteral(['all', 'enabled', 'disabled'] as const).withDefault('all'),
} as const

/** Filter view-state: clean URLs, no back-stack churn. */
export const groupStatusUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const

/**
* `custom-block-id` deep-links the Custom Blocks settings tab to a specific
* block's detail sub-view. The "create new" flow stays in local state — only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ const AuditLogs = dynamic(() =>
import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs)
)
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO))
const DomainSettings = dynamic(() =>
import('@/ee/sso/components/domain-settings').then((m) => m.DomainSettings)
)
const SessionPolicySettings = dynamic(() =>
import('@/ee/session-policy/components/session-policy-settings').then(
(m) => m.SessionPolicySettings
Expand Down Expand Up @@ -166,9 +163,6 @@ export function SettingsPage({ section }: SettingsPageProps) {
/>
)}
{effectiveSection === 'sso' && organizationId && <SSO organizationId={organizationId} />}
{effectiveSection === 'domains' && organizationId && (
<DomainSettings key={organizationId} organizationId={organizationId} />
)}
{effectiveSection === 'sessions' && organizationId && (
<SessionPolicySettings key={organizationId} organizationId={organizationId} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ describe('unified settings navigation', () => {
{ id: 'inbox', label: 'Sim mailer', section: 'system' },
{ id: 'recently-deleted', label: 'Recently deleted', section: 'system' },
{ id: 'sso', label: 'Single sign-on', section: 'enterprise' },
{ id: 'domains', label: 'Verified domains', section: 'enterprise' },
{ id: 'sessions', label: 'Session policies', section: 'enterprise' },
{ id: 'data-retention', label: 'Data retention', section: 'enterprise' },
{ id: 'data-drains', label: 'Data drains', section: 'enterprise' },
Expand Down
3 changes: 0 additions & 3 deletions apps/sim/components/settings/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ describe('settings navigation boundaries', () => {
'inbox',
'recently-deleted',
'sso',
'domains',
'sessions',
'data-retention',
'data-drains',
Expand All @@ -65,7 +64,6 @@ describe('settings navigation boundaries', () => {
'access-control',
'audit-logs',
'sso',
'domains',
'sessions',
'data-retention',
'data-drains',
Expand Down Expand Up @@ -121,7 +119,6 @@ describe('settings navigation boundaries', () => {
'billing',
'data-drains',
'data-retention',
'domains',
'organization',
'sessions',
'sso',
Expand Down
30 changes: 6 additions & 24 deletions apps/sim/components/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
HexSimple,
Key,
KeySquare,
Link,
Lock,
LogIn,
Palette,
Expand Down Expand Up @@ -43,7 +42,6 @@ export type OrganizationSettingsSection =
| 'access-control'
| 'audit-logs'
| 'sso'
| 'domains'
| 'sessions'
| 'data-retention'
| 'data-drains'
Expand Down Expand Up @@ -90,7 +88,6 @@ export type UnifiedSettingsSection =
| 'teammates'
| 'organization'
| 'sso'
| 'domains'
Comment thread
waleedlatif1 marked this conversation as resolved.
| 'whitelabeling'
| 'copilot'
| 'forks'
Expand Down Expand Up @@ -223,6 +220,8 @@ export const ACCOUNT_SETTINGS_PATH_ALIASES = {

export const ORGANIZATION_SETTINGS_PATH_ALIASES = {
organization: 'members',
// Verified domains moved into the SSO page; keep old links working.
domains: 'sso',
} as const satisfies Readonly<Record<string, OrganizationSettingsSection>>

export const WORKSPACE_SETTINGS_PATH_ALIASES = {
Expand Down Expand Up @@ -544,22 +543,6 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
organization: { id: 'sso', group: 'security', order: 4 },
},
},
{
label: 'Verified domains',
icon: Link,
docsLink: 'https://docs.sim.ai/platform/enterprise/verified-domains',
unified: {
id: 'domains',
description: 'Prove ownership of your email domains before configuring SSO.',
group: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
},
planes: {
organization: { id: 'domains', group: 'security', order: 5 },
},
},
{
label: 'Session policies',
icon: Clock,
Expand All @@ -573,7 +556,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies,
},
planes: {
organization: { id: 'sessions', group: 'security', order: 6 },
organization: { id: 'sessions', group: 'security', order: 5 },
},
},
{
Expand All @@ -590,7 +573,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention,
},
planes: {
organization: { id: 'data-retention', group: 'enterprise', order: 7 },
organization: { id: 'data-retention', group: 'enterprise', order: 6 },
},
},
{
Expand All @@ -606,7 +589,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains,
},
planes: {
organization: { id: 'data-drains', group: 'enterprise', order: 8 },
organization: { id: 'data-drains', group: 'enterprise', order: 7 },
},
},
{
Expand All @@ -622,7 +605,7 @@ export const SETTINGS_SECTION_REGISTRY: readonly SettingsSectionRegistryEntry[]
selfHostedOverride: SETTINGS_SELF_HOSTED_OVERRIDES.whitelabeling,
},
planes: {
organization: { id: 'whitelabeling', group: 'enterprise', order: 9 },
organization: { id: 'whitelabeling', group: 'enterprise', order: 8 },
},
},
{
Expand Down Expand Up @@ -758,7 +741,6 @@ export function getOrganizationSettingsFeatures(
'access-control': SETTINGS_SELF_HOSTED_OVERRIDES.accessControl,
'audit-logs': SETTINGS_SELF_HOSTED_OVERRIDES.auditLogs,
sso: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
domains: SETTINGS_SELF_HOSTED_OVERRIDES.sso,
sessions: SETTINGS_SELF_HOSTED_OVERRIDES.sessionPolicies,
'data-retention': SETTINGS_SELF_HOSTED_OVERRIDES.dataRetention,
'data-drains': SETTINGS_SELF_HOSTED_OVERRIDES.dataDrains,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ const AuditLogs = dynamic(() =>
import('@/ee/audit-logs/components/audit-logs').then((module) => module.AuditLogs)
)
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((module) => module.SSO))
const DomainSettings = dynamic(() =>
import('@/ee/sso/components/domain-settings').then((module) => module.DomainSettings)
)
const SessionPolicySettings = dynamic(() =>
import('@/ee/session-policy/components/session-policy-settings').then(
(module) => module.SessionPolicySettings
Expand Down Expand Up @@ -71,9 +68,6 @@ export function OrganizationSettingsRenderer({
}
if (section === 'audit-logs') return <AuditLogs organizationId={organizationId} />
if (section === 'sso') return <SSO organizationId={organizationId} />
if (section === 'domains') {
return <DomainSettings key={organizationId} organizationId={organizationId} />
}
if (section === 'sessions') {
return <SessionPolicySettings key={organizationId} organizationId={organizationId} />
}
Expand Down
51 changes: 48 additions & 3 deletions apps/sim/ee/access-control/components/access-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import { getEnv, isTruthy } from '@/lib/core/config/env'
import {
groupIdParam,
groupIdUrlKeys,
groupSearchParam,
groupSearchUrlKeys,
groupStatusParam,
groupStatusUrlKeys,
groupTabParam,
groupTabUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/[section]/search-params'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
Expand Down Expand Up @@ -85,6 +91,45 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
...groupIdParam.parser,
...groupIdUrlKeys,
})

// Params scoped to the detail sub-view are cleared alongside the group id, so
// a tab/search/filter can't linger on the list URL after going back. nuqs
// batches these same-tick writes into a single URL update.
const [, setGroupTab] = useQueryState(groupTabParam.key, {
...groupTabParam.parser,
...groupTabUrlKeys,
})
const [, setGroupSearch] = useQueryState(groupSearchParam.key, {
...groupSearchParam.parser,
...groupSearchUrlKeys,
})
const [, setGroupStatus] = useQueryState(groupStatusParam.key, {
...groupStatusParam.parser,
...groupStatusUrlKeys,
})

/**
* The detail view's tab/search/status params are scoped to one group, so both
* transitions reset them — otherwise a stale `group-id` that never resolves
* leaves them in the URL and the next group opens on the previous group's tab
* and filters. nuqs batches these same-tick writes into one URL update.
*/
const openGroupDetail = useCallback(
(groupId: string) => {
void setSelectedGroupId(groupId)
void setGroupTab(null)
void setGroupSearch(null)
void setGroupStatus(null)
},
[setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus]
)

const closeGroupDetail = useCallback(() => {
void setSelectedGroupId(null, { history: 'replace' })
void setGroupTab(null)
void setGroupSearch(null)
void setGroupStatus(null)
}, [setSelectedGroupId, setGroupTab, setGroupSearch, setGroupStatus])
const [showCreateModal, setShowCreateModal] = useState(false)
const [newGroupName, setNewGroupName] = useState('')
const [newGroupDescription, setNewGroupDescription] = useState('')
Expand Down Expand Up @@ -169,8 +214,8 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
workspaceOptions={workspaceOptions}
organizationWorkspaces={organizationWorkspaces}
workspacesLoading={workspacesLoading}
onBack={() => void setSelectedGroupId(null, { history: 'replace' })}
onDeleted={() => void setSelectedGroupId(null, { history: 'replace' })}
onBack={closeGroupDetail}
onDeleted={closeGroupDetail}
Comment thread
waleedlatif1 marked this conversation as resolved.
/>
)
}
Expand Down Expand Up @@ -207,7 +252,7 @@ export function AccessControl({ isOrganizationAdmin, organizationId }: AccessCon
<button
key={group.id}
type='button'
onClick={() => void setSelectedGroupId(group.id)}
onClick={() => openGroupDetail(group.id)}
className='flex items-center gap-2.5 rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
>
<div className='flex min-w-0 flex-1 flex-col'>
Expand Down
Loading
Loading