@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.
Files changed (195) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +2 -2
  3. package/dist/modules/sso/acl.js +11 -0
  4. package/dist/modules/sso/acl.js.map +7 -0
  5. package/dist/modules/sso/api/admin-context.js +27 -0
  6. package/dist/modules/sso/api/admin-context.js.map +7 -0
  7. package/dist/modules/sso/api/callback/oidc/route.js +103 -0
  8. package/dist/modules/sso/api/callback/oidc/route.js.map +7 -0
  9. package/dist/modules/sso/api/config/[id]/activate/route.js +49 -0
  10. package/dist/modules/sso/api/config/[id]/activate/route.js.map +7 -0
  11. package/dist/modules/sso/api/config/[id]/domains/route.js +96 -0
  12. package/dist/modules/sso/api/config/[id]/domains/route.js.map +7 -0
  13. package/dist/modules/sso/api/config/[id]/route.js +103 -0
  14. package/dist/modules/sso/api/config/[id]/route.js.map +7 -0
  15. package/dist/modules/sso/api/config/[id]/test/route.js +41 -0
  16. package/dist/modules/sso/api/config/[id]/test/route.js.map +7 -0
  17. package/dist/modules/sso/api/config/route.js +83 -0
  18. package/dist/modules/sso/api/config/route.js.map +7 -0
  19. package/dist/modules/sso/api/error-handler.js +28 -0
  20. package/dist/modules/sso/api/error-handler.js.map +7 -0
  21. package/dist/modules/sso/api/hrd/route.js +52 -0
  22. package/dist/modules/sso/api/hrd/route.js.map +7 -0
  23. package/dist/modules/sso/api/initiate/route.js +66 -0
  24. package/dist/modules/sso/api/initiate/route.js.map +7 -0
  25. package/dist/modules/sso/api/scim/context.js +68 -0
  26. package/dist/modules/sso/api/scim/context.js.map +7 -0
  27. package/dist/modules/sso/api/scim/logs/route.js +65 -0
  28. package/dist/modules/sso/api/scim/logs/route.js.map +7 -0
  29. package/dist/modules/sso/api/scim/tokens/[id]/route.js +42 -0
  30. package/dist/modules/sso/api/scim/tokens/[id]/route.js.map +7 -0
  31. package/dist/modules/sso/api/scim/tokens/route.js +83 -0
  32. package/dist/modules/sso/api/scim/tokens/route.js.map +7 -0
  33. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js +42 -0
  34. package/dist/modules/sso/api/scim/v2/ServiceProviderConfig/route.js.map +7 -0
  35. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js +94 -0
  36. package/dist/modules/sso/api/scim/v2/Users/[id]/route.js.map +7 -0
  37. package/dist/modules/sso/api/scim/v2/Users/route.js +86 -0
  38. package/dist/modules/sso/api/scim/v2/Users/route.js.map +7 -0
  39. package/dist/modules/sso/backend/page.js +173 -0
  40. package/dist/modules/sso/backend/page.js.map +7 -0
  41. package/dist/modules/sso/backend/page.meta.js +31 -0
  42. package/dist/modules/sso/backend/page.meta.js.map +7 -0
  43. package/dist/modules/sso/backend/sso/config/[id]/page.js +749 -0
  44. package/dist/modules/sso/backend/sso/config/[id]/page.js.map +7 -0
  45. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js +19 -0
  46. package/dist/modules/sso/backend/sso/config/[id]/page.meta.js.map +7 -0
  47. package/dist/modules/sso/backend/sso/config/new/page.js +381 -0
  48. package/dist/modules/sso/backend/sso/config/new/page.js.map +7 -0
  49. package/dist/modules/sso/backend/sso/config/new/page.meta.js +19 -0
  50. package/dist/modules/sso/backend/sso/config/new/page.meta.js.map +7 -0
  51. package/dist/modules/sso/data/entities.js +299 -0
  52. package/dist/modules/sso/data/entities.js.map +7 -0
  53. package/dist/modules/sso/data/validators.js +114 -0
  54. package/dist/modules/sso/data/validators.js.map +7 -0
  55. package/dist/modules/sso/di.js +26 -0
  56. package/dist/modules/sso/di.js.map +7 -0
  57. package/dist/modules/sso/events.js +24 -0
  58. package/dist/modules/sso/events.js.map +7 -0
  59. package/dist/modules/sso/i18n/de.json +146 -0
  60. package/dist/modules/sso/i18n/en.json +146 -0
  61. package/dist/modules/sso/i18n/es.json +146 -0
  62. package/dist/modules/sso/i18n/pl.json +146 -0
  63. package/dist/modules/sso/index.js +11 -0
  64. package/dist/modules/sso/index.js.map +7 -0
  65. package/dist/modules/sso/lib/domains.js +30 -0
  66. package/dist/modules/sso/lib/domains.js.map +7 -0
  67. package/dist/modules/sso/lib/oidc-provider.js +140 -0
  68. package/dist/modules/sso/lib/oidc-provider.js.map +7 -0
  69. package/dist/modules/sso/lib/registry.js +15 -0
  70. package/dist/modules/sso/lib/registry.js.map +7 -0
  71. package/dist/modules/sso/lib/scim-filter.js +43 -0
  72. package/dist/modules/sso/lib/scim-filter.js.map +7 -0
  73. package/dist/modules/sso/lib/scim-mapper.js +49 -0
  74. package/dist/modules/sso/lib/scim-mapper.js.map +7 -0
  75. package/dist/modules/sso/lib/scim-patch.js +63 -0
  76. package/dist/modules/sso/lib/scim-patch.js.map +7 -0
  77. package/dist/modules/sso/lib/scim-response.js +34 -0
  78. package/dist/modules/sso/lib/scim-response.js.map +7 -0
  79. package/dist/modules/sso/lib/scim-utils.js +9 -0
  80. package/dist/modules/sso/lib/scim-utils.js.map +7 -0
  81. package/dist/modules/sso/lib/state-cookie.js +67 -0
  82. package/dist/modules/sso/lib/state-cookie.js.map +7 -0
  83. package/dist/modules/sso/lib/types.js +1 -0
  84. package/dist/modules/sso/lib/types.js.map +7 -0
  85. package/dist/modules/sso/migrations/Migration20260219000000_sso.js +20 -0
  86. package/dist/modules/sso/migrations/Migration20260219000000_sso.js.map +7 -0
  87. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js +13 -0
  88. package/dist/modules/sso/migrations/Migration20260222000000_sso_add_name.js.map +7 -0
  89. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js +15 -0
  90. package/dist/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.js.map +7 -0
  91. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js +22 -0
  92. package/dist/modules/sso/migrations/Migration20260223000000_scim_tables.js.map +7 -0
  93. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js +15 -0
  94. package/dist/modules/sso/migrations/Migration20260224000000_sso_external_id.js.map +7 -0
  95. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js +17 -0
  96. package/dist/modules/sso/migrations/Migration20260224100000_sso_role_grants.js.map +7 -0
  97. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js +13 -0
  98. package/dist/modules/sso/migrations/Migration20260224200000_drop_default_role_id.js.map +7 -0
  99. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js +23 -0
  100. package/dist/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.js.map +7 -0
  101. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js +14 -0
  102. package/dist/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.js.map +7 -0
  103. package/dist/modules/sso/services/accountLinkingService.js +298 -0
  104. package/dist/modules/sso/services/accountLinkingService.js.map +7 -0
  105. package/dist/modules/sso/services/hrdService.js +18 -0
  106. package/dist/modules/sso/services/hrdService.js.map +7 -0
  107. package/dist/modules/sso/services/scimService.js +372 -0
  108. package/dist/modules/sso/services/scimService.js.map +7 -0
  109. package/dist/modules/sso/services/scimTokenService.js +94 -0
  110. package/dist/modules/sso/services/scimTokenService.js.map +7 -0
  111. package/dist/modules/sso/services/ssoConfigService.js +254 -0
  112. package/dist/modules/sso/services/ssoConfigService.js.map +7 -0
  113. package/dist/modules/sso/services/ssoService.js +125 -0
  114. package/dist/modules/sso/services/ssoService.js.map +7 -0
  115. package/dist/modules/sso/setup.js +47 -0
  116. package/dist/modules/sso/setup.js.map +7 -0
  117. package/dist/modules/sso/subscribers/user-deleted-cleanup.js +21 -0
  118. package/dist/modules/sso/subscribers/user-deleted-cleanup.js.map +7 -0
  119. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js +106 -0
  120. package/dist/modules/sso/widgets/injection/login-sso/widget.client.js.map +7 -0
  121. package/dist/modules/sso/widgets/injection/login-sso/widget.js +16 -0
  122. package/dist/modules/sso/widgets/injection/login-sso/widget.js.map +7 -0
  123. package/dist/modules/sso/widgets/injection-table.js +14 -0
  124. package/dist/modules/sso/widgets/injection-table.js.map +7 -0
  125. package/package.json +5 -4
  126. package/src/index.ts +1 -1
  127. package/src/modules/sso/acl.ts +7 -0
  128. package/src/modules/sso/api/admin-context.ts +36 -0
  129. package/src/modules/sso/api/callback/oidc/route.ts +115 -0
  130. package/src/modules/sso/api/config/[id]/activate/route.ts +53 -0
  131. package/src/modules/sso/api/config/[id]/domains/route.ts +107 -0
  132. package/src/modules/sso/api/config/[id]/route.ts +114 -0
  133. package/src/modules/sso/api/config/[id]/test/route.ts +44 -0
  134. package/src/modules/sso/api/config/route.ts +88 -0
  135. package/src/modules/sso/api/error-handler.ts +36 -0
  136. package/src/modules/sso/api/hrd/route.ts +55 -0
  137. package/src/modules/sso/api/initiate/route.ts +70 -0
  138. package/src/modules/sso/api/scim/context.ts +85 -0
  139. package/src/modules/sso/api/scim/logs/route.ts +69 -0
  140. package/src/modules/sso/api/scim/tokens/[id]/route.ts +45 -0
  141. package/src/modules/sso/api/scim/tokens/route.ts +89 -0
  142. package/src/modules/sso/api/scim/v2/ServiceProviderConfig/route.ts +40 -0
  143. package/src/modules/sso/api/scim/v2/Users/[id]/route.ts +103 -0
  144. package/src/modules/sso/api/scim/v2/Users/route.ts +94 -0
  145. package/src/modules/sso/backend/page.meta.ts +29 -0
  146. package/src/modules/sso/backend/page.tsx +232 -0
  147. package/src/modules/sso/backend/sso/config/[id]/page.meta.ts +15 -0
  148. package/src/modules/sso/backend/sso/config/[id]/page.tsx +1024 -0
  149. package/src/modules/sso/backend/sso/config/new/page.meta.ts +15 -0
  150. package/src/modules/sso/backend/sso/config/new/page.tsx +463 -0
  151. package/src/modules/sso/data/entities.ts +240 -0
  152. package/src/modules/sso/data/validators.ts +140 -0
  153. package/src/modules/sso/di.ts +25 -0
  154. package/src/modules/sso/docs/entra-id-setup.md +281 -0
  155. package/src/modules/sso/docs/google-workspace-setup.md +174 -0
  156. package/src/modules/sso/docs/sso-overview.md +218 -0
  157. package/src/modules/sso/docs/sso-security-audit-2026-02-27.md +118 -0
  158. package/src/modules/sso/docs/zitadel-setup.md +195 -0
  159. package/src/modules/sso/events.ts +21 -0
  160. package/src/modules/sso/i18n/de.json +146 -0
  161. package/src/modules/sso/i18n/en.json +146 -0
  162. package/src/modules/sso/i18n/es.json +146 -0
  163. package/src/modules/sso/i18n/pl.json +146 -0
  164. package/src/modules/sso/index.ts +7 -0
  165. package/src/modules/sso/lib/domains.ts +31 -0
  166. package/src/modules/sso/lib/oidc-provider.ts +196 -0
  167. package/src/modules/sso/lib/registry.ts +13 -0
  168. package/src/modules/sso/lib/scim-filter.ts +62 -0
  169. package/src/modules/sso/lib/scim-mapper.ts +88 -0
  170. package/src/modules/sso/lib/scim-patch.ts +88 -0
  171. package/src/modules/sso/lib/scim-response.ts +40 -0
  172. package/src/modules/sso/lib/scim-utils.ts +5 -0
  173. package/src/modules/sso/lib/state-cookie.ts +79 -0
  174. package/src/modules/sso/lib/types.ts +50 -0
  175. package/src/modules/sso/migrations/.snapshot-open-mercato.json +912 -0
  176. package/src/modules/sso/migrations/Migration20260219000000_sso.ts +21 -0
  177. package/src/modules/sso/migrations/Migration20260222000000_sso_add_name.ts +13 -0
  178. package/src/modules/sso/migrations/Migration20260222000001_sso_partial_unique_org.ts +15 -0
  179. package/src/modules/sso/migrations/Migration20260223000000_scim_tables.ts +24 -0
  180. package/src/modules/sso/migrations/Migration20260224000000_sso_external_id.ts +15 -0
  181. package/src/modules/sso/migrations/Migration20260224100000_sso_role_grants.ts +18 -0
  182. package/src/modules/sso/migrations/Migration20260224200000_drop_default_role_id.ts +13 -0
  183. package/src/modules/sso/migrations/Migration20260225000000_sso_identities_partial_unique.ts +25 -0
  184. package/src/modules/sso/migrations/Migration20260305000000_sso_role_grants_org_id.ts +14 -0
  185. package/src/modules/sso/services/accountLinkingService.ts +386 -0
  186. package/src/modules/sso/services/hrdService.ts +22 -0
  187. package/src/modules/sso/services/scimService.ts +461 -0
  188. package/src/modules/sso/services/scimTokenService.ts +136 -0
  189. package/src/modules/sso/services/ssoConfigService.ts +337 -0
  190. package/src/modules/sso/services/ssoService.ts +167 -0
  191. package/src/modules/sso/setup.ts +56 -0
  192. package/src/modules/sso/subscribers/user-deleted-cleanup.ts +33 -0
  193. package/src/modules/sso/widgets/injection/login-sso/widget.client.tsx +130 -0
  194. package/src/modules/sso/widgets/injection/login-sso/widget.ts +16 -0
  195. 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
+ }