Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions apps/docs/content/docs/en/enterprise/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ For self-hosted deployments, enterprise features can be enabled via environment
| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions |
| `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC |
| `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers |
| `INBOX_ENABLED`, `NEXT_PUBLIC_INBOX_ENABLED` | Sim Mailer inbox for outbound email |
| `WHITELABELING_ENABLED`, `NEXT_PUBLIC_WHITELABELING_ENABLED` | Custom branding and white-labeling |
| `AUDIT_LOGS_ENABLED`, `NEXT_PUBLIC_AUDIT_LOGS_ENABLED` | Audit logging for compliance and monitoring |
| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations |

### Organization Management
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/permission-groups/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ const configSchema = z.object({
hideKnowledgeBaseTab: z.boolean().optional(),
hideTablesTab: z.boolean().optional(),
hideCopilot: z.boolean().optional(),
hideIntegrationsTab: z.boolean().optional(),
hideSecretsTab: z.boolean().optional(),
hideApiKeysTab: z.boolean().optional(),
hideInboxTab: z.boolean().optional(),
hideEnvironmentTab: z.boolean().optional(),
hideFilesTab: z.boolean().optional(),
disableMcpTools: z.boolean().optional(),
disableCustomTools: z.boolean().optional(),
disableSkills: z.boolean().optional(),
hideTemplates: z.boolean().optional(),
disableInvitations: z.boolean().optional(),
disablePublicApi: z.boolean().optional(),
hideDeployApi: z.boolean().optional(),
hideDeployMcp: z.boolean().optional(),
hideDeployA2a: z.boolean().optional(),
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/permission-groups/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ const configSchema = z.object({
hideKnowledgeBaseTab: z.boolean().optional(),
hideTablesTab: z.boolean().optional(),
hideCopilot: z.boolean().optional(),
hideIntegrationsTab: z.boolean().optional(),
hideSecretsTab: z.boolean().optional(),
hideApiKeysTab: z.boolean().optional(),
hideInboxTab: z.boolean().optional(),
hideEnvironmentTab: z.boolean().optional(),
hideFilesTab: z.boolean().optional(),
disableMcpTools: z.boolean().optional(),
disableCustomTools: z.boolean().optional(),
disableSkills: z.boolean().optional(),
hideTemplates: z.boolean().optional(),
disableInvitations: z.boolean().optional(),
disablePublicApi: z.boolean().optional(),
hideDeployApi: z.boolean().optional(),
hideDeployMcp: z.boolean().optional(),
hideDeployA2a: z.boolean().optional(),
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/app/api/settings/allowed-providers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags'

export async function GET() {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

return NextResponse.json({
blacklistedProviders: getBlacklistedProvidersFromEnv(),
})
}
47 changes: 47 additions & 0 deletions apps/sim/app/api/users/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { db } from '@sim/db'
import { user, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, ne, sql } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { captureServerEvent } from '@/lib/posthog/server'

const logger = createLogger('DeleteUserAPI')

export const dynamic = 'force-dynamic'

export async function DELETE() {
const requestId = generateRequestId()

try {
const session = await getSession()

if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized account deletion attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const userId = session.user.id

captureServerEvent(userId, 'user_deleted', {})
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

await db.transaction(async (tx) => {
await tx
.update(workspace)
.set({ billedAccountUserId: sql`owner_id` })
.where(and(eq(workspace.billedAccountUserId, userId), ne(workspace.ownerId, userId)))

await tx.delete(workspace).where(eq(workspace.ownerId, userId))

await tx.delete(user).where(eq(user.id, userId))
})

logger.info(`[${requestId}] User account deleted`, { userId })

return NextResponse.json({ success: true })
} catch (error) {
logger.error(`[${requestId}] Account deletion error`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
import { prefetchGeneralSettings, prefetchUserProfile } from './prefetch'
import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch'
import { SettingsPage } from './settings'

const SECTION_TITLES: Record<string, string> = {
Expand Down Expand Up @@ -46,6 +46,7 @@ export default async function SettingsSectionPage({

void prefetchGeneralSettings(queryClient)
void prefetchUserProfile(queryClient)
void prefetchSubscriptionData(queryClient)
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

return (
<HydrationBoundary state={dehydrate(queryClient)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { QueryClient } from '@tanstack/react-query'
import { headers } from 'next/headers'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generalSettingsKeys, mapGeneralSettingsResponse } from '@/hooks/queries/general-settings'
import { subscriptionKeys } from '@/hooks/queries/subscription'
import { mapUserProfileResponse, userProfileKeys } from '@/hooks/queries/user-profile'

/**
Expand Down Expand Up @@ -35,6 +36,28 @@ export function prefetchGeneralSettings(queryClient: QueryClient) {
})
}

/**
* Prefetch subscription data server-side via internal API fetch.
* Uses the same query key as the client `useSubscriptionData` hook (with includeOrg=false)
* so data is shared via HydrationBoundary — ensuring the settings sidebar renders
* with the correct Team/Enterprise tabs on the first paint, with no flash.
*/
export function prefetchSubscriptionData(queryClient: QueryClient) {
return queryClient.prefetchQuery({
queryKey: subscriptionKeys.user(false),
queryFn: async () => {
const fwdHeaders = await getForwardedHeaders()
const baseUrl = getInternalApiBaseUrl()
const response = await fetch(`${baseUrl}/api/billing?context=user`, {
headers: fwdHeaders,
})
if (!response.ok) throw new Error(`Subscription prefetch failed: ${response.status}`)
return response.json()
},
staleTime: 5 * 60 * 1000,
})
}

/**
* Prefetch user profile server-side via internal API fetch.
* Uses the same query keys as the client `useUserProfile` hook
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { Skeleton } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { captureEvent } from '@/lib/posthog/client'
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
Expand Down Expand Up @@ -198,7 +199,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
}, [effectiveSection, sessionLoading, posthog])

return (
<div>
<div className={cn(effectiveSection === 'access-control' && 'flex h-full flex-col')}>
<h2 className='mb-7 font-medium text-[22px] text-[var(--text-primary)]'>{label}</h2>
{effectiveSection === 'general' && <General />}
{effectiveSection === 'integrations' && <Integrations />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/
import { useBrandConfig } from '@/ee/whitelabeling'
import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
import {
useDeleteAccount,
useResetPassword,
useUpdateUserProfile,
useUserProfile,
Expand Down Expand Up @@ -79,6 +80,10 @@ export function General() {
const [showResetPasswordModal, setShowResetPasswordModal] = useState(false)
const resetPassword = useResetPassword()

const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
const [deleteConfirmText, setDeleteConfirmText] = useState('')
const deleteAccount = useDeleteAccount()

const [uploadError, setUploadError] = useState<string | null>(null)

const snapToGridValue = settings?.snapToGridSize ?? 0
Expand Down Expand Up @@ -166,6 +171,23 @@ export function General() {
}
}

const handleDeleteAccountConfirm = async () => {
deleteAccount.mutate(undefined, {
onSuccess: async () => {
try {
await Promise.all([signOut(), clearUserData()])
router.push('/login')
} catch (error) {
logger.error('Error during account cleanup', { error })
router.push('/login')
}
},
onError: (error) => {
logger.error('Error deleting account:', error)
},
})
}

const handleResetPasswordConfirm = async () => {
if (!profile?.email) return

Expand Down Expand Up @@ -467,6 +489,20 @@ export function General() {
time.
</p>

{isHosted && !isAuthDisabled && (
<div className='flex items-center justify-between border-t pt-4'>
<div>
<Label>Delete account</Label>
<p className='text-[var(--text-muted)] text-small'>
Permanently delete your account and all associated data.
</p>
</div>
<Button onClick={() => setShowDeleteAccountModal(true)} variant='active'>
Delete account
</Button>
</div>
)}

{isTrainingEnabled && (
<div className='flex items-center justify-between'>
<Label htmlFor='training-controls'>Training controls</Label>
Expand Down Expand Up @@ -500,6 +536,68 @@ export function General() {
)}
</div>

{/* Delete Account Confirmation Modal */}
<Modal
open={showDeleteAccountModal}
onOpenChange={(open) => {
setShowDeleteAccountModal(open)
if (!open) {
setDeleteConfirmText('')
deleteAccount.reset()
}
}}
>
<ModalContent size='sm'>
<ModalHeader>Delete Account</ModalHeader>
<ModalBody>
<p className='text-[var(--text-secondary)]'>
This will permanently delete your account and all associated data, including
workspaces, workflows, API keys, and execution history.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
<div className='mt-3'>
<label
htmlFor='delete-account-confirm'
className='mb-1.5 block text-[var(--text-secondary)] text-sm'
>
Type{' '}
<span className='font-medium text-[var(--text-primary)]'>delete my account</span> to
confirm
</label>
<input
id='delete-account-confirm'
type='text'
value={deleteConfirmText}
onChange={(e) => setDeleteConfirmText(e.target.value)}
className='w-full rounded-md border border-[var(--border)] bg-transparent px-3 py-2 text-[var(--text-primary)] text-sm placeholder:text-[var(--text-tertiary)] focus:border-[var(--border-1)] focus:outline-none'
placeholder='delete my account'
disabled={deleteAccount.isPending}
/>
</div>
{deleteAccount.error && (
<p className='mt-2 text-[var(--text-error)] text-small'>
{deleteAccount.error.message}
</p>
)}
</ModalBody>
<ModalFooter>
<Button
onClick={() => setShowDeleteAccountModal(false)}
disabled={deleteAccount.isPending}
>
Cancel
</Button>
<Button
variant='destructive'
onClick={handleDeleteAccountConfirm}
disabled={deleteAccount.isPending || deleteConfirmText !== 'delete my account'}
>
{deleteAccount.isPending ? 'Deleting...' : 'Delete Account'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>

{/* Password Reset Confirmation Modal */}
<Modal open={showResetPasswordModal} onOpenChange={setShowResetPasswordModal}>
<ModalContent size='sm'>
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/workspace/[workspaceId]/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
return (
<div className='h-full overflow-y-auto [scrollbar-gutter:stable]'>
<div className='mx-auto flex min-h-full max-w-[900px] flex-col px-[26px] pt-9 pb-[52px]'>
<div className='mx-auto flex min-h-full max-w-[940px] flex-col px-[26px] pt-9 pb-[52px]'>
{children}
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
const isCredentialSetsEnabled = isTruthy(getEnv('NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED'))
const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED'))
const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED'))
const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED'))
const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED'))

export const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
export { isCredentialSetsEnabled }
Expand Down Expand Up @@ -106,6 +108,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: isAuditLogsEnabled,
},
{
id: 'subscription',
Expand Down Expand Up @@ -181,7 +184,7 @@ export const allNavigationItems: NavigationItem[] = [
section: 'enterprise',
requiresHosted: true,
requiresEnterprise: true,
selfHostedOverride: isBillingEnabled,
selfHostedOverride: isWhitelabelingEnabled,
},
{
id: 'admin',
Expand Down
Loading
Loading