@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,94 @@
|
|
|
1
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
2
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { resolveScimContext } from '../../context'
|
|
4
|
+
import { ScimService } from '../../../../services/scimService'
|
|
5
|
+
import { handleScimApiError } from '../../../error-handler'
|
|
6
|
+
import { scimUserPayloadSchema } from '../../../../data/validators'
|
|
7
|
+
import { buildScimError, scimJson as scimJsonResponse } from '../../../../lib/scim-response'
|
|
8
|
+
|
|
9
|
+
export const metadata = {}
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
try {
|
|
13
|
+
const ctx = await resolveScimContext(req)
|
|
14
|
+
if (!ctx.ok) return ctx.response
|
|
15
|
+
|
|
16
|
+
const body = await req.json()
|
|
17
|
+
const parsed = scimUserPayloadSchema.safeParse(body)
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
return scimJsonResponse(
|
|
20
|
+
buildScimError(400, parsed.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join('; '), 'invalidValue'),
|
|
21
|
+
400,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const baseUrl = new URL(req.url).origin
|
|
26
|
+
|
|
27
|
+
const container = await createRequestContainer()
|
|
28
|
+
const service = container.resolve<ScimService>('scimService')
|
|
29
|
+
const { resource, status } = await service.createUser(parsed.data, ctx.scope, baseUrl)
|
|
30
|
+
|
|
31
|
+
const headers: Record<string, string> = {}
|
|
32
|
+
if (status === 201) {
|
|
33
|
+
headers.Location = resource.meta.location
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return scimJsonResponse(resource, status)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return handleScimApiError(err, 'SCIM Users API')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function GET(req: Request) {
|
|
43
|
+
try {
|
|
44
|
+
const ctx = await resolveScimContext(req)
|
|
45
|
+
if (!ctx.ok) return ctx.response
|
|
46
|
+
|
|
47
|
+
const url = new URL(req.url)
|
|
48
|
+
const filter = url.searchParams.get('filter')
|
|
49
|
+
const startIndex = Math.max(1, parseInt(url.searchParams.get('startIndex') ?? '1', 10) || 1)
|
|
50
|
+
const count = Math.min(200, Math.max(1, parseInt(url.searchParams.get('count') ?? '100', 10) || 100))
|
|
51
|
+
const baseUrl = url.origin
|
|
52
|
+
|
|
53
|
+
const container = await createRequestContainer()
|
|
54
|
+
const service = container.resolve<ScimService>('scimService')
|
|
55
|
+
const result = await service.listUsers(filter, startIndex, count, ctx.scope, baseUrl)
|
|
56
|
+
|
|
57
|
+
return scimJsonResponse(result)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return handleScimApiError(err, 'SCIM Users API')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export const openApi: OpenApiRouteDoc = {
|
|
65
|
+
tag: 'SCIM',
|
|
66
|
+
summary: 'SCIM Users',
|
|
67
|
+
methods: {
|
|
68
|
+
POST: {
|
|
69
|
+
summary: 'Create SCIM user',
|
|
70
|
+
description: 'Provisions a new user via SCIM 2.0. Supports idempotency via externalId.',
|
|
71
|
+
tags: ['SSO', 'SCIM'],
|
|
72
|
+
responses: [
|
|
73
|
+
{ status: 201, description: 'User created' },
|
|
74
|
+
{ status: 200, description: 'User already exists (idempotent)' },
|
|
75
|
+
],
|
|
76
|
+
errors: [
|
|
77
|
+
{ status: 400, description: 'Invalid payload' },
|
|
78
|
+
{ status: 401, description: 'Unauthorized' },
|
|
79
|
+
{ status: 403, description: 'SSO config inactive' },
|
|
80
|
+
{ status: 409, description: 'Conflict — user already linked' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
GET: {
|
|
84
|
+
summary: 'List SCIM users',
|
|
85
|
+
description: 'Lists provisioned users with optional SCIM filter (eq operator).',
|
|
86
|
+
tags: ['SSO', 'SCIM'],
|
|
87
|
+
responses: [{ status: 200, description: 'SCIM ListResponse' }],
|
|
88
|
+
errors: [
|
|
89
|
+
{ status: 401, description: 'Unauthorized' },
|
|
90
|
+
{ status: 403, description: 'SSO config inactive' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const ssoIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{
|
|
6
|
+
width: 16,
|
|
7
|
+
height: 16,
|
|
8
|
+
viewBox: '0 0 24 24',
|
|
9
|
+
fill: 'none',
|
|
10
|
+
stroke: 'currentColor',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
strokeLinecap: 'round',
|
|
13
|
+
strokeLinejoin: 'round',
|
|
14
|
+
},
|
|
15
|
+
React.createElement('path', { d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export const metadata = {
|
|
19
|
+
requireAuth: true,
|
|
20
|
+
requireFeatures: ['sso.config.view'],
|
|
21
|
+
pageTitle: 'Single Sign-On',
|
|
22
|
+
pageTitleKey: 'sso.admin.title',
|
|
23
|
+
pageGroup: 'Auth',
|
|
24
|
+
pageGroupKey: 'settings.sections.auth',
|
|
25
|
+
pageOrder: 520,
|
|
26
|
+
icon: ssoIcon,
|
|
27
|
+
pageContext: 'settings' as const,
|
|
28
|
+
breadcrumb: [{ label: 'Single Sign-On', labelKey: 'sso.admin.title' }],
|
|
29
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
6
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
|
+
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
8
|
+
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
13
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
|
+
import Link from 'next/link'
|
|
15
|
+
|
|
16
|
+
interface SsoConfigRow {
|
|
17
|
+
id: string
|
|
18
|
+
name: string | null
|
|
19
|
+
protocol: string
|
|
20
|
+
issuer: string | null
|
|
21
|
+
allowedDomains: string[]
|
|
22
|
+
isActive: boolean
|
|
23
|
+
hasClientSecret: boolean
|
|
24
|
+
organizationId: string
|
|
25
|
+
tenantId: string | null
|
|
26
|
+
createdAt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ListResponse {
|
|
30
|
+
items: SsoConfigRow[]
|
|
31
|
+
total: number
|
|
32
|
+
totalPages: number
|
|
33
|
+
isSuperAdmin?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fallback: ListResponse = { items: [], total: 0, totalPages: 1 }
|
|
37
|
+
|
|
38
|
+
export default function SsoConfigListPage() {
|
|
39
|
+
const router = useRouter()
|
|
40
|
+
const t = useT()
|
|
41
|
+
|
|
42
|
+
const [data, setData] = React.useState<ListResponse>(fallback)
|
|
43
|
+
const [isLoading, setIsLoading] = React.useState(true)
|
|
44
|
+
const [page, setPage] = React.useState(1)
|
|
45
|
+
const [search, setSearch] = React.useState('')
|
|
46
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
47
|
+
|
|
48
|
+
const isSuperAdmin = !!data.isSuperAdmin
|
|
49
|
+
|
|
50
|
+
const fetchData = React.useCallback(async () => {
|
|
51
|
+
setIsLoading(true)
|
|
52
|
+
const params = new URLSearchParams()
|
|
53
|
+
params.set('page', String(page))
|
|
54
|
+
params.set('pageSize', '50')
|
|
55
|
+
if (search) params.set('search', search)
|
|
56
|
+
|
|
57
|
+
const call = await apiCall<ListResponse>(`/api/sso/config?${params}`, undefined, { fallback })
|
|
58
|
+
if (call.ok && call.result) {
|
|
59
|
+
setData(call.result)
|
|
60
|
+
}
|
|
61
|
+
setIsLoading(false)
|
|
62
|
+
}, [page, search])
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => { fetchData() }, [fetchData])
|
|
65
|
+
|
|
66
|
+
const handleDelete = async (row: SsoConfigRow) => {
|
|
67
|
+
if (row.isActive) {
|
|
68
|
+
flash(t('sso.admin.error.deleteActive', 'Cannot delete an active SSO configuration — deactivate it first'), 'error')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const confirmed = await confirm({
|
|
73
|
+
title: t('sso.admin.delete.title', 'Delete SSO Configuration'),
|
|
74
|
+
text: t('sso.admin.delete.confirm', 'Are you sure? This will remove the SSO configuration. Users with linked SSO identities will need to use password login.'),
|
|
75
|
+
confirmText: t('common.delete', 'Delete'),
|
|
76
|
+
variant: 'destructive',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (!confirmed) return
|
|
80
|
+
|
|
81
|
+
await apiCallOrThrow(`/api/sso/config/${row.id}`, { method: 'DELETE' }, {
|
|
82
|
+
errorMessage: t('sso.admin.error.deleteFailed', 'Failed to delete SSO configuration'),
|
|
83
|
+
})
|
|
84
|
+
flash(t('sso.admin.delete.success', 'SSO configuration deleted'), 'success')
|
|
85
|
+
fetchData()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleToggleActivation = async (row: SsoConfigRow) => {
|
|
89
|
+
try {
|
|
90
|
+
await apiCallOrThrow(
|
|
91
|
+
`/api/sso/config/${row.id}/activate`,
|
|
92
|
+
{
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ active: !row.isActive }),
|
|
96
|
+
},
|
|
97
|
+
{ errorMessage: t('sso.admin.error.activationFailed', 'Failed to update activation status') },
|
|
98
|
+
)
|
|
99
|
+
flash(
|
|
100
|
+
row.isActive
|
|
101
|
+
? t('sso.admin.deactivated', 'SSO configuration deactivated')
|
|
102
|
+
: t('sso.admin.activated', 'SSO configuration activated'),
|
|
103
|
+
'success',
|
|
104
|
+
)
|
|
105
|
+
fetchData()
|
|
106
|
+
} catch {
|
|
107
|
+
// apiCallOrThrow already flashes the error
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleTestConnection = async (row: SsoConfigRow) => {
|
|
112
|
+
try {
|
|
113
|
+
const call = await apiCallOrThrow<{ ok: boolean; error?: string }>(
|
|
114
|
+
`/api/sso/config/${row.id}/test`,
|
|
115
|
+
{ method: 'POST' },
|
|
116
|
+
{ errorMessage: t('sso.admin.error.testFailed', 'Connection test failed') },
|
|
117
|
+
)
|
|
118
|
+
if (call.result?.ok) {
|
|
119
|
+
flash(t('sso.admin.test.success', 'Discovery successful — issuer is reachable'), 'success')
|
|
120
|
+
} else {
|
|
121
|
+
flash(call.result?.error || t('sso.admin.test.failed', 'Discovery failed'), 'error')
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// apiCallOrThrow already flashes the error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const columns = React.useMemo<ColumnDef<SsoConfigRow>[]>(() => {
|
|
129
|
+
const cols: ColumnDef<SsoConfigRow>[] = [
|
|
130
|
+
{
|
|
131
|
+
accessorKey: 'name',
|
|
132
|
+
header: t('sso.admin.column.name', 'Name'),
|
|
133
|
+
cell: ({ row }) => row.original.name || row.original.issuer || '—',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
accessorKey: 'protocol',
|
|
137
|
+
header: t('sso.admin.column.protocol', 'Protocol'),
|
|
138
|
+
cell: ({ row }) => row.original.protocol.toUpperCase(),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
accessorKey: 'allowedDomains',
|
|
142
|
+
header: t('sso.admin.column.domains', 'Domains'),
|
|
143
|
+
cell: ({ row }) => row.original.allowedDomains.join(', ') || '—',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
accessorKey: 'isActive',
|
|
147
|
+
header: t('sso.admin.column.status', 'Status'),
|
|
148
|
+
cell: ({ row }) => (
|
|
149
|
+
<span className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${row.original.isActive ? 'bg-green-50 text-green-700' : 'bg-gray-100 text-gray-600'}`}>
|
|
150
|
+
{row.original.isActive
|
|
151
|
+
? t('sso.admin.status.active', 'Active')
|
|
152
|
+
: t('sso.admin.status.inactive', 'Inactive')}
|
|
153
|
+
</span>
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
accessorKey: 'createdAt',
|
|
158
|
+
header: t('sso.admin.column.created', 'Created'),
|
|
159
|
+
cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
return cols
|
|
164
|
+
}, [t, isSuperAdmin])
|
|
165
|
+
|
|
166
|
+
const hasConfigs = data.items.length > 0 || search
|
|
167
|
+
const canCreateNew = isSuperAdmin || data.items.length === 0
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Page>
|
|
171
|
+
<PageBody>
|
|
172
|
+
{!hasConfigs && !isLoading ? (
|
|
173
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
174
|
+
<h3 className="text-lg font-semibold mb-2">
|
|
175
|
+
{t('sso.admin.empty.title', 'No SSO configured')}
|
|
176
|
+
</h3>
|
|
177
|
+
<p className="text-muted-foreground mb-4 max-w-md">
|
|
178
|
+
{t('sso.admin.empty.description', 'Configure Single Sign-On to let your users authenticate with your identity provider.')}
|
|
179
|
+
</p>
|
|
180
|
+
<Button asChild>
|
|
181
|
+
<Link href="/backend/sso/config/new">
|
|
182
|
+
{t('sso.admin.empty.cta', 'Configure SSO')}
|
|
183
|
+
</Link>
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
) : (
|
|
187
|
+
<DataTable<SsoConfigRow>
|
|
188
|
+
title={t('sso.admin.title', 'Single Sign-On')}
|
|
189
|
+
actions={canCreateNew ? (
|
|
190
|
+
<Button asChild size="sm">
|
|
191
|
+
<Link href="/backend/sso/config/new">
|
|
192
|
+
{t('sso.admin.new', 'New SSO Config')}
|
|
193
|
+
</Link>
|
|
194
|
+
</Button>
|
|
195
|
+
) : undefined}
|
|
196
|
+
columns={columns}
|
|
197
|
+
data={data.items}
|
|
198
|
+
searchValue={search}
|
|
199
|
+
onSearchChange={(value) => { setSearch(value); setPage(1) }}
|
|
200
|
+
searchPlaceholder={t('sso.admin.search', 'Search by name or issuer...')}
|
|
201
|
+
onRowClick={(row) => router.push(`/backend/sso/config/${row.id}`)}
|
|
202
|
+
rowActions={(row) => (
|
|
203
|
+
<RowActions
|
|
204
|
+
items={[
|
|
205
|
+
{ id: 'edit', label: t('common.edit', 'Edit'), onSelect: () => router.push(`/backend/sso/config/${row.id}`) },
|
|
206
|
+
{ id: 'test', label: t('sso.admin.action.test', 'Verify Discovery'), onSelect: () => handleTestConnection(row) },
|
|
207
|
+
{
|
|
208
|
+
id: 'toggle',
|
|
209
|
+
label: row.isActive
|
|
210
|
+
? t('sso.admin.action.deactivate', 'Deactivate')
|
|
211
|
+
: t('sso.admin.action.activate', 'Activate'),
|
|
212
|
+
onSelect: () => handleToggleActivation(row),
|
|
213
|
+
},
|
|
214
|
+
{ id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },
|
|
215
|
+
]}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
pagination={{
|
|
219
|
+
page,
|
|
220
|
+
pageSize: 50,
|
|
221
|
+
total: data.total,
|
|
222
|
+
totalPages: data.totalPages,
|
|
223
|
+
onPageChange: setPage,
|
|
224
|
+
}}
|
|
225
|
+
isLoading={isLoading}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
{ConfirmDialogElement}
|
|
229
|
+
</PageBody>
|
|
230
|
+
</Page>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const metadata = {
|
|
2
|
+
requireAuth: true,
|
|
3
|
+
requireFeatures: ['sso.config.view'],
|
|
4
|
+
pageTitle: 'SSO Configuration',
|
|
5
|
+
pageTitleKey: 'sso.admin.detail.title',
|
|
6
|
+
pageGroup: 'Auth',
|
|
7
|
+
pageGroupKey: 'settings.sections.auth',
|
|
8
|
+
pageOrder: 522,
|
|
9
|
+
pageContext: 'settings' as const,
|
|
10
|
+
navHidden: true,
|
|
11
|
+
breadcrumb: [
|
|
12
|
+
{ label: 'Single Sign-On', labelKey: 'sso.admin.title', href: '/backend/sso' },
|
|
13
|
+
{ label: 'Configuration', labelKey: 'sso.admin.detail.title' },
|
|
14
|
+
],
|
|
15
|
+
}
|