@open-mercato/enterprise 0.4.6-develop-15c18897fc → 0.4.6-develop-34aa847ce6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/modules/sso/acl.js +11 -0
- package/dist/modules/sso/acl.js.map +7 -0
- package/dist/modules/sso/api/admin-context.js +27 -0
- package/dist/modules/sso/api/admin-context.js.map +7 -0
- package/dist/modules/sso/api/callback/oidc/route.js +103 -0
- package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
- package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
- package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/route.js +103 -0
- package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
- package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
- package/dist/modules/sso/api/config/route.js +83 -0
- package/dist/modules/sso/api/config/route.js.map +7 -0
- package/dist/modules/sso/api/error-handler.js +28 -0
- package/dist/modules/sso/api/error-handler.js.map +7 -0
- package/dist/modules/sso/api/hrd/route.js +52 -0
- package/dist/modules/sso/api/hrd/route.js.map +7 -0
- package/dist/modules/sso/api/initiate/route.js +66 -0
- package/dist/modules/sso/api/initiate/route.js.map +7 -0
- package/dist/modules/sso/api/scim/context.js +68 -0
- package/dist/modules/sso/api/scim/context.js.map +7 -0
- package/dist/modules/sso/api/scim/logs/route.js +65 -0
- package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
- package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
- package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/scim/tokens/route.js +83 -0
- package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
- package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
- package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
- package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
- package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
- package/dist/modules/sso/backend/page.js +173 -0
- package/dist/modules/sso/backend/page.js.map +7 -0
- package/dist/modules/sso/backend/page.meta.js +31 -0
- package/dist/modules/sso/backend/page.meta.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
- package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
- package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
- package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
- package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
- package/dist/modules/sso/data/entities.js +299 -0
- package/dist/modules/sso/data/entities.js.map +7 -0
- package/dist/modules/sso/data/validators.js +114 -0
- package/dist/modules/sso/data/validators.js.map +7 -0
- package/dist/modules/sso/di.js +26 -0
- package/dist/modules/sso/di.js.map +7 -0
- package/dist/modules/sso/events.js +24 -0
- package/dist/modules/sso/events.js.map +7 -0
- package/dist/modules/sso/i18n/de.json +146 -0
- package/dist/modules/sso/i18n/en.json +146 -0
- package/dist/modules/sso/i18n/es.json +146 -0
- package/dist/modules/sso/i18n/pl.json +146 -0
- package/dist/modules/sso/index.js +11 -0
- package/dist/modules/sso/index.js.map +7 -0
- package/dist/modules/sso/lib/domains.js +30 -0
- package/dist/modules/sso/lib/domains.js.map +7 -0
- package/dist/modules/sso/lib/oidc-provider.js +140 -0
- package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
- package/dist/modules/sso/lib/registry.js +15 -0
- package/dist/modules/sso/lib/registry.js.map +7 -0
- package/dist/modules/sso/lib/scim-filter.js +43 -0
- package/dist/modules/sso/lib/scim-filter.js.map +7 -0
- package/dist/modules/sso/lib/scim-mapper.js +49 -0
- package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
- package/dist/modules/sso/lib/scim-patch.js +63 -0
- package/dist/modules/sso/lib/scim-patch.js.map +7 -0
- package/dist/modules/sso/lib/scim-response.js +34 -0
- package/dist/modules/sso/lib/scim-response.js.map +7 -0
- package/dist/modules/sso/lib/scim-utils.js +9 -0
- package/dist/modules/sso/lib/scim-utils.js.map +7 -0
- package/dist/modules/sso/lib/state-cookie.js +67 -0
- package/dist/modules/sso/lib/state-cookie.js.map +7 -0
- package/dist/modules/sso/lib/types.js +1 -0
- package/dist/modules/sso/lib/types.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
- package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
- package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
- package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
- package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
- package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
- package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
- package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
- package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
- package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
- package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
- package/dist/modules/sso/services/accountLinkingService.js +298 -0
- package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
- package/dist/modules/sso/services/hrdService.js +18 -0
- package/dist/modules/sso/services/hrdService.js.map +7 -0
- package/dist/modules/sso/services/scimService.js +372 -0
- package/dist/modules/sso/services/scimService.js.map +7 -0
- package/dist/modules/sso/services/scimTokenService.js +94 -0
- package/dist/modules/sso/services/scimTokenService.js.map +7 -0
- package/dist/modules/sso/services/ssoConfigService.js +254 -0
- package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
- package/dist/modules/sso/services/ssoService.js +125 -0
- package/dist/modules/sso/services/ssoService.js.map +7 -0
- package/dist/modules/sso/setup.js +47 -0
- package/dist/modules/sso/setup.js.map +7 -0
- package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
- package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
- package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
- package/dist/modules/sso/widgets/injection-table.js +14 -0
- package/dist/modules/sso/widgets/injection-table.js.map +7 -0
- package/package.json +5 -4
- package/src/index.ts +1 -1
- package/src/modules/sso/acl.ts +7 -0
- package/src/modules/sso/api/admin-context.ts +36 -0
- package/src/modules/sso/api/callback/oidc/route.ts +115 -0
- package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
- package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
- package/src/modules/sso/api/config/[id]/route.ts +114 -0
- package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
- package/src/modules/sso/api/config/route.ts +88 -0
- package/src/modules/sso/api/error-handler.ts +36 -0
- package/src/modules/sso/api/hrd/route.ts +55 -0
- package/src/modules/sso/api/initiate/route.ts +70 -0
- package/src/modules/sso/api/scim/context.ts +85 -0
- package/src/modules/sso/api/scim/logs/route.ts +69 -0
- package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
- package/src/modules/sso/api/scim/tokens/route.ts +89 -0
- package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
- package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
- package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
- package/src/modules/sso/backend/page.meta.ts +29 -0
- package/src/modules/sso/backend/page.tsx +232 -0
- package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
- package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
- package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
- package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
- package/src/modules/sso/data/entities.ts +240 -0
- package/src/modules/sso/data/validators.ts +140 -0
- package/src/modules/sso/di.ts +25 -0
- package/src/modules/sso/docs/entra-id-setup.md +281 -0
- package/src/modules/sso/docs/google-workspace-setup.md +174 -0
- package/src/modules/sso/docs/sso-overview.md +218 -0
- package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
- package/src/modules/sso/docs/zitadel-setup.md +195 -0
- package/src/modules/sso/events.ts +21 -0
- package/src/modules/sso/i18n/de.json +146 -0
- package/src/modules/sso/i18n/en.json +146 -0
- package/src/modules/sso/i18n/es.json +146 -0
- package/src/modules/sso/i18n/pl.json +146 -0
- package/src/modules/sso/index.ts +7 -0
- package/src/modules/sso/lib/domains.ts +31 -0
- package/src/modules/sso/lib/oidc-provider.ts +196 -0
- package/src/modules/sso/lib/registry.ts +13 -0
- package/src/modules/sso/lib/scim-filter.ts +62 -0
- package/src/modules/sso/lib/scim-mapper.ts +88 -0
- package/src/modules/sso/lib/scim-patch.ts +88 -0
- package/src/modules/sso/lib/scim-response.ts +40 -0
- package/src/modules/sso/lib/scim-utils.ts +5 -0
- package/src/modules/sso/lib/state-cookie.ts +79 -0
- package/src/modules/sso/lib/types.ts +50 -0
- package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
- package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
- package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
- package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
- package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
- package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
- package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
- package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
- package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
- package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
- package/src/modules/sso/services/accountLinkingService.ts +386 -0
- package/src/modules/sso/services/hrdService.ts +22 -0
- package/src/modules/sso/services/scimService.ts +461 -0
- package/src/modules/sso/services/scimTokenService.ts +136 -0
- package/src/modules/sso/services/ssoConfigService.ts +337 -0
- package/src/modules/sso/services/ssoService.ts +167 -0
- package/src/modules/sso/setup.ts +56 -0
- package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
- package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
- package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
- package/src/modules/sso/widgets/injection-table.ts +12 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
6
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
|
+
import { FormHeader } from "@open-mercato/ui/backend/forms";
|
|
8
|
+
import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
9
|
+
import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
11
|
+
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
12
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
13
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
14
|
+
function SsoConfigDetailPage() {
|
|
15
|
+
const params = useParams();
|
|
16
|
+
const configId = params?.slug && Array.isArray(params.slug) ? params.slug[2] : Array.isArray(params?.id) ? params.id[0] : params?.id;
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const searchParams = useSearchParams();
|
|
19
|
+
const t = useT();
|
|
20
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
21
|
+
const [config, setConfig] = React.useState(null);
|
|
22
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
23
|
+
const [error, setError] = React.useState(null);
|
|
24
|
+
const [activeTab, setActiveTab] = React.useState("general");
|
|
25
|
+
const [showActivationBanner, setShowActivationBanner] = React.useState(searchParams?.get("created") === "1");
|
|
26
|
+
const [activationError, setActivationError] = React.useState(null);
|
|
27
|
+
const [isActivating, setIsActivating] = React.useState(false);
|
|
28
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
29
|
+
contextId: `sso-config:${configId ?? "pending"}`
|
|
30
|
+
});
|
|
31
|
+
const runMutationWithContext = React.useCallback(
|
|
32
|
+
async (operation, mutationPayload) => {
|
|
33
|
+
return runMutation({
|
|
34
|
+
operation,
|
|
35
|
+
mutationPayload,
|
|
36
|
+
context: { configId, retryLastMutation }
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
[configId, retryLastMutation, runMutation]
|
|
40
|
+
);
|
|
41
|
+
const [name, setName] = React.useState("");
|
|
42
|
+
const [issuer, setIssuer] = React.useState("");
|
|
43
|
+
const [clientId, setClientId] = React.useState("");
|
|
44
|
+
const [newClientSecret, setNewClientSecret] = React.useState("");
|
|
45
|
+
const [showSecretField, setShowSecretField] = React.useState(false);
|
|
46
|
+
const [jitEnabled, setJitEnabled] = React.useState(true);
|
|
47
|
+
const [autoLinkByEmail, setAutoLinkByEmail] = React.useState(true);
|
|
48
|
+
const [isSaving, setIsSaving] = React.useState(false);
|
|
49
|
+
const [domainInput, setDomainInput] = React.useState("");
|
|
50
|
+
const [domainError, setDomainError] = React.useState("");
|
|
51
|
+
const fetchConfig = React.useCallback(async () => {
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
const call = await apiCall(`/api/sso/config/${configId}`);
|
|
54
|
+
if (call.ok && call.result) {
|
|
55
|
+
const c = call.result;
|
|
56
|
+
setConfig(c);
|
|
57
|
+
setName(c.name ?? "");
|
|
58
|
+
setIssuer(c.issuer ?? "");
|
|
59
|
+
setClientId(c.clientId ?? "");
|
|
60
|
+
setJitEnabled(c.jitEnabled);
|
|
61
|
+
setAutoLinkByEmail(c.autoLinkByEmail);
|
|
62
|
+
setError(null);
|
|
63
|
+
} else {
|
|
64
|
+
setError(t("sso.admin.error.loadFailed", "Failed to load SSO configuration"));
|
|
65
|
+
}
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}, [configId, t]);
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
fetchConfig();
|
|
70
|
+
}, [fetchConfig]);
|
|
71
|
+
const handleSave = async () => {
|
|
72
|
+
setIsSaving(true);
|
|
73
|
+
try {
|
|
74
|
+
const payload = { name, issuer, clientId, jitEnabled, autoLinkByEmail };
|
|
75
|
+
if (newClientSecret) payload.clientSecret = newClientSecret;
|
|
76
|
+
await runMutationWithContext(
|
|
77
|
+
() => apiCallOrThrow(
|
|
78
|
+
`/api/sso/config/${configId}`,
|
|
79
|
+
{
|
|
80
|
+
method: "PUT",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: JSON.stringify(payload)
|
|
83
|
+
},
|
|
84
|
+
{ errorMessage: t("sso.admin.error.saveFailed", "Failed to save SSO configuration") }
|
|
85
|
+
),
|
|
86
|
+
payload
|
|
87
|
+
);
|
|
88
|
+
flash(t("sso.admin.saved", "SSO configuration saved"), "success");
|
|
89
|
+
setNewClientSecret("");
|
|
90
|
+
setShowSecretField(false);
|
|
91
|
+
fetchConfig();
|
|
92
|
+
} catch {
|
|
93
|
+
} finally {
|
|
94
|
+
setIsSaving(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const handleToggleActivation = async () => {
|
|
98
|
+
if (!config) return;
|
|
99
|
+
setActivationError(null);
|
|
100
|
+
setIsActivating(true);
|
|
101
|
+
try {
|
|
102
|
+
await runMutationWithContext(
|
|
103
|
+
() => apiCallOrThrow(
|
|
104
|
+
`/api/sso/config/${configId}/activate`,
|
|
105
|
+
{
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: JSON.stringify({ active: !config.isActive })
|
|
109
|
+
},
|
|
110
|
+
{ errorMessage: t("sso.admin.error.activationFailed", "Failed to update activation status") }
|
|
111
|
+
),
|
|
112
|
+
{ active: !config.isActive }
|
|
113
|
+
);
|
|
114
|
+
flash(
|
|
115
|
+
config.isActive ? t("sso.admin.deactivated", "SSO configuration deactivated") : t("sso.admin.activated", "SSO configuration activated"),
|
|
116
|
+
"success"
|
|
117
|
+
);
|
|
118
|
+
setShowActivationBanner(false);
|
|
119
|
+
fetchConfig();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
const isNoDomains = message.toLowerCase().includes("no allowed domains");
|
|
123
|
+
if (isNoDomains) {
|
|
124
|
+
setActivationError(t("sso.admin.error.noDomainsForActivation", "Add at least one allowed email domain before activating"));
|
|
125
|
+
setActiveTab("domains");
|
|
126
|
+
} else {
|
|
127
|
+
setActivationError(message);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
setIsActivating(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const handleTestConnection = async () => {
|
|
134
|
+
try {
|
|
135
|
+
const call = await runMutationWithContext(
|
|
136
|
+
() => apiCallOrThrow(
|
|
137
|
+
`/api/sso/config/${configId}/test`,
|
|
138
|
+
{ method: "POST" },
|
|
139
|
+
{ errorMessage: t("sso.admin.error.testFailed", "Connection test failed") }
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
if (call.result?.ok) {
|
|
143
|
+
flash(t("sso.admin.test.success", "Discovery successful \u2014 issuer is reachable"), "success");
|
|
144
|
+
} else {
|
|
145
|
+
flash(call.result?.error || t("sso.admin.test.failed", "Discovery failed"), "error");
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const handleDelete = async () => {
|
|
151
|
+
if (!config) return;
|
|
152
|
+
if (config.isActive) {
|
|
153
|
+
flash(t("sso.admin.error.deleteActive", "Cannot delete an active SSO configuration \u2014 deactivate it first"), "error");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const confirmed = await confirm({
|
|
157
|
+
title: t("sso.admin.delete.title", "Delete SSO Configuration"),
|
|
158
|
+
text: t("sso.admin.delete.confirm", "Are you sure? This will remove the SSO configuration."),
|
|
159
|
+
confirmText: t("common.delete", "Delete"),
|
|
160
|
+
variant: "destructive"
|
|
161
|
+
});
|
|
162
|
+
if (!confirmed) return;
|
|
163
|
+
await runMutationWithContext(
|
|
164
|
+
() => apiCallOrThrow(`/api/sso/config/${configId}`, { method: "DELETE" }, {
|
|
165
|
+
errorMessage: t("sso.admin.error.deleteFailed", "Failed to delete SSO configuration")
|
|
166
|
+
}),
|
|
167
|
+
{ id: configId }
|
|
168
|
+
);
|
|
169
|
+
flash(t("sso.admin.delete.success", "SSO configuration deleted"), "success");
|
|
170
|
+
router.push("/backend/sso");
|
|
171
|
+
};
|
|
172
|
+
const handleAddDomain = async () => {
|
|
173
|
+
const normalized = domainInput.trim().toLowerCase();
|
|
174
|
+
if (!normalized) return;
|
|
175
|
+
const domainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
|
176
|
+
if (!domainRegex.test(normalized) || !normalized.includes(".")) {
|
|
177
|
+
setDomainError(t("sso.admin.wizard.domain.invalid", "Invalid domain format"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
await runMutationWithContext(
|
|
182
|
+
() => apiCallOrThrow(
|
|
183
|
+
`/api/sso/config/${configId}/domains`,
|
|
184
|
+
{
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "content-type": "application/json" },
|
|
187
|
+
body: JSON.stringify({ domain: normalized })
|
|
188
|
+
},
|
|
189
|
+
{ errorMessage: t("sso.admin.error.domainAddFailed", "Failed to add domain") }
|
|
190
|
+
),
|
|
191
|
+
{ domain: normalized }
|
|
192
|
+
);
|
|
193
|
+
setDomainInput("");
|
|
194
|
+
setDomainError("");
|
|
195
|
+
fetchConfig();
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const handleRemoveDomain = async (domain) => {
|
|
200
|
+
try {
|
|
201
|
+
await runMutationWithContext(
|
|
202
|
+
() => apiCallOrThrow(
|
|
203
|
+
`/api/sso/config/${configId}/domains?domain=${encodeURIComponent(domain)}`,
|
|
204
|
+
{ method: "DELETE" },
|
|
205
|
+
{ errorMessage: t("sso.admin.error.domainRemoveFailed", "Failed to remove domain") }
|
|
206
|
+
),
|
|
207
|
+
{ domain }
|
|
208
|
+
);
|
|
209
|
+
fetchConfig();
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
if (isLoading) return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(LoadingMessage, { label: t("common.loading", "Loading...") }) }) });
|
|
214
|
+
if (error || !config) return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(ErrorMessage, { label: error || t("common.notFound", "Not found") }) }) });
|
|
215
|
+
const tabs = [
|
|
216
|
+
{ id: "general", label: t("sso.admin.tab.general", "General") },
|
|
217
|
+
{ id: "domains", label: t("sso.admin.tab.domains", "Domains") },
|
|
218
|
+
{ id: "roles", label: t("sso.admin.tab.roles", "Role Mappings") },
|
|
219
|
+
{ id: "scim", label: t("sso.admin.tab.scim", "Provisioning") },
|
|
220
|
+
{ id: "activity", label: t("sso.admin.tab.activity", "Activity") }
|
|
221
|
+
];
|
|
222
|
+
const statusBadge = /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${config.isActive ? "bg-green-50 text-green-700" : "bg-gray-100 text-gray-600"}`, children: config.isActive ? t("sso.admin.status.active", "Active") : t("sso.admin.status.inactive", "Inactive") });
|
|
223
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsxs(PageBody, { children: [
|
|
224
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 max-w-3xl", children: [
|
|
225
|
+
showActivationBanner && !config.isActive && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-blue-200 bg-blue-50 p-4", children: [
|
|
226
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-blue-900 mb-3", children: t("sso.admin.banner.created", "Your SSO configuration has been created. Would you like to activate it now?") }),
|
|
227
|
+
activationError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mb-3", children: activationError }),
|
|
228
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
229
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: handleToggleActivation, disabled: isActivating, children: isActivating ? t("common.activating", "Activating...") : t("sso.admin.banner.activateNow", "Activate Now") }),
|
|
230
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowActivationBanner(false), children: t("sso.admin.banner.notYet", "Not Yet") })
|
|
231
|
+
] })
|
|
232
|
+
] }),
|
|
233
|
+
/* @__PURE__ */ jsx(
|
|
234
|
+
FormHeader,
|
|
235
|
+
{
|
|
236
|
+
mode: "detail",
|
|
237
|
+
backHref: "/backend/sso",
|
|
238
|
+
backLabel: t("sso.admin.detail.backToList", "Back to SSO"),
|
|
239
|
+
title: config.name || config.issuer || t("sso.admin.detail.title", "SSO Configuration"),
|
|
240
|
+
statusBadge,
|
|
241
|
+
actionsContent: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
242
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleTestConnection, children: t("sso.admin.action.test", "Verify Discovery") }),
|
|
243
|
+
/* @__PURE__ */ jsx(
|
|
244
|
+
Button,
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
variant: config.isActive ? "outline" : "default",
|
|
248
|
+
size: "sm",
|
|
249
|
+
onClick: handleToggleActivation,
|
|
250
|
+
children: config.isActive ? t("sso.admin.action.deactivate", "Deactivate") : t("sso.admin.action.activate", "Activate")
|
|
251
|
+
}
|
|
252
|
+
),
|
|
253
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleDelete, className: "text-destructive", children: t("common.delete", "Delete") })
|
|
254
|
+
] })
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b", children: tabs.map((tab) => /* @__PURE__ */ jsx(
|
|
258
|
+
Button,
|
|
259
|
+
{
|
|
260
|
+
type: "button",
|
|
261
|
+
variant: "ghost",
|
|
262
|
+
size: "sm",
|
|
263
|
+
className: `h-auto rounded-none border-b-2 px-4 py-2 hover:bg-transparent ${activeTab === tab.id ? "border-primary text-foreground" : "border-transparent text-muted-foreground"}`,
|
|
264
|
+
onClick: () => setActiveTab(tab.id),
|
|
265
|
+
children: tab.label
|
|
266
|
+
},
|
|
267
|
+
tab.id
|
|
268
|
+
)) }),
|
|
269
|
+
activeTab === "general" && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
270
|
+
/* @__PURE__ */ jsx("h2", { className: "mb-4 text-sm font-semibold uppercase text-muted-foreground", children: t("sso.admin.section.oidcSettings", "OIDC Settings") }),
|
|
271
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
272
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
273
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: t("sso.admin.field.name", "Configuration Name") }),
|
|
274
|
+
/* @__PURE__ */ jsx(
|
|
275
|
+
"input",
|
|
276
|
+
{
|
|
277
|
+
type: "text",
|
|
278
|
+
className: "w-full rounded-md border px-3 py-2 text-sm",
|
|
279
|
+
value: name,
|
|
280
|
+
onChange: (e) => setName(e.target.value)
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
285
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: t("sso.admin.field.protocol", "Protocol") }),
|
|
286
|
+
/* @__PURE__ */ jsx("input", { type: "text", className: "w-full rounded-md border px-3 py-2 text-sm bg-muted", value: config.protocol.toUpperCase(), disabled: true })
|
|
287
|
+
] }),
|
|
288
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
289
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: t("sso.admin.field.issuer", "Issuer URL") }),
|
|
290
|
+
/* @__PURE__ */ jsx(
|
|
291
|
+
"input",
|
|
292
|
+
{
|
|
293
|
+
type: "url",
|
|
294
|
+
className: "w-full rounded-md border px-3 py-2 text-sm",
|
|
295
|
+
value: issuer,
|
|
296
|
+
onChange: (e) => setIssuer(e.target.value)
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
] }),
|
|
300
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
301
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: t("sso.admin.field.clientId", "Client ID") }),
|
|
302
|
+
/* @__PURE__ */ jsx(
|
|
303
|
+
"input",
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
className: "w-full rounded-md border px-3 py-2 text-sm",
|
|
307
|
+
value: clientId,
|
|
308
|
+
onChange: (e) => setClientId(e.target.value)
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
] }),
|
|
312
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
313
|
+
/* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: t("sso.admin.field.clientSecret", "Client Secret") }),
|
|
314
|
+
config.hasClientSecret && !showSecretField ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
315
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("sso.admin.field.secretSet", "Client secret is configured") }),
|
|
316
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setShowSecretField(true), children: t("sso.admin.field.changeSecret", "Change") })
|
|
317
|
+
] }) : /* @__PURE__ */ jsx(
|
|
318
|
+
"input",
|
|
319
|
+
{
|
|
320
|
+
type: "password",
|
|
321
|
+
className: "w-full rounded-md border px-3 py-2 text-sm",
|
|
322
|
+
value: newClientSecret,
|
|
323
|
+
onChange: (e) => setNewClientSecret(e.target.value),
|
|
324
|
+
placeholder: config.hasClientSecret ? t("sso.admin.field.secretPlaceholder", "Enter new secret to replace existing") : t("sso.admin.field.secretRequired", "Enter client secret")
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
] }),
|
|
328
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3 pt-2", children: [
|
|
329
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3", children: [
|
|
330
|
+
/* @__PURE__ */ jsx(
|
|
331
|
+
"input",
|
|
332
|
+
{
|
|
333
|
+
type: "checkbox",
|
|
334
|
+
checked: jitEnabled,
|
|
335
|
+
onChange: (e) => setJitEnabled(e.target.checked),
|
|
336
|
+
disabled: config.hasActiveScimTokens,
|
|
337
|
+
className: "accent-primary"
|
|
338
|
+
}
|
|
339
|
+
),
|
|
340
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
341
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t("sso.admin.field.jitEnabled", "Just-in-Time Provisioning") }),
|
|
342
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground ml-2", children: config.hasActiveScimTokens ? t("sso.admin.field.jitDisabledByScim", "Unavailable \u2014 SCIM directory sync is active. Revoke SCIM tokens to enable JIT.") : t("sso.admin.field.jitEnabledDesc", "Automatically create user accounts on first SSO login") })
|
|
343
|
+
] })
|
|
344
|
+
] }),
|
|
345
|
+
/* @__PURE__ */ jsxs("label", { className: "flex items-center gap-3", children: [
|
|
346
|
+
/* @__PURE__ */ jsx(
|
|
347
|
+
"input",
|
|
348
|
+
{
|
|
349
|
+
type: "checkbox",
|
|
350
|
+
checked: autoLinkByEmail,
|
|
351
|
+
onChange: (e) => setAutoLinkByEmail(e.target.checked),
|
|
352
|
+
className: "accent-primary"
|
|
353
|
+
}
|
|
354
|
+
),
|
|
355
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
356
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t("sso.admin.field.autoLinkByEmail", "Auto-link by Email") }),
|
|
357
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground ml-2", children: t("sso.admin.field.autoLinkByEmailDesc", "Automatically link existing users by matching email address") })
|
|
358
|
+
] })
|
|
359
|
+
] })
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ jsx("div", { className: "pt-4", children: /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: isSaving, children: isSaving ? t("common.saving", "Saving...") : t("common.save", "Save") }) })
|
|
362
|
+
] })
|
|
363
|
+
] }),
|
|
364
|
+
activeTab === "domains" && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
365
|
+
/* @__PURE__ */ jsx("h2", { className: "mb-4 text-sm font-semibold uppercase text-muted-foreground", children: t("sso.admin.section.allowedDomains", "Allowed Domains") }),
|
|
366
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: t("sso.admin.wizard.domains.description", "Users with email addresses matching these domains will be redirected to your SSO provider.") }),
|
|
367
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-4", children: [
|
|
368
|
+
/* @__PURE__ */ jsx(
|
|
369
|
+
"input",
|
|
370
|
+
{
|
|
371
|
+
type: "text",
|
|
372
|
+
className: "flex-1 rounded-md border px-3 py-2 text-sm",
|
|
373
|
+
placeholder: t("sso.admin.wizard.domains.placeholder", "example.com"),
|
|
374
|
+
value: domainInput,
|
|
375
|
+
onChange: (e) => {
|
|
376
|
+
setDomainInput(e.target.value);
|
|
377
|
+
setDomainError("");
|
|
378
|
+
},
|
|
379
|
+
onKeyDown: (e) => {
|
|
380
|
+
if (e.key === "Enter") {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
handleAddDomain();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: handleAddDomain, children: t("common.add", "Add") })
|
|
388
|
+
] }),
|
|
389
|
+
domainError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mb-2", children: domainError }),
|
|
390
|
+
config.allowedDomains.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: config.allowedDomains.map((domain) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 border rounded-md", children: [
|
|
391
|
+
/* @__PURE__ */ jsx("code", { className: "text-sm font-mono", children: domain }),
|
|
392
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleRemoveDomain(domain), children: t("common.remove", "Remove") })
|
|
393
|
+
] }, domain)) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center", children: t("sso.admin.domains.empty", "No domains configured. Add at least one domain before activating SSO.") })
|
|
394
|
+
] }),
|
|
395
|
+
activeTab === "roles" && config && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx(
|
|
396
|
+
RoleMappingsTab,
|
|
397
|
+
{
|
|
398
|
+
configId,
|
|
399
|
+
appRoleMappings: config.appRoleMappings ?? {},
|
|
400
|
+
onSaved: fetchConfig,
|
|
401
|
+
runMutationWithContext
|
|
402
|
+
}
|
|
403
|
+
) }),
|
|
404
|
+
activeTab === "scim" && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx(ScimProvisioningTab, { configId, jitEnabled: config.jitEnabled, issuer: config.issuer ?? void 0, onProvisioningChange: fetchConfig, runMutationWithContext }) }),
|
|
405
|
+
activeTab === "activity" && /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx(SsoActivityTab, { configId }) })
|
|
406
|
+
] }),
|
|
407
|
+
ConfirmDialogElement
|
|
408
|
+
] }) });
|
|
409
|
+
}
|
|
410
|
+
function RoleMappingsTab({
|
|
411
|
+
configId,
|
|
412
|
+
appRoleMappings,
|
|
413
|
+
onSaved,
|
|
414
|
+
runMutationWithContext
|
|
415
|
+
}) {
|
|
416
|
+
const t = useT();
|
|
417
|
+
const [mappings, setMappings] = React.useState(appRoleMappings);
|
|
418
|
+
const [idpRoleInput, setIdpRoleInput] = React.useState("");
|
|
419
|
+
const [localRoleInput, setLocalRoleInput] = React.useState("");
|
|
420
|
+
const [roleOptions, setRoleOptions] = React.useState([]);
|
|
421
|
+
const [isSaving, setIsSaving] = React.useState(false);
|
|
422
|
+
const [inputError, setInputError] = React.useState("");
|
|
423
|
+
React.useEffect(() => {
|
|
424
|
+
setMappings(appRoleMappings);
|
|
425
|
+
}, [appRoleMappings]);
|
|
426
|
+
React.useEffect(() => {
|
|
427
|
+
const loadRoles = async () => {
|
|
428
|
+
const call = await apiCall(
|
|
429
|
+
"/api/auth/roles?page=1&pageSize=50",
|
|
430
|
+
void 0,
|
|
431
|
+
{ fallback: { items: [] } }
|
|
432
|
+
);
|
|
433
|
+
if (call.ok && Array.isArray(call.result?.items)) {
|
|
434
|
+
const options = call.result.items.map((item) => {
|
|
435
|
+
const name = typeof item?.name === "string" ? item.name.trim() : "";
|
|
436
|
+
if (!name || name === "superadmin") return null;
|
|
437
|
+
return { value: name, label: name };
|
|
438
|
+
}).filter((opt) => !!opt);
|
|
439
|
+
setRoleOptions(options);
|
|
440
|
+
if (options.length > 0 && !localRoleInput) {
|
|
441
|
+
setLocalRoleInput(options[0].value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
loadRoles();
|
|
446
|
+
}, []);
|
|
447
|
+
const handleAdd = () => {
|
|
448
|
+
const trimmed = idpRoleInput.trim();
|
|
449
|
+
if (!trimmed) {
|
|
450
|
+
setInputError(t("sso.admin.roles.error.emptyIdpRole", "IdP role name is required"));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (!localRoleInput) {
|
|
454
|
+
setInputError(t("sso.admin.roles.error.emptyLocalRole", "Select a local role"));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (mappings[trimmed]) {
|
|
458
|
+
setInputError(t("sso.admin.roles.error.duplicate", "This IdP role is already mapped"));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
setMappings({ ...mappings, [trimmed]: localRoleInput });
|
|
462
|
+
setIdpRoleInput("");
|
|
463
|
+
setInputError("");
|
|
464
|
+
};
|
|
465
|
+
const handleRemove = (idpRole) => {
|
|
466
|
+
const updated = { ...mappings };
|
|
467
|
+
delete updated[idpRole];
|
|
468
|
+
setMappings(updated);
|
|
469
|
+
};
|
|
470
|
+
const handleSave = async () => {
|
|
471
|
+
setIsSaving(true);
|
|
472
|
+
try {
|
|
473
|
+
await runMutationWithContext(
|
|
474
|
+
() => apiCallOrThrow(
|
|
475
|
+
`/api/sso/config/${configId}`,
|
|
476
|
+
{
|
|
477
|
+
method: "PUT",
|
|
478
|
+
headers: { "content-type": "application/json" },
|
|
479
|
+
body: JSON.stringify({ appRoleMappings: mappings })
|
|
480
|
+
},
|
|
481
|
+
{ errorMessage: t("sso.admin.roles.error.saveFailed", "Failed to save role mappings") }
|
|
482
|
+
),
|
|
483
|
+
{ appRoleMappings: mappings }
|
|
484
|
+
);
|
|
485
|
+
flash(t("sso.admin.roles.saved", "Role mappings saved"), "success");
|
|
486
|
+
onSaved();
|
|
487
|
+
} catch {
|
|
488
|
+
} finally {
|
|
489
|
+
setIsSaving(false);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const mappingEntries = Object.entries(mappings);
|
|
493
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
494
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: t("sso.admin.roles.description", "Map IdP app role names to local roles. On each SSO login, SSO-sourced roles are synced \u2014 roles no longer sent by the IdP are removed, while manually-assigned roles are preserved.") }),
|
|
495
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2 mb-4", children: [
|
|
496
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
497
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", children: t("sso.admin.roles.idpRole", "IdP Role Name") }),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
"input",
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
className: "w-full rounded-md border px-3 py-2 text-sm",
|
|
503
|
+
placeholder: t("sso.admin.roles.idpRolePlaceholder", "e.g. OpenMercato.Admin"),
|
|
504
|
+
value: idpRoleInput,
|
|
505
|
+
onChange: (e) => {
|
|
506
|
+
setIdpRoleInput(e.target.value);
|
|
507
|
+
setInputError("");
|
|
508
|
+
},
|
|
509
|
+
onKeyDown: (e) => {
|
|
510
|
+
if (e.key === "Enter") {
|
|
511
|
+
e.preventDefault();
|
|
512
|
+
handleAdd();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
)
|
|
517
|
+
] }),
|
|
518
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
519
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", children: t("sso.admin.roles.localRole", "Local Role") }),
|
|
520
|
+
/* @__PURE__ */ jsx(
|
|
521
|
+
"select",
|
|
522
|
+
{
|
|
523
|
+
className: "w-full rounded-md border px-3 py-2 text-sm bg-background",
|
|
524
|
+
value: localRoleInput,
|
|
525
|
+
onChange: (e) => setLocalRoleInput(e.target.value),
|
|
526
|
+
children: roleOptions.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
] }),
|
|
530
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: handleAdd, children: t("common.add", "Add") })
|
|
531
|
+
] }),
|
|
532
|
+
inputError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mb-2", children: inputError }),
|
|
533
|
+
mappingEntries.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-2 mb-4", children: mappingEntries.map(([idpRole, localRole]) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 border rounded-md", children: [
|
|
534
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
535
|
+
/* @__PURE__ */ jsx("code", { className: "text-sm font-mono", children: idpRole }),
|
|
536
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: "\u2192" }),
|
|
537
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: localRole })
|
|
538
|
+
] }),
|
|
539
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => handleRemove(idpRole), children: t("common.remove", "Remove") })
|
|
540
|
+
] }, idpRole)) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-4 text-center mb-4", children: t("sso.admin.roles.empty", "No role mappings configured. IdP role names will be matched directly against local role names.") }),
|
|
541
|
+
/* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: isSaving, children: isSaving ? t("common.saving", "Saving...") : t("common.save", "Save") })
|
|
542
|
+
] });
|
|
543
|
+
}
|
|
544
|
+
function ScimProvisioningTab({ configId, jitEnabled, issuer, onProvisioningChange, runMutationWithContext }) {
|
|
545
|
+
const isGoogleProvider = issuer?.includes("accounts.google.com") === true;
|
|
546
|
+
const t = useT();
|
|
547
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
548
|
+
const [tokens, setTokens] = React.useState([]);
|
|
549
|
+
const [logs, setLogs] = React.useState([]);
|
|
550
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
551
|
+
const [showCreateForm, setShowCreateForm] = React.useState(false);
|
|
552
|
+
const [tokenName, setTokenName] = React.useState("");
|
|
553
|
+
const [isCreating, setIsCreating] = React.useState(false);
|
|
554
|
+
const [newlyCreatedToken, setNewlyCreatedToken] = React.useState(null);
|
|
555
|
+
const fetchData = React.useCallback(async () => {
|
|
556
|
+
setIsLoading(true);
|
|
557
|
+
const [tokensCall, logsCall] = await Promise.all([
|
|
558
|
+
apiCall(`/api/sso/scim/tokens?ssoConfigId=${configId}`),
|
|
559
|
+
apiCall(`/api/sso/scim/logs?ssoConfigId=${configId}`)
|
|
560
|
+
]);
|
|
561
|
+
if (tokensCall.ok && tokensCall.result) setTokens(tokensCall.result.items);
|
|
562
|
+
if (logsCall.ok && logsCall.result) setLogs(logsCall.result.items);
|
|
563
|
+
setIsLoading(false);
|
|
564
|
+
}, [configId]);
|
|
565
|
+
React.useEffect(() => {
|
|
566
|
+
fetchData();
|
|
567
|
+
}, [fetchData]);
|
|
568
|
+
const handleCreateToken = async () => {
|
|
569
|
+
if (!tokenName.trim()) return;
|
|
570
|
+
setIsCreating(true);
|
|
571
|
+
try {
|
|
572
|
+
const result = await runMutationWithContext(
|
|
573
|
+
() => apiCallOrThrow(
|
|
574
|
+
"/api/sso/scim/tokens",
|
|
575
|
+
{
|
|
576
|
+
method: "POST",
|
|
577
|
+
headers: { "content-type": "application/json" },
|
|
578
|
+
body: JSON.stringify({ ssoConfigId: configId, name: tokenName.trim() })
|
|
579
|
+
},
|
|
580
|
+
{ errorMessage: t("sso.admin.scim.error.createFailed", "Failed to create SCIM token") }
|
|
581
|
+
),
|
|
582
|
+
{ ssoConfigId: configId, name: tokenName.trim() }
|
|
583
|
+
);
|
|
584
|
+
if (result.result) {
|
|
585
|
+
setNewlyCreatedToken(result.result.token);
|
|
586
|
+
setShowCreateForm(false);
|
|
587
|
+
setTokenName("");
|
|
588
|
+
fetchData();
|
|
589
|
+
onProvisioningChange();
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
} finally {
|
|
593
|
+
setIsCreating(false);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
const handleRevokeToken = async (tokenId) => {
|
|
597
|
+
const confirmed = await confirm({
|
|
598
|
+
title: t("sso.admin.scim.revoke.title", "Revoke Token"),
|
|
599
|
+
text: t("sso.admin.scim.revoke.confirm", "Are you sure? This token will no longer authenticate SCIM requests."),
|
|
600
|
+
confirmText: t("sso.admin.scim.revoke.action", "Revoke"),
|
|
601
|
+
variant: "destructive"
|
|
602
|
+
});
|
|
603
|
+
if (!confirmed) return;
|
|
604
|
+
try {
|
|
605
|
+
await runMutationWithContext(
|
|
606
|
+
() => apiCallOrThrow(`/api/sso/scim/tokens/${tokenId}`, { method: "DELETE" }, {
|
|
607
|
+
errorMessage: t("sso.admin.scim.error.revokeFailed", "Failed to revoke token")
|
|
608
|
+
}),
|
|
609
|
+
{ tokenId }
|
|
610
|
+
);
|
|
611
|
+
flash(t("sso.admin.scim.revoked", "Token revoked"), "success");
|
|
612
|
+
fetchData();
|
|
613
|
+
onProvisioningChange();
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
const handleCopyEndpoint = () => {
|
|
618
|
+
const url = `${window.location.origin}/api/sso/scim/v2`;
|
|
619
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
620
|
+
flash(t("sso.admin.scim.endpointCopied", "SCIM endpoint URL copied"), "success");
|
|
621
|
+
});
|
|
622
|
+
};
|
|
623
|
+
const handleCopyToken = () => {
|
|
624
|
+
if (!newlyCreatedToken) return;
|
|
625
|
+
navigator.clipboard.writeText(newlyCreatedToken).then(() => {
|
|
626
|
+
flash(t("sso.admin.scim.tokenCopied", "Token copied to clipboard"), "success");
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
if (isLoading) return /* @__PURE__ */ jsx(LoadingMessage, { label: t("common.loading", "Loading...") });
|
|
630
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
631
|
+
isGoogleProvider && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-blue-200 bg-blue-50 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-blue-900", children: t("sso.admin.scim.googleNotSupported", "Google Workspace does not support SCIM provisioning. Users are provisioned via Just-In-Time (JIT) on first login.") }) }),
|
|
632
|
+
!isGoogleProvider && jitEnabled && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-amber-900", children: t("sso.admin.scim.jitActiveWarning", "SCIM provisioning is unavailable while JIT provisioning is enabled. Disable JIT in the General tab to configure SCIM.") }) }),
|
|
633
|
+
isGoogleProvider ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
634
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
635
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium mb-2", children: t("sso.admin.scim.endpointUrl", "SCIM Endpoint URL") }),
|
|
636
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
637
|
+
/* @__PURE__ */ jsx("code", { className: "flex-1 rounded-md border bg-muted px-3 py-2 text-sm font-mono", children: typeof window !== "undefined" ? `${window.location.origin}/api/sso/scim/v2` : "/api/sso/scim/v2" }),
|
|
638
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleCopyEndpoint, children: t("common.copy", "Copy") })
|
|
639
|
+
] })
|
|
640
|
+
] }),
|
|
641
|
+
newlyCreatedToken && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-amber-200 bg-amber-50 p-4", children: [
|
|
642
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-amber-900 mb-2", children: t("sso.admin.scim.tokenCreated", "Your SCIM token has been created. Copy it now \u2014 it will not be shown again.") }),
|
|
643
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
644
|
+
/* @__PURE__ */ jsx("code", { className: "flex-1 rounded-md border bg-white px-3 py-2 text-xs font-mono break-all", children: newlyCreatedToken }),
|
|
645
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleCopyToken, children: t("common.copy", "Copy") })
|
|
646
|
+
] }),
|
|
647
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => setNewlyCreatedToken(null), children: t("common.dismiss", "Dismiss") })
|
|
648
|
+
] }),
|
|
649
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
650
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
651
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium", children: t("sso.admin.scim.tokens", "Bearer Tokens") }),
|
|
652
|
+
!showCreateForm && /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowCreateForm(true), disabled: jitEnabled, children: t("sso.admin.scim.generateToken", "Generate Token") })
|
|
653
|
+
] }),
|
|
654
|
+
showCreateForm && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-4", children: [
|
|
655
|
+
/* @__PURE__ */ jsx(
|
|
656
|
+
"input",
|
|
657
|
+
{
|
|
658
|
+
type: "text",
|
|
659
|
+
className: "flex-1 rounded-md border px-3 py-2 text-sm",
|
|
660
|
+
placeholder: t("sso.admin.scim.tokenNamePlaceholder", "Token name (e.g., Entra ID Production)"),
|
|
661
|
+
value: tokenName,
|
|
662
|
+
onChange: (e) => setTokenName(e.target.value),
|
|
663
|
+
onKeyDown: (e) => {
|
|
664
|
+
if (e.key === "Enter") {
|
|
665
|
+
e.preventDefault();
|
|
666
|
+
handleCreateToken();
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
autoFocus: true
|
|
670
|
+
}
|
|
671
|
+
),
|
|
672
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: handleCreateToken, disabled: isCreating || !tokenName.trim(), children: isCreating ? t("common.creating", "Creating...") : t("common.create", "Create") }),
|
|
673
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
674
|
+
setShowCreateForm(false);
|
|
675
|
+
setTokenName("");
|
|
676
|
+
}, children: t("common.cancel", "Cancel") })
|
|
677
|
+
] }),
|
|
678
|
+
tokens.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-center py-8 border rounded-md", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("sso.admin.scim.noTokens", "SCIM provisioning is not configured. Generate a bearer token to enable your identity provider to sync users automatically.") }) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: tokens.map((token) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 border rounded-md", children: [
|
|
679
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
680
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
681
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: token.name }),
|
|
682
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground ml-2 font-mono", children: [
|
|
683
|
+
token.tokenPrefix,
|
|
684
|
+
"..."
|
|
685
|
+
] })
|
|
686
|
+
] }),
|
|
687
|
+
/* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${token.isActive ? "bg-green-50 text-green-700" : "bg-gray-100 text-gray-500"}`, children: token.isActive ? t("sso.admin.scim.tokenActive", "Active") : t("sso.admin.scim.tokenRevoked", "Revoked") })
|
|
688
|
+
] }),
|
|
689
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
690
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(token.createdAt).toLocaleDateString() }),
|
|
691
|
+
token.isActive && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => handleRevokeToken(token.id), className: "text-destructive", children: t("sso.admin.scim.revoke.action", "Revoke") })
|
|
692
|
+
] })
|
|
693
|
+
] }, token.id)) })
|
|
694
|
+
] }),
|
|
695
|
+
logs.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
696
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-medium mb-3", children: t("sso.admin.scim.recentActivity", "Recent Provisioning Activity") }),
|
|
697
|
+
/* @__PURE__ */ jsx("div", { className: "border rounded-md overflow-hidden", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
|
|
698
|
+
/* @__PURE__ */ jsx("thead", { className: "bg-muted/50", children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
699
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("sso.admin.scim.log.time", "Time") }),
|
|
700
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("sso.admin.scim.log.operation", "Operation") }),
|
|
701
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("sso.admin.scim.log.resource", "Resource") }),
|
|
702
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("sso.admin.scim.log.status", "Status") }),
|
|
703
|
+
/* @__PURE__ */ jsx("th", { className: "text-left px-3 py-2 font-medium", children: t("sso.admin.scim.log.error", "Error") })
|
|
704
|
+
] }) }),
|
|
705
|
+
/* @__PURE__ */ jsx("tbody", { children: logs.map((log) => /* @__PURE__ */ jsxs("tr", { className: "border-t", children: [
|
|
706
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-xs text-muted-foreground whitespace-nowrap", children: new Date(log.createdAt).toLocaleString() }),
|
|
707
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium bg-muted", children: log.operation }) }),
|
|
708
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-xs", children: log.resourceType }),
|
|
709
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2", children: /* @__PURE__ */ jsx("span", { className: `text-xs font-medium ${log.responseStatus < 300 ? "text-green-700" : "text-red-600"}`, children: log.responseStatus }) }),
|
|
710
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-2 text-xs text-muted-foreground truncate max-w-[200px]", children: log.errorMessage || "\u2014" })
|
|
711
|
+
] }, log.id)) })
|
|
712
|
+
] }) })
|
|
713
|
+
] })
|
|
714
|
+
] }),
|
|
715
|
+
ConfirmDialogElement
|
|
716
|
+
] });
|
|
717
|
+
}
|
|
718
|
+
function SsoActivityTab({ configId }) {
|
|
719
|
+
const t = useT();
|
|
720
|
+
const [identities, setIdentities] = React.useState([]);
|
|
721
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
722
|
+
React.useEffect(() => {
|
|
723
|
+
const load = async () => {
|
|
724
|
+
setIsLoading(true);
|
|
725
|
+
setIdentities([]);
|
|
726
|
+
setIsLoading(false);
|
|
727
|
+
};
|
|
728
|
+
load();
|
|
729
|
+
}, [configId]);
|
|
730
|
+
if (isLoading) return /* @__PURE__ */ jsx(LoadingMessage, { label: t("common.loading", "Loading...") });
|
|
731
|
+
if (identities.length === 0) {
|
|
732
|
+
return /* @__PURE__ */ jsx("div", { className: "text-center py-8", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("sso.admin.activity.empty", "No SSO login activity yet. Activity will appear here once users start logging in via SSO.") }) });
|
|
733
|
+
}
|
|
734
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-2", children: identities.map((identity) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 border rounded-md", children: [
|
|
735
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
736
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: identity.idpEmail }),
|
|
737
|
+
identity.idpName && /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground ml-2", children: [
|
|
738
|
+
"(",
|
|
739
|
+
identity.idpName,
|
|
740
|
+
")"
|
|
741
|
+
] })
|
|
742
|
+
] }),
|
|
743
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: identity.lastLoginAt ? new Date(identity.lastLoginAt).toLocaleString() : "\u2014" })
|
|
744
|
+
] }, identity.id)) });
|
|
745
|
+
}
|
|
746
|
+
export {
|
|
747
|
+
SsoConfigDetailPage as default
|
|
748
|
+
};
|
|
749
|
+
//# sourceMappingURL=page.js.map
|