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
2 changes: 1 addition & 1 deletion apps/sim/app/api/organizations/[id]/whitelabel/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const GET = defineInternalJsonRoute({
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ organizationId: params.id }),
useCase: getOrganizationWhitelabel,
present: (data) => ({ success: true, data }),
present: ({ settings, isEnterprise }) => ({ success: true, data: settings, isEnterprise }),
})

export const PUT = defineInternalJsonRoute({
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/ee/whitelabeling/components/branding-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export function BrandingProvider({
viewerIsHostOrganizationMember,
initialOrgSettings,
}: BrandingProviderProps) {
const { data: orgSettings } = useWhitelabelSettings(
const { data: orgWhitelabel } = useWhitelabelSettings(
viewerIsHostOrganizationMember ? (hostOrganizationId ?? undefined) : undefined
)
const orgSettings = orgWhitelabel?.settings

const effectiveOrgSettings =
orgSettings !== undefined ? orgSettings : (initialOrgSettings ?? null)
Expand Down
30 changes: 7 additions & 23 deletions apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import Image from 'next/image'
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
import { isEnterprise } from '@/lib/billing/plan-helpers'
import { HEX_COLOR_REGEX } from '@/lib/branding'
import type { OrganizationWhitelabelSettings } from '@/lib/branding/types'
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
Expand All @@ -23,7 +21,6 @@ import {
useWhitelabelSettings,
type WhitelabelSettingsPayload,
} from '@/ee/whitelabeling/hooks/whitelabel'
import { useOrganizationBilling } from '@/hooks/queries/organization'
import { useWorkspacesQuery } from '@/hooks/queries/workspace'

const logger = createLogger('WhitelabelingSettings')
Expand Down Expand Up @@ -407,18 +404,13 @@ function WhitelabelingForm({ initialSettings, orgId, uploadWorkspaceId }: Whitel
)
}

/** Gates on the entitlement returned by the whitelabel read — the same check saving enforces. */
export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSettingsProps) {
const { billingEnabled } = useDeploymentShape()
const {
data: organizationBillingData,
isPending: organizationBillingLoading,
error: organizationBillingError,
} = useOrganizationBilling(orgId, { enabled: billingEnabled })
const { data: workspaces } = useWorkspacesQuery(true)
const uploadWorkspaceId = workspaces?.find((workspace) => workspace.organizationId === orgId)?.id
const { data: savedSettings, error: settingsError, isLoading } = useWhitelabelSettings(orgId)
const { data: whitelabel, error: settingsError, isLoading } = useWhitelabelSettings(orgId)

if (isLoading || (billingEnabled && organizationBillingLoading)) {
if (isLoading) {
return (
<SettingsPanel
actions={saveDiscardActions({
Expand All @@ -432,32 +424,24 @@ export function WhitelabelingSettings({ organizationId: orgId }: WhitelabelingSe
)
}

if (!savedSettings) {
if (!whitelabel) {
return (
<SettingsEmptyState tone='error'>
{getErrorMessage(settingsError, 'Failed to load whitelabeling settings')}
</SettingsEmptyState>
)
}

if (billingEnabled && organizationBillingData === undefined && organizationBillingError) {
if (!whitelabel.isEnterprise) {
Comment thread
waleedlatif1 marked this conversation as resolved.
return (
<SettingsEmptyState tone='error'>
{getErrorMessage(organizationBillingError, 'Failed to load organization billing')}
</SettingsEmptyState>
)
}

if (billingEnabled && !isEnterprise(organizationBillingData?.data?.subscriptionPlan)) {
return (
<SettingsEmptyState>Whitelabeling is available on Enterprise plans only.</SettingsEmptyState>
<SettingsEmptyState>Whitelabeling requires an active Enterprise plan.</SettingsEmptyState>
Comment thread
waleedlatif1 marked this conversation as resolved.
)
}

return (
<WhitelabelingForm
key={orgId}
initialSettings={savedSettings}
initialSettings={whitelabel.settings}
orgId={orgId}
uploadWorkspaceId={uploadWorkspaceId}
/>
Expand Down
15 changes: 11 additions & 4 deletions apps/sim/ee/whitelabeling/hooks/whitelabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,26 @@ export const whitelabelKeys = {
settings: (orgId: string) => [...whitelabelKeys.settingsList(), orgId] as const,
}

export interface OrganizationWhitelabel {
/** Whether the organization may save whitelabel settings: the entitlement the update enforces. */
isEnterprise: boolean
settings: OrganizationWhitelabelSettings
}

async function fetchWhitelabelSettings(
orgId: string,
signal?: AbortSignal
): Promise<OrganizationWhitelabelSettings> {
const { data } = await requestJson(getOrganizationWhitelabelContract, {
): Promise<OrganizationWhitelabel> {
const { data, isEnterprise } = await requestJson(getOrganizationWhitelabelContract, {
params: { id: orgId },
signal,
})
return data
// A server from before the field omits it; saving still enforces the entitlement.
return { isEnterprise: isEnterprise ?? true, settings: data }
}

/**
* Hook to fetch whitelabel settings for an organization.
* Hook to fetch an organization's whitelabel settings and whether it is entitled to change them.
*/
export function useWhitelabelSettings(orgId: string | undefined) {
return useQuery({
Expand Down
13 changes: 12 additions & 1 deletion apps/sim/lib/api/contracts/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,13 +672,24 @@ const organizationWhitelabelEnvelopeResponseSchema = z.object({
data: organizationWhitelabelSettingsResponseSchema,
})

/**
* The read also carries the entitlement the update enforces, so the settings page can gate on it
* without a separate billing read. It sits beside `data` rather than inside it: `data` keeps the
* settings shape clients already parse, so a client from before this field reads the same response.
* It is optional so a client reading a server from before this field still parses the settings.
*/
const organizationWhitelabelReadResponseSchema =
organizationWhitelabelEnvelopeResponseSchema.extend({
isEnterprise: z.boolean().optional(),
})

export const getOrganizationWhitelabelContract = defineRouteContract({
method: 'GET',
path: '/api/organizations/[id]/whitelabel',
params: organizationParamsSchema,
response: {
mode: 'json',
schema: organizationWhitelabelEnvelopeResponseSchema,
schema: organizationWhitelabelReadResponseSchema,
},
})

Expand Down
12 changes: 7 additions & 5 deletions apps/sim/lib/mothership/tools/server/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,13 @@ const adapters: Record<string, SettingAdapter> = {
'organization/members': { get: readSettingsRoster },
'organization/whitelabeling': {
schema: updateOrganizationWhitelabelBodySchema.strict(),
get: (context) =>
getOrganizationWhitelabel.execute({
principal: context.principal,
input: organizationInput(context),
}),
get: async (context) =>
(
await getOrganizationWhitelabel.execute({
principal: context.principal,
input: organizationInput(context),
})
).settings,
update: async (context, changes) =>
(
await updateOrganizationWhitelabel.execute({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('organization configuration authorization', () => {
queueTableRows(organization, [{ settings: { brandName: 'Acme' } }])
await expect(
getOrganizationWhitelabel.execute({ principal, input: { organizationId: 'org' } })
).resolves.toEqual({ brandName: 'Acme' })
).resolves.toEqual({ isEnterprise: true, settings: { brandName: 'Acme' } })
queueTableRows(member, [])
await expect(
getOrganizationWhitelabel.execute({ principal, input: { organizationId: 'org' } })
Expand Down
15 changes: 9 additions & 6 deletions apps/sim/lib/organizations/application/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,15 @@ function configuredRetention(
export const getOrganizationWhitelabel = defineOrganizationConfigurationUseCase({
operation: organizationConfigurationOperations.readWhitelabel,
async execute({ input }: { input: OrganizationInput }) {
const [row] = await db
.select({ settings: organization.whitelabelSettings })
.from(organization)
.where(eq(organization.id, input.organizationId))
.limit(1)
return requireOrganization(row).settings ?? {}
const [[row], isEnterprise] = await Promise.all([
db
.select({ settings: organization.whitelabelSettings })
.from(organization)
.where(eq(organization.id, input.organizationId))
.limit(1),
isOrganizationFeatureEntitled(input.organizationId, isWhitelabelingEnabled),
])
return { isEnterprise, settings: requireOrganization(row).settings ?? {} }
},
})

Expand Down
Loading