From 35b408c076471f57b4f7b10c821904ce4a9bd89c Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 1 May 2026 14:05:15 +0100 Subject: [PATCH] fix --- .../public/locales/en-US/translation.json | 25 +++- .../layouts/global/global-app-shell.tsx | 4 +- .../components/settings/settings-queries.tsx | 8 ++ .../components/settings/settings-sidebar.tsx | 6 +- .../components/create-scim-token-modal.tsx | 78 ++++++++++++ .../src/ee/scim/components/enable-scim.tsx | 55 ++++++++ .../components/revoke-scim-token-modal.tsx | 59 +++++++++ .../components/scim-token-created-modal.tsx | 63 +++++++++ .../ee/scim/components/scim-token-table.tsx | 120 ++++++++++++++++++ .../src/ee/scim/components/scim-url-panel.tsx | 30 +++++ apps/client/src/ee/scim/index.ts | 2 + .../src/ee/scim/queries/scim-token-query.ts | 72 +++++++++++ .../ee/scim/services/scim-token-service.ts | 27 ++++ .../src/ee/scim/types/scim-token.types.ts | 22 ++++ .../security/components/sso-provider-list.tsx | 4 +- .../client/src/ee/security/pages/security.tsx | 109 +++++++++++++++- .../workspace/types/workspace.types.ts | 1 + .../workspace/dto/update-workspace.dto.ts | 4 + .../workspace/services/workspace.service.ts | 12 +- .../migrations/20260501T092214-scim.ts | 1 - apps/server/src/database/types/db.d.ts | 1 - apps/server/src/ee | 2 +- 22 files changed, 690 insertions(+), 15 deletions(-) create mode 100644 apps/client/src/ee/scim/components/create-scim-token-modal.tsx create mode 100644 apps/client/src/ee/scim/components/enable-scim.tsx create mode 100644 apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx create mode 100644 apps/client/src/ee/scim/components/scim-token-created-modal.tsx create mode 100644 apps/client/src/ee/scim/components/scim-token-table.tsx create mode 100644 apps/client/src/ee/scim/components/scim-url-panel.tsx create mode 100644 apps/client/src/ee/scim/index.ts create mode 100644 apps/client/src/ee/scim/queries/scim-token-query.ts create mode 100644 apps/client/src/ee/scim/services/scim-token-service.ts create mode 100644 apps/client/src/ee/scim/types/scim-token.types.ts diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 3bfe7c9f..08f83bb8 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -880,5 +880,28 @@ "Try a different search term.": "Try a different search term.", "Try again": "Try again", "Untitled chat": "Untitled chat", - "What can I help you with?": "What can I help you with?" + "What can I help you with?": "What can I help you with?", + "Are you sure you want to revoke this SCIM token": "Are you sure you want to revoke this SCIM token", + "Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure your identity provider with this URL to provision users and groups.", + "Create SCIM token": "Create SCIM token", + "Created by": "Created by", + "Custom": "Custom", + "Enable SCIM": "Enable SCIM", + "Enter a descriptive name": "Enter a descriptive name", + "I've saved my SCIM token": "I've saved my SCIM token", + "Important": "Important", + "Make sure to copy your SCIM token now. You won't be able to see it again!": "Make sure to copy your SCIM token now. You won't be able to see it again!", + "Never": "Never", + "Revoke SCIM token": "Revoke SCIM token", + "SCIM endpoint URL": "SCIM endpoint URL", + "SCIM provisioning": "SCIM provisioning", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM takes precedence over SSO group sync while enabled.", + "SCIM token": "SCIM token", + "SCIM token created": "SCIM token created", + "SCIM token created successfully": "SCIM token created successfully", + "SCIM tokens": "SCIM tokens", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.", + "Toggle SCIM provisioning": "Toggle SCIM provisioning", + "Token": "Token" } diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index 64bd3dde..6e842a05 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -116,7 +116,9 @@ export default function GlobalAppShell({ {isSettingsRoute ? ( - {children} + + {children} + ) : ( children )} diff --git a/apps/client/src/components/settings/settings-queries.tsx b/apps/client/src/components/settings/settings-queries.tsx index e9562a85..892857f5 100644 --- a/apps/client/src/components/settings/settings-queries.tsx +++ b/apps/client/src/components/settings/settings-queries.tsx @@ -13,6 +13,7 @@ import { getShares } from "@/features/share/services/share-service.ts"; import { getApiKeys } from "@/ee/api-key"; import { getAuditLogs } from "@/ee/audit/services/audit-service"; import { getVerificationList } from "@/ee/page-verification/services/page-verification-service"; +import { getScimTokens } from "@/ee/scim/services/scim-token-service"; export const prefetchWorkspaceMembers = () => { const params: QueryParams = { limit: 100, query: "" }; @@ -98,3 +99,10 @@ export const prefetchVerifiedPages = () => { queryFn: () => getVerificationList(params), }); }; + +export const prefetchScimTokens = () => { + queryClient.prefetchQuery({ + queryKey: ["scim-token-list", { cursor: undefined }], + queryFn: () => getScimTokens({}), + }); +}; diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx index 90d89d13..8ea1f59f 100644 --- a/apps/client/src/components/settings/settings-sidebar.tsx +++ b/apps/client/src/components/settings/settings-sidebar.tsx @@ -31,6 +31,7 @@ import { prefetchBilling, prefetchGroups, prefetchLicense, + prefetchScimTokens, prefetchShares, prefetchSpaces, prefetchSsoProviders, @@ -204,7 +205,10 @@ export default function SettingsSidebar() { } break; case "Security & SSO": - prefetchHandler = prefetchSsoProviders; + prefetchHandler = () => { + prefetchSsoProviders(); + prefetchScimTokens(); + }; break; case "Public sharing": prefetchHandler = prefetchShares; diff --git a/apps/client/src/ee/scim/components/create-scim-token-modal.tsx b/apps/client/src/ee/scim/components/create-scim-token-modal.tsx new file mode 100644 index 00000000..9a1a8976 --- /dev/null +++ b/apps/client/src/ee/scim/components/create-scim-token-modal.tsx @@ -0,0 +1,78 @@ +import { Modal, TextInput, Button, Group, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zod4Resolver } from "mantine-form-zod-resolver"; +import { z } from "zod/v4"; +import { useTranslation } from "react-i18next"; +import { useCreateScimTokenMutation } from "@/ee/scim/queries/scim-token-query"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +interface CreateScimTokenModalProps { + opened: boolean; + onClose: () => void; + onSuccess: (response: IScimToken) => void; +} + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), +}); +type FormValues = z.infer; + +export function CreateScimTokenModal({ + opened, + onClose, + onSuccess, +}: CreateScimTokenModalProps) { + const { t } = useTranslation(); + const createMutation = useCreateScimTokenMutation(); + + const form = useForm({ + validate: zod4Resolver(formSchema), + initialValues: { name: "" }, + }); + + const handleSubmit = async (data: FormValues) => { + try { + const created = await createMutation.mutateAsync({ name: data.name }); + onSuccess(created); + form.reset(); + onClose(); + } catch (err) { + // + } + }; + + const handleClose = () => { + form.reset(); + onClose(); + }; + + return ( + +
handleSubmit(values))}> + + + + + + + + +
+
+ ); +} diff --git a/apps/client/src/ee/scim/components/enable-scim.tsx b/apps/client/src/ee/scim/components/enable-scim.tsx new file mode 100644 index 00000000..0f8204eb --- /dev/null +++ b/apps/client/src/ee/scim/components/enable-scim.tsx @@ -0,0 +1,55 @@ +import { Group, Text, Switch, Tooltip } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { useHasFeature } from "@/ee/hooks/use-feature.ts"; +import { Feature } from "@/ee/features.ts"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts"; + +export default function EnableScim() { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState(workspace?.isScimEnabled ?? false); + const hasAccess = useHasFeature(Feature.SCIM); + const upgradeLabel = useUpgradeLabel(); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ isScimEnabled: value }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + +
+ {t("Enable SCIM")} + + {t( + "Automatically provision users and groups from your identity provider via SCIM.", + )} + +
+ + + + +
+ ); +} diff --git a/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx b/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx new file mode 100644 index 00000000..9755614b --- /dev/null +++ b/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx @@ -0,0 +1,59 @@ +import { Modal, Text, Button, Group, Stack } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useRevokeScimTokenMutation } from "@/ee/scim/queries/scim-token-query"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +interface RevokeScimTokenModalProps { + opened: boolean; + onClose: () => void; + scimToken: IScimToken | null; +} + +export function RevokeScimTokenModal({ + opened, + onClose, + scimToken, +}: RevokeScimTokenModalProps) { + const { t } = useTranslation(); + const revokeMutation = useRevokeScimTokenMutation(); + + const handleRevoke = async () => { + if (!scimToken) return; + await revokeMutation.mutateAsync({ tokenId: scimToken.id }); + onClose(); + }; + + return ( + + + + {t("Are you sure you want to revoke this SCIM token")}{" "} + {scimToken?.name}? + + + {t( + "This action cannot be undone. Your identity provider will stop syncing immediately.", + )} + + + + + + + + + ); +} diff --git a/apps/client/src/ee/scim/components/scim-token-created-modal.tsx b/apps/client/src/ee/scim/components/scim-token-created-modal.tsx new file mode 100644 index 00000000..681a5e6f --- /dev/null +++ b/apps/client/src/ee/scim/components/scim-token-created-modal.tsx @@ -0,0 +1,63 @@ +import { + Modal, + Text, + Stack, + Alert, + Group, + Button, + TextInput, +} from "@mantine/core"; +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import CopyTextButton from "@/components/common/copy.tsx"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +interface ScimTokenCreatedModalProps { + opened: boolean; + onClose: () => void; + scimToken: IScimToken | null; +} + +export function ScimTokenCreatedModal({ + opened, + onClose, + scimToken, +}: ScimTokenCreatedModalProps) { + const { t } = useTranslation(); + if (!scimToken) return null; + + return ( + + + } + title={t("Important")} + color="red" + > + {t( + "Make sure to copy your SCIM token now. You won't be able to see it again!", + )} + + +
+ + {t("SCIM token")} + + + + + +
+ + +
+
+ ); +} diff --git a/apps/client/src/ee/scim/components/scim-token-table.tsx b/apps/client/src/ee/scim/components/scim-token-table.tsx new file mode 100644 index 00000000..45402bb6 --- /dev/null +++ b/apps/client/src/ee/scim/components/scim-token-table.tsx @@ -0,0 +1,120 @@ +import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core"; +import { IconDots, IconTrash } from "@tabler/icons-react"; +import { format } from "date-fns"; +import { useTranslation } from "react-i18next"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import React from "react"; +import NoTableResults from "@/components/common/no-table-results"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +interface ScimTokenTableProps { + tokens: IScimToken[]; + isLoading?: boolean; + onRevoke?: (token: IScimToken) => void; +} + +export function ScimTokenTable({ + tokens, + isLoading, + onRevoke, +}: ScimTokenTableProps) { + const { t } = useTranslation(); + + const formatDate = (date: Date | string | null) => { + if (!date) return t("Never"); + return format(new Date(date), "MMM dd, yyyy"); + }; + + return ( + + + + + {t("Name")} + {t("Token")} + {t("Created by")} + {t("Last used")} + {t("Created")} + + + + + + {tokens && tokens.length > 0 ? ( + tokens.map((token) => ( + + + + {token.name} + + + + + + ••••{token.tokenLastFour} + + + + {token.creator ? ( + + + + + {token.creator.name} + + + + ) : ( + + + — + + + )} + + + + {formatDate(token.lastUsedAt)} + + + + + + {formatDate(token.createdAt)} + + + + + + + + + + + + {onRevoke && ( + } + color="red" + onClick={() => onRevoke(token)} + > + {t("Revoke")} + + )} + + + + + )) + ) : ( + + )} + +
+
+ ); +} diff --git a/apps/client/src/ee/scim/components/scim-url-panel.tsx b/apps/client/src/ee/scim/components/scim-url-panel.tsx new file mode 100644 index 00000000..6aa78820 --- /dev/null +++ b/apps/client/src/ee/scim/components/scim-url-panel.tsx @@ -0,0 +1,30 @@ +import { Group, Stack, Text, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CopyTextButton from "@/components/common/copy.tsx"; + +export function ScimUrlPanel() { + const { t } = useTranslation(); + const scimUrl = `${window.location.origin}/api/scim/v2`; + + return ( + + + {t("SCIM endpoint URL")} + + + {t( + "Configure your identity provider with this URL to provision users and groups.", + )} + + + + + + + ); +} diff --git a/apps/client/src/ee/scim/index.ts b/apps/client/src/ee/scim/index.ts new file mode 100644 index 00000000..e246200d --- /dev/null +++ b/apps/client/src/ee/scim/index.ts @@ -0,0 +1,2 @@ +export * from "./types/scim-token.types"; +export * from "./services/scim-token-service"; diff --git a/apps/client/src/ee/scim/queries/scim-token-query.ts b/apps/client/src/ee/scim/queries/scim-token-query.ts new file mode 100644 index 00000000..06ab0912 --- /dev/null +++ b/apps/client/src/ee/scim/queries/scim-token-query.ts @@ -0,0 +1,72 @@ +import { IPagination, QueryParams } from "@/lib/types.ts"; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, + UseQueryResult, +} from "@tanstack/react-query"; +import { + createScimToken, + getScimTokens, + revokeScimToken, +} from "@/ee/scim/services/scim-token-service"; +import { + IScimToken, + ICreateScimTokenRequest, + IRevokeScimTokenRequest, +} from "@/ee/scim/types/scim-token.types"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; + +export function useGetScimTokensQuery( + params?: QueryParams, +): UseQueryResult, Error> { + return useQuery({ + queryKey: ["scim-token-list", params], + queryFn: () => getScimTokens(params), + staleTime: 0, + gcTime: 0, + placeholderData: keepPreviousData, + }); +} + +export function useCreateScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => createScimToken(data), + onSuccess: () => { + notifications.show({ message: t("SCIM token created successfully") }); + queryClient.invalidateQueries({ + predicate: (item) => + ["scim-token-list"].includes(item.queryKey[0] as string), + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} + +export function useRevokeScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => revokeScimToken(data), + onSuccess: () => { + notifications.show({ message: t("Revoked successfully") }); + queryClient.invalidateQueries({ + predicate: (item) => + ["scim-token-list"].includes(item.queryKey[0] as string), + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} diff --git a/apps/client/src/ee/scim/services/scim-token-service.ts b/apps/client/src/ee/scim/services/scim-token-service.ts new file mode 100644 index 00000000..916c5468 --- /dev/null +++ b/apps/client/src/ee/scim/services/scim-token-service.ts @@ -0,0 +1,27 @@ +import api from "@/lib/api-client"; +import { + IScimToken, + ICreateScimTokenRequest, + IRevokeScimTokenRequest, +} from "@/ee/scim/types/scim-token.types"; +import { IPagination, QueryParams } from "@/lib/types.ts"; + +export async function getScimTokens( + params?: QueryParams, +): Promise> { + const req = await api.post("/scim-tokens", { ...params }); + return req.data; +} + +export async function createScimToken( + data: ICreateScimTokenRequest, +): Promise { + const req = await api.post("/scim-tokens/create", data); + return req.data; +} + +export async function revokeScimToken( + data: IRevokeScimTokenRequest, +): Promise { + await api.post("/scim-tokens/revoke", data); +} diff --git a/apps/client/src/ee/scim/types/scim-token.types.ts b/apps/client/src/ee/scim/types/scim-token.types.ts new file mode 100644 index 00000000..ec0a22dc --- /dev/null +++ b/apps/client/src/ee/scim/types/scim-token.types.ts @@ -0,0 +1,22 @@ +import { IUser } from "@/features/user/types/user.types.ts"; + +export interface IScimToken { + id: string; + name: string; + token?: string; + tokenLastFour: string; + isEnabled: boolean; + creatorId: string; + workspaceId: string; + lastUsedAt: string | null; + createdAt: string; + creator?: Partial; +} + +export interface ICreateScimTokenRequest { + name: string; +} + +export interface IRevokeScimTokenRequest { + tokenId: string; +} diff --git a/apps/client/src/ee/security/components/sso-provider-list.tsx b/apps/client/src/ee/security/components/sso-provider-list.tsx index 7aac7d11..3603f959 100644 --- a/apps/client/src/ee/security/components/sso-provider-list.tsx +++ b/apps/client/src/ee/security/components/sso-provider-list.tsx @@ -69,8 +69,8 @@ export default function SsoProviderList() { return ( <> - - + +
{t("Name")} diff --git a/apps/client/src/ee/security/pages/security.tsx b/apps/client/src/ee/security/pages/security.tsx index 254809c7..1a0baf94 100644 --- a/apps/client/src/ee/security/pages/security.tsx +++ b/apps/client/src/ee/security/pages/security.tsx @@ -1,8 +1,9 @@ import { Helmet } from "react-helmet-async"; import { getAppName, isCloud } from "@/lib/config.ts"; import SettingsTitle from "@/components/settings/settings-title.tsx"; -import { Divider, Title } from "@mantine/core"; -import React from "react"; +import { Alert, Button, Card, Divider, Group, Space, Title } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; +import React, { useState } from "react"; import useUserRole from "@/hooks/use-user-role.tsx"; import SsoProviderList from "@/ee/security/components/sso-provider-list.tsx"; import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx"; @@ -12,16 +13,37 @@ import { useTranslation } from "react-i18next"; import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx"; import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx"; import TrashRetention from "@/ee/security/components/trash-retention.tsx"; - +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; +import { useGetScimTokensQuery } from "@/ee/scim/queries/scim-token-query"; +import { ScimUrlPanel } from "@/ee/scim/components/scim-url-panel"; +import { ScimTokenTable } from "@/ee/scim/components/scim-token-table"; +import { CreateScimTokenModal } from "@/ee/scim/components/create-scim-token-modal"; +import { ScimTokenCreatedModal } from "@/ee/scim/components/scim-token-created-modal"; +import { RevokeScimTokenModal } from "@/ee/scim/components/revoke-scim-token-modal"; +import EnableScim from "@/ee/scim/components/enable-scim"; +import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; +import Paginate from "@/components/common/paginate"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; export default function Security() { const { t } = useTranslation(); const { isAdmin } = useUserRole(); const hasCustomSso = useHasFeature(Feature.SSO_CUSTOM); - const hasRetention = useHasFeature(Feature.RETENTION); - const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS); + const hasScim = useHasFeature(Feature.SCIM); + const [workspace] = useAtom(workspaceAtom); + const isScimEnabled = workspace?.isScimEnabled ?? false; + + const { cursor, goNext, goPrev } = useCursorPaginate(); + const { data: scimData, isLoading: scimLoading } = useGetScimTokensQuery( + hasScim && isScimEnabled ? { cursor } : undefined, + ); + + const [createOpen, setCreateOpen] = useState(false); + const [createdToken, setCreatedToken] = useState(null); + const [revokeTarget, setRevokeTarget] = useState(null); if (!isAdmin) { return null; @@ -45,7 +67,7 @@ export default function Security() { - Single sign-on (SSO) + {t("Single sign-on (SSO)")} @@ -66,6 +88,81 @@ export default function Security() { )} + + {hasScim && ( + <> + + + + {t("SCIM provisioning")} + + + } + color="blue" + variant="light" + mb="md" + > + {t("SCIM takes precedence over SSO group sync while enabled.")} + + + + + + + + + {isScimEnabled && ( + <> + + + + {t("SCIM tokens")} + + + + + + + + + + {scimData?.items.length > 0 && ( + goNext(scimData?.meta?.nextCursor)} + onPrev={goPrev} + /> + )} + + setCreateOpen(false)} + onSuccess={setCreatedToken} + /> + + setCreatedToken(null)} + scimToken={createdToken} + /> + + setRevokeTarget(null)} + scimToken={revokeTarget} + /> + + )} + + )} ); } diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index f733f73a..eda89dd5 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -28,6 +28,7 @@ export interface IWorkspace { trashRetentionDays?: number; restrictApiToAdmins?: boolean; allowMemberTemplates?: boolean; + isScimEnabled?: boolean; } export interface IWorkspaceSettings { diff --git a/apps/server/src/core/workspace/dto/update-workspace.dto.ts b/apps/server/src/core/workspace/dto/update-workspace.dto.ts index a08b1e52..25697a4b 100644 --- a/apps/server/src/core/workspace/dto/update-workspace.dto.ts +++ b/apps/server/src/core/workspace/dto/update-workspace.dto.ts @@ -41,6 +41,10 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) { @IsBoolean() mcpEnabled: boolean; + @IsOptional() + @IsBoolean() + isScimEnabled: boolean; + @IsOptional() @IsBoolean() aiChat: boolean; diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 72608951..267eb13b 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -331,7 +331,8 @@ export class WorkspaceService { typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' || typeof updateWorkspaceDto.mcpEnabled !== 'undefined' || typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' || - typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' + typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' || + typeof updateWorkspaceDto.isScimEnabled !== 'undefined' ) { const ws = await this.db .selectFrom('workspaces') @@ -351,6 +352,14 @@ export class WorkspaceService { } } + if (typeof updateWorkspaceDto.isScimEnabled !== 'undefined') { + if (!this.licenseCheckService.hasFeature(ws.licenseKey, Feature.SCIM, ws.plan)) { + throw new ForbiddenException( + 'This feature requires a valid license', + ); + } + } + if ( typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' || typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' || @@ -535,6 +544,7 @@ export class WorkspaceService { 'enforceSso', 'enforceMfa', 'emailDomains', + 'isScimEnabled', ], updateWorkspaceDto, workspaceBefore, diff --git a/apps/server/src/database/migrations/20260501T092214-scim.ts b/apps/server/src/database/migrations/20260501T092214-scim.ts index 2026fa59..69e4bf56 100644 --- a/apps/server/src/database/migrations/20260501T092214-scim.ts +++ b/apps/server/src/database/migrations/20260501T092214-scim.ts @@ -9,7 +9,6 @@ export async function up(db: Kysely): Promise { .addColumn('name', 'varchar', (col) => col.notNull()) .addColumn('token_hash', 'varchar', (col) => col.notNull()) .addColumn('token_last_four', 'varchar(4)', (col) => col.notNull()) - .addColumn('expires_at', 'timestamptz') .addColumn('last_used_at', 'timestamptz') .addColumn('is_enabled', 'boolean', (col) => col.notNull().defaultTo(true)) .addColumn('creator_id', 'uuid', (col) => diff --git a/apps/server/src/database/types/db.d.ts b/apps/server/src/database/types/db.d.ts index 3143e756..ef2c02a0 100644 --- a/apps/server/src/database/types/db.d.ts +++ b/apps/server/src/database/types/db.d.ts @@ -417,7 +417,6 @@ export interface Notifications { export interface ScimTokens { createdAt: Generated; deletedAt: Timestamp | null; - expiresAt: Timestamp | null; id: Generated; isEnabled: Generated; lastUsedAt: Timestamp | null; diff --git a/apps/server/src/ee b/apps/server/src/ee index 8c045de7..c10c6459 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 8c045de7c1d76de811daaa540e976d281f03a43d +Subproject commit c10c6459277f02bd05a88139b05b79c5b033a381