@open-mercato/core 0.10.1-develop.7847.1.7d83dc5794 → 0.10.1-develop.7857.1.259928eb85
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/attachments/api/file/[id]/route.js +6 -1
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +16 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +11 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/cli.js +2 -0
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +11 -0
- package/dist/modules/attachments/data/validators.js.map +2 -2
- package/dist/modules/attachments/index.js +10 -0
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-query.js +5 -1
- package/dist/modules/attachments/lib/access-query.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/driverFactory.js +54 -33
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js +11 -1
- package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
- package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/storageErrors.js +22 -0
- package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
- package/dist/modules/customers/api/companies/[id]/route.js +4 -9
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +4 -9
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +23 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/jest.config.cjs +1 -1
- package/package.json +8 -8
- package/src/modules/attachments/api/file/[id]/route.ts +7 -1
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
- package/src/modules/attachments/api/library/[id]/route.ts +14 -1
- package/src/modules/attachments/api/route.ts +11 -3
- package/src/modules/attachments/cli.ts +2 -0
- package/src/modules/attachments/data/validators.ts +11 -0
- package/src/modules/attachments/i18n/de.json +1 -0
- package/src/modules/attachments/i18n/en.json +1 -0
- package/src/modules/attachments/i18n/es.json +1 -0
- package/src/modules/attachments/i18n/ko.json +1 -0
- package/src/modules/attachments/i18n/pl.json +1 -0
- package/src/modules/attachments/index.ts +12 -0
- package/src/modules/attachments/lib/access-query.ts +5 -1
- package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
- package/src/modules/attachments/lib/drivers/index.ts +13 -0
- package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
- package/src/modules/attachments/lib/storageErrors.ts +21 -0
- package/src/modules/customer_accounts/AGENTS.md +1 -1
- package/src/modules/customers/api/companies/[id]/route.ts +6 -13
- package/src/modules/customers/api/people/[id]/route.ts +6 -13
- package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
- package/src/modules/customers/lib/todoCompatibility.ts +28 -0
- package/src/modules/sales/AGENTS.md +3 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
|
|
2
|
+
import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
|
|
1
3
|
import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../lib/access-runner.js";
|
|
2
4
|
import { attachmentAccessErrorResponse } from "../../../lib/access-errors.js";
|
|
3
5
|
import { NextResponse } from "next/server";
|
|
@@ -19,7 +21,7 @@ import { resolveAttachmentRequestScope } from "@open-mercato/core/modules/attach
|
|
|
19
21
|
const metadata = {
|
|
20
22
|
GET: { requireAuth: false }
|
|
21
23
|
};
|
|
22
|
-
async function
|
|
24
|
+
async function readAttachmentFile(req, context) {
|
|
23
25
|
const { id } = await context.params;
|
|
24
26
|
if (!id) {
|
|
25
27
|
return NextResponse.json(
|
|
@@ -54,6 +56,7 @@ async function GET(req, context) {
|
|
|
54
56
|
const partition = await em.findOne(AttachmentPartition, {
|
|
55
57
|
code: attachment.partitionCode
|
|
56
58
|
});
|
|
59
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
57
60
|
if (!partition) {
|
|
58
61
|
return NextResponse.json(
|
|
59
62
|
{ error: "Partition misconfigured" },
|
|
@@ -98,6 +101,7 @@ async function GET(req, context) {
|
|
|
98
101
|
const responseBody = new Uint8Array(buffer);
|
|
99
102
|
return new NextResponse(responseBody, { status: 200, headers });
|
|
100
103
|
}
|
|
104
|
+
const GET = withAttachmentStorageErrors(readAttachmentFile);
|
|
101
105
|
const openApi = {
|
|
102
106
|
tag: attachmentsTag,
|
|
103
107
|
summary: "Download attachment file",
|
|
@@ -113,6 +117,7 @@ const openApi = {
|
|
|
113
117
|
}
|
|
114
118
|
],
|
|
115
119
|
errors: [
|
|
120
|
+
{ status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
|
|
116
121
|
{
|
|
117
122
|
status: 400,
|
|
118
123
|
description: "Missing attachment ID",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/attachments/api/file/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\
|
|
5
|
-
"mappings": "AAAA,SAAS,+BAA+B,gCAAgC;AACxE,SAAS,qCAAqC;AAC9C,SAAsB,oBAAoB;AAE1C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,uBAAuB,wBAAwB;AACxD,SAAS,SAAS;AAClB,SAAS,gBAAgB,6BAA6B;AACtD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,qCAAqC;AAEvC,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,
|
|
4
|
+
"sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from \"next/server\";\nimport type { OpenApiRouteDoc } from \"@open-mercato/shared/lib/openapi\";\nimport { getAuthFromRequest } from \"@open-mercato/shared/lib/auth/server\";\nimport { createRequestContainer } from \"@open-mercato/shared/lib/di/container\";\nimport {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nasync function readAttachmentFile(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const container = await createRequestContainer();\n const em = container.resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (container.resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req);\n if (requestScope.denied) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth;\n const findFilter: Record<string, unknown> = { id };\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId;\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: renderInline ? 'render' : 'read',\n context: createAttachmentAccessContext(container),\n });\n if (!access.ok) return attachmentAccessErrorResponse(access);\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n });\n let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const headers: Record<string, string> = {\n \"Cache-Control\": access.protected ? \"private, no-store\" : checkAttachmentAccess(null, attachment, partition).ok\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentFile)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
|
|
2
|
+
import { assertAttachmentStoragePartitionExists } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
|
|
1
3
|
import { createAttachmentAccessContext, evaluateAttachmentAccess } from "../../../../lib/access-runner.js";
|
|
2
4
|
import { attachmentAccessErrorResponse } from "../../../../lib/access-errors.js";
|
|
3
5
|
import { NextResponse } from "next/server";
|
|
@@ -31,7 +33,7 @@ const querySchema = z.object({
|
|
|
31
33
|
const metadata = {
|
|
32
34
|
GET: { requireAuth: false }
|
|
33
35
|
};
|
|
34
|
-
async function
|
|
36
|
+
async function readAttachmentImage(req, context) {
|
|
35
37
|
const auth = await getAuthFromRequest(req);
|
|
36
38
|
const { id } = await context.params;
|
|
37
39
|
if (!id) {
|
|
@@ -62,6 +64,7 @@ async function GET(req, context) {
|
|
|
62
64
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
63
65
|
}
|
|
64
66
|
const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode });
|
|
67
|
+
assertAttachmentStoragePartitionExists(Boolean(partition));
|
|
65
68
|
if (!partition) {
|
|
66
69
|
return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
|
|
67
70
|
}
|
|
@@ -134,6 +137,7 @@ async function GET(req, context) {
|
|
|
134
137
|
return NextResponse.json({ error: "Failed to render image" }, { status: 500 });
|
|
135
138
|
}
|
|
136
139
|
}
|
|
140
|
+
const GET = withAttachmentStorageErrors(readAttachmentImage);
|
|
137
141
|
const openApi = {
|
|
138
142
|
tag: attachmentsTag,
|
|
139
143
|
summary: "Serve resized images",
|
|
@@ -149,6 +153,7 @@ const openApi = {
|
|
|
149
153
|
}
|
|
150
154
|
],
|
|
151
155
|
errors: [
|
|
156
|
+
{ status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
|
|
152
157
|
{ status: 400, description: "Invalid parameters, missing ID, or non-image attachment", schema: attachmentErrorSchema },
|
|
153
158
|
{ status: 401, description: "Unauthorized - authentication required for private partitions", schema: attachmentErrorSchema },
|
|
154
159
|
{ status: 403, description: "Forbidden - insufficient permissions", schema: attachmentErrorSchema },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\
|
|
5
|
-
"mappings": "AAAA,SAAS,+BAA+B,gCAAgC;AACxE,SAAS,qCAAqC;AAC9C,SAAsB,oBAAoB;AAC1C,OAAO,WAAmC;AAC1C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,YAAY,2BAA2B;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uBAAuB,wBAAwB;AAExD,SAAS,gBAAkC,6BAA6B;AACxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,qCAAqC;AAC9C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,aAAa,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAEvE,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EACzD,QAAQ,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAI,EAAE,SAAS;AAAA,EAC1D,UAAU,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC,EAAE,SAAS;AAClD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM;AAC5B;AAEA,
|
|
4
|
+
"sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'\nimport { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'\nimport { attachmentAccessErrorResponse } from '../../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentRequestScope } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nasync function readAttachmentImage(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const scopedAuth = auth ? { ...auth, orgId: requestScope.organizationId } : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n assertAttachmentStoragePartitionExists(Boolean(partition))\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = await evaluateAttachmentAccess({\n auth: scopedAuth, attachment, partition, action: 'render', context: createAttachmentAccessContext(container),\n })\n if (!access.ok) return attachmentAccessErrorResponse(access)\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': access.protected ? 'private, no-store' : checkAttachmentAccess(null, attachment, partition).ok ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const GET = withAttachmentStorageErrors(readAttachmentImage)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
|
|
2
|
+
import { getAttachmentStoragePolicy } from "../../../lib/drivers/storageValidation.js";
|
|
1
3
|
import { LockMode } from "@mikro-orm/core";
|
|
2
4
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
5
|
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
@@ -244,6 +246,18 @@ async function deleteAttachment(req, ctx) {
|
|
|
244
246
|
tenantId: auth.tenantId,
|
|
245
247
|
organizationId: auth.orgId
|
|
246
248
|
};
|
|
249
|
+
if (getAttachmentStoragePolicy() === "strict") {
|
|
250
|
+
const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, void 0, {
|
|
251
|
+
tenantId: auth.tenantId,
|
|
252
|
+
organizationId: auth.orgId
|
|
253
|
+
});
|
|
254
|
+
if (!preflightRecord) return throwAttachmentAccessError(404);
|
|
255
|
+
await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: "delete" });
|
|
256
|
+
await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {
|
|
257
|
+
tenantId: preflightRecord.tenantId ?? auth.tenantId,
|
|
258
|
+
organizationId: preflightRecord.organizationId ?? auth.orgId
|
|
259
|
+
});
|
|
260
|
+
}
|
|
247
261
|
const outcome = await em.transactional(async (tx) => {
|
|
248
262
|
const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
|
|
249
263
|
tenantId: auth.tenantId,
|
|
@@ -289,7 +303,7 @@ async function deleteAttachment(req, ctx) {
|
|
|
289
303
|
return NextResponse.json({ ok: true });
|
|
290
304
|
}
|
|
291
305
|
const PATCH = withAttachmentAccessErrors(updateAttachment);
|
|
292
|
-
const DELETE = withAttachmentAccessErrors(deleteAttachment);
|
|
306
|
+
const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment));
|
|
293
307
|
const openApi = {
|
|
294
308
|
tag: attachmentsTag,
|
|
295
309
|
summary: "Attachment detail management",
|
|
@@ -334,6 +348,7 @@ const openApi = {
|
|
|
334
348
|
{ status: 200, description: "Attachment deleted successfully", schema: z.object({ ok: z.literal(true) }) }
|
|
335
349
|
],
|
|
336
350
|
errors: [
|
|
351
|
+
{ status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
|
|
337
352
|
{ status: 400, description: "Invalid attachment ID", schema: attachmentErrorSchema },
|
|
338
353
|
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
339
354
|
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/attachments/api/library/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(deleteAttachment)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'\nimport { getAttachmentStoragePolicy } from '../../../lib/drivers/storageValidation'\nimport { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n if (getAttachmentStoragePolicy() === 'strict') {\n const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!preflightRecord) return throwAttachmentAccessError(404)\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: 'delete' })\n await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {\n tenantId: preflightRecord.tenantId ?? auth.tenantId, organizationId: preflightRecord.organizationId ?? auth.orgId,\n })\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment))\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["metadata", "record"]
|
|
7
7
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { attachmentErrorSchema } from "./openapi.js";
|
|
2
|
+
import { withAttachmentStorageErrors } from "@open-mercato/core/modules/attachments/lib/storageErrors";
|
|
3
|
+
import { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from "@open-mercato/core/modules/attachments/lib/drivers/storageValidation";
|
|
1
4
|
import { LockMode } from "@mikro-orm/core";
|
|
2
5
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
6
|
import { prepareAttachmentMutation } from "../lib/access-mutation.js";
|
|
@@ -256,7 +259,7 @@ async function listRecordAttachments(req) {
|
|
|
256
259
|
}, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
|
|
257
260
|
}
|
|
258
261
|
const GET = withAttachmentAccessErrors(listRecordAttachments);
|
|
259
|
-
async function
|
|
262
|
+
async function uploadAttachment(req) {
|
|
260
263
|
const { t } = await resolveTranslations();
|
|
261
264
|
const auth = await getAuthFromRequest(req);
|
|
262
265
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -334,7 +337,7 @@ async function POST(req) {
|
|
|
334
337
|
if (uploadScope.denied) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
335
338
|
const orgId = uploadScope.organizationId;
|
|
336
339
|
if (!orgId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
337
|
-
await ensureDefaultPartitions(em);
|
|
340
|
+
if (getAttachmentStoragePolicy() === "legacy") await ensureDefaultPartitions(em);
|
|
338
341
|
let partitionFromField = null;
|
|
339
342
|
let fieldMaxAttachmentSizeMb = null;
|
|
340
343
|
if (fieldKey) {
|
|
@@ -407,6 +410,7 @@ async function POST(req) {
|
|
|
407
410
|
let partition = null;
|
|
408
411
|
for (const code of partitionCodeCandidates) {
|
|
409
412
|
const record = await em.findOne(AttachmentPartition, { code });
|
|
413
|
+
assertAttachmentStoragePartitionExists(Boolean(record));
|
|
410
414
|
if (record) {
|
|
411
415
|
partition = record;
|
|
412
416
|
break;
|
|
@@ -416,6 +420,7 @@ async function POST(req) {
|
|
|
416
420
|
partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode });
|
|
417
421
|
}
|
|
418
422
|
if (!partition) {
|
|
423
|
+
assertAttachmentStoragePartitionExists(false);
|
|
419
424
|
return NextResponse.json({ error: "Storage partition is not configured." }, { status: 400 });
|
|
420
425
|
}
|
|
421
426
|
await ensureAttachmentPartitionProtection(em, partition);
|
|
@@ -700,7 +705,8 @@ async function deleteAttachment(req) {
|
|
|
700
705
|
}
|
|
701
706
|
return NextResponse.json({ ok: true });
|
|
702
707
|
}
|
|
703
|
-
const
|
|
708
|
+
const POST = withAttachmentAccessErrors(withAttachmentStorageErrors(uploadAttachment));
|
|
709
|
+
const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment));
|
|
704
710
|
const openApi = {
|
|
705
711
|
summary: "Manage entity attachments",
|
|
706
712
|
description: "Upload and list attachments associated with module entities and records.",
|
|
@@ -731,6 +737,7 @@ const openApi = {
|
|
|
731
737
|
{ status: 200, description: "Attachment stored successfully", schema: uploadResponseSchema }
|
|
732
738
|
],
|
|
733
739
|
errors: [
|
|
740
|
+
{ status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
|
|
734
741
|
{ status: 400, description: "Payload validation error", schema: errorSchema },
|
|
735
742
|
{ status: 401, description: "Unauthorized", schema: errorSchema },
|
|
736
743
|
{ status: 403, description: "Attachment violates field constraints", schema: errorSchema }
|
|
@@ -745,6 +752,7 @@ const openApi = {
|
|
|
745
752
|
{ status: 404, description: "Attachment not found", schema: errorSchema }
|
|
746
753
|
],
|
|
747
754
|
errors: [
|
|
755
|
+
{ status: 503, description: "Attachment storage configuration rejected by policy", schema: attachmentErrorSchema },
|
|
748
756
|
{ status: 400, description: "Missing attachment identifier", schema: errorSchema },
|
|
749
757
|
{ status: 401, description: "Unauthorized", schema: errorSchema },
|
|
750
758
|
{ status: 403, description: "Owner policy denies access", schema: errorSchema },
|