diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index c61b7d9b..56709bbe 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -621,7 +621,8 @@ "Revoked successfully": "Revoked successfully", "Select expiration date": "Select expiration date", "This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.", - "Update API key": "Update API key", + "Update": "Update", + "Update {{credential}}": "Update {{credential}}", "Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace", "Restrict API key creation to admins": "Restrict API key creation to admins", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Only admins and owners can create new API keys. Existing member keys will continue to work.", @@ -894,6 +895,7 @@ "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.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.", "SCIM token": "SCIM token", "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.", diff --git a/apps/client/src/ee/api-key/components/update-api-key-modal.tsx b/apps/client/src/ee/api-key/components/update-api-key-modal.tsx index e4370eac..0e66d680 100644 --- a/apps/client/src/ee/api-key/components/update-api-key-modal.tsx +++ b/apps/client/src/ee/api-key/components/update-api-key-modal.tsx @@ -53,7 +53,7 @@ export function UpdateApiKeyModal({
handleSubmit(values))}> diff --git a/apps/client/src/ee/audit/lib/audit-event-labels.ts b/apps/client/src/ee/audit/lib/audit-event-labels.ts index e3850cd3..7fc55b3d 100644 --- a/apps/client/src/ee/audit/lib/audit-event-labels.ts +++ b/apps/client/src/ee/audit/lib/audit-event-labels.ts @@ -34,6 +34,7 @@ export const auditEventLabels: Record = { "api_key.deleted": "Deleted API key", "scim_token.created": "Created SCIM token", + "scim_token.updated": "Updated SCIM token", "scim_token.deleted": "Deleted SCIM token", "space.created": "Created space", @@ -181,6 +182,7 @@ export const eventFilterOptions: EventGroup[] = [ group: "SCIM token", items: [ { value: "scim_token.created", label: "Created SCIM token" }, + { value: "scim_token.updated", label: "Updated SCIM token" }, { value: "scim_token.deleted", label: "Deleted 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 index 45402bb6..81b596ee 100644 --- a/apps/client/src/ee/scim/components/scim-token-table.tsx +++ b/apps/client/src/ee/scim/components/scim-token-table.tsx @@ -1,5 +1,5 @@ import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core"; -import { IconDots, IconTrash } from "@tabler/icons-react"; +import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react"; import { format } from "date-fns"; import { useTranslation } from "react-i18next"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; @@ -10,12 +10,14 @@ import { IScimToken } from "@/ee/scim/types/scim-token.types"; interface ScimTokenTableProps { tokens: IScimToken[]; isLoading?: boolean; + onUpdate?: (token: IScimToken) => void; onRevoke?: (token: IScimToken) => void; } export function ScimTokenTable({ tokens, isLoading, + onUpdate, onRevoke, }: ScimTokenTableProps) { const { t } = useTranslation(); @@ -96,6 +98,14 @@ export function ScimTokenTable({ + {onUpdate && ( + } + onClick={() => onUpdate(token)} + > + {t("Rename")} + + )} {onRevoke && ( } diff --git a/apps/client/src/ee/scim/components/update-scim-token-modal.tsx b/apps/client/src/ee/scim/components/update-scim-token-modal.tsx new file mode 100644 index 00000000..104e215f --- /dev/null +++ b/apps/client/src/ee/scim/components/update-scim-token-modal.tsx @@ -0,0 +1,77 @@ +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 { useEffect } from "react"; +import { useUpdateScimTokenMutation } from "@/ee/scim/queries/scim-token-query"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), +}); +type FormValues = z.infer; + +interface UpdateScimTokenModalProps { + opened: boolean; + onClose: () => void; + scimToken: IScimToken | null; +} + +export function UpdateScimTokenModal({ + opened, + onClose, + scimToken, +}: UpdateScimTokenModalProps) { + const { t } = useTranslation(); + const updateMutation = useUpdateScimTokenMutation(); + + const form = useForm({ + validate: zod4Resolver(formSchema), + initialValues: { name: "" }, + }); + + useEffect(() => { + if (opened && scimToken) { + form.setValues({ name: scimToken.name }); + } + }, [opened, scimToken]); + + const handleSubmit = async (data: FormValues) => { + if (!scimToken) return; + await updateMutation.mutateAsync({ + tokenId: scimToken.id, + name: data.name, + }); + onClose(); + }; + + return ( + + handleSubmit(values))}> + + + + + + + + + + + ); +} diff --git a/apps/client/src/ee/scim/queries/scim-token-query.ts b/apps/client/src/ee/scim/queries/scim-token-query.ts index 783443dc..999f4d20 100644 --- a/apps/client/src/ee/scim/queries/scim-token-query.ts +++ b/apps/client/src/ee/scim/queries/scim-token-query.ts @@ -10,11 +10,13 @@ import { createScimToken, getScimTokens, revokeScimToken, + updateScimToken, } from "@/ee/scim/services/scim-token-service"; import { IScimToken, ICreateScimTokenRequest, IRevokeScimTokenRequest, + IUpdateScimTokenRequest, } from "@/ee/scim/types/scim-token.types"; import { notifications } from "@mantine/notifications"; import { useTranslation } from "react-i18next"; @@ -25,8 +27,6 @@ export function useGetScimTokensQuery( return useQuery({ queryKey: ["scim-token-list", params], queryFn: () => getScimTokens(params), - staleTime: 0, - gcTime: 0, placeholderData: keepPreviousData, }); } @@ -55,6 +55,26 @@ export function useCreateScimTokenMutation() { }); } +export function useUpdateScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => updateScimToken(data), + onSuccess: () => { + notifications.show({ message: t("Updated 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(); diff --git a/apps/client/src/ee/scim/services/scim-token-service.ts b/apps/client/src/ee/scim/services/scim-token-service.ts index 916c5468..27e73035 100644 --- a/apps/client/src/ee/scim/services/scim-token-service.ts +++ b/apps/client/src/ee/scim/services/scim-token-service.ts @@ -3,6 +3,7 @@ import { IScimToken, ICreateScimTokenRequest, IRevokeScimTokenRequest, + IUpdateScimTokenRequest, } from "@/ee/scim/types/scim-token.types"; import { IPagination, QueryParams } from "@/lib/types.ts"; @@ -20,6 +21,12 @@ export async function createScimToken( return req.data; } +export async function updateScimToken( + data: IUpdateScimTokenRequest, +): Promise { + await api.post("/scim-tokens/update", data); +} + export async function revokeScimToken( data: IRevokeScimTokenRequest, ): Promise { diff --git a/apps/client/src/ee/scim/types/scim-token.types.ts b/apps/client/src/ee/scim/types/scim-token.types.ts index ec0a22dc..07650129 100644 --- a/apps/client/src/ee/scim/types/scim-token.types.ts +++ b/apps/client/src/ee/scim/types/scim-token.types.ts @@ -17,6 +17,11 @@ export interface ICreateScimTokenRequest { name: string; } +export interface IUpdateScimTokenRequest { + tokenId: string; + name: string; +} + export interface IRevokeScimTokenRequest { tokenId: string; } diff --git a/apps/client/src/ee/security/pages/security.tsx b/apps/client/src/ee/security/pages/security.tsx index a922a448..ee634351 100644 --- a/apps/client/src/ee/security/pages/security.tsx +++ b/apps/client/src/ee/security/pages/security.tsx @@ -1,7 +1,16 @@ import { Helmet } from "react-helmet-async"; import { getAppName, isCloud } from "@/lib/config.ts"; import SettingsTitle from "@/components/settings/settings-title.tsx"; -import { Alert, Button, Card, Divider, Group, Space, Title } from "@mantine/core"; +import { + Alert, + Button, + Card, + Divider, + Group, + Space, + Title, + Tooltip, +} from "@mantine/core"; import { IconInfoCircle } from "@tabler/icons-react"; import React, { useState } from "react"; import useUserRole from "@/hooks/use-user-role.tsx"; @@ -23,11 +32,14 @@ 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 { UpdateScimTokenModal } from "@/ee/scim/components/update-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"; +const SCIM_TOKEN_LIMIT = 5; + export default function Security() { const { t } = useTranslation(); const { isAdmin } = useUserRole(); @@ -43,6 +55,7 @@ export default function Security() { const [createOpen, setCreateOpen] = useState(false); const [createdToken, setCreatedToken] = useState(null); + const [updateTarget, setUpdateTarget] = useState(null); const [revokeTarget, setRevokeTarget] = useState(null); if (!isAdmin) { @@ -118,17 +131,30 @@ export default function Security() { {t("SCIM tokens")} - + + + @@ -156,6 +182,12 @@ export default function Security() { scimToken={createdToken} /> + setUpdateTarget(null)} + scimToken={updateTarget} + /> + setRevokeTarget(null)} diff --git a/apps/server/src/common/events/audit-events.ts b/apps/server/src/common/events/audit-events.ts index 39a1e5d3..d8be76f8 100644 --- a/apps/server/src/common/events/audit-events.ts +++ b/apps/server/src/common/events/audit-events.ts @@ -25,6 +25,7 @@ export const AuditEvent = { // SCIM Tokens SCIM_TOKEN_CREATED: 'scim_token.created', + SCIM_TOKEN_UPDATED: 'scim_token.updated', SCIM_TOKEN_DELETED: 'scim_token.deleted', // Space diff --git a/apps/server/src/ee b/apps/server/src/ee index c10c6459..10982907 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit c10c6459277f02bd05a88139b05b79c5b033a381 +Subproject commit 109829076c8d81f6d77836820a5caa63ae8d073f