@open-mercato/core 0.6.7-develop.6606.1.3b1ec9b1ea → 0.6.7-develop.6622.1.4d29ddaca4
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/dist/modules/auth/api/session/refresh.js +12 -11
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/lib/rateLimitCheck.js +4 -4
- package/dist/modules/auth/lib/rateLimitCheck.js.map +2 -2
- package/dist/modules/catalog/api/categories/route.js +8 -14
- package/dist/modules/catalog/api/categories/route.js.map +2 -2
- package/dist/modules/catalog/api/tags/route.js +3 -10
- package/dist/modules/catalog/api/tags/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js +15 -17
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/email-threads/route.js +6 -4
- package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/emails/route.js +19 -17
- package/dist/modules/customers/api/people/[id]/emails/route.js.map +2 -2
- package/dist/modules/customers/components/detail/CustomFieldValuesList.js +22 -5
- package/dist/modules/customers/components/detail/CustomFieldValuesList.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +5 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/customFieldUtils.js +10 -3
- package/dist/modules/customers/components/detail/customFieldUtils.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.meta.js +4 -2
- package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.meta.js +4 -2
- package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +10 -7
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +6 -4
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +4 -2
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +7 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +7 -5
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/logs/route.js +4 -2
- package/dist/modules/integrations/api/logs/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +4 -2
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/api/umes-read.js +2 -1
- package/dist/modules/integrations/api/umes-read.js.map +2 -2
- package/dist/modules/integrations/lib/organization-scope.js +12 -0
- package/dist/modules/integrations/lib/organization-scope.js.map +7 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js +10 -1
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/sales/api/document-history/route.js +6 -9
- package/dist/modules/sales/api/document-history/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/accept/route.js +3 -3
- package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
- package/dist/modules/sales/api/returns/[id]/route.js +6 -9
- package/dist/modules/sales/api/returns/[id]/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +6 -4
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/workflows.js +1 -1
- package/dist/modules/sales/workflows.js.map +1 -1
- package/dist/modules/workflows/api/definitions/[id]/route.js +10 -1
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +44 -3
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/session/refresh.ts +19 -11
- package/src/modules/auth/lib/rateLimitCheck.ts +4 -4
- package/src/modules/catalog/api/categories/route.ts +14 -16
- package/src/modules/catalog/api/tags/route.ts +6 -10
- package/src/modules/catalog/i18n/pl.json +9 -9
- package/src/modules/customer_accounts/i18n/pl.json +129 -129
- package/src/modules/customers/api/interactions/[id]/visibility/route.ts +24 -20
- package/src/modules/customers/api/people/[id]/email-threads/route.ts +10 -6
- package/src/modules/customers/api/people/[id]/emails/route.ts +25 -21
- package/src/modules/customers/components/detail/CustomFieldValuesList.tsx +28 -4
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +7 -2
- package/src/modules/customers/components/detail/customFieldUtils.ts +15 -3
- package/src/modules/data_sync/i18n/pl.json +68 -68
- package/src/modules/directory/backend/directory/organizations/[id]/edit/page.meta.ts +4 -2
- package/src/modules/directory/backend/directory/tenants/[id]/edit/page.meta.ts +4 -2
- package/src/modules/integrations/api/[id]/credentials/route.ts +10 -7
- package/src/modules/integrations/api/[id]/health/route.ts +6 -4
- package/src/modules/integrations/api/[id]/route.ts +4 -2
- package/src/modules/integrations/api/[id]/state/route.ts +7 -5
- package/src/modules/integrations/api/[id]/version/route.ts +7 -5
- package/src/modules/integrations/api/logs/route.ts +4 -2
- package/src/modules/integrations/api/route.ts +4 -2
- package/src/modules/integrations/api/umes-read.ts +2 -1
- package/src/modules/integrations/i18n/de.json +0 -76
- package/src/modules/integrations/i18n/en.json +0 -76
- package/src/modules/integrations/i18n/es.json +0 -76
- package/src/modules/integrations/i18n/pl.json +0 -76
- package/src/modules/integrations/lib/organization-scope.ts +27 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +12 -1
- package/src/modules/sales/api/document-history/route.ts +10 -9
- package/src/modules/sales/api/quotes/accept/route.ts +3 -3
- package/src/modules/sales/api/returns/[id]/route.ts +10 -9
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +26 -19
- package/src/modules/sales/components/documents/AddressesSection.tsx +8 -4
- package/src/modules/sales/i18n/pl.json +24 -24
- package/src/modules/sales/workflows.ts +1 -1
- package/src/modules/staff/i18n/pl.json +178 -178
- package/src/modules/workflows/api/definitions/[id]/route.ts +14 -1
- package/src/modules/workflows/i18n/pl.json +128 -128
- package/src/modules/workflows/lib/event-trigger-service.ts +87 -8
package/.turbo/turbo-build.log
CHANGED
|
@@ -26,6 +26,16 @@ function parseCookie(req, name) {
|
|
|
26
26
|
const m = cookie.match(new RegExp("(?:^|;\\s*)" + name + "=([^;]+)"));
|
|
27
27
|
return m ? decodeURIComponent(m[1]) : null;
|
|
28
28
|
}
|
|
29
|
+
function buildStaffJwtClaims({ user, roles, session }) {
|
|
30
|
+
return {
|
|
31
|
+
sub: String(user.id),
|
|
32
|
+
sid: session ? String(session.id) : void 0,
|
|
33
|
+
tenantId: user.tenantId ? String(user.tenantId) : null,
|
|
34
|
+
orgId: user.organizationId ? String(user.organizationId) : null,
|
|
35
|
+
email: user.email,
|
|
36
|
+
roles
|
|
37
|
+
};
|
|
38
|
+
}
|
|
29
39
|
function clearStaffAuthCookies(response) {
|
|
30
40
|
response.cookies.set("auth_token", "", {
|
|
31
41
|
httpOnly: true,
|
|
@@ -61,8 +71,7 @@ async function GET(req) {
|
|
|
61
71
|
buildSafeRedirectResponse(req, "/login?redirect=" + encodeURIComponent(redirectTo))
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
|
-
const
|
|
65
|
-
const jwt = signJwt({ sub: String(user.id), sid: session ? String(session.id) : void 0, tenantId: String(user.tenantId), orgId: String(user.organizationId), email: user.email, roles });
|
|
74
|
+
const jwt = signJwt(buildStaffJwtClaims(ctx));
|
|
66
75
|
const res = buildSafeRedirectResponse(req, redirectTo);
|
|
67
76
|
res.cookies.set("auth_token", jwt, { httpOnly: true, path: "/", sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 60 * 60 * 8 });
|
|
68
77
|
return res;
|
|
@@ -104,15 +113,7 @@ async function POST(req) {
|
|
|
104
113
|
}, { status: 401 })
|
|
105
114
|
);
|
|
106
115
|
}
|
|
107
|
-
const
|
|
108
|
-
const jwt = signJwt({
|
|
109
|
-
sub: String(user.id),
|
|
110
|
-
sid: session ? String(session.id) : void 0,
|
|
111
|
-
tenantId: String(user.tenantId),
|
|
112
|
-
orgId: String(user.organizationId),
|
|
113
|
-
email: user.email,
|
|
114
|
-
roles
|
|
115
|
-
});
|
|
116
|
+
const jwt = signJwt(buildStaffJwtClaims(ctx));
|
|
116
117
|
const res = NextResponse.json({
|
|
117
118
|
ok: true,
|
|
118
119
|
accessToken: jwt,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/auth/api/session/refresh.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AAEvC,SAAS,eAAe;AACxB,SAAS,2BAA2B;AACpC,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AACnC,SAAS,2BAA2B,kCAAkC;AACtE,SAAS,4BAA4B;AACrC,SAAS,mCAAmC;AAC5C,SAAS,4BAA4B;AACrC,SAAS,SAAS;AAElB,MAAM,yBAAyB,4BAA4B,WAAW;AAAA,EACpE,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;AACD,MAAM,2BAA2B,4BAA4B,cAAc;AAAA,EACzE,QAAQ;AAAA,EAAI,UAAU;AAAA,EAAI,eAAe;AAAA,EAAI,WAAW;AAC1D,CAAC;AAED,SAAS,YAAY,KAAc,MAA6B;AAC9D,QAAM,SAAS,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAC5C,QAAM,IAAI,OAAO,MAAM,IAAI,OAAO,gBAAgB,OAAO,UAAU,CAAC;AACpE,SAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,IAAI;AACxC;AAEA,SAAS,sBAAsB,UAAwB;AACrD,WAAS,QAAQ,IAAI,cAAc,IAAI;AAAA,IACrC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ;AAAA,EACV,CAAC;AACD,WAAS,QAAQ,IAAI,iBAAiB,IAAI;AAAA,IACxC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,QAAQ;AAAA,EACV,CAAC;AACD,SAAO;AACT;AAEA,eAAsB,IAAI,KAAc;AACtC,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,UAAU,2BAA2B,GAAG,KAAK,IAAI;AACvD,QAAM,aAAa,qBAAqB,IAAI,aAAa,IAAI,UAAU,GAAG,SAAS,GAAG;AACtF,QAAM,QAAQ,YAAY,KAAK,eAAe;AAC9C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,MACL,0BAA0B,KAAK,qBAAqB,mBAAmB,UAAU,CAAC;AAAA,IACpF;AAAA,EACF;AACA,QAAM,IAAI,MAAM,uBAAuB;AACvC,QAAM,OAAO,EAAE,QAAqB,aAAa;AACjD,QAAM,MAAM,MAAM,KAAK,wBAAwB,KAAK;AACpD,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,MACL,0BAA0B,KAAK,qBAAqB,mBAAmB,UAAU,CAAC;AAAA,IACpF;AAAA,EACF;AACA,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { refreshSessionRequestSchema } from '@open-mercato/core/modules/auth/data/validators'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { buildSafeRedirectResponse, resolveTrustedRedirectBase } from '@open-mercato/core/modules/auth/lib/requestRedirect'\nimport { sanitizeRedirectPath } from '@open-mercato/core/modules/auth/lib/safeRedirect'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport { z } from 'zod'\n\nconst refreshRateLimitConfig = readEndpointRateLimitConfig('REFRESH', {\n points: 15, duration: 60, blockDuration: 60, keyPrefix: 'refresh',\n})\nconst refreshIpRateLimitConfig = readEndpointRateLimitConfig('REFRESH_IP', {\n points: 60, duration: 60, blockDuration: 60, keyPrefix: 'refresh-ip',\n})\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\ntype RefreshedSession = NonNullable<Awaited<ReturnType<AuthService['refreshFromSessionToken']>>>\n\n// Scope claims must stay absent rather than stringified when the user has no tenant/org:\n// `String(null)` yields the literal \"null\", which is not a UUID, so session-integrity\n// resolution rejects the token it just minted and the caller is stuck in a refresh loop.\n// Mirrors how `api/login.ts` builds the same claims.\nfunction buildStaffJwtClaims({ user, roles, session }: RefreshedSession) {\n return {\n sub: String(user.id),\n sid: session ? String(session.id) : undefined,\n tenantId: user.tenantId ? String(user.tenantId) : null,\n orgId: user.organizationId ? String(user.organizationId) : null,\n email: user.email,\n roles,\n }\n}\n\nfunction clearStaffAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n response.cookies.set('session_token', '', {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 0,\n })\n return response\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedRedirectBase(req) ?? url.origin\n const redirectTo = sanitizeRedirectPath(url.searchParams.get('redirect'), baseUrl, '/')\n const token = parseCookie(req, 'session_token')\n if (!token) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n if (!ctx) {\n return clearStaffAuthCookies(\n buildSafeRedirectResponse(req, '/login?redirect=' + encodeURIComponent(redirectTo))\n )\n }\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n const res = buildSafeRedirectResponse(req, redirectTo)\n res.cookies.set('auth_token', jwt, { httpOnly: true, path: '/', sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 8 })\n return res\n}\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n let token: string | null = null\n\n try {\n const body = await req.json()\n const parsed = refreshSessionRequestSchema.safeParse(body)\n if (parsed.success) {\n token = parsed.data.refreshToken\n }\n } catch {\n // Invalid JSON\n }\n\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: refreshIpRateLimitConfig,\n compoundConfig: refreshRateLimitConfig,\n compoundIdentifier: token ?? undefined,\n })\n if (rateLimitError) return rateLimitError\n\n if (!token) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidPayload', 'Missing or invalid refresh token'),\n }, { status: 400 })\n )\n }\n\n const c = await createRequestContainer()\n const auth = c.resolve<AuthService>('authService')\n const ctx = await auth.refreshFromSessionToken(token)\n\n if (!ctx) {\n return clearStaffAuthCookies(\n NextResponse.json({\n ok: false,\n error: translate('auth.session.refresh.errors.invalidToken', 'Invalid or expired refresh token'),\n }, { status: 401 })\n )\n }\n\n const jwt = signJwt(buildStaffJwtClaims(ctx))\n\n const res = NextResponse.json({\n ok: true,\n accessToken: jwt,\n expiresIn: 60 * 60 * 8,\n })\n\n res.cookies.set('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\n return res\n}\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nconst refreshQuerySchema = z.object({\n redirect: z.string().optional().describe('Absolute or relative URL to redirect after refresh'),\n})\n\nconst refreshSuccessSchema = z.object({\n ok: z.literal(true),\n accessToken: z.string().describe('New JWT access token'),\n expiresIn: z.number().describe('Token expiration time in seconds'),\n})\n\nconst refreshErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Refresh session token',\n methods: {\n GET: {\n summary: 'Refresh auth cookie from session token (browser)',\n description: 'Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.',\n query: refreshQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to target location when session is valid', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Refresh access token (API/mobile)',\n description: 'Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.',\n requestBody: { schema: refreshSessionRequestSchema, contentType: 'application/json' },\n responses: [\n { status: 200, description: 'New access token issued', schema: refreshSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Missing refresh token', schema: refreshErrorSchema },\n { status: 401, description: 'Invalid or expired token', schema: refreshErrorSchema },\n { status: 429, description: 'Too many refresh attempts', schema: rateLimitErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getCachedRateLimiterService } from "@open-mercato/core/bootstrap";
|
|
2
|
-
import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
2
|
+
import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_FALLBACK_KEY } from "@open-mercato/shared/lib/ratelimit/helpers";
|
|
3
3
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
4
4
|
import { computeEmailHash } from "@open-mercato/core/modules/auth/lib/emailHash";
|
|
5
5
|
async function checkAuthRateLimit(options) {
|
|
@@ -14,14 +14,14 @@ async function checkAuthRateLimit(options) {
|
|
|
14
14
|
const rateLimiterService = getCachedRateLimiterService();
|
|
15
15
|
if (!rateLimiterService) return { error: null, compoundKey: null };
|
|
16
16
|
const clientIp = getClientIp(options.req, rateLimiterService.trustProxyDepth);
|
|
17
|
-
|
|
17
|
+
const clientKey = clientIp ?? RATE_LIMIT_FALLBACK_KEY;
|
|
18
18
|
const { translate } = await resolveTranslations();
|
|
19
19
|
const errorMessage = translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK);
|
|
20
|
-
const ipError = await checkRateLimit(rateLimiterService, options.ipConfig,
|
|
20
|
+
const ipError = await checkRateLimit(rateLimiterService, options.ipConfig, clientKey, errorMessage);
|
|
21
21
|
if (ipError) return { error: ipError, compoundKey: null };
|
|
22
22
|
if (options.compoundConfig && options.compoundIdentifier) {
|
|
23
23
|
const hash = computeEmailHash(options.compoundIdentifier);
|
|
24
|
-
const compoundKey = `${
|
|
24
|
+
const compoundKey = `${clientKey}:${hash}`;
|
|
25
25
|
const compoundError = await checkRateLimit(rateLimiterService, options.compoundConfig, compoundKey, errorMessage);
|
|
26
26
|
if (compoundError) return { error: compoundError, compoundKey };
|
|
27
27
|
return { error: null, compoundKey };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/lib/rateLimitCheck.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimitConfig } from '@open-mercato/shared/lib/ratelimit/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'\n\nexport interface CheckAuthRateLimitOptions {\n req: Request\n ipConfig: RateLimitConfig\n compoundConfig?: RateLimitConfig\n /** Raw identifier for compound key (e.g., email). Hashed internally before use. */\n compoundIdentifier?: string\n}\n\nexport interface CheckAuthRateLimitResult {\n error: NextResponse | null\n compoundKey: string | null\n}\n\n/**\n * Fail-open rate limit check for auth endpoints.\n * Layer 1: IP-only check with ipConfig.\n * Layer 2 (optional): compound IP + hashed identifier check with compoundConfig.\n */\nexport async function checkAuthRateLimit(options: CheckAuthRateLimitOptions): Promise<CheckAuthRateLimitResult> {\n try {\n const isIntegrationTestMode = process.env.OM_TEST_MODE === '1' && process.env.OM_TEST_AUTH_RATE_LIMIT_MODE === 'opt-in'\n if (isIntegrationTestMode) {\n const rateLimitHeader = options.req.headers.get('x-om-test-rate-limit')\n if (rateLimitHeader !== 'on') {\n return { error: null, compoundKey: null }\n }\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n if (!rateLimiterService) return { error: null, compoundKey: null }\n\n const clientIp = getClientIp(options.req, rateLimiterService.trustProxyDepth)\n
|
|
5
|
-
"mappings": "AACA,SAAS,mCAAmC;AAC5C,SAAS,gBAAgB,aAAa,sBAAsB,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getCachedRateLimiterService } from '@open-mercato/core/bootstrap'\nimport { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_FALLBACK_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimitConfig } from '@open-mercato/shared/lib/ratelimit/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'\n\nexport interface CheckAuthRateLimitOptions {\n req: Request\n ipConfig: RateLimitConfig\n compoundConfig?: RateLimitConfig\n /** Raw identifier for compound key (e.g., email). Hashed internally before use. */\n compoundIdentifier?: string\n}\n\nexport interface CheckAuthRateLimitResult {\n error: NextResponse | null\n compoundKey: string | null\n}\n\n/**\n * Fail-open rate limit check for auth endpoints.\n * Layer 1: IP-only check with ipConfig.\n * Layer 2 (optional): compound IP + hashed identifier check with compoundConfig.\n */\nexport async function checkAuthRateLimit(options: CheckAuthRateLimitOptions): Promise<CheckAuthRateLimitResult> {\n try {\n const isIntegrationTestMode = process.env.OM_TEST_MODE === '1' && process.env.OM_TEST_AUTH_RATE_LIMIT_MODE === 'opt-in'\n if (isIntegrationTestMode) {\n const rateLimitHeader = options.req.headers.get('x-om-test-rate-limit')\n if (rateLimitHeader !== 'on') {\n return { error: null, compoundKey: null }\n }\n }\n\n const rateLimiterService = getCachedRateLimiterService()\n if (!rateLimiterService) return { error: null, compoundKey: null }\n\n const clientIp = getClientIp(options.req, rateLimiterService.trustProxyDepth)\n const clientKey = clientIp ?? RATE_LIMIT_FALLBACK_KEY\n\n const { translate } = await resolveTranslations()\n const errorMessage = translate(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK)\n\n const ipError = await checkRateLimit(rateLimiterService, options.ipConfig, clientKey, errorMessage)\n if (ipError) return { error: ipError, compoundKey: null }\n\n if (options.compoundConfig && options.compoundIdentifier) {\n const hash = computeEmailHash(options.compoundIdentifier)\n const compoundKey = `${clientKey}:${hash}`\n const compoundError = await checkRateLimit(rateLimiterService, options.compoundConfig, compoundKey, errorMessage)\n if (compoundError) return { error: compoundError, compoundKey }\n return { error: null, compoundKey }\n }\n\n return { error: null, compoundKey: null }\n } catch {\n return { error: null, compoundKey: null }\n }\n}\n\n/**\n * Best-effort reset of a compound rate-limit key after successful authentication.\n * Never throws \u2014 wrapped in try/catch.\n */\nexport async function resetAuthRateLimit(compoundKey: string, config: RateLimitConfig): Promise<void> {\n try {\n const rateLimiterService = getCachedRateLimiterService()\n if (rateLimiterService) {\n await rateLimiterService.delete(compoundKey, config)\n }\n } catch {\n // best-effort \u2014 don't fail the request if counter reset fails\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,mCAAmC;AAC5C,SAAS,gBAAgB,aAAa,sBAAsB,2BAA2B,+BAA+B;AAEtH,SAAS,2BAA2B;AACpC,SAAS,wBAAwB;AAoBjC,eAAsB,mBAAmB,SAAuE;AAC9G,MAAI;AACF,UAAM,wBAAwB,QAAQ,IAAI,iBAAiB,OAAO,QAAQ,IAAI,iCAAiC;AAC/G,QAAI,uBAAuB;AACzB,YAAM,kBAAkB,QAAQ,IAAI,QAAQ,IAAI,sBAAsB;AACtE,UAAI,oBAAoB,MAAM;AAC5B,eAAO,EAAE,OAAO,MAAM,aAAa,KAAK;AAAA,MAC1C;AAAA,IACF;AAEA,UAAM,qBAAqB,4BAA4B;AACvD,QAAI,CAAC,mBAAoB,QAAO,EAAE,OAAO,MAAM,aAAa,KAAK;AAEjE,UAAM,WAAW,YAAY,QAAQ,KAAK,mBAAmB,eAAe;AAC5E,UAAM,YAAY,YAAY;AAE9B,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,eAAe,UAAU,sBAAsB,yBAAyB;AAE9E,UAAM,UAAU,MAAM,eAAe,oBAAoB,QAAQ,UAAU,WAAW,YAAY;AAClG,QAAI,QAAS,QAAO,EAAE,OAAO,SAAS,aAAa,KAAK;AAExD,QAAI,QAAQ,kBAAkB,QAAQ,oBAAoB;AACxD,YAAM,OAAO,iBAAiB,QAAQ,kBAAkB;AACxD,YAAM,cAAc,GAAG,SAAS,IAAI,IAAI;AACxC,YAAM,gBAAgB,MAAM,eAAe,oBAAoB,QAAQ,gBAAgB,aAAa,YAAY;AAChH,UAAI,cAAe,QAAO,EAAE,OAAO,eAAe,YAAY;AAC9D,aAAO,EAAE,OAAO,MAAM,YAAY;AAAA,IACpC;AAEA,WAAO,EAAE,OAAO,MAAM,aAAa,KAAK;AAAA,EAC1C,QAAQ;AACN,WAAO,EAAE,OAAO,MAAM,aAAa,KAAK;AAAA,EAC1C;AACF;AAMA,eAAsB,mBAAmB,aAAqB,QAAwC;AACpG,MAAI;AACF,UAAM,qBAAqB,4BAA4B;AACvD,QAAI,oBAAoB;AACtB,YAAM,mBAAmB,OAAO,aAAa,MAAM;AAAA,IACrD;AAAA,EACF,QAAQ;AAAA,EAER;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
|
5
5
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
6
6
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
7
7
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
8
|
+
import { resolveOrganizationScopeFilter } from "@open-mercato/core/modules/directory/utils/organizationScopeFilter";
|
|
8
9
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
10
|
import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
10
11
|
import { E } from "../../../../generated/entities.ids.generated.js";
|
|
@@ -111,18 +112,11 @@ async function GET(req) {
|
|
|
111
112
|
{ status: 400 }
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const organizationId = preferredOrg ?? (Array.isArray(allowed) && allowed.length ? allowed[0] : null);
|
|
117
|
-
if (!organizationId || Array.isArray(allowed) && allowed.length && !allowed.includes(organizationId)) {
|
|
118
|
-
return NextResponse.json(
|
|
119
|
-
{ items: [], error: translate("catalog.errors.organization_required", "Organization context is required.") },
|
|
120
|
-
{ status: 400 }
|
|
121
|
-
);
|
|
122
|
-
}
|
|
115
|
+
const orgFilter = resolveOrganizationScopeFilter(scope, auth);
|
|
116
|
+
const responseOrganizationId = orgFilter.rbacOrganizationId ?? null;
|
|
123
117
|
const categories = await em.find(
|
|
124
118
|
CatalogProductCategory,
|
|
125
|
-
{
|
|
119
|
+
{ ...orgFilter.where, tenantId, deletedAt: null },
|
|
126
120
|
{ orderBy: { name: "ASC" } }
|
|
127
121
|
);
|
|
128
122
|
const categoryMap = new Map(categories.map((cat) => [String(cat.id), cat]));
|
|
@@ -178,7 +172,7 @@ async function GET(req) {
|
|
|
178
172
|
const organizationIdByRecord = {};
|
|
179
173
|
for (const id of recordIds) {
|
|
180
174
|
tenantIdByRecord[id] = tenantId;
|
|
181
|
-
organizationIdByRecord[id] = organizationId;
|
|
175
|
+
organizationIdByRecord[id] = categoryMap.get(id)?.organizationId ?? null;
|
|
182
176
|
}
|
|
183
177
|
const cfValues = recordIds.length ? await loadCustomFieldValues({
|
|
184
178
|
em,
|
|
@@ -206,7 +200,7 @@ async function GET(req) {
|
|
|
206
200
|
descendantCount: node.descendantIds.length,
|
|
207
201
|
isActive: node.isActive,
|
|
208
202
|
updatedAt: category?.updatedAt ? new Date(category.updatedAt).toISOString() : null,
|
|
209
|
-
organizationId,
|
|
203
|
+
organizationId: category?.organizationId ?? null,
|
|
210
204
|
tenantId,
|
|
211
205
|
...cfValues[recordId] ?? {}
|
|
212
206
|
};
|
|
@@ -218,7 +212,7 @@ async function GET(req) {
|
|
|
218
212
|
page,
|
|
219
213
|
pageSize,
|
|
220
214
|
totalPages,
|
|
221
|
-
organizationId,
|
|
215
|
+
organizationId: responseOrganizationId,
|
|
222
216
|
tenantId
|
|
223
217
|
});
|
|
224
218
|
}
|
|
@@ -239,7 +233,7 @@ const categoryListItemSchema = z.object({
|
|
|
239
233
|
descendantCount: z.number(),
|
|
240
234
|
isActive: z.boolean(),
|
|
241
235
|
updatedAt: z.string().nullable().optional(),
|
|
242
|
-
organizationId: z.string().uuid(),
|
|
236
|
+
organizationId: z.string().uuid().nullable(),
|
|
243
237
|
tenantId: z.string().uuid()
|
|
244
238
|
});
|
|
245
239
|
const openApi = createCatalogCrudOpenApi({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/api/categories/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport { CatalogProductCategory } from '../../data/entities'\nimport { categoryCreateSchema, categoryUpdateSchema } from '../../data/validators'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { computeHierarchyForCategories } from '../../lib/categoryHierarchy'\nimport {\n createCatalogCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['catalog.categories.view'] },\n POST: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst viewSchema = z\n .object({\n view: z.enum(['manage', 'tree']).default('manage'),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(200).default(50),\n search: z.string().optional(),\n status: z.enum(['all', 'active', 'inactive']).optional(),\n ids: z.string().optional(),\n })\n .passthrough()\n\ntype QueryShape = z.infer<typeof viewSchema>\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CatalogProductCategory,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n actions: {\n create: {\n commandId: 'catalog.categories.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(categoryCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.categoryId ?? null }),\n status: 201,\n },\n update: {\n commandId: 'catalog.categories.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(categoryUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'catalog.categories.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n if (!id) throw new CrudHttpError(400, { error: translate('catalog.errors.id_required', 'Record identifier is required.') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\ntype ManageCategoryRow = {\n id: string\n name: string\n slug: string | null\n description: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n treePath: string\n pathLabel: string\n childCount: number\n descendantCount: number\n isActive: boolean\n updatedAt: string | null\n organizationId: string\n tenantId: string\n}\n\ntype TreeNode = {\n id: string\n name: string\n parentId: string | null\n depth: number\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n isActive: boolean\n children: TreeNode[]\n}\n\nfunction sanitizeSearch(term?: string | null): string {\n if (!term) return ''\n return term.trim().toLowerCase()\n}\n\nfunction parseIds(raw?: string | null): string[] | null {\n if (!raw) return null\n const parts = raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0)\n return parts.length ? Array.from(new Set(parts)) : null\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [] }, { status: 401 })\n\n const url = new URL(req.url)\n const parsed = viewSchema.safeParse({\n view: url.searchParams.get('view') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n ids: url.searchParams.get('ids') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], error: 'Invalid query' }, { status: 400 })\n }\n const query: QueryShape = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { translate } = await resolveTranslations()\n\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json(\n { items: [], error: translate('catalog.errors.tenant_required', 'Tenant context is required.') },\n { status: 400 }\n )\n }\n\n const allowed = scope?.filterIds ?? scope?.allowedIds ?? (auth.orgId ? [auth.orgId] : null)\n const preferredOrg = scope?.selectedId ?? auth.orgId ?? null\n const organizationId = preferredOrg ?? (Array.isArray(allowed) && allowed.length ? allowed[0]! : null)\n\n if (!organizationId || (Array.isArray(allowed) && allowed.length && !allowed.includes(organizationId))) {\n return NextResponse.json(\n { items: [], error: translate('catalog.errors.organization_required', 'Organization context is required.') },\n { status: 400 }\n )\n }\n\n const categories = await em.find(\n CatalogProductCategory,\n { organizationId, tenantId, deletedAt: null },\n { orderBy: { name: 'ASC' } }\n )\n const categoryMap = new Map(categories.map((cat) => [String(cat.id), cat]))\n const hierarchy = computeHierarchyForCategories(categories)\n\n if (query.view === 'tree') {\n const nodes = new Map<string, TreeNode>()\n const roots: TreeNode[] = []\n for (const entry of hierarchy.ordered) {\n const node: TreeNode = {\n id: entry.id,\n name: entry.name,\n parentId: entry.parentId,\n depth: entry.depth,\n pathLabel: entry.pathLabel,\n ancestorIds: entry.ancestorIds,\n childIds: entry.childIds,\n descendantIds: entry.descendantIds,\n isActive: entry.isActive,\n children: [],\n }\n nodes.set(entry.id, node)\n if (entry.parentId && nodes.has(entry.parentId)) {\n nodes.get(entry.parentId)!.children.push(node)\n } else {\n roots.push(node)\n }\n }\n return NextResponse.json({ items: roots })\n }\n\n const status = query.status ?? 'all'\n const search = sanitizeSearch(query.search ?? null)\n const ids = parseIds(query.ids)\n const idSet = ids ? new Set(ids) : null\n let rows = hierarchy.ordered\n\n if (status === 'active') rows = rows.filter((node) => node.isActive)\n if (status === 'inactive') rows = rows.filter((node) => !node.isActive)\n if (search) {\n rows = rows.filter((node) => {\n const label = node.pathLabel.toLowerCase()\n return node.name.toLowerCase().includes(search) || label.includes(search)\n })\n }\n if (idSet && idSet.size) {\n rows = rows.filter((node) => idSet.has(node.id))\n }\n\n const total = rows.length\n const pageSize = query.pageSize\n const page = query.page\n const start = (page - 1) * pageSize\n const paged = rows.slice(start, start + pageSize)\n\n const recordIds = paged.map((node) => node.id)\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const id of recordIds) {\n tenantIdByRecord[id] = tenantId\n organizationIdByRecord[id] = organizationId\n }\n const cfValues = recordIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.catalog.catalog_product_category,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: tenantId ? [tenantId] : [],\n })\n : {}\n\n const items: ManageCategoryRow[] = paged.map((node) => {\n const category = categoryMap.get(node.id)\n const parentName = node.parentId ? hierarchy.map.get(node.parentId)?.name ?? null : null\n const recordId = node.id\n return {\n id: recordId,\n name: node.name,\n slug: category?.slug ?? null,\n description: category?.description ?? null,\n parentId: node.parentId,\n parentName,\n depth: node.depth,\n treePath: node.treePath,\n pathLabel: node.pathLabel,\n childCount: node.childIds.length,\n descendantCount: node.descendantIds.length,\n isActive: node.isActive,\n updatedAt: category?.updatedAt ? new Date(category.updatedAt).toISOString() : null,\n organizationId,\n tenantId,\n ...(cfValues[recordId] ?? {}),\n }\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n return NextResponse.json({\n items,\n total,\n page,\n pageSize,\n totalPages,\n organizationId,\n tenantId,\n })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst categoryListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n parentId: z.string().uuid().nullable().optional(),\n parentName: z.string().nullable().optional(),\n depth: z.number(),\n treePath: z.string(),\n pathLabel: z.string(),\n childCount: z.number(),\n descendantCount: z.number(),\n isActive: z.boolean(),\n updatedAt: z.string().nullable().optional(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n})\n\nexport const openApi = createCatalogCrudOpenApi({\n resourceName: 'Category',\n pluralName: 'Categories',\n querySchema: viewSchema,\n listResponseSchema: createPagedListResponseSchema(categoryListItemSchema),\n create: {\n schema: categoryCreateSchema,\n description: 'Creates a new product category.',\n },\n update: {\n schema: categoryUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates an existing category by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a category by id.',\n },\n})\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport { CatalogProductCategory } from '../../data/entities'\nimport { categoryCreateSchema, categoryUpdateSchema } from '../../data/validators'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { computeHierarchyForCategories } from '../../lib/categoryHierarchy'\nimport {\n createCatalogCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['catalog.categories.view'] },\n POST: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['catalog.categories.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst viewSchema = z\n .object({\n view: z.enum(['manage', 'tree']).default('manage'),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(200).default(50),\n search: z.string().optional(),\n status: z.enum(['all', 'active', 'inactive']).optional(),\n ids: z.string().optional(),\n })\n .passthrough()\n\ntype QueryShape = z.infer<typeof viewSchema>\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CatalogProductCategory,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n actions: {\n create: {\n commandId: 'catalog.categories.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(categoryCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.categoryId ?? null }),\n status: 201,\n },\n update: {\n commandId: 'catalog.categories.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(categoryUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'catalog.categories.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n if (!id) throw new CrudHttpError(400, { error: translate('catalog.errors.id_required', 'Record identifier is required.') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\ntype ManageCategoryRow = {\n id: string\n name: string\n slug: string | null\n description: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n treePath: string\n pathLabel: string\n childCount: number\n descendantCount: number\n isActive: boolean\n updatedAt: string | null\n organizationId: string | null\n tenantId: string\n}\n\ntype TreeNode = {\n id: string\n name: string\n parentId: string | null\n depth: number\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n isActive: boolean\n children: TreeNode[]\n}\n\nfunction sanitizeSearch(term?: string | null): string {\n if (!term) return ''\n return term.trim().toLowerCase()\n}\n\nfunction parseIds(raw?: string | null): string[] | null {\n if (!raw) return null\n const parts = raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0)\n return parts.length ? Array.from(new Set(parts)) : null\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [] }, { status: 401 })\n\n const url = new URL(req.url)\n const parsed = viewSchema.safeParse({\n view: url.searchParams.get('view') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n ids: url.searchParams.get('ids') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], error: 'Invalid query' }, { status: 400 })\n }\n const query: QueryShape = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { translate } = await resolveTranslations()\n\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json(\n { items: [], error: translate('catalog.errors.tenant_required', 'Tenant context is required.') },\n { status: 400 }\n )\n }\n\n // Scope by the caller's visible organizations. Under \"All organizations\"\n // (super-admin) `where` is empty, so the tree scopes by tenant only instead\n // of 400-ing; restricted callers get their `filterIds` `$in` guard.\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n // Caller's resolved org echoed at the top level; null under \"All organizations\"\n // where the tree spans orgs and each row carries its own org instead.\n const responseOrganizationId = orgFilter.rbacOrganizationId ?? null\n\n const categories = await em.find(\n CatalogProductCategory,\n { ...orgFilter.where, tenantId, deletedAt: null },\n { orderBy: { name: 'ASC' } }\n )\n const categoryMap = new Map(categories.map((cat) => [String(cat.id), cat]))\n const hierarchy = computeHierarchyForCategories(categories)\n\n if (query.view === 'tree') {\n const nodes = new Map<string, TreeNode>()\n const roots: TreeNode[] = []\n for (const entry of hierarchy.ordered) {\n const node: TreeNode = {\n id: entry.id,\n name: entry.name,\n parentId: entry.parentId,\n depth: entry.depth,\n pathLabel: entry.pathLabel,\n ancestorIds: entry.ancestorIds,\n childIds: entry.childIds,\n descendantIds: entry.descendantIds,\n isActive: entry.isActive,\n children: [],\n }\n nodes.set(entry.id, node)\n if (entry.parentId && nodes.has(entry.parentId)) {\n nodes.get(entry.parentId)!.children.push(node)\n } else {\n roots.push(node)\n }\n }\n return NextResponse.json({ items: roots })\n }\n\n const status = query.status ?? 'all'\n const search = sanitizeSearch(query.search ?? null)\n const ids = parseIds(query.ids)\n const idSet = ids ? new Set(ids) : null\n let rows = hierarchy.ordered\n\n if (status === 'active') rows = rows.filter((node) => node.isActive)\n if (status === 'inactive') rows = rows.filter((node) => !node.isActive)\n if (search) {\n rows = rows.filter((node) => {\n const label = node.pathLabel.toLowerCase()\n return node.name.toLowerCase().includes(search) || label.includes(search)\n })\n }\n if (idSet && idSet.size) {\n rows = rows.filter((node) => idSet.has(node.id))\n }\n\n const total = rows.length\n const pageSize = query.pageSize\n const page = query.page\n const start = (page - 1) * pageSize\n const paged = rows.slice(start, start + pageSize)\n\n const recordIds = paged.map((node) => node.id)\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const id of recordIds) {\n tenantIdByRecord[id] = tenantId\n organizationIdByRecord[id] = categoryMap.get(id)?.organizationId ?? null\n }\n const cfValues = recordIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.catalog.catalog_product_category,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: tenantId ? [tenantId] : [],\n })\n : {}\n\n const items: ManageCategoryRow[] = paged.map((node) => {\n const category = categoryMap.get(node.id)\n const parentName = node.parentId ? hierarchy.map.get(node.parentId)?.name ?? null : null\n const recordId = node.id\n return {\n id: recordId,\n name: node.name,\n slug: category?.slug ?? null,\n description: category?.description ?? null,\n parentId: node.parentId,\n parentName,\n depth: node.depth,\n treePath: node.treePath,\n pathLabel: node.pathLabel,\n childCount: node.childIds.length,\n descendantCount: node.descendantIds.length,\n isActive: node.isActive,\n updatedAt: category?.updatedAt ? new Date(category.updatedAt).toISOString() : null,\n organizationId: category?.organizationId ?? null,\n tenantId,\n ...(cfValues[recordId] ?? {}),\n }\n })\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n return NextResponse.json({\n items,\n total,\n page,\n pageSize,\n totalPages,\n organizationId: responseOrganizationId,\n tenantId,\n })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst categoryListItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n slug: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n parentId: z.string().uuid().nullable().optional(),\n parentName: z.string().nullable().optional(),\n depth: z.number(),\n treePath: z.string(),\n pathLabel: z.string(),\n childCount: z.number(),\n descendantCount: z.number(),\n isActive: z.boolean(),\n updatedAt: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid(),\n})\n\nexport const openApi = createCatalogCrudOpenApi({\n resourceName: 'Category',\n pluralName: 'Categories',\n querySchema: viewSchema,\n listResponseSchema: createPagedListResponseSchema(categoryListItemSchema),\n create: {\n schema: categoryCreateSchema,\n description: 'Creates a new product category.',\n },\n update: {\n schema: categoryUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates an existing category by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a category by id.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
|
+
import { resolveOrganizationScopeFilter } from "@open-mercato/core/modules/directory/utils/organizationScopeFilter";
|
|
6
7
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
8
|
import { CatalogProductTag } from "../../data/entities.js";
|
|
8
9
|
import { createPagedListResponseSchema } from "../openapi.js";
|
|
@@ -40,17 +41,9 @@ async function GET(req) {
|
|
|
40
41
|
{ status: 400 }
|
|
41
42
|
);
|
|
42
43
|
}
|
|
43
|
-
const
|
|
44
|
-
const preferredOrg = scope?.selectedId ?? auth.orgId ?? null;
|
|
45
|
-
const organizationId = preferredOrg ?? (Array.isArray(allowed) && allowed.length ? allowed[0] : null);
|
|
46
|
-
if (!organizationId || Array.isArray(allowed) && allowed.length && !allowed.includes(organizationId)) {
|
|
47
|
-
return NextResponse.json(
|
|
48
|
-
{ items: [], error: translate("catalog.errors.organization_required", "Organization context is required.") },
|
|
49
|
-
{ status: 400 }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
44
|
+
const orgFilter = resolveOrganizationScopeFilter(scope, auth);
|
|
52
45
|
const where = {
|
|
53
|
-
|
|
46
|
+
...orgFilter.where,
|
|
54
47
|
tenantId
|
|
55
48
|
};
|
|
56
49
|
const search = query.search?.trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/api/tags/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CatalogProductTag } from '../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createPagedListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['catalog.products.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst querySchema = z\n .object({\n search: z.string().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(200).default(50),\n })\n .passthrough()\n\ntype QueryShape = z.infer<typeof querySchema>\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [] }, { status: 401 })\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({\n search: url.searchParams.get('search') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], error: 'Invalid query' }, { status: 400 })\n }\n const query: QueryShape = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { translate } = await resolveTranslations()\n\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json(\n { items: [], error: translate('catalog.errors.tenant_required', 'Tenant context is required.') },\n { status: 400 }\n )\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,0CAA0C;AACnD,SAAS,2BAA2B;AACpC,SAAS,yBAAyB;AAElC,SAAS,qCAAqC;AAC9C,SAAS,yBAAyB;AAElC,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,uBAAuB,EAAE;AACvE;AAEO,MAAM,WAAW;AAExB,MAAM,cAAc,EACjB,OAAO;AAAA,EACN,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AACxD,CAAC,EACA,YAAY;AAIf,eAAsB,IAAI,KAAc;AACtC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,aAAa,KAAK,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAElE,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,SAAS,YAAY,UAAU;AAAA,IACnC,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,IAC1C,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,EAChD,CAAC;AACD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,gBAAgB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjF;AACA,QAAM,QAAoB,OAAO;AAEjC,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,QAAM,WAAW,OAAO,YAAY,KAAK,YAAY;AACrD,MAAI,CAAC,UAAU;AACb,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,CAAC,GAAG,OAAO,UAAU,kCAAkC,6BAA6B,EAAE;AAAA,MAC/F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CatalogProductTag } from '../../data/entities'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createPagedListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['catalog.products.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst querySchema = z\n .object({\n search: z.string().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(200).default(50),\n })\n .passthrough()\n\ntype QueryShape = z.infer<typeof querySchema>\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ items: [] }, { status: 401 })\n\n const url = new URL(req.url)\n const parsed = querySchema.safeParse({\n search: url.searchParams.get('search') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json({ items: [], error: 'Invalid query' }, { status: 400 })\n }\n const query: QueryShape = parsed.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const { translate } = await resolveTranslations()\n\n const tenantId = scope?.tenantId ?? auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json(\n { items: [], error: translate('catalog.errors.tenant_required', 'Tenant context is required.') },\n { status: 400 }\n )\n }\n\n // Scope by the caller's visible organizations. Under \"All organizations\"\n // (super-admin) `where` is empty, so the list scopes by tenant only instead\n // of 400-ing; restricted callers get their `filterIds` `$in` guard.\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n\n const where: Record<string, unknown> = {\n ...orgFilter.where,\n tenantId,\n }\n const search = query.search?.trim()\n if (search) {\n where.label = { $ilike: `%${escapeLikePattern(search)}%` }\n }\n\n const limit = query.pageSize\n const offset = (query.page - 1) * query.pageSize\n const [records, total] = await em.findAndCount(\n CatalogProductTag,\n where,\n { limit, offset, orderBy: { label: 'asc' } }\n )\n\n return NextResponse.json({\n items: records.map((record) => ({\n id: record.id,\n label: record.label,\n slug: record.slug,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n })),\n total,\n })\n}\n\nconst tagListItemSchema = z.object({\n id: z.string().uuid(),\n label: z.string(),\n slug: z.string().nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Catalog',\n summary: 'Product Tag management',\n methods: {\n GET: {\n summary: 'List product tags',\n description: 'Returns a paginated collection of product tags scoped to the authenticated organization.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated product tags',\n schema: createPagedListResponseSchema(tagListItemSchema),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -41,34 +41,18 @@ async function PATCH(req, context) {
|
|
|
41
41
|
const container = await createRequestContainer();
|
|
42
42
|
const em = container.resolve("em").fork();
|
|
43
43
|
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
|
|
44
|
-
const organizationId = scope?.selectedId ?? auth.orgId ?? null;
|
|
45
|
-
const dscope = { tenantId: auth.tenantId, organizationId };
|
|
46
44
|
const userId = resolveAuthActorId(auth);
|
|
47
|
-
const guardResult = await validateCrudMutationGuard(container, {
|
|
48
|
-
tenantId: auth.tenantId,
|
|
49
|
-
organizationId,
|
|
50
|
-
userId,
|
|
51
|
-
resourceKind: "customers.interaction",
|
|
52
|
-
resourceId: id,
|
|
53
|
-
operation: "custom",
|
|
54
|
-
requestMethod: req.method,
|
|
55
|
-
requestHeaders: req.headers
|
|
56
|
-
});
|
|
57
|
-
if (guardResult && !guardResult.ok) {
|
|
58
|
-
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
59
|
-
}
|
|
60
45
|
const interaction = await findOneWithDecryption(
|
|
61
46
|
em,
|
|
62
47
|
CustomerInteraction,
|
|
63
48
|
{
|
|
64
49
|
id,
|
|
65
50
|
tenantId: auth.tenantId,
|
|
66
|
-
organizationId,
|
|
67
51
|
deletedAt: null,
|
|
68
52
|
interactionType: "email"
|
|
69
53
|
},
|
|
70
54
|
void 0,
|
|
71
|
-
|
|
55
|
+
{ tenantId: auth.tenantId, organizationId: scope?.selectedId ?? auth.orgId ?? null }
|
|
72
56
|
);
|
|
73
57
|
if (!interaction) {
|
|
74
58
|
return NextResponse.json({ error: "Email not found" }, { status: 404 });
|
|
@@ -77,6 +61,20 @@ async function PATCH(req, context) {
|
|
|
77
61
|
if (!isAuthor) {
|
|
78
62
|
return NextResponse.json({ error: "Email not found" }, { status: 404 });
|
|
79
63
|
}
|
|
64
|
+
const organizationId = interaction.organizationId ?? null;
|
|
65
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
66
|
+
tenantId: auth.tenantId,
|
|
67
|
+
organizationId,
|
|
68
|
+
userId,
|
|
69
|
+
resourceKind: "customers.interaction",
|
|
70
|
+
resourceId: id,
|
|
71
|
+
operation: "custom",
|
|
72
|
+
requestMethod: req.method,
|
|
73
|
+
requestHeaders: req.headers
|
|
74
|
+
});
|
|
75
|
+
if (guardResult && !guardResult.ok) {
|
|
76
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
77
|
+
}
|
|
80
78
|
if (interaction.visibility === body.visibility) {
|
|
81
79
|
return NextResponse.json({ ok: true, changed: false });
|
|
82
80
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/api/interactions/%5Bid%5D/visibility/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomerInteraction } from '../../../../data/entities'\nimport type { InteractionUpdateInput } from '../../../../data/validators'\nimport { resolveAuthActorId } from '../../../../lib/interactionRequestContext'\nimport { emitCustomersEvent } from '../../../../events'\n\nexport const metadata = {\n path: '/customers/interactions/[id]/visibility',\n PATCH: {\n requireAuth: true,\n requireFeatures: ['customers.email.compose'],\n },\n}\n\nconst bodySchema = z.object({ visibility: z.enum(['private', 'shared']) }).strict()\n\ntype RouteContext = { params: Promise<{ id: string }> | { id: string } }\n\nexport async function PATCH(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid interaction id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAE7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,0CAA0C;AACnD,SAAS,2BAA2B;AAEpC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AAE5B,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,OAAO;AAAA,IACL,aAAa;AAAA,IACb,iBAAiB,CAAC,yBAAyB;AAAA,EAC7C;AACF;AAEA,MAAM,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC,EAAE,CAAC,EAAE,OAAO;AAIlF,eAAsB,MAAM,KAAc,SAA0C;AAClF,QAAM,EAAE,GAAG,IAAI,MAAM,QAAQ;AAC7B,MAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,SAAS;AAC5C,WAAO,aAAa,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/E;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,WAAW,MAAM,MAAM,aAAa,KAAK,IAAI,CAAC;AAAA,EACvD,SAAS,KAAK;AACZ,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,uBAAuB;AAAA,MACrE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport {\n validateCrudMutationGuard,\n runCrudMutationGuardAfterSuccess,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomerInteraction } from '../../../../data/entities'\nimport type { InteractionUpdateInput } from '../../../../data/validators'\nimport { resolveAuthActorId } from '../../../../lib/interactionRequestContext'\nimport { emitCustomersEvent } from '../../../../events'\n\nexport const metadata = {\n path: '/customers/interactions/[id]/visibility',\n PATCH: {\n requireAuth: true,\n requireFeatures: ['customers.email.compose'],\n },\n}\n\nconst bodySchema = z.object({ visibility: z.enum(['private', 'shared']) }).strict()\n\ntype RouteContext = { params: Promise<{ id: string }> | { id: string } }\n\nexport async function PATCH(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid interaction id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const userId = resolveAuthActorId(auth)\n\n // Load the interaction by its tenant-unique id (not a hand-rolled selected\n // org) so this keeps working under the \"All organizations\" scope. The record's\n // own organization then becomes the concrete org the guard, command, and audit\n // event are attributed to.\n const interaction = (await findOneWithDecryption(\n em,\n CustomerInteraction,\n {\n id,\n tenantId: auth.tenantId,\n deletedAt: null,\n interactionType: 'email',\n } as any,\n undefined,\n { tenantId: auth.tenantId as string, organizationId: scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null },\n )) as { id: string; organizationId?: string | null; authorUserId?: string | null; visibility?: string | null } | null\n\n if (!interaction) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n // Personal mailbox privacy (v1: strict owner-only): ONLY the author may flip\n // their own email's visibility \u2014 no admin bypass. Return 404 (not 403) for\n // everyone else so we don't leak the row's existence \u2014 this also covers\n // non-authors who cannot see a private email in the first place. This owner\n // check is stricter than any org gate, so no separate org read guard is needed.\n const isAuthor = !!interaction.authorUserId && interaction.authorUserId === auth.sub\n if (!isAuthor) {\n return NextResponse.json({ error: 'Email not found' }, { status: 404 })\n }\n\n const organizationId = interaction.organizationId ?? null\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId,\n userId,\n resourceKind: 'customers.interaction',\n resourceId: id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n // No-op: visibility is already at the requested value.\n if (interaction.visibility === body.visibility) {\n return NextResponse.json({ ok: true, changed: false })\n }\n\n const previousVisibility = (interaction.visibility ?? 'private') as 'private' | 'shared'\n\n // Route the write through the interactions update command so the change runs\n // the full mutation pipeline \u2014 query-index refresh, audit log and undo \u2014\n // instead of a raw em.flush() that would leave the indexed `entity_indexes`\n // doc stale. Authorization (author-only, v1) was already enforced above; the\n // command only owns persistence and side effects.\n const commandBus = container.resolve('commandBus') as CommandBus\n await commandBus.execute<InteractionUpdateInput, { interactionId: string }>(\n 'customers.interactions.update',\n {\n input: { id, visibility: body.visibility },\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n },\n )\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId,\n userId,\n resourceKind: 'customers.interaction',\n resourceId: id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n // Emit audit event best-effort \u2014 failure must NOT roll back the DB flush.\n try {\n await emitCustomersEvent('customers.email.visibility_changed', {\n interactionId: interaction.id,\n previousVisibility,\n nextVisibility: body.visibility,\n authorUserId: interaction.authorUserId ?? null,\n actorUserId: auth.sub,\n // v1: strict owner-only \u2014 only the author reaches this point, so a\n // visibility change is never an admin bypass.\n adminBypass: false,\n tenantId: auth.tenantId,\n organizationId,\n })\n } catch {\n /* swallow \u2014 audit emission must not block the response */\n }\n\n return NextResponse.json({ ok: true, changed: true })\n}\n\nexport const openApi = {\n tags: ['Customers', 'Email'],\n methods: {\n PATCH: {\n summary: 'Flip an email interaction visibility (private \u2194 shared)',\n tags: ['Customers', 'Email'],\n responses: [\n { status: 200, description: 'Updated' },\n { status: 400, description: 'Invalid id' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Email not found or not visible to caller' },\n { status: 422, description: 'Invalid body' },\n ],\n },\n },\n}\n\nexport default PATCH\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,6 +4,7 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
6
|
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
7
|
+
import { isOrganizationReadAccessAllowed } from "@open-mercato/core/modules/directory/utils/organizationScopeGuard";
|
|
7
8
|
import { CustomerEntity } from "../../../../data/entities.js";
|
|
8
9
|
import { buildPersonEmailThreads } from "../../../../lib/personEmailThreads.js";
|
|
9
10
|
const metadata = {
|
|
@@ -24,9 +25,7 @@ async function GET(req, context) {
|
|
|
24
25
|
}
|
|
25
26
|
const container = await createRequestContainer();
|
|
26
27
|
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
|
|
27
|
-
const organizationId = scope?.selectedId ?? auth.orgId ?? null;
|
|
28
28
|
const em = container.resolve("em").fork();
|
|
29
|
-
const dscope = { tenantId: auth.tenantId, organizationId };
|
|
30
29
|
const person = await findOneWithDecryption(
|
|
31
30
|
em,
|
|
32
31
|
CustomerEntity,
|
|
@@ -34,15 +33,18 @@ async function GET(req, context) {
|
|
|
34
33
|
id: personId,
|
|
35
34
|
kind: "person",
|
|
36
35
|
tenantId: auth.tenantId,
|
|
37
|
-
organizationId,
|
|
38
36
|
deletedAt: null
|
|
39
37
|
},
|
|
40
38
|
void 0,
|
|
41
|
-
|
|
39
|
+
{ tenantId: auth.tenantId, organizationId: scope?.selectedId ?? auth.orgId ?? null }
|
|
42
40
|
);
|
|
43
41
|
if (!person) {
|
|
44
42
|
return NextResponse.json({ error: "Person not found" }, { status: 404 });
|
|
45
43
|
}
|
|
44
|
+
const organizationId = person.organizationId ?? null;
|
|
45
|
+
if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {
|
|
46
|
+
return NextResponse.json({ error: "Person not found" }, { status: 404 });
|
|
47
|
+
}
|
|
46
48
|
const viewerUserId = auth.isApiKey ? null : auth.sub ?? null;
|
|
47
49
|
const threads = await buildPersonEmailThreads(em, {
|
|
48
50
|
personId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/api/people/%5Bid%5D/email-threads/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { CustomerEntity } from '../../../../data/entities'\nimport { buildPersonEmailThreads } from '../../../../lib/personEmailThreads'\n\nexport const metadata = {\n path: '/customers/people/[id]/email-threads',\n GET: {\n requireAuth: true,\n requireFeatures: ['customers.people.view'],\n },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { id: personId } = await context.params\n if (!z.string().uuid().safeParse(personId).success) {\n return NextResponse.json({ error: 'Invalid person id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,0CAA0C;AACnD,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AAEjC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,IACb,iBAAiB,CAAC,uBAAuB;AAAA,EAC3C;AACF;AAMA,eAAsB,IAAI,KAAc,SAA0C;AAChF,QAAM,EAAE,IAAI,SAAS,IAAI,MAAM,QAAQ;AACvC,MAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,QAAQ,EAAE,SAAS;AAClD,WAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1E;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerEntity } from '../../../../data/entities'\nimport { buildPersonEmailThreads } from '../../../../lib/personEmailThreads'\n\nexport const metadata = {\n path: '/customers/people/[id]/email-threads',\n GET: {\n requireAuth: true,\n requireFeatures: ['customers.people.view'],\n },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { id: personId } = await context.params\n if (!z.string().uuid().safeParse(personId).success) {\n return NextResponse.json({ error: 'Invalid person id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n\n // Verify the Person exists in the caller's tenant, then fail-closed on the\n // record's own organization \u2014 same pattern as the [id] detail route. Loading\n // by tenant + id (not a hand-rolled selected org) keeps this working under the\n // \"All organizations\" scope, where no concrete org is carried.\n const person = await findOneWithDecryption(\n em,\n CustomerEntity,\n {\n id: personId,\n kind: 'person',\n tenantId: auth.tenantId,\n deletedAt: null,\n } as never,\n undefined,\n { tenantId: auth.tenantId as string, organizationId: scope?.selectedId ?? (auth as { orgId?: string | null }).orgId ?? null },\n )\n if (!person) {\n return NextResponse.json({ error: 'Person not found' }, { status: 404 })\n }\n const organizationId = (person as { organizationId?: string | null }).organizationId ?? null\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId })) {\n return NextResponse.json({ error: 'Person not found' }, { status: 404 })\n }\n\n const viewerUserId = auth.isApiKey ? null : auth.sub ?? null\n\n const threads = await buildPersonEmailThreads(em, {\n personId,\n tenantId: auth.tenantId as string,\n organizationId,\n viewerUserId,\n // The v1 visibility filter is owner-only and ignores `userFeatures`; match\n // the sibling read routes (people/[id], interactions) by passing `undefined`\n // rather than paying an RBAC round-trip the filter discards.\n userFeatures: undefined,\n })\n\n return NextResponse.json({ threads })\n}\n\nexport const openApi = {\n tags: ['Customers', 'Email'],\n methods: {\n GET: {\n summary: 'List a Person\\'s email threads (Gmail-style conversation grouping)',\n tags: ['Customers', 'Email'],\n responses: [\n { status: 200, description: 'Email threads for the person, grouped by conversation' },\n { status: 400, description: 'Invalid person id' },\n { status: 401, description: 'Unauthorized' },\n { status: 403, description: 'Missing customers.people.view feature' },\n { status: 404, description: 'Person not found' },\n ],\n },\n },\n}\n\nexport default GET\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,0CAA0C;AACnD,SAAS,uCAAuC;AAChD,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AAEjC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,KAAK;AAAA,IACH,aAAa;AAAA,IACb,iBAAiB,CAAC,uBAAuB;AAAA,EAC3C;AACF;AAMA,eAAsB,IAAI,KAAc,SAA0C;AAChF,QAAM,EAAE,IAAI,SAAS,IAAI,MAAM,QAAQ;AACvC,MAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,QAAQ,EAAE,SAAS;AAClD,WAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1E;AAEA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,UAAU;AACjC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAM3D,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,UAAU,KAAK;AAAA,MACf,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,EAAE,UAAU,KAAK,UAAoB,gBAAgB,OAAO,cAAe,KAAmC,SAAS,KAAK;AAAA,EAC9H;AACA,MAAI,CAAC,QAAQ;AACX,WAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzE;AACA,QAAM,iBAAkB,OAA8C,kBAAkB;AACxF,MAAI,CAAC,gCAAgC,EAAE,OAAO,MAAM,eAAe,CAAC,GAAG;AACrE,WAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzE;AAEA,QAAM,eAAe,KAAK,WAAW,OAAO,KAAK,OAAO;AAExD,QAAM,UAAU,MAAM,wBAAwB,IAAI;AAAA,IAChD;AAAA,IACA,UAAU,KAAK;AAAA,IACf;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA,cAAc;AAAA,EAChB,CAAC;AAED,SAAO,aAAa,KAAK,EAAE,QAAQ,CAAC;AACtC;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,aAAa,OAAO;AAAA,EAC3B,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,MAAM,CAAC,aAAa,OAAO;AAAA,MAC3B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wDAAwD;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,QAChD,EAAE,QAAQ,KAAK,aAAa,eAAe;AAAA,QAC3C,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|