@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,88 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { SsoConfigService } from '../../services/ssoConfigService'
|
|
5
|
+
import { ssoConfigAdminCreateSchema, ssoConfigListQuerySchema } from '../../data/validators'
|
|
6
|
+
import { resolveSsoAdminContext } from '../admin-context'
|
|
7
|
+
import { handleSsoAdminApiError } from '../error-handler'
|
|
8
|
+
|
|
9
|
+
export const metadata = {
|
|
10
|
+
GET: { requireAuth: true, requireFeatures: ['sso.config.view'] },
|
|
11
|
+
POST: { requireAuth: true, requireFeatures: ['sso.config.manage'] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
17
|
+
|
|
18
|
+
const url = new URL(req.url)
|
|
19
|
+
const query = ssoConfigListQuerySchema.parse({
|
|
20
|
+
page: url.searchParams.get('page') ?? undefined,
|
|
21
|
+
pageSize: url.searchParams.get('pageSize') ?? undefined,
|
|
22
|
+
search: url.searchParams.get('search') ?? undefined,
|
|
23
|
+
organizationId: url.searchParams.get('organizationId') ?? undefined,
|
|
24
|
+
tenantId: url.searchParams.get('tenantId') ?? undefined,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const container = await createRequestContainer()
|
|
28
|
+
const service = container.resolve<SsoConfigService>('ssoConfigService')
|
|
29
|
+
const result = await service.list(scope, query)
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({ ...result, isSuperAdmin: scope.isSuperAdmin })
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return handleSsoAdminApiError(err, 'SSO Config API')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function POST(req: Request) {
|
|
38
|
+
try {
|
|
39
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
40
|
+
|
|
41
|
+
const body = await req.json()
|
|
42
|
+
const parsed = ssoConfigAdminCreateSchema.safeParse(body)
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const container = await createRequestContainer()
|
|
48
|
+
const service = container.resolve<SsoConfigService>('ssoConfigService')
|
|
49
|
+
const config = await service.create(scope, parsed.data)
|
|
50
|
+
|
|
51
|
+
return NextResponse.json(config, { status: 201 })
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return handleSsoAdminApiError(err, 'SSO Config API')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const openApi: OpenApiRouteDoc = {
|
|
58
|
+
tag: 'SSO',
|
|
59
|
+
summary: 'SSO Configuration',
|
|
60
|
+
methods: {
|
|
61
|
+
GET: {
|
|
62
|
+
summary: 'List SSO configurations',
|
|
63
|
+
description: 'Returns paginated SSO configurations. Admins see their org only; superadmins see all.',
|
|
64
|
+
tags: ['SSO'],
|
|
65
|
+
responses: [{ status: 200, description: 'Paginated list of SSO configs' }],
|
|
66
|
+
errors: [
|
|
67
|
+
{ status: 401, description: 'Unauthorized' },
|
|
68
|
+
{ status: 403, description: 'Forbidden — requires sso.config.view' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
POST: {
|
|
72
|
+
summary: 'Create SSO configuration',
|
|
73
|
+
description: 'Creates a new SSO configuration for an organization. One config per org.',
|
|
74
|
+
tags: ['SSO'],
|
|
75
|
+
requestBody: {
|
|
76
|
+
contentType: 'application/json',
|
|
77
|
+
schema: ssoConfigAdminCreateSchema,
|
|
78
|
+
},
|
|
79
|
+
responses: [{ status: 201, description: 'SSO config created' }],
|
|
80
|
+
errors: [
|
|
81
|
+
{ status: 400, description: 'Invalid input' },
|
|
82
|
+
{ status: 401, description: 'Unauthorized' },
|
|
83
|
+
{ status: 403, description: 'Forbidden — requires sso.config.manage' },
|
|
84
|
+
{ status: 409, description: 'Config already exists for this organization' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { SsoAdminAuthError } from './admin-context'
|
|
3
|
+
import { SsoConfigError } from '../services/ssoConfigService'
|
|
4
|
+
import { ScimTokenError } from '../services/scimTokenService'
|
|
5
|
+
import { ScimServiceError } from '../services/scimService'
|
|
6
|
+
import { scimJson, buildScimError } from '../lib/scim-response'
|
|
7
|
+
|
|
8
|
+
interface SsoHttpError {
|
|
9
|
+
message: string
|
|
10
|
+
statusCode: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSsoHttpError(err: unknown): err is SsoHttpError {
|
|
14
|
+
return (
|
|
15
|
+
err instanceof SsoAdminAuthError ||
|
|
16
|
+
err instanceof SsoConfigError ||
|
|
17
|
+
err instanceof ScimTokenError ||
|
|
18
|
+
err instanceof ScimServiceError
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function handleSsoAdminApiError(err: unknown, label: string): NextResponse {
|
|
23
|
+
if (isSsoHttpError(err)) {
|
|
24
|
+
return NextResponse.json({ error: err.message }, { status: err.statusCode })
|
|
25
|
+
}
|
|
26
|
+
console.error(`[${label}] Error:`, err)
|
|
27
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function handleScimApiError(err: unknown, label: string): Response {
|
|
31
|
+
if (err instanceof ScimServiceError) {
|
|
32
|
+
return scimJson(buildScimError(err.statusCode, err.message), err.statusCode)
|
|
33
|
+
}
|
|
34
|
+
console.error(`[${label}] Error:`, err)
|
|
35
|
+
return scimJson(buildScimError(500, 'Internal server error'), 500)
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { HrdService } from '../../services/hrdService'
|
|
5
|
+
import { hrdRequestSchema } from '../../data/validators'
|
|
6
|
+
|
|
7
|
+
export async function POST(req: Request) {
|
|
8
|
+
try {
|
|
9
|
+
const body = await req.json()
|
|
10
|
+
const parsed = hrdRequestSchema.safeParse(body)
|
|
11
|
+
|
|
12
|
+
if (!parsed.success) {
|
|
13
|
+
return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const container = await createRequestContainer()
|
|
17
|
+
const hrdService = container.resolve<HrdService>('hrdService')
|
|
18
|
+
const config = await hrdService.findActiveConfigByEmailDomain(parsed.data.email)
|
|
19
|
+
|
|
20
|
+
if (!config) {
|
|
21
|
+
return NextResponse.json({ hasSso: false })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
hasSso: true,
|
|
26
|
+
configId: config.id,
|
|
27
|
+
protocol: config.protocol,
|
|
28
|
+
})
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error('[SSO HRD] Error:', err)
|
|
31
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const openApi: OpenApiRouteDoc = {
|
|
36
|
+
tag: 'SSO',
|
|
37
|
+
summary: 'Home Realm Discovery',
|
|
38
|
+
methods: {
|
|
39
|
+
POST: {
|
|
40
|
+
summary: 'Check if email domain has SSO configured',
|
|
41
|
+
description: 'Given an email address, determines if the associated organization has an active SSO configuration. Called from the login page before authentication.',
|
|
42
|
+
tags: ['SSO'],
|
|
43
|
+
requestBody: {
|
|
44
|
+
contentType: 'application/json',
|
|
45
|
+
schema: hrdRequestSchema,
|
|
46
|
+
},
|
|
47
|
+
responses: [
|
|
48
|
+
{ status: 200, description: 'HRD result' },
|
|
49
|
+
],
|
|
50
|
+
errors: [
|
|
51
|
+
{ status: 400, description: 'Invalid request' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { toAbsoluteUrl } from '@open-mercato/shared/lib/url'
|
|
4
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
|
+
import { SsoService } from '../../services/ssoService'
|
|
6
|
+
import { ssoInitiateSchema } from '../../data/validators'
|
|
7
|
+
import { emitSsoEvent } from '../../events'
|
|
8
|
+
|
|
9
|
+
function sanitizeReturnUrl(raw: string | null): string {
|
|
10
|
+
const value = raw || '/backend'
|
|
11
|
+
if (!value.startsWith('/') || value.startsWith('//')) return '/backend'
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(value, 'http://localhost')
|
|
14
|
+
if (parsed.origin !== 'http://localhost') return '/backend'
|
|
15
|
+
return parsed.pathname + parsed.search + parsed.hash
|
|
16
|
+
} catch {
|
|
17
|
+
return '/backend'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function GET(req: Request) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(req.url)
|
|
24
|
+
const configId = url.searchParams.get('configId')
|
|
25
|
+
const rawReturnUrl = url.searchParams.get('returnUrl')
|
|
26
|
+
|
|
27
|
+
const parsed = ssoInitiateSchema.safeParse({ configId, returnUrl: rawReturnUrl ?? undefined })
|
|
28
|
+
if (!parsed.success || !parsed.data.configId) {
|
|
29
|
+
return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_config'))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const returnUrl = sanitizeReturnUrl(rawReturnUrl)
|
|
33
|
+
const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')
|
|
34
|
+
const container = await createRequestContainer()
|
|
35
|
+
const ssoService = container.resolve<SsoService>('ssoService')
|
|
36
|
+
|
|
37
|
+
const { redirectUrl, stateCookie } = await ssoService.initiateLogin(parsed.data.configId, returnUrl, redirectUri)
|
|
38
|
+
|
|
39
|
+
const res = NextResponse.redirect(redirectUrl)
|
|
40
|
+
res.cookies.set('sso_state', stateCookie, {
|
|
41
|
+
httpOnly: true,
|
|
42
|
+
path: '/',
|
|
43
|
+
sameSite: 'lax',
|
|
44
|
+
secure: process.env.NODE_ENV !== 'development',
|
|
45
|
+
maxAge: 300,
|
|
46
|
+
})
|
|
47
|
+
return res
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('[SSO Initiate] Error:', err)
|
|
50
|
+
void emitSsoEvent('sso.login.failed', {
|
|
51
|
+
reason: err instanceof Error ? err.message : 'initiate_failed',
|
|
52
|
+
}).catch((e) => console.error('[SSO Event]', e))
|
|
53
|
+
return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_failed'))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const openApi: OpenApiRouteDoc = {
|
|
58
|
+
tag: 'SSO',
|
|
59
|
+
summary: 'Initiate SSO login',
|
|
60
|
+
methods: {
|
|
61
|
+
GET: {
|
|
62
|
+
summary: 'Start SSO login flow',
|
|
63
|
+
description: 'Redirects the browser to the configured IdP authorization endpoint. Sets an encrypted sso_state cookie for CSRF protection.',
|
|
64
|
+
tags: ['SSO'],
|
|
65
|
+
responses: [
|
|
66
|
+
{ status: 302, description: 'Redirect to IdP authorization endpoint', mediaType: 'text/html' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
2
|
+
import { ScimTokenService } from '../../services/scimTokenService'
|
|
3
|
+
import { SsoConfig } from '../../data/entities'
|
|
4
|
+
import { buildScimError, scimJson } from '../../lib/scim-response'
|
|
5
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
+
|
|
7
|
+
export interface ScimScope {
|
|
8
|
+
ssoConfigId: string
|
|
9
|
+
organizationId: string
|
|
10
|
+
tenantId: string | null
|
|
11
|
+
config: SsoConfig
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function resolveScimContext(req: Request): Promise<
|
|
15
|
+
| { ok: true; scope: ScimScope }
|
|
16
|
+
| { ok: false; response: Response }
|
|
17
|
+
> {
|
|
18
|
+
const authHeader = req.headers.get('authorization')
|
|
19
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
response: scimJson(
|
|
23
|
+
buildScimError(401, 'Bearer token required'),
|
|
24
|
+
401,
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rawToken = authHeader.slice(7)
|
|
30
|
+
if (!rawToken) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
response: scimJson(buildScimError(401, 'Bearer token required'), 401),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const container = await createRequestContainer()
|
|
38
|
+
const scimTokenService = container.resolve<ScimTokenService>('scimTokenService')
|
|
39
|
+
const verified = await scimTokenService.verifyToken(rawToken)
|
|
40
|
+
|
|
41
|
+
if (!verified) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
response: scimJson(buildScimError(401, 'Invalid or revoked token'), 401),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const em = container.resolve<EntityManager>('em')
|
|
49
|
+
const config = await em.findOne(SsoConfig, {
|
|
50
|
+
id: verified.ssoConfigId,
|
|
51
|
+
organizationId: verified.organizationId,
|
|
52
|
+
deletedAt: null,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!config) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
response: scimJson(buildScimError(403, 'SSO configuration not found'), 403),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!config.isActive) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
response: scimJson(buildScimError(403, 'SSO configuration is not active'), 403),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config.jitEnabled) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
response: scimJson(buildScimError(403, 'SCIM provisioning is unavailable — JIT provisioning is enabled on this configuration'), 403),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
scope: {
|
|
79
|
+
ssoConfigId: config.id,
|
|
80
|
+
organizationId: config.organizationId,
|
|
81
|
+
tenantId: verified.tenantId,
|
|
82
|
+
config,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
5
|
+
import { ScimProvisioningLog } from '../../../data/entities'
|
|
6
|
+
import { resolveSsoAdminContext } from '../../admin-context'
|
|
7
|
+
import { handleSsoAdminApiError } from '../../error-handler'
|
|
8
|
+
|
|
9
|
+
export const metadata = {
|
|
10
|
+
GET: { requireAuth: true, requireFeatures: ['sso.config.view'] },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function GET(req: Request) {
|
|
14
|
+
try {
|
|
15
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
16
|
+
|
|
17
|
+
const url = new URL(req.url)
|
|
18
|
+
const ssoConfigId = url.searchParams.get('ssoConfigId')
|
|
19
|
+
if (!ssoConfigId) {
|
|
20
|
+
return NextResponse.json({ error: 'ssoConfigId is required' }, { status: 400 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const where: Record<string, unknown> = { ssoConfigId }
|
|
24
|
+
if (!scope.isSuperAdmin && scope.organizationId) {
|
|
25
|
+
where.organizationId = scope.organizationId
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const container = await createRequestContainer()
|
|
29
|
+
const em = container.resolve<EntityManager>('em')
|
|
30
|
+
|
|
31
|
+
const logs = await em.find(ScimProvisioningLog, where, {
|
|
32
|
+
orderBy: { createdAt: 'desc' },
|
|
33
|
+
limit: 50,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return NextResponse.json({
|
|
37
|
+
items: logs.map((log) => ({
|
|
38
|
+
id: log.id,
|
|
39
|
+
operation: log.operation,
|
|
40
|
+
resourceType: log.resourceType,
|
|
41
|
+
resourceId: log.resourceId,
|
|
42
|
+
scimExternalId: log.scimExternalId,
|
|
43
|
+
responseStatus: log.responseStatus,
|
|
44
|
+
errorMessage: log.errorMessage,
|
|
45
|
+
createdAt: log.createdAt.toISOString(),
|
|
46
|
+
})),
|
|
47
|
+
})
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return handleSsoAdminApiError(err, 'SCIM Logs API')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const openApi: OpenApiRouteDoc = {
|
|
54
|
+
tag: 'SCIM',
|
|
55
|
+
summary: 'SCIM Provisioning Logs',
|
|
56
|
+
methods: {
|
|
57
|
+
GET: {
|
|
58
|
+
summary: 'List recent provisioning log entries',
|
|
59
|
+
description: 'Returns the last 50 SCIM provisioning log entries for a given SSO config.',
|
|
60
|
+
tags: ['SSO', 'SCIM'],
|
|
61
|
+
responses: [{ status: 200, description: 'List of provisioning log entries' }],
|
|
62
|
+
errors: [
|
|
63
|
+
{ status: 400, description: 'Missing ssoConfigId' },
|
|
64
|
+
{ status: 401, description: 'Unauthorized' },
|
|
65
|
+
{ status: 403, description: 'Forbidden — requires sso.scim.manage' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { ScimTokenService } from '../../../../services/scimTokenService'
|
|
5
|
+
import { resolveSsoAdminContext } from '../../../admin-context'
|
|
6
|
+
import { handleSsoAdminApiError } from '../../../error-handler'
|
|
7
|
+
|
|
8
|
+
export const metadata = {
|
|
9
|
+
DELETE: { requireAuth: true, requireFeatures: ['sso.scim.manage'] },
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RouteContext = { params: Promise<{ id: string }> }
|
|
13
|
+
|
|
14
|
+
export async function DELETE(req: Request, ctx: RouteContext) {
|
|
15
|
+
try {
|
|
16
|
+
const { id } = await ctx.params
|
|
17
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
18
|
+
|
|
19
|
+
const container = await createRequestContainer()
|
|
20
|
+
const service = container.resolve<ScimTokenService>('scimTokenService')
|
|
21
|
+
await service.revokeToken(id, scope)
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({ ok: true })
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return handleSsoAdminApiError(err, 'SCIM Tokens API')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const openApi: OpenApiRouteDoc = {
|
|
30
|
+
tag: 'SSO',
|
|
31
|
+
summary: 'SCIM Token by ID',
|
|
32
|
+
methods: {
|
|
33
|
+
DELETE: {
|
|
34
|
+
summary: 'Revoke SCIM token',
|
|
35
|
+
description: 'Revokes (deactivates) a SCIM token. The token can no longer be used for authentication.',
|
|
36
|
+
tags: ['SSO', 'SCIM'],
|
|
37
|
+
responses: [{ status: 200, description: 'Token revoked' }],
|
|
38
|
+
errors: [
|
|
39
|
+
{ status: 401, description: 'Unauthorized' },
|
|
40
|
+
{ status: 403, description: 'Forbidden — requires sso.scim.manage' },
|
|
41
|
+
{ status: 404, description: 'Token not found' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { ScimTokenService } from '../../../services/scimTokenService'
|
|
5
|
+
import { createScimTokenSchema, scimTokenListSchema } from '../../../data/validators'
|
|
6
|
+
import { resolveSsoAdminContext } from '../../admin-context'
|
|
7
|
+
import { handleSsoAdminApiError } from '../../error-handler'
|
|
8
|
+
|
|
9
|
+
export const metadata = {
|
|
10
|
+
GET: { requireAuth: true, requireFeatures: ['sso.config.view'] },
|
|
11
|
+
POST: { requireAuth: true, requireFeatures: ['sso.scim.manage'] },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
17
|
+
|
|
18
|
+
const url = new URL(req.url)
|
|
19
|
+
const parsed = scimTokenListSchema.safeParse({
|
|
20
|
+
ssoConfigId: url.searchParams.get('ssoConfigId') ?? undefined,
|
|
21
|
+
})
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const container = await createRequestContainer()
|
|
27
|
+
const service = container.resolve<ScimTokenService>('scimTokenService')
|
|
28
|
+
const tokens = await service.listTokens(parsed.data.ssoConfigId, scope)
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ items: tokens })
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return handleSsoAdminApiError(err, 'SCIM Tokens API')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function POST(req: Request) {
|
|
37
|
+
try {
|
|
38
|
+
const { scope } = await resolveSsoAdminContext(req)
|
|
39
|
+
|
|
40
|
+
const body = await req.json()
|
|
41
|
+
const parsed = createScimTokenSchema.safeParse(body)
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
return NextResponse.json({ error: 'Invalid request', details: parsed.error.flatten() }, { status: 400 })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const container = await createRequestContainer()
|
|
47
|
+
const service = container.resolve<ScimTokenService>('scimTokenService')
|
|
48
|
+
const result = await service.generateToken(parsed.data.ssoConfigId, parsed.data.name, scope)
|
|
49
|
+
|
|
50
|
+
return NextResponse.json(result, { status: 201 })
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return handleSsoAdminApiError(err, 'SCIM Tokens API')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export const openApi: OpenApiRouteDoc = {
|
|
58
|
+
tag: 'SSO',
|
|
59
|
+
summary: 'SCIM Token Management',
|
|
60
|
+
methods: {
|
|
61
|
+
GET: {
|
|
62
|
+
summary: 'List SCIM tokens',
|
|
63
|
+
description: 'Returns SCIM tokens for a given SSO config. Token hashes are never exposed.',
|
|
64
|
+
tags: ['SSO', 'SCIM'],
|
|
65
|
+
responses: [{ status: 200, description: 'List of SCIM tokens' }],
|
|
66
|
+
errors: [
|
|
67
|
+
{ status: 400, description: 'Missing or invalid ssoConfigId' },
|
|
68
|
+
{ status: 401, description: 'Unauthorized' },
|
|
69
|
+
{ status: 403, description: 'Forbidden — requires sso.scim.manage' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
POST: {
|
|
73
|
+
summary: 'Create SCIM token',
|
|
74
|
+
description: 'Generates a new SCIM bearer token. The raw token is returned once and cannot be retrieved again.',
|
|
75
|
+
tags: ['SSO', 'SCIM'],
|
|
76
|
+
requestBody: {
|
|
77
|
+
contentType: 'application/json',
|
|
78
|
+
schema: createScimTokenSchema,
|
|
79
|
+
},
|
|
80
|
+
responses: [{ status: 201, description: 'SCIM token created — raw token included in response' }],
|
|
81
|
+
errors: [
|
|
82
|
+
{ status: 400, description: 'Invalid input' },
|
|
83
|
+
{ status: 401, description: 'Unauthorized' },
|
|
84
|
+
{ status: 403, description: 'Forbidden — requires sso.scim.manage' },
|
|
85
|
+
{ status: 409, description: 'Conflict — cannot create SCIM token while JIT is enabled' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
2
|
+
import { scimJson } from '../../../../lib/scim-response'
|
|
3
|
+
|
|
4
|
+
export const metadata = {}
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return scimJson({
|
|
8
|
+
schemas: ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
|
|
9
|
+
documentationUri: 'https://open-mercato.com/docs/scim',
|
|
10
|
+
patch: { supported: true },
|
|
11
|
+
bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
|
|
12
|
+
filter: { supported: true, maxResults: 200 },
|
|
13
|
+
changePassword: { supported: false },
|
|
14
|
+
sort: { supported: false },
|
|
15
|
+
etag: { supported: false },
|
|
16
|
+
authenticationSchemes: [
|
|
17
|
+
{
|
|
18
|
+
type: 'oauthbearertoken',
|
|
19
|
+
name: 'OAuth Bearer Token',
|
|
20
|
+
description: 'Authentication scheme using the OAuth Bearer Token standard',
|
|
21
|
+
specUri: 'https://www.rfc-editor.org/info/rfc6750',
|
|
22
|
+
primary: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const openApi: OpenApiRouteDoc = {
|
|
29
|
+
tag: 'SCIM',
|
|
30
|
+
summary: 'SCIM Service Provider Configuration',
|
|
31
|
+
methods: {
|
|
32
|
+
GET: {
|
|
33
|
+
summary: 'Get SCIM service provider configuration',
|
|
34
|
+
description: 'Returns SCIM 2.0 ServiceProviderConfig. No authentication required — used by identity providers during connection testing.',
|
|
35
|
+
tags: ['SSO', 'SCIM'],
|
|
36
|
+
responses: [{ status: 200, description: 'SCIM ServiceProviderConfig' }],
|
|
37
|
+
errors: [],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { parseScimPatchOperations } from '../../../../../lib/scim-patch'
|
|
6
|
+
import { scimJson } from '../../../../../lib/scim-response'
|
|
7
|
+
import { handleScimApiError } from '../../../../error-handler'
|
|
8
|
+
|
|
9
|
+
export const metadata = {}
|
|
10
|
+
|
|
11
|
+
type RouteContext = { params: Promise<{ id: string }> }
|
|
12
|
+
|
|
13
|
+
export async function GET(req: Request, ctx: RouteContext) {
|
|
14
|
+
try {
|
|
15
|
+
const { id } = await ctx.params
|
|
16
|
+
const scimCtx = await resolveScimContext(req)
|
|
17
|
+
if (!scimCtx.ok) return scimCtx.response
|
|
18
|
+
|
|
19
|
+
const baseUrl = new URL(req.url).origin
|
|
20
|
+
const container = await createRequestContainer()
|
|
21
|
+
const service = container.resolve<ScimService>('scimService')
|
|
22
|
+
const resource = await service.getUser(id, scimCtx.scope, baseUrl)
|
|
23
|
+
|
|
24
|
+
return scimJson(resource)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return handleScimApiError(err, 'SCIM Users API')
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function PATCH(req: Request, ctx: RouteContext) {
|
|
31
|
+
try {
|
|
32
|
+
const { id } = await ctx.params
|
|
33
|
+
const scimCtx = await resolveScimContext(req)
|
|
34
|
+
if (!scimCtx.ok) return scimCtx.response
|
|
35
|
+
|
|
36
|
+
const body = await req.json()
|
|
37
|
+
const operations = parseScimPatchOperations(body)
|
|
38
|
+
const baseUrl = new URL(req.url).origin
|
|
39
|
+
|
|
40
|
+
const container = await createRequestContainer()
|
|
41
|
+
const service = container.resolve<ScimService>('scimService')
|
|
42
|
+
const resource = await service.patchUser(id, operations, scimCtx.scope, baseUrl)
|
|
43
|
+
|
|
44
|
+
return scimJson(resource)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return handleScimApiError(err, 'SCIM Users API')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function DELETE(req: Request, ctx: RouteContext) {
|
|
51
|
+
try {
|
|
52
|
+
const { id } = await ctx.params
|
|
53
|
+
const scimCtx = await resolveScimContext(req)
|
|
54
|
+
if (!scimCtx.ok) return scimCtx.response
|
|
55
|
+
|
|
56
|
+
const container = await createRequestContainer()
|
|
57
|
+
const service = container.resolve<ScimService>('scimService')
|
|
58
|
+
await service.deleteUser(id, scimCtx.scope)
|
|
59
|
+
|
|
60
|
+
return new Response(null, { status: 204 })
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return handleScimApiError(err, 'SCIM Users API')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const openApi: OpenApiRouteDoc = {
|
|
68
|
+
tag: 'SCIM',
|
|
69
|
+
summary: 'SCIM User by ID',
|
|
70
|
+
methods: {
|
|
71
|
+
GET: {
|
|
72
|
+
summary: 'Get SCIM user',
|
|
73
|
+
description: 'Returns a single provisioned user by SCIM ID.',
|
|
74
|
+
tags: ['SSO', 'SCIM'],
|
|
75
|
+
responses: [{ status: 200, description: 'SCIM User resource' }],
|
|
76
|
+
errors: [
|
|
77
|
+
{ status: 401, description: 'Unauthorized' },
|
|
78
|
+
{ status: 404, description: 'User not found' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
PATCH: {
|
|
82
|
+
summary: 'Patch SCIM user',
|
|
83
|
+
description: 'Updates user attributes via SCIM PatchOp. Supports active/inactive toggling.',
|
|
84
|
+
tags: ['SSO', 'SCIM'],
|
|
85
|
+
responses: [{ status: 200, description: 'Updated SCIM User resource' }],
|
|
86
|
+
errors: [
|
|
87
|
+
{ status: 400, description: 'Invalid PatchOp' },
|
|
88
|
+
{ status: 401, description: 'Unauthorized' },
|
|
89
|
+
{ status: 404, description: 'User not found' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
DELETE: {
|
|
93
|
+
summary: 'Delete SCIM user',
|
|
94
|
+
description: 'Deactivates the user and revokes sessions.',
|
|
95
|
+
tags: ['SSO', 'SCIM'],
|
|
96
|
+
responses: [{ status: 204, description: 'No content' }],
|
|
97
|
+
errors: [
|
|
98
|
+
{ status: 401, description: 'Unauthorized' },
|
|
99
|
+
{ status: 404, description: 'User not found' },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}
|