@open-mercato/core 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6784.1.f80b9afce5
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +3 -3
- package/agentic/standalone-guide.md +13 -0
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/cli.js +1 -1
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +4 -6
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +1 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +29 -44
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +2 -2
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/api/login.js +4 -2
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
- package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
- package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
- package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
- package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
- package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +6 -0
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/lib/visibilityFilter.js +6 -4
- package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +22 -9
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
- package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
- package/dist/modules/dashboards/lib/access.js +5 -3
- package/dist/modules/dashboards/lib/access.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +10 -3
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +31 -8
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +4 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +2 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
- package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
- package/dist/modules/entities/lib/entityAcl.js +47 -15
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/inbox_ops/ai-tools.js +5 -2
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/messages/lib/routeHelpers.js +1 -3
- package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/query_index/di.js +3 -0
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +2 -2
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +83 -17
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +13 -7
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +53 -1
- package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
- package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
- package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
- package/dist/modules/sales/commands/documents.js +17 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/staff/api/interceptors.js +5 -3
- package/dist/modules/staff/api/interceptors.js.map +2 -2
- package/dist/modules/sync_excel/api/upload/route.js +15 -2
- package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +7 -7
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/auth/AGENTS.md +5 -3
- package/src/modules/auth/cli.ts +1 -1
- package/src/modules/auth/lib/backendChrome.tsx +5 -11
- package/src/modules/auth/lib/setup-app.ts +1 -0
- package/src/modules/auth/services/rbacService.ts +41 -46
- package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
- package/src/modules/communication_channels/lib/access-control.ts +3 -3
- package/src/modules/customer_accounts/AGENTS.md +3 -3
- package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
- package/src/modules/customer_accounts/api/login.ts +4 -2
- package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
- package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
- package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
- package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
- package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
- package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
- package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
- package/src/modules/customer_accounts/setup.ts +7 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
- package/src/modules/customers/lib/visibilityFilter.ts +7 -5
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
- package/src/modules/dashboards/api/layout/route.ts +22 -9
- package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
- package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
- package/src/modules/dashboards/lib/access.ts +5 -3
- package/src/modules/directory/api/organization-branding/route.ts +12 -1
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +2 -0
- package/src/modules/directory/i18n/de.json +3 -1
- package/src/modules/directory/i18n/en.json +3 -1
- package/src/modules/directory/i18n/es.json +3 -1
- package/src/modules/directory/i18n/pl.json +3 -1
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
- package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
- package/src/modules/entities/lib/entityAcl.ts +47 -18
- package/src/modules/inbox_ops/ai-tools.ts +5 -3
- package/src/modules/messages/lib/routeHelpers.ts +4 -6
- package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
- package/src/modules/query_index/di.ts +6 -0
- package/src/modules/query_index/lib/coverage.ts +9 -6
- package/src/modules/query_index/lib/indexer.ts +105 -19
- package/src/modules/query_index/lib/search-tokens.ts +19 -8
- package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
- package/src/modules/query_index/subscribers/delete_one.ts +65 -1
- package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
- package/src/modules/sales/commands/documents.ts +27 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/staff/api/interceptors.ts +6 -4
- package/src/modules/sync_excel/api/upload/route.ts +15 -2
- package/src/modules/workflows/lib/activity-executor.ts +12 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/invitations/accept.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { invitationAcceptSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = invitationAcceptSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const result = await customerInvitationService.acceptInvitation(\n parsed.data.token,\n parsed.data.password,\n parsed.data.displayName,\n )\n if (!result) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired invitation' }, { status: 400 })\n }\n\n const { user, invitation } = result\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAIvC,SAAS,iCAAiC;AAC1C,SAAS,mBAAmB;AAErB,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,SAAS,uBAAuB,UAAU,IAAI;AACpD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClI;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,4BAA4B,UAAU,QAAQ,2BAA2B;AAC/E,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AAEnE,QAAM,SAAS,MAAM,0BAA0B;AAAA,IAC7C,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,EACd;AACA,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,gCAAgC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjG;AAEA,QAAM,EAAE,MAAM,WAAW,IAAI;AAC7B,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { invitationAcceptSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerInvitationService } from '@open-mercato/core/modules/customer_accounts/services/customerInvitationService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = invitationAcceptSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const customerInvitationService = container.resolve('customerInvitationService') as CustomerInvitationService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const result = await customerInvitationService.acceptInvitation(\n parsed.data.token,\n parsed.data.password,\n parsed.data.displayName,\n )\n if (!result) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired invitation' }, { status: 400 })\n }\n\n const { user, invitation } = result\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.user.created', {\n id: user.id,\n email: user.email,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n invitationId: invitation.id,\n }).catch(() => undefined)\n\n void emitCustomerAccountsEvent('customer_accounts.invitation.accepted', {\n invitationId: invitation.id,\n userId: user.id,\n tenantId: user.tenantId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: true,\n },\n resolvedFeatures,\n }, { status: 201 })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst acceptSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Accept customer invitation',\n description: 'Accepts an invitation, creates the user account, assigns roles, and auto-logs in.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: invitationAcceptSchema,\n description: 'Invitation acceptance with token, password, and display name.',\n },\n responses: [\n { status: 201, description: 'Invitation accepted and user created', schema: acceptSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or expired invitation', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Accept customer invitation',\n description: 'Handles invitation acceptance for customer accounts.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAIvC,SAAS,iCAAiC;AAC1C,SAAS,mBAAmB;AAErB,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,SAAS,uBAAuB,UAAU,IAAI;AACpD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClI;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,4BAA4B,UAAU,QAAQ,2BAA2B;AAC/E,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AAEnE,QAAM,SAAS,MAAM,0BAA0B;AAAA,IAC7C,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,EACd;AACA,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,gCAAgC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjG;AAEA,QAAM,EAAE,MAAM,WAAW,IAAI;AAC7B,QAAM,mBAAmB,MAAM,oBAAoB,qBAAqB,KAAK,IAAI;AAAA,IAC/E,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AAED,QAAM,KAAK,YAAY,KAAK,CAAC;AAC7B,QAAM,YAAY,IAAI,QAAQ,IAAI,YAAY,KAAK;AACnD,QAAM,EAAE,UAAU,IAAI,IAAI,MAAM,uBAAuB,cAAc,MAAM,kBAAkB,IAAI,SAAS;AAE1G,OAAK,0BAA0B,kCAAkC;AAAA,IAC/D,IAAI,KAAK;AAAA,IACT,OAAO,KAAK;AAAA,IACZ,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,IACrB,cAAc,WAAW;AAAA,EAC3B,CAAC,EAAE,MAAM,MAAM,MAAS;AAExB,OAAK,0BAA0B,yCAAyC;AAAA,IACtE,cAAc,WAAW;AAAA,IACzB,QAAQ,KAAK;AAAA,IACb,UAAU,KAAK;AAAA,EACjB,CAAC,EAAE,MAAM,MAAM,MAAS;AAExB,QAAM,MAAM,aAAa,KAAK;AAAA,IAC5B,IAAI;AAAA,IACJ,MAAM;AAAA,MACJ,IAAI,KAAK;AAAA,MACT,OAAO,KAAK;AAAA,MACZ,aAAa,KAAK;AAAA,MAClB,eAAe;AAAA,IACjB;AAAA,IACA;AAAA,EACF,GAAG,EAAE,QAAQ,IAAI,CAAC;AAElB,MAAI,QAAQ,IAAI,uBAAuB,KAAK;AAAA,IAC1C,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ,KAAK,KAAK;AAAA,EACpB,CAAC;AACD,MAAI,QAAQ,IAAI,0BAA0B,UAAU;AAAA,IAClD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ,KAAK,KAAK,KAAK;AAAA,EACzB,CAAC;AAED,SAAO;AACT;AAEA,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,MAAM,EAAE,OAAO;AAAA,IACb,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,IACpB,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,IACxB,aAAa,EAAE,OAAO;AAAA,IACtB,eAAe,EAAE,QAAQ;AAAA,EAC3B,CAAC;AAAA,EACD,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC;AACtC,CAAC;AAED,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAExE,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,yBAAyB;AAAA,EAChC,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,wCAAwC,QAAQ,oBAAoB;AAAA,EAClG;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,iCAAiC,QAAQ,YAAY;AAAA,EACnF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,SAAS,EAAE,MAAM,UAAU;AAC7B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -88,8 +88,10 @@ async function POST(req) {
|
|
|
88
88
|
if (compoundKey) {
|
|
89
89
|
await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig);
|
|
90
90
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
91
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
92
|
+
tenantId,
|
|
93
|
+
organizationId: user.organizationId
|
|
94
|
+
});
|
|
93
95
|
const ip = getClientIp(req, 0);
|
|
94
96
|
const userAgent = req.headers.get("user-agent") || null;
|
|
95
97
|
const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/api/login.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { loginSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n resetAuthRateLimit,\n customerLoginRateLimitConfig,\n customerLoginIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the real-account and unknown/inactive/locked/no-hash login branches\n// so account existence cannot be inferred from a timing side channel. Mirrors signup.ts.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = loginSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid credentials' }, { status: 400 })\n }\n\n const { email, password } = parsed.data\n let tenantId: string\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n\n const { error: rateLimitError, compoundKey } = await checkAuthRateLimit({\n req,\n ipConfig: customerLoginIpRateLimitConfig,\n compoundConfig: customerLoginRateLimitConfig,\n compoundIdentifier: email,\n })\n if (rateLimitError) return rateLimitError\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const user = await customerUserService.findByEmail(email, tenantId)\n\n // Equalize response timing and error responses across every failed-login branch so an\n // attacker cannot enumerate which emails have an account in this tenant. Unknown,\n // password-less, inactive, locked, wrong-password, and unverified accounts all run a\n // bcrypt comparison and return the same generic 401 \u2014 lockout/deactivation guidance is\n // conveyed out-of-band (e.g. email), never in the synchronous response.\n if (!user || !user.passwordHash) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_credentials', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.isActive) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'inactive', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (customerUserService.checkLockout(user)) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'locked', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n const passwordValid = await customerUserService.verifyPassword(user, password)\n if (!passwordValid) {\n await customerUserService.incrementFailedAttempts(user)\n void emitCustomerAccountsEvent('customer_accounts.login.failed', { email, reason: 'invalid_password', tenantId }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n if (!user.emailVerifiedAt) {\n void emitCustomerAccountsEvent('customer_accounts.login.failed', {\n email,\n reason: 'email_not_verified',\n tenantId,\n }).catch(() => undefined)\n return NextResponse.json({ ok: false, error: 'Invalid email or password' }, { status: 401 })\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n if (compoundKey) {\n await resetAuthRateLimit(compoundKey, customerLoginRateLimitConfig)\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt, session } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Authenticate customer credentials',\n description: 'Validates customer credentials and issues JWT + session cookies.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: loginSchema,\n description: 'Login payload with email and password.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Invalid credentials', schema: errorSchema },\n { status: 429, description: 'Too many login attempts', schema: rateLimitErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer login',\n description: 'Handles customer authentication and session issuance.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -37,8 +37,10 @@ async function POST(req) {
|
|
|
37
37
|
}
|
|
38
38
|
await customerUserService.resetFailedAttempts(user);
|
|
39
39
|
await customerUserService.updateLastLoginAt(user);
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
41
|
+
tenantId: user.tenantId,
|
|
42
|
+
organizationId: user.organizationId
|
|
43
|
+
});
|
|
42
44
|
const ip = getClientIp(req, 0);
|
|
43
45
|
const userAgent = req.headers.get("user-agent") || null;
|
|
44
46
|
const { rawToken, jwt } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/magic-link/verify.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkVerifySchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = magicLinkVerifySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid token' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const result = await customerTokenService.verifyEmailToken(parsed.data.token, 'magic_link')\n if (!result) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired token' }, { status: 400 })\n }\n\n const user = await customerUserService.findById(result.userId, result.tenantId)\n if (!user || !user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account not found or deactivated' }, { status: 401 })\n }\n\n // Auto-verify email on magic link login\n if (!user.emailVerifiedAt) {\n await em.nativeUpdate(CustomerUser, { id: user.id }, { emailVerifiedAt: new Date() })\n user.emailVerifiedAt = new Date()\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,6BAA6B;AACtC,SAAS,8BAA8B;AAKvC,SAAS,oBAAoB;AAC7B,SAAS,iCAAiC;AAC1C,SAAS,mBAAmB;AAErB,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,SAAS,sBAAsB,UAAU,IAAI;AACnD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,gBAAgB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,uBAAuB,UAAU,QAAQ,sBAAsB;AACrE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,SAAS,MAAM,qBAAqB,iBAAiB,OAAO,KAAK,OAAO,YAAY;AAC1F,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5F;AAEA,QAAM,OAAO,MAAM,oBAAoB,SAAS,OAAO,QAAQ,OAAO,QAAQ;AAC9E,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,mCAAmC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpG;AAGA,MAAI,CAAC,KAAK,iBAAiB;AACzB,UAAM,GAAG,aAAa,cAAc,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,iBAAiB,oBAAI,KAAK,EAAE,CAAC;AACpF,SAAK,kBAAkB,oBAAI,KAAK;AAAA,EAClC;AAEA,QAAM,oBAAoB,oBAAoB,IAAI;AAClD,QAAM,oBAAoB,kBAAkB,IAAI;AAEhD,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { magicLinkVerifySchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport { getClientIp } from '@open-mercato/shared/lib/ratelimit/helpers'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = magicLinkVerifySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid token' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const result = await customerTokenService.verifyEmailToken(parsed.data.token, 'magic_link')\n if (!result) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired token' }, { status: 400 })\n }\n\n const user = await customerUserService.findById(result.userId, result.tenantId)\n if (!user || !user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account not found or deactivated' }, { status: 401 })\n }\n\n // Auto-verify email on magic link login\n if (!user.emailVerifiedAt) {\n await em.nativeUpdate(CustomerUser, { id: user.id }, { emailVerifiedAt: new Date() })\n user.emailVerifiedAt = new Date()\n }\n\n await customerUserService.resetFailedAttempts(user)\n await customerUserService.updateLastLoginAt(user)\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n })\n\n const ip = getClientIp(req, 0)\n const userAgent = req.headers.get('user-agent') || null\n const { rawToken, jwt } = await customerSessionService.createSession(user, resolvedFeatures, ip, userAgent)\n\n void emitCustomerAccountsEvent('customer_accounts.login.success', {\n id: user.id,\n email: user.email,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }).catch(() => undefined)\n\n const res = NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: true,\n },\n resolvedFeatures,\n })\n\n res.cookies.set('customer_auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n res.cookies.set('customer_session_token', rawToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 24 * 30,\n })\n\n return res\n}\n\nconst loginSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string().email(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n }),\n resolvedFeatures: z.array(z.string()),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Verify magic link token',\n description: 'Validates the magic link token, auto-verifies email, and creates a session.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: magicLinkVerifySchema,\n description: 'Magic link verification token.',\n },\n responses: [\n { status: 200, description: 'Login successful', schema: loginSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid or expired token', schema: errorSchema },\n { status: 401, description: 'Account not found', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Verify customer magic link',\n description: 'Handles magic link verification and auto-login.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getCustomerAuthFromRequest } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
-
import {
|
|
5
|
+
import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
|
|
6
6
|
const metadata = { requireAuth: false };
|
|
7
7
|
const requestSchema = z.object({
|
|
8
8
|
features: z.array(z.string()).min(1).max(100)
|
|
@@ -25,9 +25,10 @@ async function POST(req) {
|
|
|
25
25
|
tenantId: auth.tenantId,
|
|
26
26
|
organizationId: auth.orgId
|
|
27
27
|
});
|
|
28
|
-
const granted =
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const granted = body.features.filter((feature) => authorizeFeatures([feature], {
|
|
29
|
+
grantedFeatures: acl.features,
|
|
30
|
+
unrestricted: acl.isPortalAdmin
|
|
31
|
+
}));
|
|
31
32
|
return NextResponse.json({ ok: true, granted });
|
|
32
33
|
}
|
|
33
34
|
const methodDoc = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/feature-check.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C,SAAS,8BAA8B;AAEvC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nconst requestSchema = z.object({\n features: z.array(z.string()).min(1).max(100),\n})\n\nexport async function POST(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n let body: z.infer<typeof requestSchema>\n try {\n const raw = await req.json()\n body = requestSchema.parse(raw)\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n const acl = await customerRbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const granted = body.features.filter((feature) => authorizeFeatures([feature], {\n grantedFeatures: acl.features,\n unrestricted: acl.isPortalAdmin,\n }))\n\n return NextResponse.json({ ok: true, granted })\n}\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Check customer portal feature access',\n description: 'Checks which of the requested features the authenticated customer user has. Used by portal menu injection for feature-gating.',\n tags: ['Customer Portal'],\n requestBody: {\n schema: requestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Feature check result',\n schema: z.object({\n ok: z.literal(true),\n granted: z.array(z.string()),\n }),\n },\n ],\n errors: [\n { status: 401, description: 'Not authenticated', schema: z.object({ ok: z.literal(false), error: z.string() }) },\n { status: 400, description: 'Invalid request', schema: z.object({ ok: z.literal(false), error: z.string() }) },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Portal feature check',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C,SAAS,8BAA8B;AAEvC,SAAS,yBAAyB;AAE3B,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAC9C,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,WAAO,cAAc,MAAM,GAAG;AAAA,EAChC,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uBAAuB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,MAAM,MAAM,oBAAoB,QAAQ,KAAK,KAAK;AAAA,IACtD,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,QAAM,UAAU,KAAK,SAAS,OAAO,CAAC,YAAY,kBAAkB,CAAC,OAAO,GAAG;AAAA,IAC7E,iBAAiB,IAAI;AAAA,IACrB,cAAc,IAAI;AAAA,EACpB,CAAC,CAAC;AAEF,SAAO,aAAa,KAAK,EAAE,IAAI,MAAM,QAAQ,CAAC;AAChD;AAEA,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,iBAAiB;AAAA,EACxB,aAAa;AAAA,IACX,QAAQ;AAAA,EACV;AAAA,EACA,WAAW;AAAA,IACT;AAAA,MACE,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,QAAQ,EAAE,OAAO;AAAA,QACf,IAAI,EAAE,QAAQ,IAAI;AAAA,QAClB,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,IAC/G,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,EAC/G;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,UAAU;AAC7B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -40,12 +40,14 @@ async function GET(req) {
|
|
|
40
40
|
return NextResponse.json({ ok: false, error: "Organization not found" }, { status: 404 });
|
|
41
41
|
}
|
|
42
42
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
43
|
-
const grantedFeatures =
|
|
43
|
+
const grantedFeatures = await rbac.getEffectiveFeatures(auth.sub, {
|
|
44
|
+
tenantId: auth.tenantId,
|
|
45
|
+
organizationId: auth.orgId
|
|
46
|
+
});
|
|
44
47
|
const groups = buildPortalNav({
|
|
45
48
|
routes: getFrontendRouteManifests(),
|
|
46
49
|
orgSlug,
|
|
47
|
-
grantedFeatures
|
|
48
|
-
isPortalAdmin: acl.isPortalAdmin
|
|
50
|
+
grantedFeatures
|
|
49
51
|
});
|
|
50
52
|
return NextResponse.json({
|
|
51
53
|
ok: true,
|
|
@@ -57,7 +59,7 @@ async function GET(req) {
|
|
|
57
59
|
}
|
|
58
60
|
const getMethodDoc = {
|
|
59
61
|
summary: "Portal sidebar navigation",
|
|
60
|
-
description: "Returns the portal sidebar for the authenticated customer. Items are derived from each portal page's `nav` metadata and filtered by `requireCustomerFeatures` against
|
|
62
|
+
description: "Returns the portal sidebar for the authenticated customer. Items are derived from each portal page's `nav` metadata and filtered by `requireCustomerFeatures` against concrete effective features.",
|
|
61
63
|
tags: ["Customer Portal"],
|
|
62
64
|
responses: [{ status: 200, description: "Portal sidebar groups", schema: navResponseSchema }],
|
|
63
65
|
errors: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/nav.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getFrontendRouteManifests } from '@open-mercato/shared/modules/registry'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { buildPortalNav } from '@open-mercato/ui/portal/utils/nav'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nconst navItemSchema = z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n href: z.string(),\n icon: z.string().optional(),\n order: z.number(),\n})\n\nconst navGroupSchema = z.object({\n id: z.enum(['main', 'account']),\n items: z.array(navItemSchema),\n})\n\nconst navResponseSchema = z.object({\n ok: z.literal(true),\n orgSlug: z.string(),\n groups: z.array(navGroupSchema),\n grantedFeatures: z.array(z.string()),\n isPortalAdmin: z.boolean(),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbac = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const org = await findOrganizationInTenant(em, auth.orgId, auth.tenantId)\n const orgSlug = org?.slug ?? ''\n if (!orgSlug) {\n return NextResponse.json({ ok: false, error: 'Organization not found' }, { status: 404 })\n }\n\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId, organizationId: auth.orgId })\n const grantedFeatures =
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAE1C,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAExB,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO;AAAA,EACb,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAM,EAAE,OAAO;AAAA,EACf,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,KAAK,CAAC,QAAQ,SAAS,CAAC;AAAA,EAC9B,OAAO,EAAE,MAAM,aAAa;AAC9B,CAAC;AAED,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,SAAS,EAAE,OAAO;AAAA,EAClB,QAAQ,EAAE,MAAM,cAAc;AAAA,EAC9B,iBAAiB,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,EACnC,eAAe,EAAE,QAAQ;AAC3B,CAAC;AAED,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAExE,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAQ,qBAAqB;AACpD,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,MAAM,MAAM,yBAAyB,IAAI,KAAK,OAAO,KAAK,QAAQ;AACxE,QAAM,UAAU,KAAK,QAAQ;AAC7B,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AAEA,QAAM,MAAM,MAAM,KAAK,QAAQ,KAAK,KAAK,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM,CAAC;AAChG,QAAM,kBAAkB,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getFrontendRouteManifests } from '@open-mercato/shared/modules/registry'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { buildPortalNav } from '@open-mercato/ui/portal/utils/nav'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nconst navItemSchema = z.object({\n id: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n href: z.string(),\n icon: z.string().optional(),\n order: z.number(),\n})\n\nconst navGroupSchema = z.object({\n id: z.enum(['main', 'account']),\n items: z.array(navItemSchema),\n})\n\nconst navResponseSchema = z.object({\n ok: z.literal(true),\n orgSlug: z.string(),\n groups: z.array(navGroupSchema),\n grantedFeatures: z.array(z.string()),\n isPortalAdmin: z.boolean(),\n})\n\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nexport async function GET(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbac = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const org = await findOrganizationInTenant(em, auth.orgId, auth.tenantId)\n const orgSlug = org?.slug ?? ''\n if (!orgSlug) {\n return NextResponse.json({ ok: false, error: 'Organization not found' }, { status: 404 })\n }\n\n const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId, organizationId: auth.orgId })\n const grantedFeatures = await rbac.getEffectiveFeatures(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const groups = buildPortalNav({\n routes: getFrontendRouteManifests(),\n orgSlug,\n grantedFeatures,\n })\n\n return NextResponse.json({\n ok: true,\n orgSlug,\n groups,\n grantedFeatures,\n isPortalAdmin: acl.isPortalAdmin,\n })\n}\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'Portal sidebar navigation',\n description:\n 'Returns the portal sidebar for the authenticated customer. Items are derived from each portal page\\'s `nav` metadata and filtered by `requireCustomerFeatures` against concrete effective features.',\n tags: ['Customer Portal'],\n responses: [{ status: 200, description: 'Portal sidebar groups', schema: navResponseSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 404, description: 'Organization not found', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Portal navigation',\n methods: { GET: getMethodDoc },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,kCAAkC;AAC3C,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAE1C,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAExB,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,EAAE,OAAO;AAAA,EACb,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAM,EAAE,OAAO;AAAA,EACf,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,OAAO,EAAE,OAAO;AAClB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,KAAK,CAAC,QAAQ,SAAS,CAAC;AAAA,EAC9B,OAAO,EAAE,MAAM,aAAa;AAC9B,CAAC;AAED,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,SAAS,EAAE,OAAO;AAAA,EAClB,QAAQ,EAAE,MAAM,cAAc;AAAA,EAC9B,iBAAiB,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,EACnC,eAAe,EAAE,QAAQ;AAC3B,CAAC;AAED,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAExE,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,UAAU,QAAQ,qBAAqB;AACpD,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,MAAM,MAAM,yBAAyB,IAAI,KAAK,OAAO,KAAK,QAAQ;AACxE,QAAM,UAAU,KAAK,QAAQ;AAC7B,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AAEA,QAAM,MAAM,MAAM,KAAK,QAAQ,KAAK,KAAK,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM,CAAC;AAChG,QAAM,kBAAkB,MAAM,KAAK,qBAAqB,KAAK,KAAK;AAAA,IAChE,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AAED,QAAM,SAAS,eAAe;AAAA,IAC5B,QAAQ,0BAA0B;AAAA,IAClC;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,aAAa,KAAK;AAAA,IACvB,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,IAAI;AAAA,EACrB,CAAC;AACH;AAEA,MAAM,eAAiC;AAAA,EACrC,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,iBAAiB;AAAA,EACxB,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,kBAAkB,CAAC;AAAA,EAC5F,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,YAAY;AAAA,IACrE,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,EAC5E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,KAAK,aAAa;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -19,6 +19,10 @@ async function GET(req) {
|
|
|
19
19
|
return NextResponse.json({ ok: false, error: "User not found" }, { status: 404 });
|
|
20
20
|
}
|
|
21
21
|
const acl = await customerRbacService.loadAcl(user.id, { tenantId: user.tenantId, organizationId: user.organizationId });
|
|
22
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
23
|
+
tenantId: user.tenantId,
|
|
24
|
+
organizationId: user.organizationId
|
|
25
|
+
});
|
|
22
26
|
const userRoles = await em.find(CustomerUserRole, {
|
|
23
27
|
user: user.id,
|
|
24
28
|
deletedAt: null
|
|
@@ -42,7 +46,7 @@ async function GET(req) {
|
|
|
42
46
|
createdAt: user.createdAt
|
|
43
47
|
},
|
|
44
48
|
roles,
|
|
45
|
-
resolvedFeatures
|
|
49
|
+
resolvedFeatures,
|
|
46
50
|
isPortalAdmin: acl.isPortalAdmin
|
|
47
51
|
});
|
|
48
52
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/profile.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { profileUpdateSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function GET(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const user = await customerUserService.findById(auth.sub, auth.tenantId)\n if (!user) {\n return NextResponse.json({ ok: false, error: 'User not found' }, { status: 404 })\n }\n\n const acl = await customerRbacService.loadAcl(user.id, { tenantId: user.tenantId, organizationId: user.organizationId })\n\n const userRoles = await em.find(CustomerUserRole, {\n user: user.id as any,\n deletedAt: null,\n }, { populate: ['role'] })\n const roles = userRoles.map((ur) => ({\n id: (ur.role as any).id,\n name: (ur.role as any).name,\n slug: (ur.role as any).slug,\n }))\n\n return NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n customerEntityId: user.customerEntityId,\n personEntityId: user.personEntityId,\n isActive: user.isActive,\n lastLoginAt: user.lastLoginAt,\n createdAt: user.createdAt,\n },\n roles,\n resolvedFeatures
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,4BAA4B,8BAA8B;AACnE,SAAS,8BAA8B;AAGvC,SAAS,wBAAwB;AACjC,SAAS,2BAA2B;AAE7B,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC3F;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AACnE,QAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,QAAM,OAAO,MAAM,oBAAoB,SAAS,KAAK,KAAK,KAAK,QAAQ;AACvE,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAClF;AAEA,QAAM,MAAM,MAAM,oBAAoB,QAAQ,KAAK,IAAI,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,eAAe,CAAC;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getCustomerAuthFromRequest, requireCustomerFeature } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { profileUpdateSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function GET(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const user = await customerUserService.findById(auth.sub, auth.tenantId)\n if (!user) {\n return NextResponse.json({ ok: false, error: 'User not found' }, { status: 404 })\n }\n\n const acl = await customerRbacService.loadAcl(user.id, { tenantId: user.tenantId, organizationId: user.organizationId })\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n })\n\n const userRoles = await em.find(CustomerUserRole, {\n user: user.id as any,\n deletedAt: null,\n }, { populate: ['role'] })\n const roles = userRoles.map((ur) => ({\n id: (ur.role as any).id,\n name: (ur.role as any).name,\n slug: (ur.role as any).slug,\n }))\n\n return NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n emailVerified: !!user.emailVerifiedAt,\n customerEntityId: user.customerEntityId,\n personEntityId: user.personEntityId,\n isActive: user.isActive,\n lastLoginAt: user.lastLoginAt,\n createdAt: user.createdAt,\n },\n roles,\n resolvedFeatures,\n isPortalAdmin: acl.isPortalAdmin,\n })\n}\n\nexport async function PUT(req: Request) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n try {\n await requireCustomerFeature(auth, ['portal.account.manage'], customerRbacService)\n } catch (response) {\n return response as NextResponse\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = profileUpdateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed' }, { status: 400 })\n }\n\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n\n const user = await customerUserService.findById(auth.sub, auth.tenantId)\n if (!user) {\n return NextResponse.json({ ok: false, error: 'User not found' }, { status: 404 })\n }\n\n await customerUserService.updateProfile(user, parsed.data)\n\n return NextResponse.json({\n ok: true,\n user: {\n id: user.id,\n email: user.email,\n displayName: user.displayName,\n },\n })\n}\n\nconst profileSchema = z.object({\n ok: z.literal(true),\n user: z.object({\n id: z.string().uuid(),\n email: z.string(),\n displayName: z.string(),\n emailVerified: z.boolean(),\n customerEntityId: z.string().uuid().nullable(),\n personEntityId: z.string().uuid().nullable(),\n isActive: z.boolean(),\n lastLoginAt: z.string().datetime().nullable(),\n createdAt: z.string().datetime(),\n }),\n roles: z.array(z.object({ id: z.string().uuid(), name: z.string(), slug: z.string() })),\n resolvedFeatures: z.array(z.string()),\n isPortalAdmin: z.boolean(),\n})\n\nconst putSuccessSchema = z.object({\n ok: z.literal(true),\n user: z.object({ id: z.string().uuid(), email: z.string(), displayName: z.string() }),\n})\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst getMethodDoc: OpenApiMethodDoc = {\n summary: 'Get customer profile',\n description: 'Returns the authenticated customer user profile with roles and permissions.',\n tags: ['Customer Portal'],\n responses: [{ status: 200, description: 'Profile data', schema: profileSchema }],\n errors: [{ status: 401, description: 'Not authenticated', schema: errorSchema }],\n}\n\nconst putMethodDoc: OpenApiMethodDoc = {\n summary: 'Update customer profile',\n description: 'Updates the authenticated customer user profile.',\n tags: ['Customer Portal'],\n requestBody: { schema: profileUpdateSchema },\n responses: [{ status: 200, description: 'Profile updated', schema: putSuccessSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer profile',\n methods: { GET: getMethodDoc, PUT: putMethodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -26,15 +26,15 @@ async function POST(req) {
|
|
|
26
26
|
if (!user || user.deletedAt || !user.isActive) {
|
|
27
27
|
return NextResponse.json({ ok: false, error: "Account not active" }, { status: 401 });
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {
|
|
30
30
|
tenantId: user.tenantId,
|
|
31
31
|
organizationId: user.organizationId
|
|
32
32
|
});
|
|
33
|
-
const result = await customerSessionService.refreshSession(decodedToken,
|
|
33
|
+
const result = await customerSessionService.refreshSession(decodedToken, resolvedFeatures);
|
|
34
34
|
if (!result) {
|
|
35
35
|
return NextResponse.json({ ok: false, error: "Session refresh failed" }, { status: 401 });
|
|
36
36
|
}
|
|
37
|
-
const res = NextResponse.json({ ok: true, resolvedFeatures
|
|
37
|
+
const res = NextResponse.json({ ok: true, resolvedFeatures });
|
|
38
38
|
res.cookies.set("customer_auth_token", result.jwt, {
|
|
39
39
|
httpOnly: true,
|
|
40
40
|
path: "/",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customer_accounts/api/portal/sessions-refresh.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { readCookieFromHeader } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const cookieHeader = req.headers.get('cookie') || ''\n const sessionToken = readCookieFromHeader(cookieHeader, 'customer_session_token')\n if (!sessionToken) {\n return NextResponse.json({ ok: false, error: 'No session token' }, { status: 401 })\n }\n\n let decodedToken: string\n try {\n decodedToken = decodeURIComponent(sessionToken)\n } catch {\n decodedToken = sessionToken\n }\n\n const container = await createRequestContainer()\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const session = await customerSessionService.findByToken(decodedToken)\n if (!session) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired session' }, { status: 401 })\n }\n\n const user = session.user as any\n if (!user || user.deletedAt || !user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account not active' }, { status: 401 })\n }\n\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AAE9B,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,eAAe,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAClD,QAAM,eAAe,qBAAqB,cAAc,wBAAwB;AAChF,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpF;AAEA,MAAI;AACJ,MAAI;AACF,mBAAe,mBAAmB,YAAY;AAAA,EAChD,QAAQ;AACN,mBAAe;AAAA,EACjB;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AAEnE,QAAM,UAAU,MAAM,uBAAuB,YAAY,YAAY;AACrE,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9F;AAEA,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,QAAQ,KAAK,aAAa,CAAC,KAAK,UAAU;AAC7C,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACtF;AAEA,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { readCookieFromHeader } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\nexport async function POST(req: Request) {\n const cookieHeader = req.headers.get('cookie') || ''\n const sessionToken = readCookieFromHeader(cookieHeader, 'customer_session_token')\n if (!sessionToken) {\n return NextResponse.json({ ok: false, error: 'No session token' }, { status: 401 })\n }\n\n let decodedToken: string\n try {\n decodedToken = decodeURIComponent(sessionToken)\n } catch {\n decodedToken = sessionToken\n }\n\n const container = await createRequestContainer()\n const customerSessionService = container.resolve('customerSessionService') as CustomerSessionService\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n\n const session = await customerSessionService.findByToken(decodedToken)\n if (!session) {\n return NextResponse.json({ ok: false, error: 'Invalid or expired session' }, { status: 401 })\n }\n\n const user = session.user as any\n if (!user || user.deletedAt || !user.isActive) {\n return NextResponse.json({ ok: false, error: 'Account not active' }, { status: 401 })\n }\n\n const resolvedFeatures = await customerRbacService.getEffectiveFeatures(user.id, {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n })\n\n const result = await customerSessionService.refreshSession(decodedToken, resolvedFeatures)\n if (!result) {\n return NextResponse.json({ ok: false, error: 'Session refresh failed' }, { status: 401 })\n }\n\n const res = NextResponse.json({ ok: true, resolvedFeatures })\n\n res.cookies.set('customer_auth_token', result.jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n\n return res\n}\n\nconst successSchema = z.object({ ok: z.literal(true), resolvedFeatures: z.array(z.string()) })\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Refresh customer JWT from session token',\n description: 'Uses the session cookie to issue a fresh JWT access token.',\n tags: ['Customer Portal'],\n responses: [{ status: 200, description: 'Token refreshed', schema: successSchema }],\n errors: [{ status: 401, description: 'Invalid session', schema: errorSchema }],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Refresh customer session',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,8BAA8B;AAGvC,SAAS,4BAA4B;AAE9B,MAAM,WAAqD,EAAE,aAAa,MAAM;AAEvF,eAAsB,KAAK,KAAc;AACvC,QAAM,eAAe,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAClD,QAAM,eAAe,qBAAqB,cAAc,wBAAwB;AAChF,MAAI,CAAC,cAAc;AACjB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpF;AAEA,MAAI;AACJ,MAAI;AACF,mBAAe,mBAAmB,YAAY;AAAA,EAChD,QAAQ;AACN,mBAAe;AAAA,EACjB;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,yBAAyB,UAAU,QAAQ,wBAAwB;AACzE,QAAM,sBAAsB,UAAU,QAAQ,qBAAqB;AAEnE,QAAM,UAAU,MAAM,uBAAuB,YAAY,YAAY;AACrE,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9F;AAEA,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,QAAQ,KAAK,aAAa,CAAC,KAAK,UAAU;AAC7C,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,qBAAqB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACtF;AAEA,QAAM,mBAAmB,MAAM,oBAAoB,qBAAqB,KAAK,IAAI;AAAA,IAC/E,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AAED,QAAM,SAAS,MAAM,uBAAuB,eAAe,cAAc,gBAAgB;AACzF,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AAEA,QAAM,MAAM,aAAa,KAAK,EAAE,IAAI,MAAM,iBAAiB,CAAC;AAE5D,MAAI,QAAQ,IAAI,uBAAuB,OAAO,KAAK;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ,KAAK,KAAK;AAAA,EACpB,CAAC;AAED,SAAO;AACT;AAEA,MAAM,gBAAgB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,GAAG,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAC7F,MAAM,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC;AAExE,MAAM,YAA8B;AAAA,EAClC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,iBAAiB;AAAA,EACxB,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,cAAc,CAAC;AAAA,EAClF,QAAQ,CAAC,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,YAAY,CAAC;AAC/E;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,UAAU;AAC7B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -46,7 +46,11 @@ async function validateUserState(sub, tenantId, orgId, iat) {
|
|
|
46
46
|
const { CustomerRbacService } = await import("@open-mercato/core/modules/customer_accounts/services/customerRbacService");
|
|
47
47
|
const rbac = container.resolve("customerRbacService");
|
|
48
48
|
const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId });
|
|
49
|
-
|
|
49
|
+
const resolvedFeatures = await rbac.getEffectiveFeatures(sub, {
|
|
50
|
+
tenantId,
|
|
51
|
+
organizationId: orgId
|
|
52
|
+
});
|
|
53
|
+
return { valid: true, resolvedFeatures, isPortalAdmin: acl.isPortalAdmin };
|
|
50
54
|
}
|
|
51
55
|
async function getCustomerAuthFromRequest(req) {
|
|
52
56
|
const cookieHeader = req.headers.get("cookie") || "";
|
|
@@ -95,7 +99,8 @@ async function getCustomerAuthFromRequest(req) {
|
|
|
95
99
|
displayName: String(payload.displayName || ""),
|
|
96
100
|
customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,
|
|
97
101
|
personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,
|
|
98
|
-
resolvedFeatures: userState.resolvedFeatures
|
|
102
|
+
resolvedFeatures: userState.resolvedFeatures,
|
|
103
|
+
isPortalAdmin: userState.isPortalAdmin
|
|
99
104
|
};
|
|
100
105
|
} catch {
|
|
101
106
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerAuth.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { verifyAudienceJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\n\nexport interface CustomerAuthContext {\n sub: string\n sid: string\n type: 'customer'\n tenantId: string\n orgId: string\n email: string\n displayName: string\n customerEntityId?: string | null\n personEntityId?: string | null\n resolvedFeatures: string[]\n}\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/services/customerSessionService'),\n ])\n const container = await createRequestContainer()\n const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n const session = await service.findActiveSessionById(sessionId)\n return session !== null\n } catch {\n // Fail closed: if we cannot verify the session, treat the token as revoked to prevent\n // replay of leaked JWTs when the backend is partially degraded.\n return false\n }\n}\n\nexport function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nexport type UserValidationResult =\n | { valid: false }\n | { valid: true; resolvedFeatures: string[] }\n\nexport async function validateUserState(\n sub: string,\n tenantId: string,\n orgId: string,\n iat: unknown,\n): Promise<UserValidationResult> {\n const [{ createRequestContainer }, { CustomerUser }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/data/entities'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const user = await findOneWithDecryption(em, CustomerUser, { id: sub }, {\n fields: ['sessionsRevokedAt', 'deletedAt', 'isActive'],\n })\n if (!user) return { valid: false }\n if (user.deletedAt) return { valid: false }\n if (!user.isActive) return { valid: false }\n if (user.sessionsRevokedAt && typeof iat === 'number' && iat * 1000 < user.sessionsRevokedAt.getTime()) {\n return { valid: false }\n }\n\n const { CustomerRbacService } = await import(\n '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\n )\n const rbac = container.resolve('customerRbacService') as InstanceType<typeof CustomerRbacService>\n const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId })\n return { valid: true, resolvedFeatures
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB,iBAAiB;AAE7C,SAAS,6BAA6B;AACtC,SAAS,6BAA6B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { verifyAudienceJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport type { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CUSTOMER_JWT_AUDIENCE } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\n\nexport interface CustomerAuthContext {\n sub: string\n sid: string\n type: 'customer'\n tenantId: string\n orgId: string\n email: string\n displayName: string\n customerEntityId?: string | null\n personEntityId?: string | null\n resolvedFeatures: string[]\n isPortalAdmin?: boolean\n}\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const [{ createRequestContainer }, { CustomerSessionService }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/services/customerSessionService'),\n ])\n const container = await createRequestContainer()\n const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n const session = await service.findActiveSessionById(sessionId)\n return session !== null\n } catch {\n // Fail closed: if we cannot verify the session, treat the token as revoked to prevent\n // replay of leaked JWTs when the backend is partially degraded.\n return false\n }\n}\n\nexport function readCookieFromHeader(header: string | null | undefined, name: string): string | undefined {\n if (!header) return undefined\n const parts = header.split(';')\n for (const part of parts) {\n const trimmed = part.trim()\n if (trimmed.startsWith(`${name}=`)) {\n return trimmed.slice(name.length + 1)\n }\n }\n return undefined\n}\n\nexport type UserValidationResult =\n | { valid: false }\n | { valid: true; resolvedFeatures: string[]; isPortalAdmin: boolean }\n\nexport async function validateUserState(\n sub: string,\n tenantId: string,\n orgId: string,\n iat: unknown,\n): Promise<UserValidationResult> {\n const [{ createRequestContainer }, { CustomerUser }] = await Promise.all([\n import('@open-mercato/shared/lib/di/container'),\n import('@open-mercato/core/modules/customer_accounts/data/entities'),\n ])\n const container = await createRequestContainer()\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const user = await findOneWithDecryption(em, CustomerUser, { id: sub }, {\n fields: ['sessionsRevokedAt', 'deletedAt', 'isActive'],\n })\n if (!user) return { valid: false }\n if (user.deletedAt) return { valid: false }\n if (!user.isActive) return { valid: false }\n if (user.sessionsRevokedAt && typeof iat === 'number' && iat * 1000 < user.sessionsRevokedAt.getTime()) {\n return { valid: false }\n }\n\n const { CustomerRbacService } = await import(\n '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\n )\n const rbac = container.resolve('customerRbacService') as InstanceType<typeof CustomerRbacService>\n const acl = await rbac.loadAcl(sub, { tenantId, organizationId: orgId })\n const resolvedFeatures = await rbac.getEffectiveFeatures(sub, {\n tenantId,\n organizationId: orgId,\n })\n return { valid: true, resolvedFeatures, isPortalAdmin: acl.isPortalAdmin }\n}\n\nexport async function getCustomerAuthFromRequest(req: Request): Promise<CustomerAuthContext | null> {\n const cookieHeader = req.headers.get('cookie') || ''\n const authHeader = (req.headers.get('authorization') || '').trim()\n\n let token: string | undefined\n if (authHeader.toLowerCase().startsWith('bearer ')) {\n token = authHeader.slice(7).trim()\n }\n if (!token) {\n const cookieValue = readCookieFromHeader(cookieHeader, 'customer_auth_token')\n if (cookieValue) {\n try {\n token = decodeURIComponent(cookieValue)\n } catch {\n // Malformed percent-encoding; use raw value\n token = cookieValue\n }\n }\n }\n if (!token) return null\n\n try {\n let payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n // Legacy fallback: try raw JWT_SECRET for pre-migration customer tokens\n if (!payload) {\n payload = verifyJwt(token) as Record<string, unknown> | null\n if (payload) payload._legacyToken = true\n }\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid && payload._legacyToken !== true) return null\n const stillActive = sid ? await assertSessionStillActive(sid) : true\n if (!stillActive) return null\n\n const userState = await validateUserState(\n String(payload.sub),\n String(payload.tenantId),\n String(payload.orgId),\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: String(payload.sub),\n sid,\n type: 'customer',\n tenantId: String(payload.tenantId),\n orgId: String(payload.orgId),\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n isPortalAdmin: userState.isPortalAdmin,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\nexport async function requireCustomerAuth(req: Request): Promise<CustomerAuthContext> {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n throw NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n return auth\n}\n\nexport async function requireCustomerFeature(\n auth: CustomerAuthContext,\n features: string[],\n rbac: CustomerRbacService,\n): Promise<void> {\n if (!features.length) return\n const ok = await rbac.userHasAllFeatures(\n auth.sub,\n features,\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!ok) {\n throw NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -50,7 +50,8 @@ async function getCustomerAuthFromCookies(options) {
|
|
|
50
50
|
displayName: String(payload.displayName || ""),
|
|
51
51
|
customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,
|
|
52
52
|
personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,
|
|
53
|
-
resolvedFeatures: userState.resolvedFeatures
|
|
53
|
+
resolvedFeatures: userState.resolvedFeatures,
|
|
54
|
+
isPortalAdmin: userState.isPortalAdmin
|
|
54
55
|
};
|
|
55
56
|
} catch {
|
|
56
57
|
return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/customerAuthServer.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Server-side customer auth helpers for Next.js server components.\n *\n * Uses the `cookies()` API from `next/headers` to read the customer auth token\n * from cookies \u2014 analogous to `getAuthFromCookies()` for staff auth.\n */\n\nimport { cookies } from 'next/headers'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { verifyAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { CUSTOMER_JWT_AUDIENCE, CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { validateUserState } from './customerAuth'\nimport type { CustomerAuthContext } from './customerAuth'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'customer-auth-server' })\n\nexport type { CustomerAuthContext }\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const container = await createRequestContainer()\n const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n const session = await service.findActiveSessionById(sessionId)\n return session !== null\n } catch {\n return false\n }\n}\n\n/**\n * Read and verify customer auth from cookies in server components.\n *\n * Returns the customer auth context if a valid customer JWT is found,\n * or null if not authenticated.\n *\n * @example\n * ```tsx\n * // In a server component or catch-all route\n * import { getCustomerAuthFromCookies } from '@open-mercato/core/modules/customer_accounts/lib/customerAuthServer'\n *\n * const customerAuth = await getCustomerAuthFromCookies()\n * if (!customerAuth) redirect('/login')\n * ```\n */\nexport async function getCustomerAuthFromCookies(\n options?: { expectedTenantId?: string },\n): Promise<CustomerAuthContext | null> {\n const cookieStore = await cookies()\n const token = cookieStore.get('customer_auth_token')?.value\n if (!token) return null\n\n try {\n const payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid) return null\n const tenantId = String(payload.tenantId)\n if (options?.expectedTenantId && options.expectedTenantId !== tenantId) {\n // Cross-host JWT replay defense. See spec rev 5 Customer Authentication\n // section: the host-resolved tenant is the authoritative scope; mismatched\n // JWTs are rejected as if unauthenticated.\n return null\n }\n const stillActive = await assertSessionStillActive(sid)\n if (!stillActive) return null\n\n const userState = await validateUserState(\n String(payload.sub),\n tenantId,\n String(payload.orgId),\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: String(payload.sub),\n sid,\n type: 'customer',\n tenantId,\n orgId: String(payload.orgId),\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\n/**\n * Convenience wrapper for custom-domain pages: resolves the host, calls\n * getCustomerAuthFromCookies with the host-resolved tenant as expectedTenantId.\n *\n * Use from server components rendered under custom domains.\n */\nexport async function getCustomerAuthForHost(\n host: string | null | undefined,\n): Promise<CustomerAuthContext | null> {\n if (!host) return getCustomerAuthFromCookies()\n\n try {\n const hostname = tryNormalizeHostname(host)\n if (!hostname) return getCustomerAuthFromCookies()\n if (platformDomains().includes(hostname)) return getCustomerAuthFromCookies()\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as InstanceType<typeof DomainMappingService>\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') return null\n return getCustomerAuthFromCookies({ expectedTenantId: resolved.tenantId })\n } catch (err) {\n logger.warn('Domain resolve failed; falling back to platform cookie', { err })\n return getCustomerAuthFromCookies()\n }\n}\n"],
|
|
5
|
-
"mappings": "AAOA,SAAS,eAAe;AACxB,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAClC,SAAS,6BAAqD;AAE9D,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,mBAAmB,EAAE,MAAM,EAAE,WAAW,uBAAuB,CAAC;AAI5F,eAAe,yBAAyB,WAAqC;AAC3E,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,wBAAwB;AAC1D,UAAM,UAAU,MAAM,QAAQ,sBAAsB,SAAS;AAC7D,WAAO,YAAY;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBA,eAAsB,2BACpB,SACqC;AACrC,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,qBAAqB,GAAG;AACtD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,UAAU,kBAAkB,uBAAuB,KAAK;AAC9D,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,SAAS,WAAY,QAAO;AACxC,UAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,WAAW,OAAO,QAAQ,QAAQ;AACxC,QAAI,SAAS,oBAAoB,QAAQ,qBAAqB,UAAU;AAItE,aAAO;AAAA,IACT;AACA,UAAM,cAAc,MAAM,yBAAyB,GAAG;AACtD,QAAI,CAAC,YAAa,QAAO;AAEzB,UAAM,YAAY,MAAM;AAAA,MACtB,OAAO,QAAQ,GAAG;AAAA,MAClB;AAAA,MACA,OAAO,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,IACV;AACA,QAAI,CAAC,UAAU,MAAO,QAAO;AAE7B,WAAO;AAAA,MACL,KAAK,OAAO,QAAQ,GAAG;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,OAAO,OAAO,QAAQ,SAAS,EAAE;AAAA,MACjC,aAAa,OAAO,QAAQ,eAAe,EAAE;AAAA,MAC7C,kBAAkB,QAAQ,mBAAmB,OAAO,QAAQ,gBAAgB,IAAI;AAAA,MAChF,gBAAgB,QAAQ,iBAAiB,OAAO,QAAQ,cAAc,IAAI;AAAA,MAC1E,kBAAkB,UAAU;AAAA,
|
|
4
|
+
"sourcesContent": ["/**\n * Server-side customer auth helpers for Next.js server components.\n *\n * Uses the `cookies()` API from `next/headers` to read the customer auth token\n * from cookies \u2014 analogous to `getAuthFromCookies()` for staff auth.\n */\n\nimport { cookies } from 'next/headers'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { verifyAudienceJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { CUSTOMER_JWT_AUDIENCE, CustomerSessionService } from '@open-mercato/core/modules/customer_accounts/services/customerSessionService'\nimport { DomainMappingService } from '@open-mercato/core/modules/customer_accounts/services/domainMappingService'\nimport { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'\nimport { platformDomains } from '@open-mercato/core/modules/customer_accounts/lib/platformDomains'\nimport { validateUserState } from './customerAuth'\nimport type { CustomerAuthContext } from './customerAuth'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'customer-auth-server' })\n\nexport type { CustomerAuthContext }\n\nasync function assertSessionStillActive(sessionId: string): Promise<boolean> {\n try {\n const container = await createRequestContainer()\n const service = container.resolve('customerSessionService') as InstanceType<typeof CustomerSessionService>\n const session = await service.findActiveSessionById(sessionId)\n return session !== null\n } catch {\n return false\n }\n}\n\n/**\n * Read and verify customer auth from cookies in server components.\n *\n * Returns the customer auth context if a valid customer JWT is found,\n * or null if not authenticated.\n *\n * @example\n * ```tsx\n * // In a server component or catch-all route\n * import { getCustomerAuthFromCookies } from '@open-mercato/core/modules/customer_accounts/lib/customerAuthServer'\n *\n * const customerAuth = await getCustomerAuthFromCookies()\n * if (!customerAuth) redirect('/login')\n * ```\n */\nexport async function getCustomerAuthFromCookies(\n options?: { expectedTenantId?: string },\n): Promise<CustomerAuthContext | null> {\n const cookieStore = await cookies()\n const token = cookieStore.get('customer_auth_token')?.value\n if (!token) return null\n\n try {\n const payload = verifyAudienceJwt(CUSTOMER_JWT_AUDIENCE, token) as Record<string, unknown> | null\n if (!payload) return null\n if (payload.type !== 'customer') return null\n const sid = typeof payload.sid === 'string' ? payload.sid : ''\n if (!sid) return null\n const tenantId = String(payload.tenantId)\n if (options?.expectedTenantId && options.expectedTenantId !== tenantId) {\n // Cross-host JWT replay defense. See spec rev 5 Customer Authentication\n // section: the host-resolved tenant is the authoritative scope; mismatched\n // JWTs are rejected as if unauthenticated.\n return null\n }\n const stillActive = await assertSessionStillActive(sid)\n if (!stillActive) return null\n\n const userState = await validateUserState(\n String(payload.sub),\n tenantId,\n String(payload.orgId),\n payload.iat,\n )\n if (!userState.valid) return null\n\n return {\n sub: String(payload.sub),\n sid,\n type: 'customer',\n tenantId,\n orgId: String(payload.orgId),\n email: String(payload.email || ''),\n displayName: String(payload.displayName || ''),\n customerEntityId: payload.customerEntityId ? String(payload.customerEntityId) : null,\n personEntityId: payload.personEntityId ? String(payload.personEntityId) : null,\n resolvedFeatures: userState.resolvedFeatures,\n isPortalAdmin: userState.isPortalAdmin,\n }\n } catch {\n // Invalid or expired JWT \u2014 treat as unauthenticated\n return null\n }\n}\n\n/**\n * Convenience wrapper for custom-domain pages: resolves the host, calls\n * getCustomerAuthFromCookies with the host-resolved tenant as expectedTenantId.\n *\n * Use from server components rendered under custom domains.\n */\nexport async function getCustomerAuthForHost(\n host: string | null | undefined,\n): Promise<CustomerAuthContext | null> {\n if (!host) return getCustomerAuthFromCookies()\n\n try {\n const hostname = tryNormalizeHostname(host)\n if (!hostname) return getCustomerAuthFromCookies()\n if (platformDomains().includes(hostname)) return getCustomerAuthFromCookies()\n\n const container = await createRequestContainer()\n const service = container.resolve('domainMappingService') as InstanceType<typeof DomainMappingService>\n const resolved = await service.resolveByHostname(hostname)\n if (!resolved || resolved.status !== 'active') return null\n return getCustomerAuthFromCookies({ expectedTenantId: resolved.tenantId })\n } catch (err) {\n logger.warn('Domain resolve failed; falling back to platform cookie', { err })\n return getCustomerAuthFromCookies()\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,eAAe;AACxB,SAAS,8BAA8B;AACvC,SAAS,yBAAyB;AAClC,SAAS,6BAAqD;AAE9D,SAAS,4BAA4B;AACrC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAElC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,mBAAmB,EAAE,MAAM,EAAE,WAAW,uBAAuB,CAAC;AAI5F,eAAe,yBAAyB,WAAqC;AAC3E,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,wBAAwB;AAC1D,UAAM,UAAU,MAAM,QAAQ,sBAAsB,SAAS;AAC7D,WAAO,YAAY;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAiBA,eAAsB,2BACpB,SACqC;AACrC,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,QAAQ,YAAY,IAAI,qBAAqB,GAAG;AACtD,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI;AACF,UAAM,UAAU,kBAAkB,uBAAuB,KAAK;AAC9D,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,SAAS,WAAY,QAAO;AACxC,UAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,WAAW,OAAO,QAAQ,QAAQ;AACxC,QAAI,SAAS,oBAAoB,QAAQ,qBAAqB,UAAU;AAItE,aAAO;AAAA,IACT;AACA,UAAM,cAAc,MAAM,yBAAyB,GAAG;AACtD,QAAI,CAAC,YAAa,QAAO;AAEzB,UAAM,YAAY,MAAM;AAAA,MACtB,OAAO,QAAQ,GAAG;AAAA,MAClB;AAAA,MACA,OAAO,QAAQ,KAAK;AAAA,MACpB,QAAQ;AAAA,IACV;AACA,QAAI,CAAC,UAAU,MAAO,QAAO;AAE7B,WAAO;AAAA,MACL,KAAK,OAAO,QAAQ,GAAG;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,OAAO,OAAO,QAAQ,KAAK;AAAA,MAC3B,OAAO,OAAO,QAAQ,SAAS,EAAE;AAAA,MACjC,aAAa,OAAO,QAAQ,eAAe,EAAE;AAAA,MAC7C,kBAAkB,QAAQ,mBAAmB,OAAO,QAAQ,gBAAgB,IAAI;AAAA,MAChF,gBAAgB,QAAQ,iBAAiB,OAAO,QAAQ,cAAc,IAAI;AAAA,MAC1E,kBAAkB,UAAU;AAAA,MAC5B,eAAe,UAAU;AAAA,IAC3B;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,uBACpB,MACqC;AACrC,MAAI,CAAC,KAAM,QAAO,2BAA2B;AAE7C,MAAI;AACF,UAAM,WAAW,qBAAqB,IAAI;AAC1C,QAAI,CAAC,SAAU,QAAO,2BAA2B;AACjD,QAAI,gBAAgB,EAAE,SAAS,QAAQ,EAAG,QAAO,2BAA2B;AAE5E,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,UAAU,UAAU,QAAQ,sBAAsB;AACxD,UAAM,WAAW,MAAM,QAAQ,kBAAkB,QAAQ;AACzD,QAAI,CAAC,YAAY,SAAS,WAAW,SAAU,QAAO;AACtD,WAAO,2BAA2B,EAAE,kBAAkB,SAAS,SAAS,CAAC;AAAA,EAC3E,SAAS,KAAK;AACZ,WAAO,KAAK,0DAA0D,EAAE,IAAI,CAAC;AAC7E,WAAO,2BAA2B;AAAA,EACpC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|