diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
index 22511883b7a..dd916ade3c1 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx
@@ -6,7 +6,6 @@ import {
Button,
Chip,
ChipCombobox,
- ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalError,
@@ -18,11 +17,9 @@ import {
Label,
Switch,
Tooltip,
- toast,
} from '@sim/emcn'
import { Camera, Check, CircleInfo, Pencil } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
-import { getErrorMessage } from '@sim/utils/errors'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
@@ -50,7 +47,6 @@ import { useBrandConfig } from '@/ee/whitelabeling'
import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
import {
useResetPassword,
- useSignOutOtherDevices,
useUpdateUserProfile,
useUserProfile,
} from '@/hooks/queries/user-profile'
@@ -122,8 +118,6 @@ export function General() {
})
const [showResetPasswordModal, setShowResetPasswordModal] = useState(false)
const resetPassword = useResetPassword()
- const [showSignOutOtherDevicesModal, setShowSignOutOtherDevicesModal] = useState(false)
- const signOutOtherDevices = useSignOutOtherDevices()
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
@@ -227,14 +221,6 @@ export function General() {
)
}
- const confirmSignOutOtherDevices = () => {
- signOutOtherDevices.mutate(undefined, {
- onSuccess: () => toast.success('Signed out of other devices'),
- onError: (error) => toast.error(getErrorMessage(error)),
- onSettled: () => setShowSignOutOtherDevicesModal(false),
- })
- }
-
const handleThemeChange = async (value: string) => {
await updateSetting.mutateAsync({ key: 'theme', value: value as 'system' | 'light' | 'dark' })
}
@@ -302,11 +288,6 @@ export function General() {
...(session?.user?.id && !isAuthDisabled
? [
{ id: 'sign-out', text: 'Sign out', onSelect: () => signOutAndRedirect(router.push) },
- {
- id: 'sign-out-other-devices',
- text: 'Sign out other devices',
- onSelect: () => setShowSignOutOtherDevicesModal(true),
- },
{
id: 'reset-password',
text: 'Reset password',
@@ -659,19 +640,6 @@ export function General() {
/>
-
-
onTransferOwnership() }]
: []),
- ...(canManage && isSelf && !isOwner
- ? [
- {
- label: 'Leave organization',
- destructive: true,
- onSelect: () =>
- onRemoveMember({
- id: member.memberId,
- role: member.role,
- user: {
- id: member.userId,
- name: member.name,
- email: member.email,
- image: member.image,
- },
- }),
- },
- ]
- : []),
])}
/>
)
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
index e60537723d2..df43655f00c 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/team-management.tsx
@@ -3,6 +3,7 @@
import { useState } from 'react'
import { Plus } from '@sim/emcn'
import { createLogger } from '@sim/logger'
+import { isOrgAdminRole } from '@sim/platform-authz/predicates'
import { getErrorMessage } from '@sim/utils/errors'
import { useSettingsUnsavedGuard } from '@/components/settings/use-settings-unsaved-guard'
import { useSession } from '@/lib/auth/client'
@@ -10,7 +11,7 @@ import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { APP_ENTRY_PATH } from '@/lib/navigation/paths'
-import { generateSlug, isAdminOrOwner, type Member } from '@/lib/workspaces/organization'
+import { generateSlug, type Member } from '@/lib/workspaces/organization'
import { InviteModal } from '@/app/workspace/[workspaceId]/components/invite-modal'
import {
SettingsEmptyState,
@@ -30,6 +31,7 @@ import {
useMemberRemovalImpact,
useOrganization,
useOrganizationRoster,
+ useOrganizationSummary,
useRemoveMember,
useTransferOwnership,
} from '@/hooks/queries/organization'
@@ -83,7 +85,33 @@ export function TeamManagement({
const hasTeamPlan = subscriptionAccess.hasUsableTeamAccess
const hasEnterprisePlan = subscriptionAccess.hasUsableEnterpriseAccess
- const adminOrOwner = isAdminOrOwner(organization, session?.user?.email)
+ const {
+ data: roster,
+ isLoading: isLoadingRoster,
+ error: rosterError,
+ isFetchedAfterMount: isRosterFetchedAfterMount,
+ isFetching: isRosterFetching,
+ refetch: refetchRoster,
+ } = useOrganizationRoster(organizationId)
+
+ // Only a row naming the caller (the roster) or the server's own role read is authoritative:
+ // Better Auth's full-organization read caps its member list and can omit or lag the caller.
+ const rosterRole = roster?.members.find((member) => member.userId === session?.user?.id)?.role
+ const organizationSummary = useOrganizationSummary(organizationId, {
+ enabled: rosterError !== null,
+ refetchOnMount: 'always',
+ })
+ // Fetched on this mount, so the identity-scoped role belongs to the current session.
+ const summaryRole = organizationSummary.isFetchedAfterMount
+ ? organizationSummary.data?.userRole
+ : undefined
+ const confirmedRole = rosterRole ?? summaryRole
+ const adminOrOwner = isOrgAdminRole(
+ confirmedRole ??
+ organization?.members.find((member) => member.user.email === session?.user?.email)?.role ??
+ 'member'
+ )
+ const canLeave = confirmedRole !== undefined && confirmedRole !== 'owner'
const {
data: organizationBillingData,
@@ -94,15 +122,6 @@ export function TeamManagement({
refetch: refetchOrganizationBilling,
} = useOrganizationPlanSeats(organizationId, { enabled: billingEnabled && adminOrOwner })
- const {
- data: roster,
- isLoading: isLoadingRoster,
- error: rosterError,
- isFetchedAfterMount: isRosterFetchedAfterMount,
- isFetching: isRosterFetching,
- refetch: refetchRoster,
- } = useOrganizationRoster(organizationId)
-
const removeMemberMutation = useRemoveMember()
const transferOwnershipMutation = useTransferOwnership()
const openBillingPortal = useOpenBillingPortal()
@@ -224,6 +243,17 @@ export function TeamManagement({
})
}
+ const handleLeave = () => {
+ if (!session?.user) return
+ setRemoveMemberDialog({
+ open: true,
+ memberId: session.user.id,
+ memberName: 'yourself',
+ isSelfRemoval: true,
+ isExternalRemoval: false,
+ })
+ }
+
const confirmRemoveMember = async () => {
const { memberId, isSelfRemoval } = removeMemberDialog
if (!session?.user || !memberId) return
@@ -365,20 +395,31 @@ export function TeamManagement({
onChange: setMemberQuery,
placeholder: 'Search members...',
}}
- actions={
- adminOrOwner
+ actions={[
+ ...(canLeave
? [
{
+ id: 'leave-organization',
+ text: 'Leave organization',
+ variant: 'destructive' as const,
+ onSelect: handleLeave,
+ },
+ ]
+ : []),
+ ...(adminOrOwner
+ ? [
+ {
+ id: 'invite',
text: 'Invite',
icon: Plus,
- variant: 'primary',
+ variant: 'primary' as const,
onSelect: () => setInviteModalOpen(true),
disabled: invitationsDisabled,
tooltip: invitationsDisabled ? 'Invitations are disabled' : undefined,
},
]
- : []
- }
+ : []),
+ ]}
>
{billingEnabled &&
adminOrOwner &&
diff --git a/apps/sim/hooks/queries/organization.ts b/apps/sim/hooks/queries/organization.ts
index 6d58a60e748..f05215db781 100644
--- a/apps/sim/hooks/queries/organization.ts
+++ b/apps/sim/hooks/queries/organization.ts
@@ -49,8 +49,13 @@ export const ORGANIZATION_REMOVAL_IMPACT_STALE_TIME = 0
export { organizationKeys }
/** Revalidates organization membership and chat policy through the shared authorized read. */
-export function useOrganizationSummary(organizationId: string) {
+export function useOrganizationSummary(
+ organizationId: string,
+ options?: { enabled?: boolean; refetchOnMount?: 'always' }
+) {
return useQuery({
+ enabled: options?.enabled ?? true,
+ refetchOnMount: options?.refetchOnMount ?? true,
queryKey: organizationKeys.summary(organizationId),
queryFn: ({ signal }) =>
requestJson(getOrganizationContract, {
diff --git a/apps/sim/hooks/queries/user-profile.ts b/apps/sim/hooks/queries/user-profile.ts
index f7bd779f471..a482ebfbbbe 100644
--- a/apps/sim/hooks/queries/user-profile.ts
+++ b/apps/sim/hooks/queries/user-profile.ts
@@ -8,7 +8,6 @@ import {
type UpdateUserProfileBody,
updateUserProfileContract,
} from '@/lib/api/contracts/user'
-import { client } from '@/lib/auth/client'
import {
mapUserProfileResponse,
USER_PROFILE_STALE_TIME,
@@ -89,16 +88,3 @@ export function useResetPassword() {
},
})
}
-
-/**
- * Ends every session except this one. Better Auth's plugin endpoint is called through the auth
- * client, as no Sim contract types it.
- */
-export function useSignOutOtherDevices() {
- return useMutation({
- mutationFn: async () => {
- const { error } = await client.revokeOtherSessions()
- if (error) throw new Error(error.message ?? 'Failed to sign out other devices')
- },
- })
-}
diff --git a/apps/sim/lib/workspaces/organization/index.ts b/apps/sim/lib/workspaces/organization/index.ts
index 20f39ac9f43..4ae911f56b7 100644
--- a/apps/sim/lib/workspaces/organization/index.ts
+++ b/apps/sim/lib/workspaces/organization/index.ts
@@ -1,8 +1,4 @@
// Export types
export type { Member } from '@/lib/workspaces/organization/types'
// Export utility functions
-export {
- generateSlug,
- getUserRole,
- isAdminOrOwner,
-} from '@/lib/workspaces/organization/utils'
+export { generateSlug } from '@/lib/workspaces/organization/utils'
diff --git a/apps/sim/lib/workspaces/organization/types.ts b/apps/sim/lib/workspaces/organization/types.ts
index ce817a5d5c9..58940e5a382 100644
--- a/apps/sim/lib/workspaces/organization/types.ts
+++ b/apps/sim/lib/workspaces/organization/types.ts
@@ -10,13 +10,3 @@ export interface Member {
role: string
user?: User
}
-
-export interface Organization {
- id: string
- name: string
- slug: string
- logo?: string | null
- members?: Member[]
- createdAt: string | Date
- [key: string]: unknown
-}
diff --git a/apps/sim/lib/workspaces/organization/utils.ts b/apps/sim/lib/workspaces/organization/utils.ts
index f81470f3aec..7b64801e85b 100644
--- a/apps/sim/lib/workspaces/organization/utils.ts
+++ b/apps/sim/lib/workspaces/organization/utils.ts
@@ -3,34 +3,6 @@
* These are pure functions that compute values from organization data
*/
-import { isOrgAdminRole } from '@sim/platform-authz/predicates'
-import type { Organization } from '@/lib/workspaces/organization/types'
-
-/**
- * Get the role of a user in an organization
- */
-export function getUserRole(
- organization: Organization | null | undefined,
- userEmail?: string
-): string {
- if (!userEmail || !organization?.members) {
- return 'member'
- }
- const currentMember = organization.members.find((m) => m.user?.email === userEmail)
- return currentMember?.role ?? 'member'
-}
-
-/**
- * Check if a user is an admin or owner in an organization
- */
-export function isAdminOrOwner(
- organization: Organization | null | undefined,
- userEmail?: string
-): boolean {
- const role = getUserRole(organization, userEmail)
- return isOrgAdminRole(role)
-}
-
/**
* Generate a URL-friendly slug from a name
*/
diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json
index e4448b682b2..262099b9b3f 100644
--- a/scripts/check-unused-exports.baseline.json
+++ b/scripts/check-unused-exports.baseline.json
@@ -5235,8 +5235,6 @@
"apps/sim/lib/workspaces/operations/receipts.ts#WorkspaceOperationIssue",
"apps/sim/lib/workspaces/operations/receipts.ts#WorkspaceOperationKind",
"apps/sim/lib/workspaces/operations/receipts.ts#WorkspaceOperationStatus",
- "apps/sim/lib/workspaces/organization/index.ts#getUserRole",
- "apps/sim/lib/workspaces/organization/utils.ts#getUserRole",
"apps/sim/lib/workspaces/permissions/utils.ts#MemberRoleSource",
"apps/sim/lib/workspaces/policy.ts#CONTACT_OWNER_TO_UPGRADE_REASON",
"apps/sim/lib/workspaces/policy.ts#UPGRADE_TO_INVITE_REASON",