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 @@ -6,7 +6,6 @@ import {
Button,
Chip,
ChipCombobox,
ChipConfirmModal,
ChipModal,
ChipModalBody,
ChipModalError,
Expand All @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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' })
}
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -659,19 +640,6 @@ export function General() {
/>
</ChipModal>

<ChipConfirmModal
open={showSignOutOtherDevicesModal}
onOpenChange={setShowSignOutOtherDevicesModal}
title='Sign out other devices'
text='This device and your authorized apps stay signed in.'
confirm={{
label: 'Sign out',
onClick: confirmSignOutOtherDevices,
pending: signOutOtherDevices.isPending,
pendingLabel: 'Signing out...',
}}
/>

<DeleteAccountModal
open={showDeleteAccountModal}
onOpenChange={setShowDeleteAccountModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,25 +194,6 @@ export function OrganizationMemberLists({
...(isSelf && isOwner && onTransferOwnership
? [{ label: 'Transfer ownership', onSelect: () => 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,
},
}),
},
]
: []),
])}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
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'
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,
Expand All @@ -30,6 +31,7 @@ import {
useMemberRemovalImpact,
useOrganization,
useOrganizationRoster,
useOrganizationSummary,
useRemoveMember,
useTransferOwnership,
} from '@/hooks/queries/organization'
Expand Down Expand Up @@ -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
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
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,
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down
7 changes: 6 additions & 1 deletion apps/sim/hooks/queries/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
14 changes: 0 additions & 14 deletions apps/sim/hooks/queries/user-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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')
},
})
}
6 changes: 1 addition & 5 deletions apps/sim/lib/workspaces/organization/index.ts
Original file line number Diff line number Diff line change
@@ -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'
10 changes: 0 additions & 10 deletions apps/sim/lib/workspaces/organization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
28 changes: 0 additions & 28 deletions apps/sim/lib/workspaces/organization/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
2 changes: 0 additions & 2 deletions scripts/check-unused-exports.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading