This commit is contained in:
Philipinho
2026-05-01 14:47:38 +01:00
parent 46923e19f5
commit 1a66e7fbe3
11 changed files with 168 additions and 12 deletions
@@ -621,7 +621,8 @@
"Revoked successfully": "Revoked successfully", "Revoked successfully": "Revoked successfully",
"Select expiration date": "Select expiration date", "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.", "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", "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", "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.", "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 endpoint URL": "SCIM endpoint URL",
"SCIM provisioning": "SCIM provisioning", "SCIM provisioning": "SCIM provisioning",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM takes precedence over SSO group sync while enabled.", "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 token": "SCIM token",
"SCIM tokens": "SCIM tokens", "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.", "This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.",
@@ -53,7 +53,7 @@ export function UpdateApiKeyModal({
<Modal <Modal
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={t("Update API key")} title={t("Update {{credential}}", { credential: t("API key") })}
size="md" size="md"
> >
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}> <form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
@@ -34,6 +34,7 @@ export const auditEventLabels: Record<string, string> = {
"api_key.deleted": "Deleted API key", "api_key.deleted": "Deleted API key",
"scim_token.created": "Created SCIM token", "scim_token.created": "Created SCIM token",
"scim_token.updated": "Updated SCIM token",
"scim_token.deleted": "Deleted SCIM token", "scim_token.deleted": "Deleted SCIM token",
"space.created": "Created space", "space.created": "Created space",
@@ -181,6 +182,7 @@ export const eventFilterOptions: EventGroup[] = [
group: "SCIM token", group: "SCIM token",
items: [ items: [
{ value: "scim_token.created", label: "Created SCIM token" }, { value: "scim_token.created", label: "Created SCIM token" },
{ value: "scim_token.updated", label: "Updated SCIM token" },
{ value: "scim_token.deleted", label: "Deleted SCIM token" }, { value: "scim_token.deleted", label: "Deleted SCIM token" },
], ],
}, },
@@ -1,5 +1,5 @@
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core"; 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 { format } from "date-fns";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
@@ -10,12 +10,14 @@ import { IScimToken } from "@/ee/scim/types/scim-token.types";
interface ScimTokenTableProps { interface ScimTokenTableProps {
tokens: IScimToken[]; tokens: IScimToken[];
isLoading?: boolean; isLoading?: boolean;
onUpdate?: (token: IScimToken) => void;
onRevoke?: (token: IScimToken) => void; onRevoke?: (token: IScimToken) => void;
} }
export function ScimTokenTable({ export function ScimTokenTable({
tokens, tokens,
isLoading, isLoading,
onUpdate,
onRevoke, onRevoke,
}: ScimTokenTableProps) { }: ScimTokenTableProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -96,6 +98,14 @@ export function ScimTokenTable({
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
{onUpdate && (
<Menu.Item
leftSection={<IconEdit size={16} />}
onClick={() => onUpdate(token)}
>
{t("Rename")}
</Menu.Item>
)}
{onRevoke && ( {onRevoke && (
<Menu.Item <Menu.Item
leftSection={<IconTrash size={16} />} leftSection={<IconTrash size={16} />}
@@ -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<typeof formSchema>;
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<FormValues>({
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 (
<Modal
opened={opened}
onClose={onClose}
title={t("Update {{credential}}", { credential: t("SCIM token") })}
size="md"
>
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
<Stack gap="md">
<TextInput
label={t("Name")}
placeholder={t("Enter a descriptive name")}
required
{...form.getInputProps("name")}
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onClose}>
{t("Cancel")}
</Button>
<Button type="submit" loading={updateMutation.isPending}>
{t("Update")}
</Button>
</Group>
</Stack>
</form>
</Modal>
);
}
@@ -10,11 +10,13 @@ import {
createScimToken, createScimToken,
getScimTokens, getScimTokens,
revokeScimToken, revokeScimToken,
updateScimToken,
} from "@/ee/scim/services/scim-token-service"; } from "@/ee/scim/services/scim-token-service";
import { import {
IScimToken, IScimToken,
ICreateScimTokenRequest, ICreateScimTokenRequest,
IRevokeScimTokenRequest, IRevokeScimTokenRequest,
IUpdateScimTokenRequest,
} from "@/ee/scim/types/scim-token.types"; } from "@/ee/scim/types/scim-token.types";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -25,8 +27,6 @@ export function useGetScimTokensQuery(
return useQuery({ return useQuery({
queryKey: ["scim-token-list", params], queryKey: ["scim-token-list", params],
queryFn: () => getScimTokens(params), queryFn: () => getScimTokens(params),
staleTime: 0,
gcTime: 0,
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}); });
} }
@@ -55,6 +55,26 @@ export function useCreateScimTokenMutation() {
}); });
} }
export function useUpdateScimTokenMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<void, Error, IUpdateScimTokenRequest>({
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() { export function useRevokeScimTokenMutation() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -3,6 +3,7 @@ import {
IScimToken, IScimToken,
ICreateScimTokenRequest, ICreateScimTokenRequest,
IRevokeScimTokenRequest, IRevokeScimTokenRequest,
IUpdateScimTokenRequest,
} from "@/ee/scim/types/scim-token.types"; } from "@/ee/scim/types/scim-token.types";
import { IPagination, QueryParams } from "@/lib/types.ts"; import { IPagination, QueryParams } from "@/lib/types.ts";
@@ -20,6 +21,12 @@ export async function createScimToken(
return req.data; return req.data;
} }
export async function updateScimToken(
data: IUpdateScimTokenRequest,
): Promise<void> {
await api.post("/scim-tokens/update", data);
}
export async function revokeScimToken( export async function revokeScimToken(
data: IRevokeScimTokenRequest, data: IRevokeScimTokenRequest,
): Promise<void> { ): Promise<void> {
@@ -17,6 +17,11 @@ export interface ICreateScimTokenRequest {
name: string; name: string;
} }
export interface IUpdateScimTokenRequest {
tokenId: string;
name: string;
}
export interface IRevokeScimTokenRequest { export interface IRevokeScimTokenRequest {
tokenId: string; tokenId: string;
} }
+34 -2
View File
@@ -1,7 +1,16 @@
import { Helmet } from "react-helmet-async"; import { Helmet } from "react-helmet-async";
import { getAppName, isCloud } from "@/lib/config.ts"; import { getAppName, isCloud } from "@/lib/config.ts";
import SettingsTitle from "@/components/settings/settings-title.tsx"; 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 { IconInfoCircle } from "@tabler/icons-react";
import React, { useState } from "react"; import React, { useState } from "react";
import useUserRole from "@/hooks/use-user-role.tsx"; 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 { CreateScimTokenModal } from "@/ee/scim/components/create-scim-token-modal";
import { ScimTokenCreatedModal } from "@/ee/scim/components/scim-token-created-modal"; import { ScimTokenCreatedModal } from "@/ee/scim/components/scim-token-created-modal";
import { RevokeScimTokenModal } from "@/ee/scim/components/revoke-scim-token-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 EnableScim from "@/ee/scim/components/enable-scim";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { IScimToken } from "@/ee/scim/types/scim-token.types"; import { IScimToken } from "@/ee/scim/types/scim-token.types";
const SCIM_TOKEN_LIMIT = 5;
export default function Security() { export default function Security() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
@@ -43,6 +55,7 @@ export default function Security() {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [createdToken, setCreatedToken] = useState<IScimToken | null>(null); const [createdToken, setCreatedToken] = useState<IScimToken | null>(null);
const [updateTarget, setUpdateTarget] = useState<IScimToken | null>(null);
const [revokeTarget, setRevokeTarget] = useState<IScimToken | null>(null); const [revokeTarget, setRevokeTarget] = useState<IScimToken | null>(null);
if (!isAdmin) { if (!isAdmin) {
@@ -118,17 +131,30 @@ export default function Security() {
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={5}>{t("SCIM tokens")}</Title> <Title order={5}>{t("SCIM tokens")}</Title>
<Button onClick={() => setCreateOpen(true)}> <Tooltip
label={t(
"You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.",
{ max: SCIM_TOKEN_LIMIT },
)}
disabled={(scimData?.items.length ?? 0) < SCIM_TOKEN_LIMIT}
refProp="rootRef"
>
<Button
onClick={() => setCreateOpen(true)}
disabled={(scimData?.items.length ?? 0) >= SCIM_TOKEN_LIMIT}
>
{t("Create {{credential}}", { {t("Create {{credential}}", {
credential: t("SCIM token"), credential: t("SCIM token"),
})} })}
</Button> </Button>
</Tooltip>
</Group> </Group>
<Card shadow="sm" radius="sm"> <Card shadow="sm" radius="sm">
<ScimTokenTable <ScimTokenTable
tokens={scimData?.items} tokens={scimData?.items}
isLoading={scimLoading} isLoading={scimLoading}
onUpdate={setUpdateTarget}
onRevoke={setRevokeTarget} onRevoke={setRevokeTarget}
/> />
</Card> </Card>
@@ -156,6 +182,12 @@ export default function Security() {
scimToken={createdToken} scimToken={createdToken}
/> />
<UpdateScimTokenModal
opened={!!updateTarget}
onClose={() => setUpdateTarget(null)}
scimToken={updateTarget}
/>
<RevokeScimTokenModal <RevokeScimTokenModal
opened={!!revokeTarget} opened={!!revokeTarget}
onClose={() => setRevokeTarget(null)} onClose={() => setRevokeTarget(null)}
@@ -25,6 +25,7 @@ export const AuditEvent = {
// SCIM Tokens // SCIM Tokens
SCIM_TOKEN_CREATED: 'scim_token.created', SCIM_TOKEN_CREATED: 'scim_token.created',
SCIM_TOKEN_UPDATED: 'scim_token.updated',
SCIM_TOKEN_DELETED: 'scim_token.deleted', SCIM_TOKEN_DELETED: 'scim_token.deleted',
// Space // Space