@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,15 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
requireAuth: true,
|
|
3
|
+
requireFeatures: ['sso.config.manage'],
|
|
4
|
+
pageTitle: 'Configure SSO',
|
|
5
|
+
pageTitleKey: 'sso.admin.create.title',
|
|
6
|
+
pageGroup: 'Auth',
|
|
7
|
+
pageGroupKey: 'settings.sections.auth',
|
|
8
|
+
pageOrder: 521,
|
|
9
|
+
pageContext: 'settings' as const,
|
|
10
|
+
navHidden: true,
|
|
11
|
+
breadcrumb: [
|
|
12
|
+
{ label: 'Single Sign-On', labelKey: 'sso.admin.title', href: '/backend/sso' },
|
|
13
|
+
{ label: 'Configure SSO', labelKey: 'sso.admin.create.title' },
|
|
14
|
+
],
|
|
15
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
11
|
+
|
|
12
|
+
type WizardStep = 'protocol' | 'credentials' | 'domains' | 'options' | 'review'
|
|
13
|
+
|
|
14
|
+
const STEPS: WizardStep[] = ['protocol', 'credentials', 'domains', 'options', 'review']
|
|
15
|
+
|
|
16
|
+
interface WizardState {
|
|
17
|
+
name: string
|
|
18
|
+
protocol: 'oidc'
|
|
19
|
+
issuer: string
|
|
20
|
+
clientId: string
|
|
21
|
+
clientSecret: string
|
|
22
|
+
domains: string[]
|
|
23
|
+
jitEnabled: boolean
|
|
24
|
+
autoLinkByEmail: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const initialState: WizardState = {
|
|
28
|
+
name: '',
|
|
29
|
+
protocol: 'oidc',
|
|
30
|
+
issuer: '',
|
|
31
|
+
clientId: '',
|
|
32
|
+
clientSecret: '',
|
|
33
|
+
domains: [],
|
|
34
|
+
jitEnabled: true,
|
|
35
|
+
autoLinkByEmail: true,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function SsoConfigCreateWizard() {
|
|
39
|
+
const router = useRouter()
|
|
40
|
+
const t = useT()
|
|
41
|
+
const [step, setStep] = React.useState<WizardStep>('protocol')
|
|
42
|
+
const [state, setState] = React.useState<WizardState>(initialState)
|
|
43
|
+
const [domainInput, setDomainInput] = React.useState('')
|
|
44
|
+
const [domainError, setDomainError] = React.useState('')
|
|
45
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
46
|
+
const [testResult, setTestResult] = React.useState<{ ok: boolean; error?: string } | null>(null)
|
|
47
|
+
const [isTesting, setIsTesting] = React.useState(false)
|
|
48
|
+
|
|
49
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
50
|
+
contextId: 'sso-config-create',
|
|
51
|
+
})
|
|
52
|
+
const runMutationWithContext = React.useCallback(
|
|
53
|
+
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
54
|
+
return runMutation({
|
|
55
|
+
operation,
|
|
56
|
+
mutationPayload,
|
|
57
|
+
context: { retryLastMutation },
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
[retryLastMutation, runMutation],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
const checkExisting = async () => {
|
|
65
|
+
const call = await apiCall<{ items: { id: string }[] }>('/api/sso/config?pageSize=1')
|
|
66
|
+
if (call.ok && call.result && call.result.items.length > 0) {
|
|
67
|
+
flash(t('sso.admin.error.alreadyExists', 'An SSO configuration already exists for this organization'), 'error')
|
|
68
|
+
router.replace(`/backend/sso/config/${call.result.items[0].id}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
checkExisting()
|
|
72
|
+
}, [router, t])
|
|
73
|
+
|
|
74
|
+
const currentStepIndex = STEPS.indexOf(step)
|
|
75
|
+
|
|
76
|
+
const callbackUrl = typeof window !== 'undefined'
|
|
77
|
+
? `${window.location.origin}/api/sso/callback/oidc`
|
|
78
|
+
: '/api/sso/callback/oidc'
|
|
79
|
+
|
|
80
|
+
const goNext = () => {
|
|
81
|
+
const nextIndex = currentStepIndex + 1
|
|
82
|
+
if (nextIndex < STEPS.length) setStep(STEPS[nextIndex])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const goBack = () => {
|
|
86
|
+
const prevIndex = currentStepIndex - 1
|
|
87
|
+
if (prevIndex >= 0) setStep(STEPS[prevIndex])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handleAddDomain = () => {
|
|
91
|
+
const normalized = domainInput.trim().toLowerCase()
|
|
92
|
+
if (!normalized) return
|
|
93
|
+
|
|
94
|
+
const domainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/
|
|
95
|
+
if (!domainRegex.test(normalized) || !normalized.includes('.')) {
|
|
96
|
+
setDomainError(t('sso.admin.wizard.domain.invalid', 'Invalid domain format'))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (state.domains.includes(normalized)) {
|
|
101
|
+
setDomainError(t('sso.admin.wizard.domain.duplicate', 'Domain already added'))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (state.domains.length >= 20) {
|
|
106
|
+
setDomainError(t('sso.admin.wizard.domain.limit', 'Maximum 20 domains per configuration'))
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setState((prev) => ({ ...prev, domains: [...prev.domains, normalized] }))
|
|
111
|
+
setDomainInput('')
|
|
112
|
+
setDomainError('')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handleRemoveDomain = (domain: string) => {
|
|
116
|
+
setState((prev) => ({ ...prev, domains: prev.domains.filter((d) => d !== domain) }))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handleSubmit = async () => {
|
|
120
|
+
setIsSubmitting(true)
|
|
121
|
+
try {
|
|
122
|
+
const payload = {
|
|
123
|
+
name: state.name,
|
|
124
|
+
protocol: state.protocol,
|
|
125
|
+
issuer: state.issuer,
|
|
126
|
+
clientId: state.clientId,
|
|
127
|
+
clientSecret: state.clientSecret,
|
|
128
|
+
allowedDomains: state.domains,
|
|
129
|
+
jitEnabled: state.jitEnabled,
|
|
130
|
+
autoLinkByEmail: state.autoLinkByEmail,
|
|
131
|
+
}
|
|
132
|
+
const call = await runMutationWithContext(
|
|
133
|
+
() => apiCallOrThrow<{ id: string }>(
|
|
134
|
+
'/api/sso/config',
|
|
135
|
+
{
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'content-type': 'application/json' },
|
|
138
|
+
body: JSON.stringify(payload),
|
|
139
|
+
},
|
|
140
|
+
{ errorMessage: t('sso.admin.error.createFailed', 'Failed to create SSO configuration') },
|
|
141
|
+
),
|
|
142
|
+
payload,
|
|
143
|
+
)
|
|
144
|
+
flash(t('sso.admin.created', 'SSO configuration created'), 'success')
|
|
145
|
+
router.push(`/backend/sso/config/${call.result?.id}?created=1`)
|
|
146
|
+
} catch {
|
|
147
|
+
// apiCallOrThrow handles the error
|
|
148
|
+
} finally {
|
|
149
|
+
setIsSubmitting(false)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handleTestConnection = async () => {
|
|
154
|
+
setIsTesting(true)
|
|
155
|
+
setTestResult(null)
|
|
156
|
+
try {
|
|
157
|
+
// Raw fetch is intentional: this is a pre-save OIDC discovery probe against an
|
|
158
|
+
// external IdP URL, not an internal API call, so apiCall is not applicable here.
|
|
159
|
+
const response = await fetch(state.issuer + '/.well-known/openid-configuration')
|
|
160
|
+
if (response.ok) {
|
|
161
|
+
setTestResult({ ok: true })
|
|
162
|
+
flash(t('sso.admin.test.success', 'Discovery successful — issuer is reachable'), 'success')
|
|
163
|
+
} else {
|
|
164
|
+
setTestResult({ ok: false, error: `HTTP ${response.status}` })
|
|
165
|
+
flash(t('sso.admin.test.failed', 'Discovery failed'), 'error')
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
setTestResult({ ok: false, error: String(err) })
|
|
169
|
+
flash(t('sso.admin.test.failed', 'Discovery failed — issuer is not reachable'), 'error')
|
|
170
|
+
} finally {
|
|
171
|
+
setIsTesting(false)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const canProceed = (): boolean => {
|
|
176
|
+
switch (step) {
|
|
177
|
+
case 'protocol': return true
|
|
178
|
+
case 'credentials': return !!(state.name && state.issuer && state.clientId && state.clientSecret)
|
|
179
|
+
case 'domains': return true
|
|
180
|
+
case 'options': return true
|
|
181
|
+
case 'review': return !isSubmitting
|
|
182
|
+
default: return false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Page>
|
|
188
|
+
<PageBody>
|
|
189
|
+
<div className="max-w-2xl mx-auto">
|
|
190
|
+
{/* Step indicator */}
|
|
191
|
+
<div className="flex items-center gap-2 mb-8">
|
|
192
|
+
{STEPS.map((s, i) => (
|
|
193
|
+
<React.Fragment key={s}>
|
|
194
|
+
<div
|
|
195
|
+
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
|
|
196
|
+
i <= currentStepIndex
|
|
197
|
+
? 'bg-primary text-primary-foreground'
|
|
198
|
+
: 'bg-muted text-muted-foreground'
|
|
199
|
+
}`}
|
|
200
|
+
>
|
|
201
|
+
{i + 1}
|
|
202
|
+
</div>
|
|
203
|
+
{i < STEPS.length - 1 && (
|
|
204
|
+
<div className={`flex-1 h-0.5 ${i < currentStepIndex ? 'bg-primary' : 'bg-muted'}`} />
|
|
205
|
+
)}
|
|
206
|
+
</React.Fragment>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Step content */}
|
|
211
|
+
{step === 'protocol' && (
|
|
212
|
+
<div>
|
|
213
|
+
<h2 className="text-lg font-semibold mb-4">{t('sso.admin.wizard.protocol.title', 'Select Protocol')}</h2>
|
|
214
|
+
<div className="space-y-3">
|
|
215
|
+
<label className="flex items-center gap-3 p-4 border rounded-lg cursor-pointer bg-accent/50 border-primary">
|
|
216
|
+
<input type="radio" name="protocol" value="oidc" checked readOnly className="accent-primary" />
|
|
217
|
+
<div>
|
|
218
|
+
<div className="font-medium">OpenID Connect (OIDC)</div>
|
|
219
|
+
<div className="text-sm text-muted-foreground">
|
|
220
|
+
{t('sso.admin.wizard.protocol.oidcDesc', 'Works with Zitadel, Microsoft Entra ID, Google Workspace, Okta, and more')}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</label>
|
|
224
|
+
<div className="flex items-center gap-3 p-4 border rounded-lg opacity-50 cursor-not-allowed bg-muted/30">
|
|
225
|
+
<input type="radio" name="protocol" value="saml" disabled className="accent-primary" />
|
|
226
|
+
<div>
|
|
227
|
+
<div className="font-medium">SAML 2.0</div>
|
|
228
|
+
<div className="text-sm text-muted-foreground">
|
|
229
|
+
{t('sso.admin.wizard.protocol.samlDesc', 'Coming soon')}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{step === 'credentials' && (
|
|
238
|
+
<div>
|
|
239
|
+
<h2 className="text-lg font-semibold mb-4">{t('sso.admin.wizard.credentials.title', 'OIDC Credentials')}</h2>
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
<div>
|
|
242
|
+
<label className="block text-sm font-medium mb-1">{t('sso.admin.field.name', 'Configuration Name')}</label>
|
|
243
|
+
<input
|
|
244
|
+
type="text"
|
|
245
|
+
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
246
|
+
placeholder={t('sso.admin.wizard.credentials.namePlaceholder', 'e.g., Zitadel Production')}
|
|
247
|
+
value={state.name}
|
|
248
|
+
onChange={(e) => setState((prev) => ({ ...prev, name: e.target.value }))}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
<div>
|
|
252
|
+
<label className="block text-sm font-medium mb-1">{t('sso.admin.field.issuer', 'Issuer URL')}</label>
|
|
253
|
+
<input
|
|
254
|
+
type="url"
|
|
255
|
+
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
256
|
+
placeholder="https://your-idp.example.com"
|
|
257
|
+
value={state.issuer}
|
|
258
|
+
onChange={(e) => setState((prev) => ({ ...prev, issuer: e.target.value }))}
|
|
259
|
+
/>
|
|
260
|
+
</div>
|
|
261
|
+
<div>
|
|
262
|
+
<label className="block text-sm font-medium mb-1">{t('sso.admin.field.clientId', 'Client ID')}</label>
|
|
263
|
+
<input
|
|
264
|
+
type="text"
|
|
265
|
+
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
266
|
+
value={state.clientId}
|
|
267
|
+
onChange={(e) => setState((prev) => ({ ...prev, clientId: e.target.value }))}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<label className="block text-sm font-medium mb-1">{t('sso.admin.field.clientSecret', 'Client Secret')}</label>
|
|
272
|
+
<input
|
|
273
|
+
type="password"
|
|
274
|
+
className="w-full rounded-md border px-3 py-2 text-sm"
|
|
275
|
+
value={state.clientSecret}
|
|
276
|
+
onChange={(e) => setState((prev) => ({ ...prev, clientSecret: e.target.value }))}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="rounded-md bg-muted/50 p-3">
|
|
280
|
+
<label className="block text-sm font-medium mb-1">{t('sso.admin.wizard.credentials.callbackUrl', 'Redirect URI (copy to your IdP)')}</label>
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<code className="flex-1 text-sm bg-background p-2 rounded border font-mono break-all">{callbackUrl}</code>
|
|
283
|
+
<Button
|
|
284
|
+
variant="outline"
|
|
285
|
+
size="sm"
|
|
286
|
+
onClick={() => {
|
|
287
|
+
navigator.clipboard.writeText(callbackUrl)
|
|
288
|
+
flash(t('common.copied', 'Copied to clipboard'), 'success')
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
{t('common.copy', 'Copy')}
|
|
292
|
+
</Button>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{step === 'domains' && (
|
|
300
|
+
<div>
|
|
301
|
+
<h2 className="text-lg font-semibold mb-4">{t('sso.admin.wizard.domains.title', 'Allowed Email Domains')}</h2>
|
|
302
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
303
|
+
{t('sso.admin.wizard.domains.description', 'Users with email addresses matching these domains will be redirected to your SSO provider.')}
|
|
304
|
+
</p>
|
|
305
|
+
<div className="flex items-center gap-2 mb-4">
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
className="flex-1 rounded-md border px-3 py-2 text-sm"
|
|
309
|
+
placeholder={t('sso.admin.wizard.domains.placeholder', 'example.com')}
|
|
310
|
+
value={domainInput}
|
|
311
|
+
onChange={(e) => { setDomainInput(e.target.value); setDomainError('') }}
|
|
312
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddDomain() } }}
|
|
313
|
+
/>
|
|
314
|
+
<Button variant="outline" onClick={handleAddDomain}>
|
|
315
|
+
{t('common.add', 'Add')}
|
|
316
|
+
</Button>
|
|
317
|
+
</div>
|
|
318
|
+
{domainError && <p className="text-sm text-destructive mb-2">{domainError}</p>}
|
|
319
|
+
{state.domains.length > 0 && (
|
|
320
|
+
<div className="space-y-2">
|
|
321
|
+
{state.domains.map((domain) => (
|
|
322
|
+
<div key={domain} className="flex items-center justify-between p-2 border rounded-md">
|
|
323
|
+
<code className="text-sm font-mono">{domain}</code>
|
|
324
|
+
<Button variant="ghost" size="sm" onClick={() => handleRemoveDomain(domain)}>
|
|
325
|
+
{t('common.remove', 'Remove')}
|
|
326
|
+
</Button>
|
|
327
|
+
</div>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{step === 'options' && (
|
|
335
|
+
<div>
|
|
336
|
+
<h2 className="text-lg font-semibold mb-4">{t('sso.admin.wizard.options.title', 'Options')}</h2>
|
|
337
|
+
<div className="space-y-4">
|
|
338
|
+
<label className="flex items-center gap-3">
|
|
339
|
+
<input
|
|
340
|
+
type="checkbox"
|
|
341
|
+
checked={state.jitEnabled}
|
|
342
|
+
onChange={(e) => setState((prev) => ({ ...prev, jitEnabled: e.target.checked }))}
|
|
343
|
+
className="accent-primary"
|
|
344
|
+
/>
|
|
345
|
+
<div>
|
|
346
|
+
<div className="font-medium text-sm">{t('sso.admin.field.jitEnabled', 'Just-in-Time Provisioning')}</div>
|
|
347
|
+
<div className="text-xs text-muted-foreground">
|
|
348
|
+
{t('sso.admin.field.jitEnabledDesc', 'Automatically create user accounts on first SSO login')}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</label>
|
|
352
|
+
<label className="flex items-center gap-3">
|
|
353
|
+
<input
|
|
354
|
+
type="checkbox"
|
|
355
|
+
checked={state.autoLinkByEmail}
|
|
356
|
+
onChange={(e) => setState((prev) => ({ ...prev, autoLinkByEmail: e.target.checked }))}
|
|
357
|
+
className="accent-primary"
|
|
358
|
+
/>
|
|
359
|
+
<div>
|
|
360
|
+
<div className="font-medium text-sm">{t('sso.admin.field.autoLinkByEmail', 'Auto-link by Email')}</div>
|
|
361
|
+
<div className="text-xs text-muted-foreground">
|
|
362
|
+
{t('sso.admin.field.autoLinkByEmailDesc', 'Automatically link existing users by matching email address')}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</label>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{step === 'review' && (
|
|
371
|
+
<div>
|
|
372
|
+
<h2 className="text-lg font-semibold mb-4">{t('sso.admin.wizard.review.title', 'Review & Save')}</h2>
|
|
373
|
+
<div className="space-y-4">
|
|
374
|
+
<div className="border rounded-lg divide-y">
|
|
375
|
+
<div className="flex justify-between p-3">
|
|
376
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.name', 'Name')}</span>
|
|
377
|
+
<span className="text-sm font-medium">{state.name}</span>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="flex justify-between p-3">
|
|
380
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.protocol', 'Protocol')}</span>
|
|
381
|
+
<span className="text-sm font-medium">{state.protocol.toUpperCase()}</span>
|
|
382
|
+
</div>
|
|
383
|
+
<div className="flex justify-between p-3">
|
|
384
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.issuer', 'Issuer')}</span>
|
|
385
|
+
<span className="text-sm font-medium break-all">{state.issuer}</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="flex justify-between p-3">
|
|
388
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.clientId', 'Client ID')}</span>
|
|
389
|
+
<span className="text-sm font-medium">{state.clientId}</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="flex justify-between p-3">
|
|
392
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.wizard.domains.title', 'Domains')}</span>
|
|
393
|
+
<span className="text-sm font-medium">{state.domains.join(', ') || '—'}</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div className="flex justify-between p-3">
|
|
396
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.jitEnabled', 'JIT Provisioning')}</span>
|
|
397
|
+
<span className="text-sm font-medium">{state.jitEnabled ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')}</span>
|
|
398
|
+
</div>
|
|
399
|
+
<div className="flex justify-between p-3">
|
|
400
|
+
<span className="text-sm text-muted-foreground">{t('sso.admin.field.autoLinkByEmail', 'Auto-link')}</span>
|
|
401
|
+
<span className="text-sm font-medium">{state.autoLinkByEmail ? t('common.enabled', 'Enabled') : t('common.disabled', 'Disabled')}</span>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{/* Test connection before saving */}
|
|
406
|
+
<div className="flex items-center gap-3 p-4 border rounded-lg bg-muted/30">
|
|
407
|
+
<Button
|
|
408
|
+
variant="outline"
|
|
409
|
+
onClick={handleTestConnection}
|
|
410
|
+
disabled={isTesting}
|
|
411
|
+
>
|
|
412
|
+
{isTesting
|
|
413
|
+
? t('sso.admin.wizard.review.testing', 'Testing...')
|
|
414
|
+
: t('sso.admin.action.test', 'Verify Discovery')}
|
|
415
|
+
</Button>
|
|
416
|
+
{testResult && (
|
|
417
|
+
<span className={`text-sm ${testResult.ok ? 'text-green-600' : 'text-destructive'}`}>
|
|
418
|
+
{testResult.ok
|
|
419
|
+
? t('sso.admin.test.success', 'Discovery successful')
|
|
420
|
+
: testResult.error || t('sso.admin.test.failed', 'Discovery failed')}
|
|
421
|
+
</span>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<p className="text-sm text-muted-foreground">
|
|
426
|
+
{t('sso.admin.wizard.review.note', 'The configuration will be created as inactive. You can activate it from the detail page after verifying everything is correct.')}
|
|
427
|
+
</p>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
{/* Navigation */}
|
|
433
|
+
<div className="flex justify-between mt-8 pt-4 border-t">
|
|
434
|
+
<div>
|
|
435
|
+
{currentStepIndex > 0 ? (
|
|
436
|
+
<Button variant="outline" onClick={goBack}>
|
|
437
|
+
{t('common.back', 'Back')}
|
|
438
|
+
</Button>
|
|
439
|
+
) : (
|
|
440
|
+
<Button variant="outline" onClick={() => router.push('/backend/sso')}>
|
|
441
|
+
{t('common.cancel', 'Cancel')}
|
|
442
|
+
</Button>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
<div>
|
|
446
|
+
{step === 'review' ? (
|
|
447
|
+
<Button onClick={handleSubmit} disabled={!canProceed()}>
|
|
448
|
+
{isSubmitting
|
|
449
|
+
? t('common.saving', 'Saving...')
|
|
450
|
+
: t('sso.admin.wizard.review.save', 'Create Configuration')}
|
|
451
|
+
</Button>
|
|
452
|
+
) : (
|
|
453
|
+
<Button onClick={goNext} disabled={!canProceed()}>
|
|
454
|
+
{t('common.next', 'Next')}
|
|
455
|
+
</Button>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</PageBody>
|
|
461
|
+
</Page>
|
|
462
|
+
)
|
|
463
|
+
}
|