Skip to content

Commit 3ac30c4

Browse files
committed
feat: entitlement check
1 parent f85bfe3 commit 3ac30c4

4 files changed

Lines changed: 152 additions & 92 deletions

File tree

frontend/app/src/lib/api/generated/control-plane/Api.ts

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/app/src/lib/api/generated/control-plane/data-contracts.ts

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/app/src/lib/api/organization-wrapper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ export function useOrganizationApi() {
8080
queryFn: async () =>
8181
(await controlPlaneApi.ssoConfigGet(organization)).data,
8282
}),
83+
84+
organizationEntitlementsGetQuery: (organization: string) => ({
85+
queryKey: ['organization:entitlements:get', organization] as const,
86+
queryFn: async () =>
87+
(await controlPlaneApi.organizationEntitlementsGet(organization))
88+
.data,
89+
}),
90+
8391
managementTokenListQuery: (organization: string) => ({
8492
queryKey: ['management-tokens:list', organization] as const,
8593
queryFn: async () =>

frontend/app/src/pages/main/v1/tenant-settings/organization/index.tsx

Lines changed: 119 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,20 @@ export function CloudOrganizationSettings({ orgId }: { orgId: string }) {
164164
const [newSsoDomain, setNewSsoDomain] = useState('');
165165
const [isAddingSsoDomain, setIsAddingSsoDomain] = useState(false);
166166

167+
const organizationEntitlementsQuery = useQuery({
168+
...orgApi.organizationEntitlementsGetQuery(orgId!),
169+
enabled: !!orgId && canManageSso,
170+
});
171+
const canUseSso = organizationEntitlementsQuery.data?.canSSO === true;
172+
167173
const organizationSsoDomainGetQuery = useQuery({
168174
...orgApi.organizationSsoDomainGetQuery(orgId),
169-
enabled: !!orgId && canManageSso,
175+
enabled: !!orgId && canUseSso,
170176
});
171177

172178
const organizationSsoConfigGetQuery = useQuery({
173179
...orgApi.organizationSsoConfigGetQuery(orgId),
174-
enabled: !!orgId && canManageSso,
180+
enabled: !!orgId && canUseSso,
175181
});
176182

177183
const ssoConfigUpdateMutation = useMutation({
@@ -947,101 +953,122 @@ export function CloudOrganizationSettings({ orgId }: { orgId: string }) {
947953
</TabsContent>
948954
{canManageSso && (
949955
<TabsContent value="sso">
950-
<div className="space-y-8">
951-
<CreateSSOPage orgId={orgId} />
952-
{/* Force SSO toggle */}
953-
{isOrganizationOwner && (
954-
<div className="flex items-center justify-between rounded-lg border border-border/50 bg-muted/10 p-4">
955-
<div className="space-y-0.5">
956-
<p className="text-sm font-medium">Force SSO</p>
957-
<p className="text-sm text-muted-foreground">
958-
Require all organization members to sign in with SSO.
959-
All other login methods will be disabled.
960-
</p>
961-
</div>
962-
<Switch
963-
checked={
964-
organizationSsoConfigGetQuery.data?.forceSSO ?? false
965-
}
966-
onCheckedChange={(checked) =>
967-
ssoConfigUpdateMutation.mutate(checked)
968-
}
969-
disabled={
970-
organizationSsoConfigGetQuery.isLoading ||
971-
ssoConfigUpdateMutation.isPending
972-
}
973-
/>
974-
</div>
975-
)}
976-
{/* SSO Domains Table */}
977-
{organizationSsoDomainGetQuery.isLoading ? (
978-
<div className="flex items-center justify-center py-8">
979-
<Loading />
980-
</div>
981-
) : organizationSsoDomainGetQuery.data &&
982-
organizationSsoDomainGetQuery.data.length > 0 ? (
983-
<SimpleTable
984-
data={organizationSsoDomainGetQuery.data.map((v) => ({
985-
domain: v.ssoDomain,
986-
verified: v.verified,
987-
verification_token: v.verificationToken,
988-
}))}
989-
columns={ssoDomainColumns}
990-
rowKey={(row) => row.domain}
991-
/>
992-
) : (
993-
<div className="py-16 text-center">
994-
<KeyIcon className="mx-auto mb-4 h-12 w-12 text-muted-foreground" />
995-
<h3 className="mb-2 text-lg font-medium">No SSO Domains</h3>
996-
<p className="mb-4 text-muted-foreground">
997-
Add a domain below to enable SSO for your organization.
998-
</p>
999-
</div>
1000-
)}
1001-
1002-
{/* Add New SSO Domain */}
1003-
<div className="space-y-2">
1004-
<div className="flex gap-2">
1005-
<Input
1006-
placeholder="example.com"
1007-
value={newSsoDomain}
1008-
onChange={(e) => setNewSsoDomain(e.target.value)}
1009-
onKeyDown={(e) => {
1010-
if (e.key === 'Enter') {
1011-
handleAddSsoDomain();
956+
{organizationEntitlementsQuery.isLoading ? (
957+
<div className="flex items-center justify-center py-8">
958+
<Loading />
959+
</div>
960+
) : canUseSso ? (
961+
<div className="space-y-8">
962+
<CreateSSOPage orgId={orgId} />
963+
{/* Force SSO toggle */}
964+
{isOrganizationOwner && (
965+
<div className="flex items-center justify-between rounded-lg border border-border/50 bg-muted/10 p-4">
966+
<div className="space-y-0.5">
967+
<p className="text-sm font-medium">Force SSO</p>
968+
<p className="text-sm text-muted-foreground">
969+
Require all organization members to sign in with SSO.
970+
All other login methods will be disabled.
971+
</p>
972+
</div>
973+
<Switch
974+
checked={
975+
organizationSsoConfigGetQuery.data?.forceSSO ?? false
976+
}
977+
onCheckedChange={(checked) =>
978+
ssoConfigUpdateMutation.mutate(checked)
1012979
}
1013-
}}
1014-
className="max-w-sm"
1015-
disabled={isAddingSsoDomain}
980+
disabled={
981+
organizationSsoConfigGetQuery.isLoading ||
982+
ssoConfigUpdateMutation.isPending
983+
}
984+
/>
985+
</div>
986+
)}
987+
{/* SSO Domains Table */}
988+
{organizationSsoDomainGetQuery.isLoading ? (
989+
<div className="flex items-center justify-center py-8">
990+
<Loading />
991+
</div>
992+
) : organizationSsoDomainGetQuery.data &&
993+
organizationSsoDomainGetQuery.data.length > 0 ? (
994+
<SimpleTable
995+
data={organizationSsoDomainGetQuery.data.map((v) => ({
996+
domain: v.ssoDomain,
997+
verified: v.verified,
998+
verification_token: v.verificationToken,
999+
}))}
1000+
columns={ssoDomainColumns}
1001+
rowKey={(row) => row.domain}
10161002
/>
1017-
<Button
1018-
variant="outline"
1019-
size="sm"
1020-
onClick={handleAddSsoDomain}
1021-
disabled={isAddingSsoDomain || !newSsoDomain.trim()}
1022-
leftIcon={<PlusIcon className="size-4" />}
1023-
>
1024-
Add Domain
1025-
</Button>
1026-
</div>
1027-
</div>
1028-
{organizationSsoDomainGetQuery.data &&
1029-
organizationSsoDomainGetQuery.data.length > 0 && (
1030-
<div className="rounded-md border border-muted bg-muted/30 px-4 py-3 text-sm text-muted-foreground">
1031-
<p>
1032-
To verify your domain, add a DNS TXT record with the
1033-
value:
1034-
</p>
1035-
<p className="mt-1 font-mono">
1036-
hatchet-sso-verify=&#123;verification_token&#125;
1037-
</p>
1038-
<p className="mt-2">
1039-
It may take a few minutes for DNS changes to propagate
1040-
and for the verified status to update.
1003+
) : (
1004+
<div className="py-16 text-center">
1005+
<KeyIcon className="mx-auto mb-4 h-12 w-12 text-muted-foreground" />
1006+
<h3 className="mb-2 text-lg font-medium">
1007+
No SSO Domains
1008+
</h3>
1009+
<p className="mb-4 text-muted-foreground">
1010+
Add a domain below to enable SSO for your organization.
10411011
</p>
10421012
</div>
10431013
)}
1044-
</div>
1014+
1015+
{/* Add New SSO Domain */}
1016+
<div className="space-y-2">
1017+
<div className="flex gap-2">
1018+
<Input
1019+
placeholder="example.com"
1020+
value={newSsoDomain}
1021+
onChange={(e) => setNewSsoDomain(e.target.value)}
1022+
onKeyDown={(e) => {
1023+
if (e.key === 'Enter') {
1024+
handleAddSsoDomain();
1025+
}
1026+
}}
1027+
className="max-w-sm"
1028+
disabled={isAddingSsoDomain}
1029+
/>
1030+
<Button
1031+
variant="outline"
1032+
size="sm"
1033+
onClick={handleAddSsoDomain}
1034+
disabled={isAddingSsoDomain || !newSsoDomain.trim()}
1035+
leftIcon={<PlusIcon className="size-4" />}
1036+
>
1037+
Add Domain
1038+
</Button>
1039+
</div>
1040+
</div>
1041+
{organizationSsoDomainGetQuery.data &&
1042+
organizationSsoDomainGetQuery.data.length > 0 && (
1043+
<div className="rounded-md border border-muted bg-muted/30 px-4 py-3 text-sm text-muted-foreground">
1044+
<p>
1045+
To verify your domain, add a DNS TXT record with the
1046+
value:
1047+
</p>
1048+
<p className="mt-1 font-mono">
1049+
hatchet-sso-verify=&#123;verification_token&#125;
1050+
</p>
1051+
<p className="mt-2">
1052+
It may take a few minutes for DNS changes to propagate
1053+
and for the verified status to update.
1054+
</p>
1055+
</div>
1056+
)}
1057+
</div>
1058+
) : (
1059+
<div className="py-16 text-center text-sm text-muted-foreground">
1060+
SSO is not enabled for this organization. Please{' '}
1061+
<a
1062+
href={OFFICE_HOURS_URL}
1063+
target="_blank"
1064+
rel="noopener noreferrer"
1065+
className="text-primary underline-offset-4 hover:underline"
1066+
>
1067+
contact us
1068+
</a>{' '}
1069+
to get access.
1070+
</div>
1071+
)}
10451072
</TabsContent>
10461073
)}
10471074
</Tabs>

0 commit comments

Comments
 (0)