mirror of
https://github.com/docmost/docmost.git
synced 2026-05-09 07:43:06 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 206961e842 |
@@ -391,7 +391,7 @@
|
|||||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie etwas. Geben Sie \"/\" für Befehle ein",
|
"Write anything. Enter \"/\" for commands": "Schreiben Sie etwas. Geben Sie \"/\" für Befehle ein",
|
||||||
"Write...": "\"Schreiben...\"",
|
"Write...": "\"Schreiben...\"",
|
||||||
"Column count": "Spaltenanzahl",
|
"Column count": "Spaltenanzahl",
|
||||||
"{{count}} Columns": "{{count}} Spalten",
|
"{{count}} Columns": "{count, plural, one {# Spalte} other {# Spalten}}",
|
||||||
"Equal columns": "Gleich breite Spalten",
|
"Equal columns": "Gleich breite Spalten",
|
||||||
"Left sidebar": "Linke Seitenleiste",
|
"Left sidebar": "Linke Seitenleiste",
|
||||||
"Right sidebar": "Rechte Seitenleiste",
|
"Right sidebar": "Rechte Seitenleiste",
|
||||||
|
|||||||
@@ -608,21 +608,25 @@
|
|||||||
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
|
"Image exceeds 10MB limit.": "Image exceeds 10MB limit.",
|
||||||
"Image removed successfully": "Image removed successfully",
|
"Image removed successfully": "Image removed successfully",
|
||||||
"API key": "API key",
|
"API key": "API key",
|
||||||
|
"API key created successfully": "API key created successfully",
|
||||||
"API keys": "API keys",
|
"API keys": "API keys",
|
||||||
"API management": "API management",
|
"API management": "API management",
|
||||||
|
"Are you sure you want to revoke this API key": "Are you sure you want to revoke this API key",
|
||||||
|
"Create API Key": "Create API Key",
|
||||||
"Custom expiration date": "Custom expiration date",
|
"Custom expiration date": "Custom expiration date",
|
||||||
"Enter a descriptive token name": "Enter a descriptive token name",
|
"Enter a descriptive token name": "Enter a descriptive token name",
|
||||||
"Expiration": "Expiration",
|
"Expiration": "Expiration",
|
||||||
"Expired": "Expired",
|
"Expired": "Expired",
|
||||||
"Expires": "Expires",
|
"Expires": "Expires",
|
||||||
|
"I've saved my API key": "I've saved my API key",
|
||||||
"Last use": "Last Used",
|
"Last use": "Last Used",
|
||||||
"No API keys found": "No API keys found",
|
"No API keys found": "No API keys found",
|
||||||
"No expiration": "No expiration",
|
"No expiration": "No expiration",
|
||||||
|
"Revoke API key": "Revoke API key",
|
||||||
"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": "Update",
|
"Update API key": "Update API key",
|
||||||
"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.",
|
||||||
@@ -876,29 +880,5 @@
|
|||||||
"Try a different search term.": "Try a different search term.",
|
"Try a different search term.": "Try a different search term.",
|
||||||
"Try again": "Try again",
|
"Try again": "Try again",
|
||||||
"Untitled chat": "Untitled chat",
|
"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 {{credential}}": "Are you sure you want to revoke this {{credential}}",
|
|
||||||
"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 {{credential}}": "Create {{credential}}",
|
|
||||||
"{{credential}} created": "{{credential}} created",
|
|
||||||
"{{credential}} created successfully": "{{credential}} created successfully",
|
|
||||||
"Created by": "Created by",
|
|
||||||
"Custom": "Custom",
|
|
||||||
"Enable SCIM": "Enable SCIM",
|
|
||||||
"Enter a descriptive name": "Enter a descriptive name",
|
|
||||||
"I've saved my {{credential}}": "I've saved my {{credential}}",
|
|
||||||
"Important": "Important",
|
|
||||||
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "Make sure to copy your {{credential}} now. You won't be able to see it again!",
|
|
||||||
"Never": "Never",
|
|
||||||
"Revoke {{credential}}": "Revoke {{credential}}",
|
|
||||||
"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.",
|
|
||||||
"Toggle SCIM provisioning": "Toggle SCIM provisioning",
|
|
||||||
"Token": "Token"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,9 +116,7 @@ export default function GlobalAppShell({
|
|||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
<AppShell.Main>
|
<AppShell.Main>
|
||||||
{isSettingsRoute ? (
|
{isSettingsRoute ? (
|
||||||
<Container size={900} pb={80}>
|
<Container size={900}>{children}</Container>
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { getShares } from "@/features/share/services/share-service.ts";
|
|||||||
import { getApiKeys } from "@/ee/api-key";
|
import { getApiKeys } from "@/ee/api-key";
|
||||||
import { getAuditLogs } from "@/ee/audit/services/audit-service";
|
import { getAuditLogs } from "@/ee/audit/services/audit-service";
|
||||||
import { getVerificationList } from "@/ee/page-verification/services/page-verification-service";
|
import { getVerificationList } from "@/ee/page-verification/services/page-verification-service";
|
||||||
import { getScimTokens } from "@/ee/scim/services/scim-token-service";
|
|
||||||
|
|
||||||
export const prefetchWorkspaceMembers = () => {
|
export const prefetchWorkspaceMembers = () => {
|
||||||
const params: QueryParams = { limit: 100, query: "" };
|
const params: QueryParams = { limit: 100, query: "" };
|
||||||
@@ -99,10 +98,3 @@ export const prefetchVerifiedPages = () => {
|
|||||||
queryFn: () => getVerificationList(params),
|
queryFn: () => getVerificationList(params),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prefetchScimTokens = () => {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["scim-token-list", { cursor: undefined }],
|
|
||||||
queryFn: () => getScimTokens({}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
prefetchBilling,
|
prefetchBilling,
|
||||||
prefetchGroups,
|
prefetchGroups,
|
||||||
prefetchLicense,
|
prefetchLicense,
|
||||||
prefetchScimTokens,
|
|
||||||
prefetchShares,
|
prefetchShares,
|
||||||
prefetchSpaces,
|
prefetchSpaces,
|
||||||
prefetchSsoProviders,
|
prefetchSsoProviders,
|
||||||
@@ -205,10 +204,7 @@ export default function SettingsSidebar() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "Security & SSO":
|
case "Security & SSO":
|
||||||
prefetchHandler = () => {
|
prefetchHandler = prefetchSsoProviders;
|
||||||
prefetchSsoProviders();
|
|
||||||
prefetchScimTokens();
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
case "Public sharing":
|
case "Public sharing":
|
||||||
prefetchHandler = prefetchShares;
|
prefetchHandler = prefetchShares;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function ApiKeyCreatedModal({
|
|||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("{{credential}} created", { credential: t("API key") })}
|
title={t("API key created")}
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@@ -41,8 +41,7 @@ export function ApiKeyCreatedModal({
|
|||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
{t(
|
{t(
|
||||||
"Make sure to copy your {{credential}} now. You won't be able to see it again!",
|
"Make sure to copy your API key now. You won't be able to see it again!",
|
||||||
{ credential: t("API key") },
|
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ export function ApiKeyCreatedModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button fullWidth onClick={onClose} mt="md">
|
<Button fullWidth onClick={onClose} mt="md">
|
||||||
{t("I've saved my {{credential}}", { credential: t("API key") })}
|
{t("I've saved my API key")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function CreateApiKeyModal({
|
|||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
title={t("Create {{credential}}", { credential: t("API key") })}
|
title={t("Create API Key")}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||||
|
|||||||
@@ -30,14 +30,12 @@ export function RevokeApiKeyModal({
|
|||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("Revoke {{credential}}", { credential: t("API key") })}
|
title={t("Revoke API key")}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text>
|
<Text>
|
||||||
{t("Are you sure you want to revoke this {{credential}}", {
|
{t("Are you sure you want to revoke this API key")}{" "}
|
||||||
credential: t("API key"),
|
|
||||||
})}{" "}
|
|
||||||
<strong>{apiKey?.name}</strong>?
|
<strong>{apiKey?.name}</strong>?
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function UpdateApiKeyModal({
|
|||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={t("Update {{credential}}", { credential: t("API key") })}
|
title={t("Update API key")}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ export function useCreateApiKeyMutation() {
|
|||||||
return useMutation<IApiKey, Error, ICreateApiKeyRequest>({
|
return useMutation<IApiKey, Error, ICreateApiKeyRequest>({
|
||||||
mutationFn: (data) => createApiKey(data),
|
mutationFn: (data) => createApiKey(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
notifications.show({
|
notifications.show({ message: t("API key created successfully") });
|
||||||
message: t("{{credential}} created successfully", {
|
|
||||||
credential: t("API key"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
predicate: (item) =>
|
predicate: (item) =>
|
||||||
["api-key-list"].includes(item.queryKey[0] as string),
|
["api-key-list"].includes(item.queryKey[0] as string),
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ export const auditEventLabels: Record<string, string> = {
|
|||||||
"api_key.updated": "Updated API key",
|
"api_key.updated": "Updated API key",
|
||||||
"api_key.deleted": "Deleted API key",
|
"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",
|
"space.created": "Created space",
|
||||||
"space.updated": "Updated space",
|
"space.updated": "Updated space",
|
||||||
"space.deleted": "Deleted space",
|
"space.deleted": "Deleted space",
|
||||||
@@ -178,14 +174,6 @@ export const eventFilterOptions: EventGroup[] = [
|
|||||||
{ value: "api_key.deleted", label: "Deleted API key" },
|
{ value: "api_key.deleted", label: "Deleted API key" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
group: "License",
|
group: "License",
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function PagePermissionList({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<ScrollArea.Autosize mah={400} viewportRef={viewportRef}>
|
<ScrollArea mah={250} viewportRef={viewportRef}>
|
||||||
{sortedMembers.map((member) => (
|
{sortedMembers.map((member) => (
|
||||||
<PagePermissionItem
|
<PagePermissionItem
|
||||||
key={`${member.type}-${member.id}`}
|
key={`${member.type}-${member.id}`}
|
||||||
@@ -158,7 +158,7 @@ export function PagePermissionList({
|
|||||||
<Loader size="xs" />
|
<Loader size="xs" />
|
||||||
</Center>
|
</Center>
|
||||||
)}
|
)}
|
||||||
</ScrollArea.Autosize>
|
</ScrollArea>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
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<typeof formSchema>;
|
|
||||||
|
|
||||||
export function CreateScimTokenModal({
|
|
||||||
opened,
|
|
||||||
onClose,
|
|
||||||
onSuccess,
|
|
||||||
}: CreateScimTokenModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const createMutation = useCreateScimTokenMutation();
|
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
|
||||||
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 (
|
|
||||||
<Modal
|
|
||||||
opened={opened}
|
|
||||||
onClose={handleClose}
|
|
||||||
title={t("Create {{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")}
|
|
||||||
data-autofocus
|
|
||||||
required
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button variant="default" onClick={handleClose}>
|
|
||||||
{t("Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" loading={createMutation.isPending}>
|
|
||||||
{t("Create")}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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<HTMLInputElement>) => {
|
|
||||||
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 (
|
|
||||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
|
||||||
<div>
|
|
||||||
<Text size="md">{t("Enable SCIM")}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t(
|
|
||||||
"Automatically provision users and groups from your identity provider via SCIM.",
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
|
|
||||||
<Switch
|
|
||||||
labelPosition="left"
|
|
||||||
defaultChecked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={!hasAccess}
|
|
||||||
aria-label={t("Toggle SCIM provisioning")}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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 (
|
|
||||||
<Modal
|
|
||||||
opened={opened}
|
|
||||||
onClose={onClose}
|
|
||||||
title={t("Revoke {{credential}}", { credential: t("SCIM token") })}
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Text>
|
|
||||||
{t("Are you sure you want to revoke this {{credential}}", {
|
|
||||||
credential: t("SCIM token"),
|
|
||||||
})}{" "}
|
|
||||||
<strong>{scimToken?.name}</strong>?
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t(
|
|
||||||
"This action cannot be undone. Your identity provider will stop syncing immediately.",
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button variant="default" onClick={onClose}>
|
|
||||||
{t("Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="red"
|
|
||||||
onClick={handleRevoke}
|
|
||||||
loading={revokeMutation.isPending}
|
|
||||||
>
|
|
||||||
{t("Revoke")}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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 (
|
|
||||||
<Modal
|
|
||||||
opened={opened}
|
|
||||||
onClose={onClose}
|
|
||||||
title={t("{{credential}} created", { credential: t("SCIM token") })}
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Alert
|
|
||||||
icon={<IconAlertTriangle size={16} />}
|
|
||||||
title={t("Important")}
|
|
||||||
color="red"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"Make sure to copy your {{credential}} now. You won't be able to see it again!",
|
|
||||||
{ credential: t("SCIM token") },
|
|
||||||
)}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">
|
|
||||||
{t("SCIM token")}
|
|
||||||
</Text>
|
|
||||||
<Group gap="xs" wrap="nowrap">
|
|
||||||
<TextInput
|
|
||||||
variant="filled"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
value={scimToken.token}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<CopyTextButton text={scimToken.token} />
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button fullWidth onClick={onClose} mt="md">
|
|
||||||
{t("I've saved my {{credential}}", { credential: t("SCIM token") })}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
|
|
||||||
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";
|
|
||||||
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;
|
|
||||||
onUpdate?: (token: IScimToken) => void;
|
|
||||||
onRevoke?: (token: IScimToken) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ScimTokenTable({
|
|
||||||
tokens,
|
|
||||||
isLoading,
|
|
||||||
onUpdate,
|
|
||||||
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 (
|
|
||||||
<Table.ScrollContainer minWidth={500}>
|
|
||||||
<Table highlightOnHover verticalSpacing="sm">
|
|
||||||
<Table.Thead>
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Th>{t("Name")}</Table.Th>
|
|
||||||
<Table.Th>{t("Token")}</Table.Th>
|
|
||||||
<Table.Th>{t("Created by")}</Table.Th>
|
|
||||||
<Table.Th>{t("Last used")}</Table.Th>
|
|
||||||
<Table.Th>{t("Created")}</Table.Th>
|
|
||||||
<Table.Th></Table.Th>
|
|
||||||
</Table.Tr>
|
|
||||||
</Table.Thead>
|
|
||||||
|
|
||||||
<Table.Tbody>
|
|
||||||
{tokens && tokens.length > 0 ? (
|
|
||||||
tokens.map((token) => (
|
|
||||||
<Table.Tr key={token.id}>
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz="sm" fw={500}>
|
|
||||||
{token.name}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz="sm" ff="monospace" c="dimmed">
|
|
||||||
••••{token.tokenLastFour}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
{token.creator ? (
|
|
||||||
<Table.Td>
|
|
||||||
<Group gap="4" wrap="nowrap">
|
|
||||||
<CustomAvatar
|
|
||||||
avatarUrl={token.creator?.avatarUrl}
|
|
||||||
name={token.creator.name}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Text fz="sm" lineClamp={1}>
|
|
||||||
{token.creator.name}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
) : (
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz="sm" c="dimmed">
|
|
||||||
—
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{formatDate(token.lastUsedAt)}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{formatDate(token.createdAt)}
|
|
||||||
</Text>
|
|
||||||
</Table.Td>
|
|
||||||
|
|
||||||
<Table.Td>
|
|
||||||
<Menu position="bottom-end" withinPortal>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="subtle" color="gray">
|
|
||||||
<IconDots size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
{onUpdate && (
|
|
||||||
<Menu.Item
|
|
||||||
leftSection={<IconEdit size={16} />}
|
|
||||||
onClick={() => onUpdate(token)}
|
|
||||||
>
|
|
||||||
{t("Rename")}
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
{onRevoke && (
|
|
||||||
<Menu.Item
|
|
||||||
leftSection={<IconTrash size={16} />}
|
|
||||||
color="red"
|
|
||||||
onClick={() => onRevoke(token)}
|
|
||||||
>
|
|
||||||
{t("Revoke")}
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<NoTableResults colSpan={6} />
|
|
||||||
)}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Table.ScrollContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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 (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t("SCIM endpoint URL")}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{t(
|
|
||||||
"Configure your identity provider with this URL to provision users and groups.",
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Group gap="xs" wrap="nowrap">
|
|
||||||
<TextInput
|
|
||||||
variant="filled"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
value={scimUrl}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
<CopyTextButton text={scimUrl} />
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./types/scim-token.types";
|
|
||||||
export * from "./services/scim-token-service";
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
|
||||||
import {
|
|
||||||
keepPreviousData,
|
|
||||||
useMutation,
|
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
|
||||||
UseQueryResult,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
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";
|
|
||||||
|
|
||||||
export function useGetScimTokensQuery(
|
|
||||||
params?: QueryParams,
|
|
||||||
): UseQueryResult<IPagination<IScimToken>, Error> {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["scim-token-list", params],
|
|
||||||
queryFn: () => getScimTokens(params),
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateScimTokenMutation() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return useMutation<IScimToken, Error, ICreateScimTokenRequest>({
|
|
||||||
mutationFn: (data) => createScimToken(data),
|
|
||||||
onSuccess: () => {
|
|
||||||
notifications.show({
|
|
||||||
message: t("{{credential}} created successfully", {
|
|
||||||
credential: t("SCIM token"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
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 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() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return useMutation<void, Error, IRevokeScimTokenRequest>({
|
|
||||||
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" });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import api from "@/lib/api-client";
|
|
||||||
import {
|
|
||||||
IScimToken,
|
|
||||||
ICreateScimTokenRequest,
|
|
||||||
IRevokeScimTokenRequest,
|
|
||||||
IUpdateScimTokenRequest,
|
|
||||||
} from "@/ee/scim/types/scim-token.types";
|
|
||||||
import { IPagination, QueryParams } from "@/lib/types.ts";
|
|
||||||
|
|
||||||
export async function getScimTokens(
|
|
||||||
params?: QueryParams,
|
|
||||||
): Promise<IPagination<IScimToken>> {
|
|
||||||
const req = await api.post("/scim-tokens", { ...params });
|
|
||||||
return req.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createScimToken(
|
|
||||||
data: ICreateScimTokenRequest,
|
|
||||||
): Promise<IScimToken> {
|
|
||||||
const req = await api.post<IScimToken>("/scim-tokens/create", data);
|
|
||||||
return req.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateScimToken(
|
|
||||||
data: IUpdateScimTokenRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
await api.post("/scim-tokens/update", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function revokeScimToken(
|
|
||||||
data: IRevokeScimTokenRequest,
|
|
||||||
): Promise<void> {
|
|
||||||
await api.post("/scim-tokens/revoke", data);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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<IUser>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICreateScimTokenRequest {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUpdateScimTokenRequest {
|
|
||||||
tokenId: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRevokeScimTokenRequest {
|
|
||||||
tokenId: string;
|
|
||||||
}
|
|
||||||
@@ -69,8 +69,8 @@ export default function SsoProviderList() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card shadow="sm" radius="sm">
|
<Card shadow="sm" radius="sm">
|
||||||
<Table.ScrollContainer minWidth={600} maxHeight={400}>
|
<Table.ScrollContainer minWidth={600}>
|
||||||
<Table verticalSpacing="sm" stickyHeader>
|
<Table verticalSpacing="sm">
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th>{t("Name")}</Table.Th>
|
<Table.Th>{t("Name")}</Table.Th>
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
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 {
|
import { Divider, Title } from "@mantine/core";
|
||||||
Alert,
|
import React from "react";
|
||||||
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";
|
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||||
import SsoProviderList from "@/ee/security/components/sso-provider-list.tsx";
|
import SsoProviderList from "@/ee/security/components/sso-provider-list.tsx";
|
||||||
import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx";
|
import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx";
|
||||||
@@ -22,41 +12,16 @@ import { useTranslation } from "react-i18next";
|
|||||||
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx";
|
||||||
import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx";
|
import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx";
|
||||||
import TrashRetention from "@/ee/security/components/trash-retention.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 { useHasFeature } from "@/ee/hooks/use-feature";
|
||||||
import { Feature } from "@/ee/features";
|
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 { 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() {
|
export default function Security() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isAdmin } = useUserRole();
|
const { isAdmin } = useUserRole();
|
||||||
const hasCustomSso = useHasFeature(Feature.SSO_CUSTOM);
|
const hasCustomSso = useHasFeature(Feature.SSO_CUSTOM);
|
||||||
const hasScim = useHasFeature(Feature.SCIM);
|
const hasRetention = useHasFeature(Feature.RETENTION);
|
||||||
const [workspace] = useAtom(workspaceAtom);
|
const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS);
|
||||||
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<IScimToken | null>(null);
|
|
||||||
const [updateTarget, setUpdateTarget] = useState<IScimToken | null>(null);
|
|
||||||
const [revokeTarget, setRevokeTarget] = useState<IScimToken | null>(null);
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
@@ -80,7 +45,7 @@ export default function Security() {
|
|||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<Title order={4} my="lg">
|
<Title order={4} my="lg">
|
||||||
{t("Single sign-on (SSO)")}
|
Single sign-on (SSO)
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<EnforceSso />
|
<EnforceSso />
|
||||||
@@ -101,102 +66,6 @@ export default function Security() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SsoProviderList />
|
<SsoProviderList />
|
||||||
|
|
||||||
{hasScim && (
|
|
||||||
<>
|
|
||||||
<Divider my="xl" />
|
|
||||||
|
|
||||||
<Title order={4} my="lg">
|
|
||||||
{t("SCIM provisioning")}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
icon={<IconInfoCircle size={16} />}
|
|
||||||
color="blue"
|
|
||||||
variant="light"
|
|
||||||
mb="md"
|
|
||||||
>
|
|
||||||
{t("SCIM takes precedence over SSO group sync while enabled.")}
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<EnableScim />
|
|
||||||
|
|
||||||
<Divider my="lg" />
|
|
||||||
|
|
||||||
<ScimUrlPanel />
|
|
||||||
|
|
||||||
{isScimEnabled && (
|
|
||||||
<>
|
|
||||||
<Divider my="lg" />
|
|
||||||
|
|
||||||
<Group justify="space-between" mb="md">
|
|
||||||
<Title order={5}>{t("SCIM tokens")}</Title>
|
|
||||||
<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}}", {
|
|
||||||
credential: t("SCIM token"),
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Card shadow="sm" radius="sm">
|
|
||||||
<ScimTokenTable
|
|
||||||
tokens={scimData?.items}
|
|
||||||
isLoading={scimLoading}
|
|
||||||
onUpdate={setUpdateTarget}
|
|
||||||
onRevoke={setRevokeTarget}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Space h="md" />
|
|
||||||
|
|
||||||
{scimData?.items.length > 0 && (
|
|
||||||
<Paginate
|
|
||||||
hasPrevPage={scimData?.meta?.hasPrevPage}
|
|
||||||
hasNextPage={scimData?.meta?.hasNextPage}
|
|
||||||
onNext={() => goNext(scimData?.meta?.nextCursor)}
|
|
||||||
onPrev={goPrev}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CreateScimTokenModal
|
|
||||||
opened={createOpen}
|
|
||||||
onClose={() => setCreateOpen(false)}
|
|
||||||
onSuccess={setCreatedToken}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScimTokenCreatedModal
|
|
||||||
opened={!!createdToken}
|
|
||||||
onClose={() => setCreatedToken(null)}
|
|
||||||
scimToken={createdToken}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateScimTokenModal
|
|
||||||
opened={!!updateTarget}
|
|
||||||
onClose={() => setUpdateTarget(null)}
|
|
||||||
scimToken={updateTarget}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RevokeScimTokenModal
|
|
||||||
opened={!!revokeTarget}
|
|
||||||
onClose={() => setRevokeTarget(null)}
|
|
||||||
scimToken={revokeTarget}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import EmojiCommand from "@/features/editor/extensions/emoji-command";
|
import EmojiCommand from "@/features/editor/extensions/emoji-command";
|
||||||
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion";
|
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion";
|
||||||
import MentionView from "@/features/editor/components/mention/mention-view";
|
import MentionView from "@/features/editor/components/mention/mention-view";
|
||||||
import { platformModifierKey } from "@/lib";
|
|
||||||
|
|
||||||
interface CommentEditorProps {
|
interface CommentEditorProps {
|
||||||
defaultContent?: any;
|
defaultContent?: any;
|
||||||
@@ -84,7 +83,7 @@ const CommentEditor = forwardRef(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platformModifierKey(event) && event.code === "Enter") {
|
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (onSave) onSave();
|
if (onSave) onSave();
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ export const uploadAttachmentAction = handleAttachmentUpload({
|
|||||||
},
|
},
|
||||||
validateFn: (file, allowMedia: boolean) => {
|
validateFn: (file, allowMedia: boolean) => {
|
||||||
if (
|
if (
|
||||||
(file.type.includes("image/") ||
|
(file.type.includes("image/") || file.type.includes("video/")) &&
|
||||||
file.type.includes("video/") ||
|
|
||||||
file.type === "application/pdf") &&
|
|
||||||
!allowMedia
|
!allowMedia
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import { useIdle } from "@/hooks/use-idle.ts";
|
|||||||
import { queryClient } from "@/main.tsx";
|
import { queryClient } from "@/main.tsx";
|
||||||
import { IPage } from "@/features/page/types/page.types.ts";
|
import { IPage } from "@/features/page/types/page.types.ts";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { extractPageSlugId, platformModifierKey } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
import { FIVE_MINUTES } from "@/lib/constants.ts";
|
||||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
@@ -232,11 +232,11 @@ export default function PageEditor({
|
|||||||
scrollMargin: 80,
|
scrollMargin: 80,
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keydown: (_view, event) => {
|
keydown: (_view, event) => {
|
||||||
if (platformModifierKey(event) && event.code === "KeyS") {
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (platformModifierKey(event) && event.code === "KeyK") {
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") {
|
||||||
searchSpotlight.open();
|
searchSpotlight.open();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import localEmitter from "@/lib/local-emitter.ts";
|
|||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
import { PageEditMode } from "@/features/user/types/user.types.ts";
|
||||||
import { searchSpotlight } from "@/features/search/constants.ts";
|
import { searchSpotlight } from "@/features/search/constants.ts";
|
||||||
import { platformModifierKey } from "@/lib";
|
|
||||||
|
|
||||||
export interface TitleEditorProps {
|
export interface TitleEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -91,11 +90,11 @@ export function TitleEditor({
|
|||||||
editorProps: {
|
editorProps: {
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keydown: (_view, event) => {
|
keydown: (_view, event) => {
|
||||||
if (platformModifierKey(event) && event.code === "KeyS") {
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (platformModifierKey(event) && event.code === "KeyK") {
|
if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") {
|
||||||
searchSpotlight.open();
|
searchSpotlight.open();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import classes from "./search-control.module.css";
|
import classes from "./search-control.module.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { platformModifierLabel } from "@/lib";
|
|
||||||
|
|
||||||
interface SearchControlProps extends BoxProps, ElementProps<"button"> {}
|
interface SearchControlProps extends BoxProps, ElementProps<"button"> {}
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ export function SearchControl({ className, ...others }: SearchControlProps) {
|
|||||||
{t("Search")}
|
{t("Search")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fw={700} className={classes.shortcut}>
|
<Text fw={700} className={classes.shortcut}>
|
||||||
{platformModifierLabel} + K
|
Ctrl + K
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export interface IWorkspace {
|
|||||||
trashRetentionDays?: number;
|
trashRetentionDays?: number;
|
||||||
restrictApiToAdmins?: boolean;
|
restrictApiToAdmins?: boolean;
|
||||||
allowMemberTemplates?: boolean;
|
allowMemberTemplates?: boolean;
|
||||||
isScimEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkspaceSettings {
|
export interface IWorkspaceSettings {
|
||||||
|
|||||||
@@ -100,15 +100,6 @@ export const normalizeUrl = (url: string): string => {
|
|||||||
return `https://${url}`;
|
return `https://${url}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _isApple = /mac|iphone|ipad|ipod/i.test(navigator.platform ?? "");
|
|
||||||
|
|
||||||
/// Cmd key on Apple devices, Ctrl key everywhere else
|
|
||||||
export function platformModifierKey(event: KeyboardEvent): boolean {
|
|
||||||
return _isApple ? event.metaKey : event.ctrlKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const platformModifierLabel = _isApple ? "⌘" : "Ctrl";
|
|
||||||
|
|
||||||
export function castToBoolean(value: unknown): boolean {
|
export function castToBoolean(value: unknown): boolean {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -111,7 +111,6 @@
|
|||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"scimmy": "1.3.5",
|
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"stripe": "^17.7.0",
|
"stripe": "^17.7.0",
|
||||||
"tlds": "^1.261.0",
|
"tlds": "^1.261.0",
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ export const AuditEvent = {
|
|||||||
API_KEY_UPDATED: 'api_key.updated',
|
API_KEY_UPDATED: 'api_key.updated',
|
||||||
API_KEY_DELETED: 'api_key.deleted',
|
API_KEY_DELETED: 'api_key.deleted',
|
||||||
|
|
||||||
// SCIM Tokens
|
|
||||||
SCIM_TOKEN_CREATED: 'scim_token.created',
|
|
||||||
SCIM_TOKEN_UPDATED: 'scim_token.updated',
|
|
||||||
SCIM_TOKEN_DELETED: 'scim_token.deleted',
|
|
||||||
|
|
||||||
// Space
|
// Space
|
||||||
SPACE_CREATED: 'space.created',
|
SPACE_CREATED: 'space.created',
|
||||||
SPACE_UPDATED: 'space.updated',
|
SPACE_UPDATED: 'space.updated',
|
||||||
@@ -124,7 +119,6 @@ export const AuditResource = {
|
|||||||
COMMENT: 'comment',
|
COMMENT: 'comment',
|
||||||
SHARE: 'share',
|
SHARE: 'share',
|
||||||
API_KEY: 'api_key',
|
API_KEY: 'api_key',
|
||||||
SCIM_TOKEN: 'scim_token',
|
|
||||||
SSO_PROVIDER: 'sso_provider',
|
SSO_PROVIDER: 'sso_provider',
|
||||||
WORKSPACE_INVITATION: 'workspace_invitation',
|
WORKSPACE_INVITATION: 'workspace_invitation',
|
||||||
ATTACHMENT: 'attachment',
|
ATTACHMENT: 'attachment',
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function extractBearerTokenFromHeader(
|
|||||||
request: FastifyRequest,
|
request: FastifyRequest,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||||
return type?.toLowerCase() === 'bearer' ? token : undefined;
|
return type === 'Bearer' ? token : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||||
import { GroupService } from './group.service';
|
import { GroupService } from './group.service';
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
import { KyselyDB } from '@docmost/db/types/kysely.types';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
||||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
AUDIT_SERVICE,
|
AUDIT_SERVICE,
|
||||||
IAuditService,
|
IAuditService,
|
||||||
} from '../../../integrations/audit/audit.service';
|
} from '../../../integrations/audit/audit.service';
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GroupUserService {
|
export class GroupUserService {
|
||||||
@@ -55,23 +54,17 @@ export class GroupUserService {
|
|||||||
userIds: string[],
|
userIds: string[],
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = dbOrTx(this.db, trx);
|
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||||
await this.groupService.findAndValidateGroup(groupId, workspaceId, trx);
|
|
||||||
|
|
||||||
if (userIds.length === 0) return;
|
|
||||||
|
|
||||||
// make sure we have valid workspace users
|
// make sure we have valid workspace users
|
||||||
const validUsers = await db
|
const validUsers = await this.db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(['id', 'name'])
|
.select(['id', 'name'])
|
||||||
.where('users.id', 'in', userIds)
|
.where('users.id', 'in', userIds)
|
||||||
.where('users.workspaceId', '=', workspaceId)
|
.where('users.workspaceId', '=', workspaceId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (validUsers.length === 0) return;
|
|
||||||
|
|
||||||
// prepare users to add to group
|
// prepare users to add to group
|
||||||
const groupUsersToInsert = [];
|
const groupUsersToInsert = [];
|
||||||
for (const user of validUsers) {
|
for (const user of validUsers) {
|
||||||
@@ -82,7 +75,7 @@ export class GroupUserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// batch insert new group users
|
// batch insert new group users
|
||||||
await db
|
await this.db
|
||||||
.insertInto('groupUsers')
|
.insertInto('groupUsers')
|
||||||
.values(groupUsersToInsert)
|
.values(groupUsersToInsert)
|
||||||
.onConflict((oc) => oc.columns(['userId', 'groupId']).doNothing())
|
.onConflict((oc) => oc.columns(['userId', 'groupId']).doNothing())
|
||||||
|
|||||||
@@ -216,11 +216,8 @@ export class GroupService {
|
|||||||
async findAndValidateGroup(
|
async findAndValidateGroup(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const group = await this.groupRepo.findById(groupId, workspaceId, {
|
const group = await this.groupRepo.findById(groupId, workspaceId);
|
||||||
trx,
|
|
||||||
});
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
throw new NotFoundException('Group not found');
|
throw new NotFoundException('Group not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ export class UpdateWorkspaceDto extends PartialType(CreateWorkspaceDto) {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
mcpEnabled: boolean;
|
mcpEnabled: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isScimEnabled: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
aiChat: boolean;
|
aiChat: boolean;
|
||||||
|
|||||||
@@ -331,8 +331,7 @@ export class WorkspaceService {
|
|||||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||||
typeof updateWorkspaceDto.mcpEnabled !== 'undefined' ||
|
typeof updateWorkspaceDto.mcpEnabled !== 'undefined' ||
|
||||||
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
|
typeof updateWorkspaceDto.restrictApiToAdmins !== 'undefined' ||
|
||||||
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined' ||
|
typeof updateWorkspaceDto.allowMemberTemplates !== 'undefined'
|
||||||
typeof updateWorkspaceDto.isScimEnabled !== 'undefined'
|
|
||||||
) {
|
) {
|
||||||
const ws = await this.db
|
const ws = await this.db
|
||||||
.selectFrom('workspaces')
|
.selectFrom('workspaces')
|
||||||
@@ -352,14 +351,6 @@ 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 (
|
if (
|
||||||
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
typeof updateWorkspaceDto.disablePublicSharing !== 'undefined' ||
|
||||||
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' ||
|
||||||
@@ -544,7 +535,6 @@ export class WorkspaceService {
|
|||||||
'enforceSso',
|
'enforceSso',
|
||||||
'enforceMfa',
|
'enforceMfa',
|
||||||
'emailDomains',
|
'emailDomains',
|
||||||
'isScimEnabled',
|
|
||||||
],
|
],
|
||||||
updateWorkspaceDto,
|
updateWorkspaceDto,
|
||||||
workspaceBefore,
|
workspaceBefore,
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { Kysely, sql } from 'kysely';
|
|
||||||
|
|
||||||
export async function up(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema
|
|
||||||
.createTable('scim_tokens')
|
|
||||||
.addColumn('id', 'uuid', (col) =>
|
|
||||||
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
|
|
||||||
)
|
|
||||||
.addColumn('name', 'varchar', (col) => col.notNull())
|
|
||||||
.addColumn('token_hash', 'varchar', (col) => col.notNull())
|
|
||||||
.addColumn('token_last_four', 'varchar(4)', (col) => col.notNull())
|
|
||||||
.addColumn('last_used_at', 'timestamptz')
|
|
||||||
.addColumn('is_enabled', 'boolean', (col) => col.notNull().defaultTo(true))
|
|
||||||
.addColumn('creator_id', 'uuid', (col) =>
|
|
||||||
col.references('users.id').onDelete('set null'),
|
|
||||||
)
|
|
||||||
.addColumn('workspace_id', 'uuid', (col) =>
|
|
||||||
col.references('workspaces.id').onDelete('cascade').notNull(),
|
|
||||||
)
|
|
||||||
.addColumn('created_at', 'timestamptz', (col) =>
|
|
||||||
col.notNull().defaultTo(sql`now()`),
|
|
||||||
)
|
|
||||||
.addColumn('updated_at', 'timestamptz', (col) =>
|
|
||||||
col.notNull().defaultTo(sql`now()`),
|
|
||||||
)
|
|
||||||
.addColumn('deleted_at', 'timestamptz')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_scim_tokens_token_hash')
|
|
||||||
.ifNotExists()
|
|
||||||
.on('scim_tokens')
|
|
||||||
.column('token_hash')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_scim_tokens_workspace_id')
|
|
||||||
.ifNotExists()
|
|
||||||
.on('scim_tokens')
|
|
||||||
.column('workspace_id')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('users')
|
|
||||||
.addColumn('scim_external_id', 'text')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_users_workspace_scim_external_id')
|
|
||||||
.ifNotExists()
|
|
||||||
.on('users')
|
|
||||||
.columns(['workspace_id', 'scim_external_id'])
|
|
||||||
.where('scim_external_id', 'is not', null)
|
|
||||||
.unique()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('groups')
|
|
||||||
.addColumn('scim_external_id', 'text')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createIndex('idx_groups_workspace_scim_external_id')
|
|
||||||
.ifNotExists()
|
|
||||||
.on('groups')
|
|
||||||
.columns(['workspace_id', 'scim_external_id'])
|
|
||||||
.where('scim_external_id', 'is not', null)
|
|
||||||
.unique()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('groups')
|
|
||||||
.addColumn('is_external', 'boolean', (col) =>
|
|
||||||
col.notNull().defaultTo(false),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
// Backfill: mark all non-default groups as external in workspaces with SSO group sync enabled
|
|
||||||
await sql`
|
|
||||||
UPDATE groups SET is_external = true
|
|
||||||
WHERE is_default = false
|
|
||||||
AND workspace_id IN (
|
|
||||||
SELECT workspace_id FROM auth_providers WHERE group_sync = true
|
|
||||||
)
|
|
||||||
`.execute(db);
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('workspaces')
|
|
||||||
.addColumn('is_scim_enabled', 'boolean', (col) =>
|
|
||||||
col.notNull().defaultTo(false),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
|
||||||
await db.schema.dropTable('scim_tokens').execute();
|
|
||||||
|
|
||||||
await db.schema.dropIndex('idx_users_workspace_scim_external_id').execute();
|
|
||||||
await db.schema.alterTable('users').dropColumn('scim_external_id').execute();
|
|
||||||
|
|
||||||
await db.schema.dropIndex('idx_groups_workspace_scim_external_id').execute();
|
|
||||||
await db.schema.alterTable('groups').dropColumn('scim_external_id').execute();
|
|
||||||
|
|
||||||
await db.schema.alterTable('groups').dropColumn('is_external').execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.alterTable('workspaces')
|
|
||||||
.dropColumn('is_scim_enabled')
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@docmost/db/types/entity.types';
|
} from '@docmost/db/types/entity.types';
|
||||||
import { ExpressionBuilder, sql } from 'kysely';
|
import { ExpressionBuilder, sql } from 'kysely';
|
||||||
import { PaginationOptions } from '../../pagination/pagination-options';
|
import { PaginationOptions } from '../../pagination/pagination-options';
|
||||||
import { DB, Groups } from '@docmost/db/types/db';
|
import { DB } from '@docmost/db/types/db';
|
||||||
import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
|
import { DefaultGroup } from '../../../core/group/dto/create-group.dto';
|
||||||
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
|
||||||
|
|
||||||
@@ -17,34 +17,16 @@ import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagin
|
|||||||
export class GroupRepo {
|
export class GroupRepo {
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
private baseFields: Array<keyof Groups> = [
|
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'isDefault',
|
|
||||||
'isExternal',
|
|
||||||
'creatorId',
|
|
||||||
'workspaceId',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
'deletedAt',
|
|
||||||
];
|
|
||||||
|
|
||||||
async findById(
|
async findById(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
opts?: {
|
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||||
includeMemberCount?: boolean;
|
|
||||||
includeScimExternalId?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
const db = dbOrTx(this.db, opts?.trx);
|
||||||
return db
|
return db
|
||||||
.selectFrom('groups')
|
.selectFrom('groups')
|
||||||
.select(this.baseFields)
|
.selectAll('groups')
|
||||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
|
||||||
.where('id', '=', groupId)
|
.where('id', '=', groupId)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -53,18 +35,13 @@ export class GroupRepo {
|
|||||||
async findByName(
|
async findByName(
|
||||||
groupName: string,
|
groupName: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
opts?: {
|
opts?: { includeMemberCount?: boolean; trx?: KyselyTransaction },
|
||||||
includeMemberCount?: boolean;
|
|
||||||
includeScimExternalId?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
|
||||||
},
|
|
||||||
): Promise<Group> {
|
): Promise<Group> {
|
||||||
const db = dbOrTx(this.db, opts?.trx);
|
const db = dbOrTx(this.db, opts?.trx);
|
||||||
return db
|
return db
|
||||||
.selectFrom('groups')
|
.selectFrom('groups')
|
||||||
.select(this.baseFields)
|
.selectAll('groups')
|
||||||
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
.$if(opts?.includeMemberCount, (qb) => qb.select(this.withMemberCount))
|
||||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
|
||||||
.where(sql`LOWER(name)`, '=', sql`LOWER(${groupName})`)
|
.where(sql`LOWER(name)`, '=', sql`LOWER(${groupName})`)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -74,11 +51,8 @@ export class GroupRepo {
|
|||||||
updatableGroup: UpdatableGroup,
|
updatableGroup: UpdatableGroup,
|
||||||
groupId: string,
|
groupId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
trx?: KyselyTransaction,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = dbOrTx(this.db, trx);
|
await this.db
|
||||||
|
|
||||||
await db
|
|
||||||
.updateTable('groups')
|
.updateTable('groups')
|
||||||
.set({ ...updatableGroup, updatedAt: new Date() })
|
.set({ ...updatableGroup, updatedAt: new Date() })
|
||||||
.where('id', '=', groupId)
|
.where('id', '=', groupId)
|
||||||
@@ -94,7 +68,7 @@ export class GroupRepo {
|
|||||||
return db
|
return db
|
||||||
.insertInto('groups')
|
.insertInto('groups')
|
||||||
.values(insertableGroup)
|
.values(insertableGroup)
|
||||||
.returning(this.baseFields)
|
.returningAll()
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +80,7 @@ export class GroupRepo {
|
|||||||
return (
|
return (
|
||||||
db
|
db
|
||||||
.selectFrom('groups')
|
.selectFrom('groups')
|
||||||
.select(this.baseFields)
|
.selectAll()
|
||||||
// .select((eb) => this.withMemberCount(eb))
|
// .select((eb) => this.withMemberCount(eb))
|
||||||
.where('isDefault', '=', true)
|
.where('isDefault', '=', true)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
@@ -132,7 +106,7 @@ export class GroupRepo {
|
|||||||
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
|
async getGroupsPaginated(workspaceId: string, pagination: PaginationOptions) {
|
||||||
let baseQuery = this.db
|
let baseQuery = this.db
|
||||||
.selectFrom('groups')
|
.selectFrom('groups')
|
||||||
.select(this.baseFields)
|
.selectAll('groups')
|
||||||
.select((eb) => this.withMemberCount(eb))
|
.select((eb) => this.withMemberCount(eb))
|
||||||
.where('workspaceId', '=', workspaceId);
|
.where('workspaceId', '=', workspaceId);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export class UserRepo {
|
|||||||
opts?: {
|
opts?: {
|
||||||
includePassword?: boolean;
|
includePassword?: boolean;
|
||||||
includeUserMfa?: boolean;
|
includeUserMfa?: boolean;
|
||||||
includeScimExternalId?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
trx?: KyselyTransaction;
|
||||||
},
|
},
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
@@ -54,7 +53,6 @@ export class UserRepo {
|
|||||||
.select(this.baseFields)
|
.select(this.baseFields)
|
||||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||||
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
||||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
|
||||||
.where('id', '=', userId)
|
.where('id', '=', userId)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
@@ -66,7 +64,6 @@ export class UserRepo {
|
|||||||
opts?: {
|
opts?: {
|
||||||
includePassword?: boolean;
|
includePassword?: boolean;
|
||||||
includeUserMfa?: boolean;
|
includeUserMfa?: boolean;
|
||||||
includeScimExternalId?: boolean;
|
|
||||||
trx?: KyselyTransaction;
|
trx?: KyselyTransaction;
|
||||||
},
|
},
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
@@ -76,7 +73,6 @@ export class UserRepo {
|
|||||||
.select(this.baseFields)
|
.select(this.baseFields)
|
||||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||||
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
.$if(opts?.includeUserMfa, (qb) => qb.select(this.withUserMfa))
|
||||||
.$if(opts?.includeScimExternalId, (qb) => qb.select('scimExternalId'))
|
|
||||||
.where(sql`LOWER(email)`, '=', sql`LOWER(${email})`)
|
.where(sql`LOWER(email)`, '=', sql`LOWER(${email})`)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export class WorkspaceRepo {
|
|||||||
'plan',
|
'plan',
|
||||||
'enforceMfa',
|
'enforceMfa',
|
||||||
'trashRetentionDays',
|
'trashRetentionDays',
|
||||||
'isScimEnabled',
|
|
||||||
];
|
];
|
||||||
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
constructor(@InjectKysely() private readonly db: KyselyDB) {}
|
||||||
|
|
||||||
|
|||||||
-19
@@ -213,9 +213,7 @@ export interface Groups {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
isExternal: Generated<boolean>;
|
|
||||||
name: string;
|
name: string;
|
||||||
scimExternalId: string | null;
|
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
@@ -340,7 +338,6 @@ export interface Users {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
role: string | null;
|
role: string | null;
|
||||||
scimExternalId: string | null;
|
|
||||||
settings: Json | null;
|
settings: Json | null;
|
||||||
timezone: string | null;
|
timezone: string | null;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
@@ -384,7 +381,6 @@ export interface Workspaces {
|
|||||||
enforceMfa: Generated<boolean | null>;
|
enforceMfa: Generated<boolean | null>;
|
||||||
enforceSso: Generated<boolean>;
|
enforceSso: Generated<boolean>;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
isScimEnabled: Generated<boolean>;
|
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
licenseKey: string | null;
|
licenseKey: string | null;
|
||||||
logo: string | null;
|
logo: string | null;
|
||||||
@@ -414,20 +410,6 @@ export interface Notifications {
|
|||||||
createdAt: Generated<Timestamp>;
|
createdAt: Generated<Timestamp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScimTokens {
|
|
||||||
createdAt: Generated<Timestamp>;
|
|
||||||
deletedAt: Timestamp | null;
|
|
||||||
id: Generated<string>;
|
|
||||||
isEnabled: Generated<boolean>;
|
|
||||||
lastUsedAt: Timestamp | null;
|
|
||||||
name: string;
|
|
||||||
tokenHash: string;
|
|
||||||
tokenLastFour: string;
|
|
||||||
creatorId: string | null;
|
|
||||||
updatedAt: Generated<Timestamp>;
|
|
||||||
workspaceId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Watchers {
|
export interface Watchers {
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -576,7 +558,6 @@ export interface DB {
|
|||||||
pageVerifications: PageVerifications;
|
pageVerifications: PageVerifications;
|
||||||
pageVerifiers: PageVerifiers;
|
pageVerifiers: PageVerifiers;
|
||||||
pages: Pages;
|
pages: Pages;
|
||||||
scimTokens: ScimTokens;
|
|
||||||
shares: Shares;
|
shares: Shares;
|
||||||
spaceMembers: SpaceMembers;
|
spaceMembers: SpaceMembers;
|
||||||
spaces: Spaces;
|
spaces: Spaces;
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
UserMfa as _UserMFA,
|
UserMfa as _UserMFA,
|
||||||
UserSessions,
|
UserSessions,
|
||||||
ApiKeys,
|
ApiKeys,
|
||||||
ScimTokens,
|
|
||||||
Watchers,
|
Watchers,
|
||||||
Audit as _Audit,
|
Audit as _Audit,
|
||||||
Templates,
|
Templates,
|
||||||
@@ -160,11 +159,6 @@ export type ApiKey = Selectable<ApiKeys>;
|
|||||||
export type InsertableApiKey = Insertable<ApiKeys>;
|
export type InsertableApiKey = Insertable<ApiKeys>;
|
||||||
export type UpdatableApiKey = Updateable<Omit<ApiKeys, 'id'>>;
|
export type UpdatableApiKey = Updateable<Omit<ApiKeys, 'id'>>;
|
||||||
|
|
||||||
// Scim Tokens
|
|
||||||
export type ScimToken = Selectable<ScimTokens>;
|
|
||||||
export type InsertableScimToken = Insertable<ScimTokens>;
|
|
||||||
export type UpdatableScimToken = Updateable<Omit<ScimTokens, 'id'>>;
|
|
||||||
|
|
||||||
// Page Embedding
|
// Page Embedding
|
||||||
export type PageEmbedding = Selectable<PageEmbeddings>;
|
export type PageEmbedding = Selectable<PageEmbeddings>;
|
||||||
export type InsertablePageEmbedding = Insertable<PageEmbeddings>;
|
export type InsertablePageEmbedding = Insertable<PageEmbeddings>;
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 109829076c...4101fc427b
@@ -304,11 +304,4 @@ export class EnvironmentService {
|
|||||||
getClickHouseUrl(): string {
|
getClickHouseUrl(): string {
|
||||||
return this.configService.get<string>('CLICKHOUSE_URL');
|
return this.configService.get<string>('CLICKHOUSE_URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSamlDisableRequestedAuthnContext(): boolean {
|
|
||||||
const disabled = this.configService
|
|
||||||
.get<string>('SAML_DISABLE_REQUESTED_AUTHN_CONTEXT', 'false')
|
|
||||||
.toLowerCase();
|
|
||||||
return disabled === 'true';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,22 +50,6 @@ async function bootstrap() {
|
|||||||
await app.register(fastifyMultipart);
|
await app.register(fastifyMultipart);
|
||||||
await app.register(fastifyCookie);
|
await app.register(fastifyCookie);
|
||||||
|
|
||||||
app
|
|
||||||
.getHttpAdapter()
|
|
||||||
.getInstance()
|
|
||||||
.addContentTypeParser(
|
|
||||||
'application/scim+json',
|
|
||||||
{ parseAs: 'string' },
|
|
||||||
(_, body, done) => {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(body.toString());
|
|
||||||
done(null, json);
|
|
||||||
} catch (err: any) {
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app
|
app
|
||||||
.getHttpAdapter()
|
.getHttpAdapter()
|
||||||
.getInstance()
|
.getInstance()
|
||||||
|
|||||||
+1
-2
@@ -95,8 +95,7 @@
|
|||||||
"packageManager": "pnpm@10.4.0",
|
"packageManager": "pnpm@10.4.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch",
|
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
|
||||||
"scimmy@1.3.5": "patches/scimmy@1.3.5.patch"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"prosemirror-changeset": "2.4.0",
|
"prosemirror-changeset": "2.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
diff --git a/dist/index.cjs b/dist/index.cjs
|
||||||
|
index 01d6999642c5ae990083798a1bf0ef87068e4192..891b13c6901f28a6ab413c6dbae0ea726a76a196 100644
|
||||||
|
--- a/dist/index.cjs
|
||||||
|
+++ b/dist/index.cjs
|
||||||
|
@@ -5463,7 +5463,10 @@ var ResizableNodeView = class {
|
||||||
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
|
}
|
||||||
|
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||||
|
+ document.removeEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.removeEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.removeEventListener("blur", this.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.removeEventListener("keyup", this.handleKeyUp);
|
||||||
|
};
|
||||||
|
@@ -5593,7 +5596,10 @@ var ResizableNodeView = class {
|
||||||
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
|
}
|
||||||
|
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||||
|
+ document.removeEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.removeEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.removeEventListener("blur", this.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.removeEventListener("keyup", this.handleKeyUp);
|
||||||
|
this.isResizing = false;
|
||||||
|
@@ -5796,6 +5802,8 @@ var ResizableNodeView = class {
|
||||||
|
document.addEventListener("mousemove", this.handleMouseMove);
|
||||||
|
document.addEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.addEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.addEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.addEventListener("blur", this.handleMouseUp);
|
||||||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.addEventListener("keyup", this.handleKeyUp);
|
||||||
|
}
|
||||||
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
|
index 6f357a03b038abeb5ed86967b7fc7c3e5eb1d2d6..2d2742532860821984e1ba82625821504538ebbe 100644
|
||||||
|
--- a/dist/index.js
|
||||||
|
+++ b/dist/index.js
|
||||||
|
@@ -5330,7 +5330,10 @@ var ResizableNodeView = class {
|
||||||
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
|
}
|
||||||
|
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||||
|
+ document.removeEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.removeEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.removeEventListener("blur", this.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.removeEventListener("keyup", this.handleKeyUp);
|
||||||
|
};
|
||||||
|
@@ -5460,7 +5463,10 @@ var ResizableNodeView = class {
|
||||||
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
|
}
|
||||||
|
document.removeEventListener("mousemove", this.handleMouseMove);
|
||||||
|
+ document.removeEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.removeEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.removeEventListener("blur", this.handleMouseUp);
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.removeEventListener("keyup", this.handleKeyUp);
|
||||||
|
this.isResizing = false;
|
||||||
|
@@ -5663,6 +5669,8 @@ var ResizableNodeView = class {
|
||||||
|
document.addEventListener("mousemove", this.handleMouseMove);
|
||||||
|
document.addEventListener("touchmove", this.handleTouchMove);
|
||||||
|
document.addEventListener("mouseup", this.handleMouseUp);
|
||||||
|
+ document.addEventListener("touchend", this.handleMouseUp);
|
||||||
|
+ window.addEventListener("blur", this.handleMouseUp);
|
||||||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
document.addEventListener("keyup", this.handleKeyUp);
|
||||||
|
}
|
||||||
|
diff --git a/src/lib/ResizableNodeView.ts b/src/lib/ResizableNodeView.ts
|
||||||
|
index f13e210b0aa46aefe7c31105deee3d2aa8a26cd5..9bac138dbf17c6ae6c3c129cbedb3a81bd39b60c 100644
|
||||||
|
--- a/src/lib/ResizableNodeView.ts
|
||||||
|
+++ b/src/lib/ResizableNodeView.ts
|
||||||
|
@@ -523,7 +523,10 @@ export class ResizableNodeView {
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove)
|
||||||
|
+ document.removeEventListener('touchmove', this.handleTouchMove)
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp)
|
||||||
|
+ document.removeEventListener('touchend', this.handleMouseUp)
|
||||||
|
+ window.removeEventListener('blur', this.handleMouseUp)
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown)
|
||||||
|
document.removeEventListener('keyup', this.handleKeyUp)
|
||||||
|
this.isResizing = false
|
||||||
|
@@ -774,6 +777,8 @@ export class ResizableNodeView {
|
||||||
|
document.addEventListener('mousemove', this.handleMouseMove)
|
||||||
|
document.addEventListener('touchmove', this.handleTouchMove)
|
||||||
|
document.addEventListener('mouseup', this.handleMouseUp)
|
||||||
|
+ document.addEventListener('touchend', this.handleMouseUp)
|
||||||
|
+ window.addEventListener('blur', this.handleMouseUp)
|
||||||
|
document.addEventListener('keydown', this.handleKeyDown)
|
||||||
|
document.addEventListener('keyup', this.handleKeyUp)
|
||||||
|
}
|
||||||
|
@@ -859,7 +864,10 @@ export class ResizableNodeView {
|
||||||
|
|
||||||
|
// Clean up document-level listeners
|
||||||
|
document.removeEventListener('mousemove', this.handleMouseMove)
|
||||||
|
+ document.removeEventListener('touchmove', this.handleTouchMove)
|
||||||
|
document.removeEventListener('mouseup', this.handleMouseUp)
|
||||||
|
+ document.removeEventListener('touchend', this.handleMouseUp)
|
||||||
|
+ window.removeEventListener('blur', this.handleMouseUp)
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown)
|
||||||
|
document.removeEventListener('keyup', this.handleKeyUp)
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
diff --git a/dist/cjs/lib/messages.cjs b/dist/cjs/lib/messages.cjs
|
|
||||||
index e74b8f52137e3267f3d065c4210a1114c4f32dd1..5740606b18851c0ac4f55cfa333152359e0ad135 100644
|
|
||||||
--- a/dist/cjs/lib/messages.cjs
|
|
||||||
+++ b/dist/cjs/lib/messages.cjs
|
|
||||||
@@ -502,10 +502,15 @@ class PatchOp {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-
|
|
||||||
+
|
|
||||||
+ /** Reason: Commented out to avoid failing patch requests when filters don't match.
|
|
||||||
+ * Some IdPs send patch paths like `addresses[type eq "work"].country` even if no such address exists. We can't always decide what the end user IdPs send.
|
|
||||||
+ * Since we manually control patch application, we safely ignore these cases.
|
|
||||||
+ * example error: "noTarget","detail":"Filter 'addresses[type eq \"work\"].country' does not match any values for 'add' op of operation 5 in PatchOp request body
|
|
||||||
+ */
|
|
||||||
// No targets, bail out!
|
|
||||||
- if (targets.length === 0 && op !== "remove")
|
|
||||||
- throw new lib_types.default.Error(400, "noTarget", `Filter '${path}' does not match any values for '${op}' op of operation ${index} in PatchOp request body`);
|
|
||||||
+ // if (targets.length === 0 && op !== "remove")
|
|
||||||
+ // throw new lib_types.default.Error(400, "noTarget", `Filter '${path}' does not match any values for '${op}' op of operation ${index} in PatchOp request body`);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} PatchOpDetails
|
|
||||||
Generated
-12
@@ -46,9 +46,6 @@ patchedDependencies:
|
|||||||
react-arborist@3.4.0:
|
react-arborist@3.4.0:
|
||||||
hash: 419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a
|
hash: 419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a
|
||||||
path: patches/react-arborist@3.4.0.patch
|
path: patches/react-arborist@3.4.0.patch
|
||||||
scimmy@1.3.5:
|
|
||||||
hash: 775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673
|
|
||||||
path: patches/scimmy@1.3.5.patch
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -704,9 +701,6 @@ importers:
|
|||||||
sanitize-filename:
|
sanitize-filename:
|
||||||
specifier: 1.6.3
|
specifier: 1.6.3
|
||||||
version: 1.6.3
|
version: 1.6.3
|
||||||
scimmy:
|
|
||||||
specifier: 1.3.5
|
|
||||||
version: 1.3.5(patch_hash=775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673)
|
|
||||||
socket.io:
|
socket.io:
|
||||||
specifier: ^4.8.3
|
specifier: ^4.8.3
|
||||||
version: 4.8.3
|
version: 4.8.3
|
||||||
@@ -9610,10 +9604,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
||||||
engines: {node: '>= 10.13.0'}
|
engines: {node: '>= 10.13.0'}
|
||||||
|
|
||||||
scimmy@1.3.5:
|
|
||||||
resolution: {integrity: sha512-JTrUOoqH1gMH2zZhgk01hGgY7cH9v4qUli5b3OGVVOzjAwY8h4Z2mSNH8kXjW2pz8ypzpiRuMEtFGBaWQWJz7w==}
|
|
||||||
engines: {node: '>=16'}
|
|
||||||
|
|
||||||
secure-json-parse@4.0.0:
|
secure-json-parse@4.0.0:
|
||||||
resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
|
resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
|
||||||
|
|
||||||
@@ -20954,8 +20944,6 @@ snapshots:
|
|||||||
ajv-formats: 2.1.1(ajv@8.18.0)
|
ajv-formats: 2.1.1(ajv@8.18.0)
|
||||||
ajv-keywords: 5.1.0(ajv@8.18.0)
|
ajv-keywords: 5.1.0(ajv@8.18.0)
|
||||||
|
|
||||||
scimmy@1.3.5(patch_hash=775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673): {}
|
|
||||||
|
|
||||||
secure-json-parse@4.0.0: {}
|
secure-json-parse@4.0.0: {}
|
||||||
|
|
||||||
selderee@0.11.0:
|
selderee@0.11.0:
|
||||||
|
|||||||
Reference in New Issue
Block a user