Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Replaced hardcoded "Agenda" labels with dynamic ULTICAL_APP_NAME in various components for consistency. - Introduced new AiUsageSection and CompteAiUsageSection components to track AI usage and costs. - Updated settings and metadata to reflect changes in AI cost policies and usage limits. - Enhanced user interface elements for better accessibility and user experience across admin settings.
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
import type { ApiOrgPolicy, ApiOrgSettingsResponse } from "@/lib/api/admin-org-types"
|
|
import type { OrgPolicySectionKey } from "@/lib/api/admin-org-types"
|
|
import { DEFAULT_ULTIAI_ENABLED_TOOLS } from "@/lib/ai/ultiai-tool-groups"
|
|
import type { IntegrationEntry, OrgSettingsState, FilePolicySettings, DriveMountOAuthSettings, DriveMountOAuthProviderSettings, IdentityProvidersPolicy, IdentityProvider, PluginEntry } from "@/lib/admin-settings/org-settings-types"
|
|
import { DEFAULT_ORG_PLUGINS } from "@/lib/admin-settings/default-plugins"
|
|
import { DEFAULT_MEET_POLICY } from "@/lib/meet/meet-settings-types"
|
|
import { normalizeLlmProvider } from "@/lib/llm/llm-provider-catalog"
|
|
|
|
const INTEGRATION_HREFS: Record<string, string> = {
|
|
authentik: "/admin/settings/authentication",
|
|
nextcloud: "/admin/settings/plugins",
|
|
onlyoffice: "/admin/settings/plugins",
|
|
smtp: "/admin/settings/mail-domains",
|
|
}
|
|
|
|
function mergePlugins(fromApi: PluginEntry[] | undefined): PluginEntry[] {
|
|
if (!fromApi?.length) return DEFAULT_ORG_PLUGINS.map((plugin) => ({ ...plugin }))
|
|
const byId = new Map(fromApi.map((plugin) => [plugin.id, plugin]))
|
|
const merged = DEFAULT_ORG_PLUGINS.map((plugin) => ({
|
|
...plugin,
|
|
...byId.get(plugin.id),
|
|
}))
|
|
for (const plugin of fromApi) {
|
|
if (!DEFAULT_ORG_PLUGINS.some((entry) => entry.id === plugin.id)) {
|
|
merged.push(plugin)
|
|
}
|
|
}
|
|
return merged
|
|
}
|
|
|
|
function mergeIntegrations(
|
|
fromApi: IntegrationEntry[] | undefined
|
|
): IntegrationEntry[] {
|
|
if (!fromApi?.length) return []
|
|
return fromApi.map((item) => ({
|
|
...item,
|
|
href: INTEGRATION_HREFS[item.id] ?? item.href,
|
|
}))
|
|
}
|
|
|
|
const DEFAULT_IDENTITY_PROVIDERS: IdentityProvidersPolicy = {
|
|
allow_self_enrollment: true,
|
|
default_login_source: "",
|
|
providers: [],
|
|
}
|
|
|
|
function mergeIdentityProviders(
|
|
fromApi: Partial<IdentityProvidersPolicy> | undefined
|
|
): IdentityProvidersPolicy {
|
|
return {
|
|
...DEFAULT_IDENTITY_PROVIDERS,
|
|
...fromApi,
|
|
providers: (fromApi?.providers ?? []).map((provider) => ({
|
|
...provider,
|
|
allowed_email_domains: provider.allowed_email_domains ?? [],
|
|
allowed_identities: provider.allowed_identities ?? [],
|
|
allowed_organizations: provider.allowed_organizations ?? [],
|
|
default_groups: provider.default_groups ?? [],
|
|
sync_status: provider.sync_status ?? "pending",
|
|
oauth: provider.oauth
|
|
? { ...provider.oauth, client_secret: provider.oauth.client_secret ?? "" }
|
|
: undefined,
|
|
ldap: provider.ldap
|
|
? { ...provider.ldap, bind_password: provider.ldap.bind_password ?? "" }
|
|
: undefined,
|
|
saml: provider.saml
|
|
? { ...provider.saml, signing_cert: provider.saml.signing_cert ?? "" }
|
|
: undefined,
|
|
})),
|
|
}
|
|
}
|
|
|
|
function mapProviderToApi(provider: IdentityProvider) {
|
|
return {
|
|
id: provider.id,
|
|
name: provider.name,
|
|
slug: provider.slug,
|
|
type: provider.type,
|
|
enabled: provider.enabled,
|
|
authentik_pk: provider.authentik_pk,
|
|
sync_status: provider.sync_status,
|
|
sync_error: provider.sync_error,
|
|
last_synced_at: provider.last_synced_at,
|
|
allowed_email_domains: provider.allowed_email_domains,
|
|
allowed_identities: provider.allowed_identities,
|
|
allowed_organizations: provider.allowed_organizations,
|
|
default_groups: provider.default_groups,
|
|
oauth: provider.oauth,
|
|
saml: provider.saml,
|
|
ldap: provider.ldap,
|
|
}
|
|
}
|
|
|
|
const DEFAULT_MOUNT_OAUTH_PROVIDER: DriveMountOAuthProviderSettings = {
|
|
enabled: false,
|
|
client_id: "",
|
|
client_secret: "",
|
|
}
|
|
|
|
const DEFAULT_MOUNT_OAUTH: DriveMountOAuthSettings = {
|
|
redirect_uri: "",
|
|
google: { ...DEFAULT_MOUNT_OAUTH_PROVIDER },
|
|
dropbox: { ...DEFAULT_MOUNT_OAUTH_PROVIDER },
|
|
microsoft: { ...DEFAULT_MOUNT_OAUTH_PROVIDER },
|
|
}
|
|
|
|
const DEFAULT_FILE_POLICIES: FilePolicySettings = {
|
|
max_upload_mib: 512,
|
|
allowed_extensions: "",
|
|
block_executable: true,
|
|
external_sharing: "authenticated",
|
|
default_link_expiry_days: 30,
|
|
virus_scan_enabled: false,
|
|
virustotal_api_key: "",
|
|
retention_trash_days: 30,
|
|
mount_oauth: DEFAULT_MOUNT_OAUTH,
|
|
}
|
|
|
|
function mergeFilePolicies(fromApi: Partial<FilePolicySettings> | undefined): FilePolicySettings {
|
|
const mountOAuth = fromApi?.mount_oauth
|
|
return {
|
|
...DEFAULT_FILE_POLICIES,
|
|
...fromApi,
|
|
virustotal_api_key: fromApi?.virustotal_api_key ?? "",
|
|
mount_oauth: {
|
|
...DEFAULT_MOUNT_OAUTH,
|
|
...mountOAuth,
|
|
google: { ...DEFAULT_MOUNT_OAUTH.google, ...mountOAuth?.google, client_secret: mountOAuth?.google?.client_secret ?? "" },
|
|
dropbox: { ...DEFAULT_MOUNT_OAUTH.dropbox, ...mountOAuth?.dropbox, client_secret: mountOAuth?.dropbox?.client_secret ?? "" },
|
|
microsoft: { ...DEFAULT_MOUNT_OAUTH.microsoft, ...mountOAuth?.microsoft, client_secret: mountOAuth?.microsoft?.client_secret ?? "" },
|
|
},
|
|
}
|
|
}
|
|
|
|
export function apiOrgPolicyToStore(policy: ApiOrgPolicy): Partial<OrgSettingsState> {
|
|
return {
|
|
authentik: {
|
|
enabled: policy.authentik.enabled,
|
|
api_url: policy.authentik.api_url,
|
|
slug: policy.authentik.slug,
|
|
client_id: policy.authentik.client_id,
|
|
enforce_sso: policy.authentik.enforce_sso,
|
|
allow_password_fallback: policy.authentik.allow_password_fallback,
|
|
default_groups: policy.authentik.default_groups,
|
|
},
|
|
identityProviders: mergeIdentityProviders(policy.identity_providers),
|
|
twoFactor: {
|
|
required_for_all: policy.two_factor.required_for_all,
|
|
required_for_admins: policy.two_factor.required_for_admins,
|
|
allowed_methods: policy.two_factor.allowed_methods.filter(
|
|
(m): m is "totp" | "webauthn" => m === "totp" || m === "webauthn"
|
|
),
|
|
grace_period_days: policy.two_factor.grace_period_days,
|
|
remember_device_days: policy.two_factor.remember_device_days,
|
|
},
|
|
storageQuotas: { ...policy.storage_quotas },
|
|
usageQuotas: {
|
|
llm_daily_cost_limit_eur: 2,
|
|
llm_monthly_cost_limit_eur: 35,
|
|
llm_cost_warn_threshold_pct: 80,
|
|
llm_requests_per_day: 75,
|
|
llm_tokens_per_month: 2_000_000,
|
|
search_requests_per_day: 20,
|
|
max_api_tokens_per_user: 5,
|
|
max_webhooks_per_user: 5,
|
|
...policy.usage_quotas,
|
|
},
|
|
filePolicies: mergeFilePolicies(policy.file_policies),
|
|
llm: {
|
|
...policy.llm,
|
|
providers: (policy.llm.providers ?? []).map((provider) =>
|
|
normalizeLlmProvider({
|
|
...provider,
|
|
api_key: provider.api_key ?? "",
|
|
}),
|
|
),
|
|
},
|
|
search: {
|
|
...policy.search,
|
|
web_search: policy.search.web_search ?? {
|
|
default_provider_id: "brave-default",
|
|
providers: [],
|
|
},
|
|
},
|
|
administrators: policy.administrators ?? [],
|
|
nextcloud: { ...policy.nextcloud },
|
|
mailing: { ...policy.mailing },
|
|
onlyoffice: { ...policy.onlyoffice },
|
|
richtext: {
|
|
enabled: policy.richtext?.enabled ?? true,
|
|
storage_mode: policy.richtext?.storage_mode ?? "sidecar",
|
|
export_mirror_format: policy.richtext?.export_mirror_format ?? "",
|
|
hocuspocus_url: policy.richtext?.hocuspocus_url ?? "",
|
|
},
|
|
aiAssistant: {
|
|
enabled: policy.ai_assistant?.enabled ?? false,
|
|
openwebui_internal_url: policy.ai_assistant?.openwebui_internal_url ?? "",
|
|
public_path: policy.ai_assistant?.public_path ?? "/ai",
|
|
embed_default_temporary: policy.ai_assistant?.embed_default_temporary ?? false,
|
|
default_model: policy.ai_assistant?.default_model ?? "",
|
|
enabled_tools: policy.ai_assistant?.enabled_tools ?? [...DEFAULT_ULTIAI_ENABLED_TOOLS],
|
|
chat_sync_enabled: policy.ai_assistant?.chat_sync_enabled ?? true,
|
|
chat_nc_path: policy.ai_assistant?.chat_nc_path ?? "/.ultimail/ai/chats",
|
|
models: (policy.ai_assistant?.models ?? []).map((entry) => ({
|
|
model_id: entry.model_id ?? "",
|
|
label: entry.label ?? "",
|
|
enabled: entry.enabled ?? true,
|
|
})),
|
|
},
|
|
agenda: {
|
|
default_theme_mode: policy.agenda?.default_theme_mode ?? "system",
|
|
enforce_org_theme: policy.agenda?.enforce_org_theme ?? false,
|
|
default_video_provider: policy.agenda?.default_video_provider ?? "ultimeet",
|
|
enforce_org_video_provider: policy.agenda?.enforce_org_video_provider ?? false,
|
|
video_provider_api_keys: policy.agenda?.video_provider_api_keys ?? {},
|
|
},
|
|
meet: {
|
|
...DEFAULT_MEET_POLICY,
|
|
...policy.meet,
|
|
post_actions: {
|
|
...DEFAULT_MEET_POLICY.post_actions,
|
|
...policy.meet?.post_actions,
|
|
},
|
|
},
|
|
plugins: mergePlugins(policy.plugins),
|
|
integrations: mergeIntegrations(policy.integrations as IntegrationEntry[]),
|
|
}
|
|
}
|
|
|
|
export function storeToApiOrgPolicy(state: OrgSettingsState): ApiOrgPolicy {
|
|
return {
|
|
authentik: {
|
|
enabled: state.authentik.enabled,
|
|
api_url: state.authentik.api_url,
|
|
slug: state.authentik.slug,
|
|
client_id: state.authentik.client_id,
|
|
enforce_sso: state.authentik.enforce_sso,
|
|
allow_password_fallback: state.authentik.allow_password_fallback,
|
|
default_groups: state.authentik.default_groups,
|
|
},
|
|
identity_providers: {
|
|
allow_self_enrollment: state.identityProviders.allow_self_enrollment,
|
|
default_login_source: state.identityProviders.default_login_source,
|
|
providers: state.identityProviders.providers.map(mapProviderToApi),
|
|
},
|
|
two_factor: {
|
|
required_for_all: state.twoFactor.required_for_all,
|
|
required_for_admins: state.twoFactor.required_for_admins,
|
|
allowed_methods: state.twoFactor.allowed_methods.filter(
|
|
(m): m is "totp" | "webauthn" => m === "totp" || m === "webauthn"
|
|
),
|
|
grace_period_days: state.twoFactor.grace_period_days,
|
|
remember_device_days: state.twoFactor.remember_device_days,
|
|
},
|
|
storage_quotas: { ...state.storageQuotas },
|
|
usage_quotas: { ...state.usageQuotas },
|
|
file_policies: { ...state.filePolicies },
|
|
llm: {
|
|
default_provider_id: state.llm.default_provider_id,
|
|
providers: state.llm.providers,
|
|
contact_discovery_model: state.llm.contact_discovery_model,
|
|
contact_discovery_provider_id: state.llm.contact_discovery_provider_id,
|
|
enforce_org_providers: state.llm.enforce_org_providers,
|
|
allow_user_override: state.llm.allow_user_override,
|
|
},
|
|
search: {
|
|
suite_engine: state.search.suite_engine,
|
|
meilisearch_url: state.search.meilisearch_url,
|
|
meilisearch_api_key: state.search.meilisearch_api_key,
|
|
typesense_url: state.search.typesense_url,
|
|
typesense_api_key: state.search.typesense_api_key,
|
|
web_search: state.search.web_search,
|
|
enforce_org_search: state.search.enforce_org_search,
|
|
},
|
|
administrators: state.administrators,
|
|
nextcloud: { ...state.nextcloud },
|
|
mailing: { ...state.mailing },
|
|
onlyoffice: { ...state.onlyoffice },
|
|
richtext: { ...state.richtext },
|
|
ai_assistant: { ...state.aiAssistant },
|
|
agenda: { ...state.agenda },
|
|
meet: { ...state.meet },
|
|
plugins: state.plugins.map(({ id, name, description, enabled, version }) => ({
|
|
id,
|
|
name,
|
|
description,
|
|
enabled,
|
|
version,
|
|
})),
|
|
integrations: state.integrations.map(({ id, name, description, enabled, configured }) => ({
|
|
id,
|
|
name,
|
|
description,
|
|
enabled,
|
|
configured,
|
|
})),
|
|
}
|
|
}
|
|
|
|
export function pickApiOrgPolicySections(
|
|
state: OrgSettingsState,
|
|
sections: OrgPolicySectionKey[]
|
|
): Partial<ApiOrgPolicy> {
|
|
const full = storeToApiOrgPolicy(state)
|
|
const patch: Partial<ApiOrgPolicy> = {}
|
|
for (const key of sections) {
|
|
patch[key] = full[key] as never
|
|
}
|
|
return patch
|
|
}
|
|
|
|
export type OrgSettingsMeta = {
|
|
effective: ApiOrgSettingsResponse["effective"]
|
|
secrets: ApiOrgSettingsResponse["secrets"]
|
|
envVars: ApiOrgSettingsResponse["env_vars"]
|
|
deployLocked: ApiOrgSettingsResponse["deploy_locked"]
|
|
updatedAt: string
|
|
updatedBy: string
|
|
}
|
|
|
|
export function apiOrgSettingsMeta(data: ApiOrgSettingsResponse): OrgSettingsMeta {
|
|
return {
|
|
effective: data.effective,
|
|
secrets: data.secrets,
|
|
envVars: data.env_vars ?? [],
|
|
deployLocked: data.deploy_locked ?? {},
|
|
updatedAt: data.updated_at,
|
|
updatedBy: data.updated_by,
|
|
}
|
|
}
|