@open-mercato/core 0.10.1-develop.7741.1.61a2652671 → 0.10.1-develop.7749.1.ea3edf75e3
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/generated/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- package/dist/modules/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.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/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/lib/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/api/partitions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { 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 { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\ntype AuthLike = { sub?: string; tenantId?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null | undefined\n\nfunction isSuperAdmin(auth: AuthLike): boolean {\n return auth?.isSuperAdmin === true\n}\n\nfunction canManagePartition(auth: AuthLike, entry: AttachmentPartition): boolean {\n if (!auth?.tenantId) return false\n if (entry.tenantId && entry.tenantId === auth.tenantId) return true\n if (entry.tenantId == null && isSuperAdmin(auth)) return true\n return false\n}\n\nfunction partitionVisibilityFilter(auth: AuthLike) {\n if (isSuperAdmin(auth)) return {}\n return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] }\n}\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await findWithDecryption<AttachmentPartition>(\n em,\n AttachmentPartition,\n partitionVisibilityFilter(auth),\n { orderBy: { createdAt: 'asc' } },\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { code },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usageFilter: Record<string, unknown> = { partitionCode: entry.code }\n if (entry.tenantId) usageFilter.tenantId = entry.tenantId\n const usage = await em.count(Attachment, usageFilter)\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,YAAY,2BAA2B;AAChD,SAAS,yBAAyB,+BAA+B,uBAAuB,iCAAiC;AACzH,SAAS,8BAA8B;
|
|
4
|
+
"sourcesContent": ["import { 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 { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from '../../lib/partitions'\nimport { resolvePartitionEnvKey } from '../../lib/partitionEnv'\nimport { getDeclaredAttachmentAccessRequirements } from '../../lib/access-protection'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { resolveDefaultAttachmentOcrEnabled } from '../../lib/ocrConfig'\nimport {\n attachmentsTag,\n partitionCreateSchema,\n partitionUpdateSchema,\n partitionResponseSchema,\n partitionListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst deleteSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst DEFAULT_CODES = new Set(DEFAULT_ATTACHMENT_PARTITIONS.map((entry) => entry.code))\n\ntype AuthLike = { sub?: string; tenantId?: string | null; orgId?: string | null; isSuperAdmin?: boolean } | null | undefined\n\nfunction isSuperAdmin(auth: AuthLike): boolean {\n return auth?.isSuperAdmin === true\n}\n\nfunction canManagePartition(auth: AuthLike, entry: AttachmentPartition): boolean {\n if (!auth?.tenantId) return false\n if (entry.tenantId && entry.tenantId === auth.tenantId) return true\n if (entry.tenantId == null && isSuperAdmin(auth)) return true\n return false\n}\n\nfunction partitionVisibilityFilter(auth: AuthLike) {\n if (isSuperAdmin(auth)) return {}\n return { $or: [{ tenantId: null }, { tenantId: auth?.tenantId ?? null }] }\n}\n\nfunction serializePartition(entry: AttachmentPartition) {\n return {\n id: entry.id,\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n requiresOcr: entry.requiresOcr ?? resolveDefaultAttachmentOcrEnabled(),\n ocrModel: entry.ocrModel ?? null,\n storageDriver: entry.storageDriver ?? 'local',\n configJson: entry.configJson ?? null,\n createdAt: entry.createdAt instanceof Date ? entry.createdAt.toISOString() : null,\n updatedAt: entry.updatedAt instanceof Date ? entry.updatedAt.toISOString() : null,\n envKey: resolvePartitionEnvKey(entry.code),\n }\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n} as const\n\nasync function resolveEm() {\n const { resolve } = await createRequestContainer()\n return resolve('em') as EntityManager\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const rows = await findWithDecryption<AttachmentPartition>(\n em,\n AttachmentPartition,\n partitionVisibilityFilter(auth),\n { orderBy: { createdAt: 'asc' } },\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n return NextResponse.json({ items: rows.map((entry) => serializePartition(entry)) })\n}\n\nexport async function POST(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionCreateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const code = sanitizePartitionCode(parsed.data.code)\n if (!code) {\n return NextResponse.json({ error: 'Partition code is required.' }, { status: 400 })\n }\n const em = await resolveEm()\n await ensureDefaultPartitions(em)\n const exists = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { code },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (exists) {\n return NextResponse.json({ error: 'Partition code already exists.' }, { status: 409 })\n }\n const entry = em.create(AttachmentPartition, {\n code,\n accessResolverRequirements: getDeclaredAttachmentAccessRequirements(code),\n title: parsed.data.title.trim(),\n description: parsed.data.description?.trim() ?? null,\n storageDriver: parsed.data.storageDriver ?? 'local',\n configJson: parsed.data.configJson ?? null,\n isPublic: parsed.data.isPublic ?? false,\n requiresOcr:\n typeof parsed.data.requiresOcr === 'boolean'\n ? parsed.data.requiresOcr\n : resolveDefaultAttachmentOcrEnabled(),\n ocrModel: parsed.data.ocrModel?.trim() || null,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) }, { status: 201 })\n}\n\nexport async function PUT(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n let json: unknown = null\n try {\n json = await req.json()\n } catch {\n json = null\n }\n const parsed = partitionUpdateSchema.safeParse(json)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (sanitizePartitionCode(parsed.data.code) !== entry.code) {\n return NextResponse.json({ error: 'Partition code cannot be changed.' }, { status: 400 })\n }\n entry.title = parsed.data.title.trim()\n entry.description = parsed.data.description?.trim() ?? null\n entry.isPublic = parsed.data.isPublic ?? false\n if (typeof parsed.data.requiresOcr === 'boolean') {\n entry.requiresOcr = parsed.data.requiresOcr\n }\n if (parsed.data.ocrModel !== undefined) {\n entry.ocrModel = parsed.data.ocrModel?.trim() || null\n }\n if (parsed.data.storageDriver !== undefined) {\n entry.storageDriver = parsed.data.storageDriver\n }\n if (parsed.data.configJson !== undefined) {\n entry.configJson = parsed.data.configJson ?? null\n }\n await em.persist(entry).flush()\n return NextResponse.json({ item: serializePartition(entry) })\n}\n\nexport async function DELETE(req: Request) {\n if (isPartitionSettingsLocked()) {\n return NextResponse.json(\n { error: 'Attachment partitions are managed by the environment in demo/onboarding mode.' },\n { status: 403 },\n )\n }\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const id = url.searchParams.get('id')\n const parsed = deleteSchema.safeParse({ id })\n if (!parsed.success) {\n return NextResponse.json({ error: 'Partition id is required' }, { status: 400 })\n }\n const em = await resolveEm()\n const entry = await findOneWithDecryption(\n em,\n AttachmentPartition,\n { id: parsed.data.id },\n undefined,\n { tenantId: auth.tenantId, organizationId: auth.orgId ?? null },\n )\n if (!entry || !canManagePartition(auth, entry)) {\n return NextResponse.json({ error: 'Partition not found' }, { status: 404 })\n }\n if (DEFAULT_CODES.has(entry.code)) {\n return NextResponse.json({ error: 'Default partitions cannot be removed.' }, { status: 400 })\n }\n const usageFilter: Record<string, unknown> = { partitionCode: entry.code }\n if (entry.tenantId) usageFilter.tenantId = entry.tenantId\n const usage = await em.count(Attachment, usageFilter)\n if (usage > 0) {\n return NextResponse.json({ error: 'Partition is in use and cannot be removed.' }, { status: 409 })\n }\n await em.remove(entry).flush()\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment partition management',\n methods: {\n GET: {\n summary: 'List all attachment partitions',\n description: 'Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.',\n responses: [\n { status: 200, description: 'List of partitions', schema: partitionListResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n ],\n },\n POST: {\n summary: 'Create new partition',\n description: 'Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionCreateSchema,\n },\n responses: [\n { status: 201, description: 'Partition created successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or partition code', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition code already exists', schema: attachmentErrorSchema },\n ],\n },\n PUT: {\n summary: 'Update partition',\n description: 'Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.',\n requestBody: {\n contentType: 'application/json',\n schema: partitionUpdateSchema,\n },\n responses: [\n { status: 200, description: 'Partition updated successfully', schema: partitionResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or code change attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete partition',\n description: 'Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.',\n responses: [\n { status: 200, description: 'Partition deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid ID or default partition deletion attempt', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Partitions locked in demo mode', schema: attachmentErrorSchema },\n { status: 404, description: 'Partition not found', schema: attachmentErrorSchema },\n { status: 409, description: 'Partition in use', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { LockMode } from "@mikro-orm/core";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { prepareAttachmentMutation } from "../lib/access-mutation.js";
|
|
4
|
+
import { throwAttachmentAccessError } from "../lib/access-errors.js";
|
|
5
|
+
import { createAttachmentAccessContext } from "../lib/access-runner.js";
|
|
6
|
+
import { assertAttachmentOwnerAccess, requiresAttachmentAccessScan, scanAuthorizedAttachments } from "../lib/access-query.js";
|
|
7
|
+
import { withAttachmentAccessErrors } from "../lib/access-errors.js";
|
|
1
8
|
import { NextResponse } from "next/server";
|
|
2
9
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
10
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
11
|
import { z } from "zod";
|
|
5
12
|
import { sql } from "kysely";
|
|
6
13
|
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from "../lib/imageUrls.js";
|
|
14
|
+
import { ensureAttachmentPartitionProtection } from "../lib/access-protection.js";
|
|
7
15
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from "../lib/partitions.js";
|
|
8
16
|
import { Attachment, AttachmentPartition } from "../data/entities.js";
|
|
9
17
|
import { extractAttachmentContent } from "../lib/textExtraction.js";
|
|
@@ -11,7 +19,7 @@ import { requestOcrProcessing } from "../lib/ocrQueue.js";
|
|
|
11
19
|
import { StorageDriverFactory } from "../lib/drivers/index.js";
|
|
12
20
|
import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
|
|
13
21
|
import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
|
|
14
|
-
import {
|
|
22
|
+
import { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from "../lib/storageReferences.js";
|
|
15
23
|
import { assertAttachmentScopeInvariant } from "../lib/access.js";
|
|
16
24
|
import { resolveAttachmentRequestScope } from "../lib/requestScope.js";
|
|
17
25
|
import {
|
|
@@ -168,7 +176,7 @@ function parseFormAssignments(value) {
|
|
|
168
176
|
return [];
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
|
-
async function
|
|
179
|
+
async function listRecordAttachments(req) {
|
|
172
180
|
const auth = await getAuthFromRequest(req);
|
|
173
181
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
174
182
|
const url = new URL(req.url);
|
|
@@ -191,27 +199,33 @@ async function GET(req) {
|
|
|
191
199
|
if (orgId) filter.organizationId = orgId;
|
|
192
200
|
const orderBy = { createdAt: "DESC", id: "DESC" };
|
|
193
201
|
const usePaging = typeof page === "number" && typeof pageSize === "number";
|
|
194
|
-
const total = usePaging ? await em.count(Attachment, filter) : null;
|
|
195
202
|
const currentPage = usePaging ? Math.max(1, page) : null;
|
|
196
203
|
const currentPageSize = usePaging ? pageSize : null;
|
|
197
|
-
const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize)) : null;
|
|
198
204
|
const pageOffset = usePaging ? (currentPage - 1) * currentPageSize : void 0;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const partitions = await em.find(AttachmentPartition, {});
|
|
206
|
+
const protectedScan = requiresAttachmentAccessScan(partitions);
|
|
207
|
+
const scope = { tenantId: auth.tenantId ?? null, organizationId: orgId ?? null };
|
|
208
|
+
let items;
|
|
209
|
+
let total;
|
|
210
|
+
if (protectedScan) {
|
|
211
|
+
const scanned = await scanAuthorizedAttachments({
|
|
212
|
+
auth: { ...auth, orgId },
|
|
213
|
+
context: createAttachmentAccessContext(container),
|
|
214
|
+
partitions,
|
|
215
|
+
offset: pageOffset,
|
|
216
|
+
limit: currentPageSize ?? ATTACHMENT_LIST_MAX_PAGE_SIZE,
|
|
217
|
+
loadBatch: (offset, limit) => findWithDecryption(em, Attachment, filter, { orderBy, offset, limit }, scope)
|
|
218
|
+
});
|
|
219
|
+
items = scanned.records;
|
|
220
|
+
total = usePaging ? scanned.total : null;
|
|
221
|
+
} else {
|
|
222
|
+
total = usePaging ? await em.count(Attachment, filter) : null;
|
|
223
|
+
items = await findWithDecryption(em, Attachment, filter, {
|
|
204
224
|
orderBy,
|
|
205
|
-
...usePaging ? {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
tenantId: auth.tenantId ?? null,
|
|
212
|
-
organizationId: orgId ?? null
|
|
213
|
-
}
|
|
214
|
-
);
|
|
225
|
+
...usePaging ? { limit: currentPageSize, offset: pageOffset } : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }
|
|
226
|
+
}, scope);
|
|
227
|
+
}
|
|
228
|
+
const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize)) : null;
|
|
215
229
|
return NextResponse.json({
|
|
216
230
|
items: items.map((a) => {
|
|
217
231
|
const metadata2 = readAttachmentMetadata(a.storageMetadata);
|
|
@@ -239,8 +253,9 @@ async function GET(req) {
|
|
|
239
253
|
pageSize: currentPageSize,
|
|
240
254
|
totalPages
|
|
241
255
|
} : {}
|
|
242
|
-
});
|
|
256
|
+
}, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
|
|
243
257
|
}
|
|
258
|
+
const GET = withAttachmentAccessErrors(listRecordAttachments);
|
|
244
259
|
async function POST(req) {
|
|
245
260
|
const { t } = await resolveTranslations();
|
|
246
261
|
const auth = await getAuthFromRequest(req);
|
|
@@ -403,6 +418,7 @@ async function POST(req) {
|
|
|
403
418
|
if (!partition) {
|
|
404
419
|
return NextResponse.json({ error: "Storage partition is not configured." }, { status: 400 });
|
|
405
420
|
}
|
|
421
|
+
await ensureAttachmentPartitionProtection(em, partition);
|
|
406
422
|
const requestedPublicOverride = typeof partitionOverride === "string" && partitionOverride.length > 0 && partition.code === partitionOverride && partition.isPublic === true && partition.code !== defaultPartitionCode && partition.code !== partitionFromField;
|
|
407
423
|
if (requestedPublicOverride) {
|
|
408
424
|
return NextResponse.json({ error: t("attachments.errors.publicPartitionBlocked", "Public storage partitions cannot be selected explicitly for this upload.") }, { status: 403 });
|
|
@@ -621,7 +637,7 @@ async function readTenantAttachmentUsageBytes(em, tenantId) {
|
|
|
621
637
|
throw new Error("Attachment quota accounting is unavailable.", { cause: error });
|
|
622
638
|
}
|
|
623
639
|
}
|
|
624
|
-
async function
|
|
640
|
+
async function deleteAttachment(req) {
|
|
625
641
|
const auth = await getAuthFromRequest(req);
|
|
626
642
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
627
643
|
const url = new URL(req.url);
|
|
@@ -636,20 +652,37 @@ async function DELETE(req) {
|
|
|
636
652
|
const orgId = deleteScope.organizationId;
|
|
637
653
|
const deleteFilter = { id, tenantId: auth.tenantId };
|
|
638
654
|
if (orgId) deleteFilter.organizationId = orgId;
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
655
|
+
const scopedAuth = { ...auth, orgId };
|
|
656
|
+
const outcome = await em.transactional(async (tx) => {
|
|
657
|
+
const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
|
|
658
|
+
tenantId: auth.tenantId,
|
|
659
|
+
organizationId: orgId
|
|
660
|
+
});
|
|
661
|
+
if (!candidate) return throwAttachmentAccessError(404);
|
|
662
|
+
const referenceIds = await lockAttachmentStorageReferences(tx, candidate);
|
|
663
|
+
const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id);
|
|
664
|
+
if (!removal.removed) return throwAttachmentAccessError(404);
|
|
665
|
+
const record2 = await findOneWithDecryption(tx, Attachment, deleteFilter, {
|
|
666
|
+
lockMode: LockMode.PESSIMISTIC_WRITE,
|
|
667
|
+
refresh: true
|
|
668
|
+
}, { tenantId: auth.tenantId, organizationId: orgId });
|
|
669
|
+
if (!record2) return throwAttachmentAccessError(404);
|
|
670
|
+
const guard = await prepareAttachmentMutation({ container, req, auth: scopedAuth, recordId: record2.id, operation: "delete" });
|
|
671
|
+
await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth: scopedAuth, attachment: record2, action: "delete" });
|
|
672
|
+
const driver = record2.storagePath ? await storageDriverFactory.resolveForPartition(record2.partitionCode, {
|
|
673
|
+
tenantId: record2.tenantId ?? auth.tenantId,
|
|
674
|
+
organizationId: record2.organizationId ?? orgId ?? ""
|
|
675
|
+
}) : null;
|
|
676
|
+
tx.remove(record2);
|
|
677
|
+
await tx.flush();
|
|
678
|
+
return { record: record2, driver, guard, releaseStorage: removal.releaseStorage };
|
|
679
|
+
});
|
|
680
|
+
const { record } = outcome;
|
|
643
681
|
await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {
|
|
644
682
|
logger.error("Failed to cleanup cached thumbnails", { err: error });
|
|
645
683
|
});
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
tenantId: record.tenantId ?? auth.tenantId,
|
|
649
|
-
organizationId: record.organizationId ?? orgId ?? ""
|
|
650
|
-
});
|
|
651
|
-
await delDriver.delete(record.partitionCode, record.storagePath);
|
|
652
|
-
}
|
|
684
|
+
if (outcome.driver && outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath);
|
|
685
|
+
await outcome.guard.runAfterSuccess();
|
|
653
686
|
if (dataEngine) {
|
|
654
687
|
await emitCrudSideEffects({
|
|
655
688
|
dataEngine,
|
|
@@ -667,6 +700,7 @@ async function DELETE(req) {
|
|
|
667
700
|
}
|
|
668
701
|
return NextResponse.json({ ok: true });
|
|
669
702
|
}
|
|
703
|
+
const DELETE = withAttachmentAccessErrors(deleteAttachment);
|
|
670
704
|
const openApi = {
|
|
671
705
|
summary: "Manage entity attachments",
|
|
672
706
|
description: "Upload and list attachments associated with module entities and records.",
|
|
@@ -680,7 +714,10 @@ const openApi = {
|
|
|
680
714
|
],
|
|
681
715
|
errors: [
|
|
682
716
|
{ status: 400, description: "Missing entity or record identifiers", schema: errorSchema },
|
|
683
|
-
{ status: 401, description: "Unauthorized", schema: errorSchema }
|
|
717
|
+
{ status: 401, description: "Unauthorized", schema: errorSchema },
|
|
718
|
+
{ status: 403, description: "Owner policy denies access", schema: errorSchema },
|
|
719
|
+
{ status: 404, description: "Attachment or owner is unavailable", schema: errorSchema },
|
|
720
|
+
{ status: 504, description: "Owner authorization timed out", schema: errorSchema }
|
|
684
721
|
]
|
|
685
722
|
},
|
|
686
723
|
POST: {
|
|
@@ -709,7 +746,10 @@ const openApi = {
|
|
|
709
746
|
],
|
|
710
747
|
errors: [
|
|
711
748
|
{ status: 400, description: "Missing attachment identifier", schema: errorSchema },
|
|
712
|
-
{ status: 401, description: "Unauthorized", schema: errorSchema }
|
|
749
|
+
{ status: 401, description: "Unauthorized", schema: errorSchema },
|
|
750
|
+
{ status: 403, description: "Owner policy denies access", schema: errorSchema },
|
|
751
|
+
{ status: 404, description: "Attachment or owner is unavailable", schema: errorSchema },
|
|
752
|
+
{ status: 504, description: "Owner authorization timed out", schema: errorSchema }
|
|
713
753
|
]
|
|
714
754
|
}
|
|
715
755
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/api/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { extractAttachmentContent } from '../lib/textExtraction'\nimport { requestOcrProcessing } from '../lib/ocrQueue'\nimport { StorageDriverFactory } from '../lib/drivers'\nimport { OcrService, shouldUseLlmOcr } from '../lib/ocrService'\nimport { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'\nimport { removeAttachmentRecord } from '../lib/storageReferences'\nimport { assertAttachmentScopeInvariant } from '../lib/access'\nimport { resolveAttachmentRequestScope } from '../lib/requestScope'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from '../lib/metadata'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from '../lib/security'\nimport {\n isMultipartUploadLimitError,\n isMultipartRequestWithinUploadLimit,\n parseMultipartFormDataWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '../lib/upload-limits'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AttachmentQuotaService } from '../lib/quota-service'\nimport { resolveAttachmentsUploadRateLimitConfig } from '../lib/ocrLimits'\nimport { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\n\nconst logger = createLogger('attachments')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\nconst ATTACHMENT_LIST_MAX_PAGE_SIZE = 100\n\nconst attachmentQuerySchema = z.object({\n entityId: z.string().min(1).describe('Entity identifier that owns the attachments'),\n recordId: z.string().min(1).describe('Record identifier within the entity'),\n page: z.coerce.number().min(1).optional(),\n pageSize: z.coerce.number().min(1).max(ATTACHMENT_LIST_MAX_PAGE_SIZE).optional(),\n})\n\nconst attachmentAssignmentSchema = z.object({\n type: z.string().describe('Assignment type identifier'),\n id: z.string().describe('Assignment record identifier'),\n href: z.string().nullable().optional().describe('Optional link to the related record'),\n label: z.string().nullable().optional().describe('Optional label for the assignment'),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string().describe('Attachment identifier'),\n url: z.string().describe('Public path to the stored asset'),\n fileName: z.string().describe('Original filename'),\n fileSize: z.number().int().nonnegative().describe('File size in bytes'),\n createdAt: z.string().describe('Upload timestamp (ISO 8601)'),\n mimeType: z.string().nullable().optional().describe('MIME type of the file'),\n thumbnailUrl: z.string().optional().describe('Helper route that renders a thumbnail'),\n partitionCode: z.string().optional().describe('Partition identifier'),\n tags: z.array(z.string()).optional().describe('Tags assigned to the attachment'),\n content: z.string().nullable().optional().describe('Extracted text or markdown content'),\n assignments: z.array(attachmentAssignmentSchema).optional().describe('Records that reference this attachment'),\n})\n\nconst attachmentListResponseSchema = z.object({\n items: z.array(attachmentItemSchema),\n total: z.number().int().nonnegative().optional(),\n page: z.number().int().min(1).optional(),\n pageSize: z.number().int().min(1).optional(),\n totalPages: z.number().int().min(1).optional(),\n})\n\nconst attachmentUploadBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n fieldKey: z.string().optional(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n customFields: z\n .string()\n .optional()\n .describe('JSON encoded map of custom field values collected from the upload form.'),\n})\n\nconst attachmentDeleteQuerySchema = z.object({\n id: z.string().uuid(),\n})\n\nconst uploadResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string(),\n url: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n thumbnailUrl: z.string().optional(),\n content: z.string().nullable().optional(),\n tags: z.array(z.string()).optional(),\n assignments: z.array(attachmentAssignmentSchema).optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n }),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction parseCustomFieldsEntry(value: FormDataEntryValue | null): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return {}\n try {\n const parsed = JSON.parse(trimmed)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n } catch {\n return {}\n }\n }\n if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n return { ...(value as Record<string, unknown>) }\n }\n return {}\n}\n\nfunction buildFormPayload(form: FormData): Record<string, unknown> {\n const payload: Record<string, unknown> = {}\n form.forEach((value, key) => {\n if (key === 'customFields') {\n payload.customFields = parseCustomFieldsEntry(value)\n return\n }\n payload[key] = value\n })\n return payload\n}\n\nfunction parseFormTags(value: FormDataEntryValue | null): string[] {\n if (!value) return []\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentTags(parsed)\n } catch {\n return normalizeAttachmentTags(value)\n }\n }\n return []\n}\n\nfunction parseFormAssignments(value: FormDataEntryValue | null): AttachmentAssignment[] {\n if (!value) return []\n if (typeof value !== 'string') return []\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentAssignments(parsed)\n } catch {\n return []\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsedQuery = attachmentQuerySchema.safeParse({\n entityId: url.searchParams.get('entityId') || '',\n recordId: url.searchParams.get('recordId') || '',\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'entityId and recordId are required' }, { status: 400 })\n }\n const { entityId, recordId, page, pageSize } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n const orgId = requestScope.organizationId\n const filter: Record<string, unknown> = { entityId, recordId, tenantId: auth.tenantId! }\n if (orgId) filter.organizationId = orgId\n const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC', id: 'DESC' }\n const usePaging = typeof page === 'number' && typeof pageSize === 'number'\n const total = usePaging ? await em.count(Attachment, filter) : null\n const currentPage = usePaging ? Math.max(1, page) : null\n const currentPageSize = usePaging ? pageSize : null\n const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null\n const pageOffset = usePaging ? (currentPage! - 1) * currentPageSize! : undefined\n const items = await findWithDecryption(\n em,\n Attachment,\n filter,\n {\n orderBy,\n ...(usePaging\n ? {\n limit: currentPageSize!,\n offset: pageOffset,\n }\n : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }),\n },\n {\n tenantId: auth.tenantId ?? null,\n organizationId: orgId ?? null,\n },\n )\n return NextResponse.json({\n items: items.map((a: any) => {\n const metadata = readAttachmentMetadata(a.storageMetadata)\n return {\n id: a.id,\n url: a.url,\n fileName: a.fileName,\n fileSize: a.fileSize,\n createdAt: a.createdAt,\n mimeType: a.mimeType ?? null,\n partitionCode: a.partitionCode,\n content: a.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(a.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(a.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n }\n }),\n ...(usePaging\n ? {\n total,\n page: currentPage,\n pageSize: currentPageSize,\n totalPages,\n }\n : {}),\n })\n}\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n // A superadmin browsing with \"All organizations\" selected has no concrete\n // organization scope, but an attachment must be stored under exactly one\n // organization (the scope invariant forbids partial-null rows). Reject with a\n // clear, actionable error instead of a 401 \u2014 a 401 makes the client-side\n // fetch layer treat it as session expiry, show a misleading toast, and reset\n // the in-progress form (#3764).\n if (!auth.orgId) {\n return NextResponse.json({\n error: t('attachments.errors.selectOrganization', 'Select a specific organization before uploading an attachment.'),\n }, { status: 400 })\n }\n const tenantId = auth.tenantId\n\n const container = await createRequestContainer()\n let rateLimiterService: RateLimiterService | null = null\n try {\n rateLimiterService = container.resolve('rateLimiterService') as RateLimiterService | null\n } catch (error) {\n // Fail-open: uploads proceed when the limiter cannot be resolved (minimal/test containers).\n logger.warn('Attachment upload rate limiter unavailable; throttle skipped', { err: error })\n }\n if (rateLimiterService) {\n const principal = typeof auth.sub === 'string' && auth.sub.length > 0\n ? auth.sub\n : (typeof auth.userId === 'string' && auth.userId.length > 0 ? auth.userId : null)\n if (!principal) {\n logger.warn('Attachment upload rate limit skipped: missing authenticated principal')\n } else {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n resolveAttachmentsUploadRateLimitConfig(),\n `${tenantId}:${principal}`,\n t(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n } else {\n logger.warn('Attachment upload rate limiter not registered; throttle skipped')\n }\n\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ error: 'Expected multipart/form-data' }, { status: 400 })\n }\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n throw error\n }\n const formPayload = buildFormPayload(form)\n const customFieldValues = splitCustomFieldPayload(formPayload).custom\n const entityId = String(form.get('entityId') || '')\n const recordId = String(form.get('recordId') || '')\n const fieldKey = String(form.get('fieldKey') || '')\n const file = form.get('file') as unknown as File | null\n if (!entityId || !recordId || !file) return NextResponse.json({ error: 'entityId, recordId and file are required' }, { status: 400 })\n const partitionOverrideRaw = form.get('partitionCode')\n const partitionOverride =\n typeof partitionOverrideRaw === 'string' && partitionOverrideRaw.trim().length > 0\n ? sanitizePartitionCode(partitionOverrideRaw)\n : null\n const tags = parseFormTags(form.get('tags'))\n const assignmentsFromForm = parseFormAssignments(form.get('assignments'))\n\n const em = container.resolve('em') as EntityManager\n const dataEngine = container.resolve('dataEngine')\n let attachmentQuotaService: AttachmentQuotaService | null = null\n let attachmentQuotaRecoveryScheduler: ((\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>) | null = null\n try {\n attachmentQuotaService = container.resolve('attachmentQuotaService') as AttachmentQuotaService\n attachmentQuotaRecoveryScheduler = container.resolve('attachmentQuotaRecoveryScheduler') as (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>\n } catch {\n // Legacy fallback below when the quota service is not registered in this container.\n }\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n const uploadScope = await resolveAttachmentRequestScope(container, auth, req)\n if (uploadScope.denied) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n const orgId = uploadScope.organizationId\n if (!orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n await ensureDefaultPartitions(em)\n // Optional per-field validations\n let partitionFromField: string | null = null\n let fieldMaxAttachmentSizeMb: number | null = null\n if (fieldKey) {\n try {\n const { CustomFieldDef } = await import('@open-mercato/core/modules/entities/data/entities')\n const def = await em.findOne(CustomFieldDef, {\n entityId,\n key: fieldKey,\n $and: [\n { $or: [ { tenantId: auth.tenantId }, { tenantId: null } ] },\n ],\n isActive: true,\n })\n const cfg = (def as any)?.configJson || {}\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n if (Array.isArray(cfg.acceptExtensions) && cfg.acceptExtensions.length) {\n const allowed = new Set((cfg.acceptExtensions as any[]).map((x: any) => String(x).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) return NextResponse.json({ error: 'File type not allowed' }, { status: 400 })\n }\n if (typeof cfg.maxAttachmentSizeMb === 'number' && cfg.maxAttachmentSizeMb > 0) {\n fieldMaxAttachmentSizeMb = cfg.maxAttachmentSizeMb\n }\n if (typeof cfg.partitionCode === 'string' && cfg.partitionCode.trim().length > 0) {\n partitionFromField = sanitizePartitionCode(cfg.partitionCode)\n }\n } catch {}\n }\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({\n error: t('attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'),\n }, { status: 400 })\n }\n const effectiveMaxBytes = resolveAttachmentMaxBytes(fieldMaxAttachmentSizeMb)\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n if (!attachmentQuotaService) {\n try {\n const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n } catch (error) {\n logger.error('Attachment quota accounting failed', { err: error })\n return NextResponse.json({\n error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),\n }, { status: 500 })\n }\n }\n const buf = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeUploadedFileName(file.name)\n const fileMimeType = detectAttachmentMimeType(buf, safeName, (file as any).type)\n if (isActiveContentAttachment(buf, safeName, fileMimeType)) {\n return NextResponse.json({ error: t('attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n const defaultPartitionCode = resolveDefaultPartitionCode(entityId)\n const resolvedPartitionCode = partitionOverride ?? partitionFromField ?? defaultPartitionCode\n const partitionCodeCandidates = Array.from(\n new Set(\n [partitionOverride, partitionFromField, resolvedPartitionCode].filter(\n (code): code is string => typeof code === 'string' && code.length > 0,\n ),\n ),\n )\n let partition: AttachmentPartition | null = null\n for (const code of partitionCodeCandidates) {\n const record = await em.findOne(AttachmentPartition, { code })\n if (record) {\n partition = record\n break\n }\n }\n if (!partition) {\n partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode })\n }\n if (!partition) {\n return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })\n }\n const requestedPublicOverride =\n typeof partitionOverride === 'string' &&\n partitionOverride.length > 0 &&\n partition.code === partitionOverride &&\n partition.isPublic === true &&\n partition.code !== defaultPartitionCode &&\n partition.code !== partitionFromField\n if (requestedPublicOverride) {\n return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })\n }\n const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })\n const preparedStoragePath = attachmentQuotaService\n ? uploadDriver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n })\n : undefined\n if (attachmentQuotaService && !preparedStoragePath) {\n return NextResponse.json({\n error: t('attachments.errors.quotaRecoveryUnsupported', 'Storage driver cannot participate in quota recovery.'),\n }, { status: 500 })\n }\n let quotaReservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n if (attachmentQuotaService) {\n try {\n quotaReservation = await attachmentQuotaService.reserve({\n tenantId,\n organizationId: orgId,\n bytes: buf.length,\n source: 'attachment',\n storageDriver: uploadDriver.key,\n storagePath: preparedStoragePath!,\n partitionCode: partition.code,\n })\n if (!attachmentQuotaRecoveryScheduler) throw new Error('Attachment quota recovery is unavailable.')\n await attachmentQuotaRecoveryScheduler({\n reservationId: quotaReservation.id,\n tenantId,\n organizationId: orgId,\n }, Math.max(1_000, quotaReservation.expiresAt.getTime() - Date.now()))\n if (typeof attachmentQuotaService.beginStorage === 'function') {\n await attachmentQuotaService.beginStorage(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (error) {\n if (quotaReservation) {\n await attachmentQuotaService.release(quotaReservation.id, quotaReservation.leaseToken).catch(() => {})\n quotaReservation = null\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n if (code === 'quota_target_exists') {\n return NextResponse.json({\n error: t('attachments.errors.storagePathExists', 'The target storage path already exists.'),\n }, { status: 409 })\n }\n logger.error('Attachment quota reservation failed', { err: error })\n return NextResponse.json({\n error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),\n }, { status: 500 })\n }\n }\n let storedPath: string\n try {\n const stored = await uploadDriver.store({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n buffer: buf,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n if (quotaReservation) {\n await attachmentQuotaService!.markStored(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (error) {\n logger.error('Failed to persist file', { err: error })\n if (preparedStoragePath) {\n try {\n await (uploadDriver.deleteStrict?.(partition.code, preparedStoragePath)\n ?? uploadDriver.delete(partition.code, preparedStoragePath))\n if (quotaReservation) {\n await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (cleanupError) {\n logger.error('Failed to compensate attachment storage', { err: cleanupError })\n }\n }\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n const requiresOcr =\n typeof (partition as any).requiresOcr === 'boolean'\n ? Boolean((partition as any).requiresOcr)\n : resolveDefaultAttachmentOcrEnabled()\n let extractedContent: string | null = null\n const wantsLlmOcr = requiresOcr && shouldUseLlmOcr(fileMimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n\n if (requiresOcr && !useLlmOcr) {\n const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({\n filePath: localPath,\n mimeType: fileMimeType,\n })\n } catch (error) {\n logger.error('Failed to extract attachment content', { err: error })\n } finally {\n await cleanup().catch(() => {})\n }\n }\n\n let assignments = assignmentsFromForm.slice()\n if (entityId !== LIBRARY_ENTITY_ID) {\n assignments = upsertAssignment(assignments, { type: entityId, id: recordId })\n }\n const metadata = mergeAttachmentMetadata(null, { assignments, tags })\n const attachmentId = randomUUID()\n assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: orgId })\n const att = em.create(Attachment, {\n id: attachmentId,\n entityId,\n recordId,\n organizationId: orgId,\n tenantId: auth.tenantId!,\n fileName: safeName,\n mimeType: fileMimeType,\n fileSize: buf.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n // Persist the attachment row and its custom-field values atomically so a\n // custom-field failure cannot leave behind a committed orphan attachment.\n try {\n await em.transactional(async (tx) => {\n await tx.persist(att).flush()\n if (dataEngine) {\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.attachments.attachment,\n recordId: attachmentId,\n tenantId,\n organizationId: orgId,\n values: customFieldValues,\n })\n }\n if (quotaReservation) {\n await attachmentQuotaService!.completeAttachment(quotaReservation.id, quotaReservation.leaseToken, tx)\n }\n })\n } catch (error) {\n logger.error('Failed to persist attachment with custom attributes', { err: error })\n try {\n await (uploadDriver.deleteStrict?.(partition.code, storedPath)\n ?? uploadDriver.delete(partition.code, storedPath))\n if (quotaReservation) {\n await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (cleanupError) {\n logger.error('Failed to compensate attachment persistence', { err: cleanupError })\n }\n return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })\n }\n\n if (useLlmOcr) {\n // Await so wait-queue overflow can finish the inline text fallback before\n // the response returns (background LLM OCR still schedules via setImmediate).\n try {\n await requestOcrProcessing(em, att, uploadDriver, storedPath)\n } catch (error) {\n logger.error('Failed to queue OCR processing', { err: error })\n }\n } else if (wantsLlmOcr) {\n logger.warn('OCR requested but OPENAI_API_KEY not configured, falling back to text extraction when available')\n }\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: att,\n identifiers: {\n id: att.id,\n organizationId: att.organizationId ?? null,\n tenantId: att.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({\n ok: true,\n item: {\n id: attachmentId,\n url: att.url,\n fileName: safeName,\n fileSize: buf.length,\n partitionCode: partition.code,\n thumbnailUrl: buildAttachmentImageUrl(attachmentId, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(safeName),\n }),\n content: extractedContent ?? null,\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n customFields: Object.keys(customFieldValues).length ? customFieldValues : undefined,\n },\n })\n}\n\nasync function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: string): Promise<number> {\n try {\n const db = em.getKysely<any>() as any\n const row = await db\n .selectFrom('attachments')\n .select(sql<string>`sum(file_size)`.as('total_size'))\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst() as { total_size: string | number | null } | undefined\n const total = row?.total_size\n if (typeof total === 'number') return Number.isFinite(total) ? total : 0\n if (typeof total === 'string') {\n const parsed = Number(total)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n } catch (error) {\n throw new Error('Attachment quota accounting is unavailable.', { cause: error })\n }\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const id = url.searchParams.get('id') || ''\n if (!id) return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const dataEngine = container.resolve('dataEngine')\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n // Resolve the currently selected organization (#3765) so a multi-org admin who\n // switched the header org deletes within that org, not their pinned home org.\n // A superadmin browsing with \"All organizations\" selected has no concrete org,\n // so resolution returns null and the delete falls back to a tenant-only scope\n // (#3764) \u2014 letting them remove any attachment in the tenant.\n const deleteScope = await resolveAttachmentRequestScope(container, auth, req)\n if (deleteScope.denied) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n const orgId = deleteScope.organizationId\n const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId! }\n if (orgId) deleteFilter.organizationId = orgId\n const record = await em.findOne(Attachment, deleteFilter)\n if (!record) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n const removal = await removeAttachmentRecord(em, record)\n if (!removal.removed) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {\n logger.error('Failed to cleanup cached thumbnails', { err: error })\n })\n if (record.storagePath && removal.releaseStorage) {\n const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!,\n organizationId: record.organizationId ?? orgId ?? '',\n })\n await delDriver.delete(record.partitionCode, record.storagePath)\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 ?? null,\n tenantId: record.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage entity attachments',\n description: 'Upload and list attachments associated with module entities and records.',\n methods: {\n GET: {\n summary: 'List attachments for a record',\n description: 'Returns uploaded attachments for the given entity record, ordered by newest first. Without both page and pageSize, only the newest 100 attachments are returned; pass page and pageSize to page through the rest.',\n query: attachmentQuerySchema,\n responses: [\n { status: 200, description: 'Attachments found for the record', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity or record identifiers', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Upload attachment',\n description: 'Uploads a new attachment using multipart form-data and stores metadata for later retrieval.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: attachmentUploadBodySchema,\n },\n responses: [\n { status: 200, description: 'Attachment stored successfully', schema: uploadResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Payload validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Attachment violates field constraints', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Removes an uploaded attachment and deletes the stored asset.',\n query: attachmentDeleteQuerySchema,\n responses: [\n { status: 200, description: 'Attachment deleted', schema: z.object({ ok: z.literal(true) }) },\n { status: 404, description: 'Attachment not found', schema: errorSchema },\n ],\n errors: [\n { status: 400, description: 'Missing attachment identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["metadata"]
|
|
4
|
+
"sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { prepareAttachmentMutation } from '../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../lib/access-errors'\nimport { createAttachmentAccessContext } from '../lib/access-runner'\nimport { assertAttachmentOwnerAccess, requiresAttachmentAccessScan, scanAuthorizedAttachments } from '../lib/access-query'\nimport { withAttachmentAccessErrors } from '../lib/access-errors'\nimport { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { z } from 'zod'\nimport { sql } from 'kysely'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'\nimport { ensureAttachmentPartitionProtection } from '../lib/access-protection'\nimport { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { extractAttachmentContent } from '../lib/textExtraction'\nimport { requestOcrProcessing } from '../lib/ocrQueue'\nimport { StorageDriverFactory } from '../lib/drivers'\nimport { OcrService, shouldUseLlmOcr } from '../lib/ocrService'\nimport { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../lib/storageReferences'\nimport { assertAttachmentScopeInvariant } from '../lib/access'\nimport { resolveAttachmentRequestScope } from '../lib/requestScope'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n upsertAssignment,\n type AttachmentAssignment,\n} from '../lib/metadata'\nimport { randomUUID } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../lib/crud'\nimport { E } from '#generated/entities.ids.generated'\nimport { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'\nimport {\n detectAttachmentMimeType,\n hasDangerousExecutableExtension,\n isActiveContentAttachment,\n sanitizeUploadedFileName,\n} from '../lib/security'\nimport {\n isMultipartUploadLimitError,\n isMultipartRequestWithinUploadLimit,\n parseMultipartFormDataWithinUploadLimit,\n resolveAttachmentMaxBytes,\n willExceedAttachmentTenantQuota,\n} from '../lib/upload-limits'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AttachmentQuotaService } from '../lib/quota-service'\nimport { resolveAttachmentsUploadRateLimitConfig } from '../lib/ocrLimits'\nimport { checkRateLimit, RATE_LIMIT_ERROR_FALLBACK, RATE_LIMIT_ERROR_KEY } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport type { RateLimiterService } from '@open-mercato/shared/lib/ratelimit/service'\n\nconst logger = createLogger('attachments')\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\nconst ATTACHMENT_LIST_MAX_PAGE_SIZE = 100\n\nconst attachmentQuerySchema = z.object({\n entityId: z.string().min(1).describe('Entity identifier that owns the attachments'),\n recordId: z.string().min(1).describe('Record identifier within the entity'),\n page: z.coerce.number().min(1).optional(),\n pageSize: z.coerce.number().min(1).max(ATTACHMENT_LIST_MAX_PAGE_SIZE).optional(),\n})\n\nconst attachmentAssignmentSchema = z.object({\n type: z.string().describe('Assignment type identifier'),\n id: z.string().describe('Assignment record identifier'),\n href: z.string().nullable().optional().describe('Optional link to the related record'),\n label: z.string().nullable().optional().describe('Optional label for the assignment'),\n})\n\nconst attachmentItemSchema = z.object({\n id: z.string().describe('Attachment identifier'),\n url: z.string().describe('Public path to the stored asset'),\n fileName: z.string().describe('Original filename'),\n fileSize: z.number().int().nonnegative().describe('File size in bytes'),\n createdAt: z.string().describe('Upload timestamp (ISO 8601)'),\n mimeType: z.string().nullable().optional().describe('MIME type of the file'),\n thumbnailUrl: z.string().optional().describe('Helper route that renders a thumbnail'),\n partitionCode: z.string().optional().describe('Partition identifier'),\n tags: z.array(z.string()).optional().describe('Tags assigned to the attachment'),\n content: z.string().nullable().optional().describe('Extracted text or markdown content'),\n assignments: z.array(attachmentAssignmentSchema).optional().describe('Records that reference this attachment'),\n})\n\nconst attachmentListResponseSchema = z.object({\n items: z.array(attachmentItemSchema),\n total: z.number().int().nonnegative().optional(),\n page: z.number().int().min(1).optional(),\n pageSize: z.number().int().min(1).optional(),\n totalPages: z.number().int().min(1).optional(),\n})\n\nconst attachmentUploadBodySchema = z.object({\n entityId: z.string().min(1),\n recordId: z.string().min(1),\n fieldKey: z.string().optional(),\n file: z.string().min(1).describe('Binary file payload; supplied as multipart form-data'),\n customFields: z\n .string()\n .optional()\n .describe('JSON encoded map of custom field values collected from the upload form.'),\n})\n\nconst attachmentDeleteQuerySchema = z.object({\n id: z.string().uuid(),\n})\n\nconst uploadResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string(),\n url: z.string(),\n fileName: z.string(),\n fileSize: z.number().int().nonnegative(),\n thumbnailUrl: z.string().optional(),\n content: z.string().nullable().optional(),\n tags: z.array(z.string()).optional(),\n assignments: z.array(attachmentAssignmentSchema).optional(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n }),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nconst LIBRARY_ENTITY_ID = 'attachments:library'\n\nfunction parseCustomFieldsEntry(value: FormDataEntryValue | null): Record<string, unknown> {\n if (!value) return {}\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return {}\n try {\n const parsed = JSON.parse(trimmed)\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n } catch {\n return {}\n }\n }\n if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof File)) {\n return { ...(value as Record<string, unknown>) }\n }\n return {}\n}\n\nfunction buildFormPayload(form: FormData): Record<string, unknown> {\n const payload: Record<string, unknown> = {}\n form.forEach((value, key) => {\n if (key === 'customFields') {\n payload.customFields = parseCustomFieldsEntry(value)\n return\n }\n payload[key] = value\n })\n return payload\n}\n\nfunction parseFormTags(value: FormDataEntryValue | null): string[] {\n if (!value) return []\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentTags(parsed)\n } catch {\n return normalizeAttachmentTags(value)\n }\n }\n return []\n}\n\nfunction parseFormAssignments(value: FormDataEntryValue | null): AttachmentAssignment[] {\n if (!value) return []\n if (typeof value !== 'string') return []\n const trimmed = value.trim()\n if (!trimmed) return []\n try {\n const parsed = JSON.parse(trimmed)\n return normalizeAttachmentAssignments(parsed)\n } catch {\n return []\n }\n}\n\nasync function listRecordAttachments(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const parsedQuery = attachmentQuerySchema.safeParse({\n entityId: url.searchParams.get('entityId') || '',\n recordId: url.searchParams.get('recordId') || '',\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n })\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'entityId and recordId are required' }, { status: 400 })\n }\n const { entityId, recordId, page, pageSize } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const requestScope = await resolveAttachmentRequestScope(container, auth, req)\n if (requestScope.denied) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n const orgId = requestScope.organizationId\n const filter: Record<string, unknown> = { entityId, recordId, tenantId: auth.tenantId! }\n if (orgId) filter.organizationId = orgId\n const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC', id: 'DESC' }\n const usePaging = typeof page === 'number' && typeof pageSize === 'number'\n const currentPage = usePaging ? Math.max(1, page) : null\n const currentPageSize = usePaging ? pageSize : null\n const pageOffset = usePaging ? (currentPage! - 1) * currentPageSize! : undefined\n const partitions = await em.find(AttachmentPartition, {})\n const protectedScan = requiresAttachmentAccessScan(partitions)\n const scope = { tenantId: auth.tenantId ?? null, organizationId: orgId ?? null }\n let items: Attachment[]\n let total: number | null\n if (protectedScan) {\n const scanned = await scanAuthorizedAttachments({\n auth: { ...auth, orgId }, context: createAttachmentAccessContext(container), partitions,\n offset: pageOffset, limit: currentPageSize ?? ATTACHMENT_LIST_MAX_PAGE_SIZE,\n loadBatch: (offset, limit) => findWithDecryption(em, Attachment, filter, { orderBy, offset, limit }, scope),\n })\n items = scanned.records\n total = usePaging ? scanned.total : null\n } else {\n total = usePaging ? await em.count(Attachment, filter) : null\n items = await findWithDecryption(em, Attachment, filter, {\n orderBy, ...(usePaging ? { limit: currentPageSize!, offset: pageOffset } : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }),\n }, scope)\n }\n const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null\n return NextResponse.json({\n items: items.map((a: any) => {\n const metadata = readAttachmentMetadata(a.storageMetadata)\n return {\n id: a.id,\n url: a.url,\n fileName: a.fileName,\n fileSize: a.fileSize,\n createdAt: a.createdAt,\n mimeType: a.mimeType ?? null,\n partitionCode: a.partitionCode,\n content: a.content ?? null,\n thumbnailUrl: buildAttachmentImageUrl(a.id, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(a.fileName),\n }),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n }\n }),\n ...(usePaging\n ? {\n total,\n page: currentPage,\n pageSize: currentPageSize,\n totalPages,\n }\n : {}),\n }, { headers: protectedScan ? { 'Cache-Control': 'private, no-store' } : undefined })\n}\n\nexport const GET = withAttachmentAccessErrors(listRecordAttachments)\n\nexport async function POST(req: Request) {\n const { t } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n // A superadmin browsing with \"All organizations\" selected has no concrete\n // organization scope, but an attachment must be stored under exactly one\n // organization (the scope invariant forbids partial-null rows). Reject with a\n // clear, actionable error instead of a 401 \u2014 a 401 makes the client-side\n // fetch layer treat it as session expiry, show a misleading toast, and reset\n // the in-progress form (#3764).\n if (!auth.orgId) {\n return NextResponse.json({\n error: t('attachments.errors.selectOrganization', 'Select a specific organization before uploading an attachment.'),\n }, { status: 400 })\n }\n const tenantId = auth.tenantId\n\n const container = await createRequestContainer()\n let rateLimiterService: RateLimiterService | null = null\n try {\n rateLimiterService = container.resolve('rateLimiterService') as RateLimiterService | null\n } catch (error) {\n // Fail-open: uploads proceed when the limiter cannot be resolved (minimal/test containers).\n logger.warn('Attachment upload rate limiter unavailable; throttle skipped', { err: error })\n }\n if (rateLimiterService) {\n const principal = typeof auth.sub === 'string' && auth.sub.length > 0\n ? auth.sub\n : (typeof auth.userId === 'string' && auth.userId.length > 0 ? auth.userId : null)\n if (!principal) {\n logger.warn('Attachment upload rate limit skipped: missing authenticated principal')\n } else {\n const rateLimitResponse = await checkRateLimit(\n rateLimiterService,\n resolveAttachmentsUploadRateLimitConfig(),\n `${tenantId}:${principal}`,\n t(RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FALLBACK),\n )\n if (rateLimitResponse) return rateLimitResponse\n }\n } else {\n logger.warn('Attachment upload rate limiter not registered; throttle skipped')\n }\n\n const contentType = req.headers.get('content-type') || ''\n if (!contentType.toLowerCase().includes('multipart/form-data')) {\n return NextResponse.json({ error: 'Expected multipart/form-data' }, { status: 400 })\n }\n if (!isMultipartRequestWithinUploadLimit(req.headers.get('content-length'))) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n\n let form: FormData\n try {\n form = await parseMultipartFormDataWithinUploadLimit(req)\n } catch (error) {\n if (isMultipartUploadLimitError(error)) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n throw error\n }\n const formPayload = buildFormPayload(form)\n const customFieldValues = splitCustomFieldPayload(formPayload).custom\n const entityId = String(form.get('entityId') || '')\n const recordId = String(form.get('recordId') || '')\n const fieldKey = String(form.get('fieldKey') || '')\n const file = form.get('file') as unknown as File | null\n if (!entityId || !recordId || !file) return NextResponse.json({ error: 'entityId, recordId and file are required' }, { status: 400 })\n const partitionOverrideRaw = form.get('partitionCode')\n const partitionOverride =\n typeof partitionOverrideRaw === 'string' && partitionOverrideRaw.trim().length > 0\n ? sanitizePartitionCode(partitionOverrideRaw)\n : null\n const tags = parseFormTags(form.get('tags'))\n const assignmentsFromForm = parseFormAssignments(form.get('assignments'))\n\n const em = container.resolve('em') as EntityManager\n const dataEngine = container.resolve('dataEngine')\n let attachmentQuotaService: AttachmentQuotaService | null = null\n let attachmentQuotaRecoveryScheduler: ((\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>) | null = null\n try {\n attachmentQuotaService = container.resolve('attachmentQuotaService') as AttachmentQuotaService\n attachmentQuotaRecoveryScheduler = container.resolve('attachmentQuotaRecoveryScheduler') as (\n payload: { reservationId: string; tenantId: string; organizationId: string },\n delayMs: number,\n ) => Promise<void>\n } catch {\n // Legacy fallback below when the quota service is not registered in this container.\n }\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n const uploadScope = await resolveAttachmentRequestScope(container, auth, req)\n if (uploadScope.denied) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n const orgId = uploadScope.organizationId\n if (!orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n await ensureDefaultPartitions(em)\n // Optional per-field validations\n let partitionFromField: string | null = null\n let fieldMaxAttachmentSizeMb: number | null = null\n if (fieldKey) {\n try {\n const { CustomFieldDef } = await import('@open-mercato/core/modules/entities/data/entities')\n const def = await em.findOne(CustomFieldDef, {\n entityId,\n key: fieldKey,\n $and: [\n { $or: [ { tenantId: auth.tenantId }, { tenantId: null } ] },\n ],\n isActive: true,\n })\n const cfg = (def as any)?.configJson || {}\n const ext = (file.name || '').split('.').pop()?.toLowerCase() || ''\n if (Array.isArray(cfg.acceptExtensions) && cfg.acceptExtensions.length) {\n const allowed = new Set((cfg.acceptExtensions as any[]).map((x: any) => String(x).toLowerCase().replace(/^\\./, '')))\n if (!allowed.has(ext)) return NextResponse.json({ error: 'File type not allowed' }, { status: 400 })\n }\n if (typeof cfg.maxAttachmentSizeMb === 'number' && cfg.maxAttachmentSizeMb > 0) {\n fieldMaxAttachmentSizeMb = cfg.maxAttachmentSizeMb\n }\n if (typeof cfg.partitionCode === 'string' && cfg.partitionCode.trim().length > 0) {\n partitionFromField = sanitizePartitionCode(cfg.partitionCode)\n }\n } catch {}\n }\n if (hasDangerousExecutableExtension(file.name)) {\n return NextResponse.json({\n error: t('attachments.errors.dangerousExecutable', 'Executable file types are not allowed as attachments.'),\n }, { status: 400 })\n }\n const effectiveMaxBytes = resolveAttachmentMaxBytes(fieldMaxAttachmentSizeMb)\n if (file.size > effectiveMaxBytes) {\n return NextResponse.json({\n error: t('attachments.errors.maxUploadSize', 'Attachment exceeds the maximum upload size.'),\n }, { status: 413 })\n }\n if (!attachmentQuotaService) {\n try {\n const tenantUsageBytes = await readTenantAttachmentUsageBytes(em, tenantId)\n if (willExceedAttachmentTenantQuota(tenantUsageBytes, file.size)) {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n } catch (error) {\n logger.error('Attachment quota accounting failed', { err: error })\n return NextResponse.json({\n error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),\n }, { status: 500 })\n }\n }\n const buf = Buffer.from(await file.arrayBuffer())\n const safeName = sanitizeUploadedFileName(file.name)\n const fileMimeType = detectAttachmentMimeType(buf, safeName, (file as any).type)\n if (isActiveContentAttachment(buf, safeName, fileMimeType)) {\n return NextResponse.json({ error: t('attachments.errors.activeContentBlocked', 'Active content uploads are not allowed.') }, { status: 400 })\n }\n const defaultPartitionCode = resolveDefaultPartitionCode(entityId)\n const resolvedPartitionCode = partitionOverride ?? partitionFromField ?? defaultPartitionCode\n const partitionCodeCandidates = Array.from(\n new Set(\n [partitionOverride, partitionFromField, resolvedPartitionCode].filter(\n (code): code is string => typeof code === 'string' && code.length > 0,\n ),\n ),\n )\n let partition: AttachmentPartition | null = null\n for (const code of partitionCodeCandidates) {\n const record = await em.findOne(AttachmentPartition, { code })\n if (record) {\n partition = record\n break\n }\n }\n if (!partition) {\n partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode })\n }\n if (!partition) {\n return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })\n }\n await ensureAttachmentPartitionProtection(em, partition)\n const requestedPublicOverride =\n typeof partitionOverride === 'string' &&\n partitionOverride.length > 0 &&\n partition.code === partitionOverride &&\n partition.isPublic === true &&\n partition.code !== defaultPartitionCode &&\n partition.code !== partitionFromField\n if (requestedPublicOverride) {\n return NextResponse.json({ error: t('attachments.errors.publicPartitionBlocked', 'Public storage partitions cannot be selected explicitly for this upload.') }, { status: 403 })\n }\n const uploadDriver = await storageDriverFactory.resolveForPartition(partition.code, { tenantId, organizationId: orgId })\n const preparedStoragePath = attachmentQuotaService\n ? uploadDriver.prepareStoragePath?.({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n })\n : undefined\n if (attachmentQuotaService && !preparedStoragePath) {\n return NextResponse.json({\n error: t('attachments.errors.quotaRecoveryUnsupported', 'Storage driver cannot participate in quota recovery.'),\n }, { status: 500 })\n }\n let quotaReservation: { id: string; leaseToken: string; expiresAt: Date } | null = null\n if (attachmentQuotaService) {\n try {\n quotaReservation = await attachmentQuotaService.reserve({\n tenantId,\n organizationId: orgId,\n bytes: buf.length,\n source: 'attachment',\n storageDriver: uploadDriver.key,\n storagePath: preparedStoragePath!,\n partitionCode: partition.code,\n })\n if (!attachmentQuotaRecoveryScheduler) throw new Error('Attachment quota recovery is unavailable.')\n await attachmentQuotaRecoveryScheduler({\n reservationId: quotaReservation.id,\n tenantId,\n organizationId: orgId,\n }, Math.max(1_000, quotaReservation.expiresAt.getTime() - Date.now()))\n if (typeof attachmentQuotaService.beginStorage === 'function') {\n await attachmentQuotaService.beginStorage(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (error) {\n if (quotaReservation) {\n await attachmentQuotaService.release(quotaReservation.id, quotaReservation.leaseToken).catch(() => {})\n quotaReservation = null\n }\n const code = (error as { code?: unknown })?.code\n if (code === 'quota_exceeded') {\n return NextResponse.json({\n error: t('attachments.errors.quotaExceeded', 'Attachment storage quota exceeded for this tenant.'),\n }, { status: 413 })\n }\n if (code === 'quota_target_exists') {\n return NextResponse.json({\n error: t('attachments.errors.storagePathExists', 'The target storage path already exists.'),\n }, { status: 409 })\n }\n logger.error('Attachment quota reservation failed', { err: error })\n return NextResponse.json({\n error: t('attachments.errors.quotaUnavailable', 'Storage quota accounting is unavailable.'),\n }, { status: 500 })\n }\n }\n let storedPath: string\n try {\n const stored = await uploadDriver.store({\n partitionCode: partition.code,\n orgId,\n tenantId,\n fileName: safeName,\n buffer: buf,\n storagePath: preparedStoragePath,\n })\n storedPath = stored.storagePath\n if (quotaReservation) {\n await attachmentQuotaService!.markStored(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (error) {\n logger.error('Failed to persist file', { err: error })\n if (preparedStoragePath) {\n try {\n await (uploadDriver.deleteStrict?.(partition.code, preparedStoragePath)\n ?? uploadDriver.delete(partition.code, preparedStoragePath))\n if (quotaReservation) {\n await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (cleanupError) {\n logger.error('Failed to compensate attachment storage', { err: cleanupError })\n }\n }\n return NextResponse.json({ error: 'Failed to persist attachment.' }, { status: 500 })\n }\n\n const requiresOcr =\n typeof (partition as any).requiresOcr === 'boolean'\n ? Boolean((partition as any).requiresOcr)\n : resolveDefaultAttachmentOcrEnabled()\n let extractedContent: string | null = null\n const wantsLlmOcr = requiresOcr && shouldUseLlmOcr(fileMimeType, safeName)\n const ocrService = wantsLlmOcr ? new OcrService() : null\n const useLlmOcr = Boolean(wantsLlmOcr && ocrService?.available)\n\n if (requiresOcr && !useLlmOcr) {\n const { filePath: localPath, cleanup } = await uploadDriver.toLocalPath(partition.code, storedPath)\n try {\n extractedContent = await extractAttachmentContent({\n filePath: localPath,\n mimeType: fileMimeType,\n })\n } catch (error) {\n logger.error('Failed to extract attachment content', { err: error })\n } finally {\n await cleanup().catch(() => {})\n }\n }\n\n let assignments = assignmentsFromForm.slice()\n if (entityId !== LIBRARY_ENTITY_ID) {\n assignments = upsertAssignment(assignments, { type: entityId, id: recordId })\n }\n const metadata = mergeAttachmentMetadata(null, { assignments, tags })\n const attachmentId = randomUUID()\n assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: orgId })\n const att = em.create(Attachment, {\n id: attachmentId,\n entityId,\n recordId,\n organizationId: orgId,\n tenantId: auth.tenantId!,\n fileName: safeName,\n mimeType: fileMimeType,\n fileSize: buf.length,\n partitionCode: partition.code,\n storageDriver: partition.storageDriver || 'local',\n storagePath: storedPath,\n url: buildAttachmentFileUrl(attachmentId),\n content: extractedContent,\n storageMetadata: metadata,\n })\n // Persist the attachment row and its custom-field values atomically so a\n // custom-field failure cannot leave behind a committed orphan attachment.\n try {\n await em.transactional(async (tx) => {\n await tx.persist(att).flush()\n if (dataEngine) {\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.attachments.attachment,\n recordId: attachmentId,\n tenantId,\n organizationId: orgId,\n values: customFieldValues,\n })\n }\n if (quotaReservation) {\n await attachmentQuotaService!.completeAttachment(quotaReservation.id, quotaReservation.leaseToken, tx)\n }\n })\n } catch (error) {\n logger.error('Failed to persist attachment with custom attributes', { err: error })\n try {\n await (uploadDriver.deleteStrict?.(partition.code, storedPath)\n ?? uploadDriver.delete(partition.code, storedPath))\n if (quotaReservation) {\n await attachmentQuotaService!.release(quotaReservation.id, quotaReservation.leaseToken)\n }\n } catch (cleanupError) {\n logger.error('Failed to compensate attachment persistence', { err: cleanupError })\n }\n return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })\n }\n\n if (useLlmOcr) {\n // Await so wait-queue overflow can finish the inline text fallback before\n // the response returns (background LLM OCR still schedules via setImmediate).\n try {\n await requestOcrProcessing(em, att, uploadDriver, storedPath)\n } catch (error) {\n logger.error('Failed to queue OCR processing', { err: error })\n }\n } else if (wantsLlmOcr) {\n logger.warn('OCR requested but OPENAI_API_KEY not configured, falling back to text extraction when available')\n }\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: att,\n identifiers: {\n id: att.id,\n organizationId: att.organizationId ?? null,\n tenantId: att.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({\n ok: true,\n item: {\n id: attachmentId,\n url: att.url,\n fileName: safeName,\n fileSize: buf.length,\n partitionCode: partition.code,\n thumbnailUrl: buildAttachmentImageUrl(attachmentId, {\n width: 320,\n height: 320,\n slug: slugifyAttachmentFileName(safeName),\n }),\n content: extractedContent ?? null,\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n customFields: Object.keys(customFieldValues).length ? customFieldValues : undefined,\n },\n })\n}\n\nasync function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: string): Promise<number> {\n try {\n const db = em.getKysely<any>() as any\n const row = await db\n .selectFrom('attachments')\n .select(sql<string>`sum(file_size)`.as('total_size'))\n .where('tenant_id', '=', tenantId)\n .executeTakeFirst() as { total_size: string | number | null } | undefined\n const total = row?.total_size\n if (typeof total === 'number') return Number.isFinite(total) ? total : 0\n if (typeof total === 'string') {\n const parsed = Number(total)\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n } catch (error) {\n throw new Error('Attachment quota accounting is unavailable.', { cause: error })\n }\n}\n\nasync function deleteAttachment(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const url = new URL(req.url)\n const id = url.searchParams.get('id') || ''\n if (!id) return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const dataEngine = container.resolve('dataEngine')\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n // Resolve the currently selected organization (#3765) so a multi-org admin who\n // switched the header org deletes within that org, not their pinned home org.\n // A superadmin browsing with \"All organizations\" selected has no concrete org,\n // so resolution returns null and the delete falls back to a tenant-only scope\n // (#3764) \u2014 letting them remove any attachment in the tenant.\n const deleteScope = await resolveAttachmentRequestScope(container, auth, req)\n if (deleteScope.denied) return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n const orgId = deleteScope.organizationId\n const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId! }\n if (orgId) deleteFilter.organizationId = orgId\n const scopedAuth = { ...auth, orgId }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: 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: orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth: scopedAuth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth: scopedAuth, attachment: record, action: 'delete' })\n const driver = record.storagePath ? await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? orgId ?? '',\n }) : null\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {\n logger.error('Failed to cleanup cached thumbnails', { err: error })\n })\n if (outcome.driver && outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? null,\n tenantId: record.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n return NextResponse.json({ ok: true })\n}\n\nexport const DELETE = withAttachmentAccessErrors(deleteAttachment)\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage entity attachments',\n description: 'Upload and list attachments associated with module entities and records.',\n methods: {\n GET: {\n summary: 'List attachments for a record',\n description: 'Returns uploaded attachments for the given entity record, ordered by newest first. Without both page and pageSize, only the newest 100 attachments are returned; pass page and pageSize to page through the rest.',\n query: attachmentQuerySchema,\n responses: [\n { status: 200, description: 'Attachments found for the record', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity or record identifiers', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Owner policy denies access', schema: errorSchema },\n { status: 404, description: 'Attachment or owner is unavailable', schema: errorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Upload attachment',\n description: 'Uploads a new attachment using multipart form-data and stores metadata for later retrieval.',\n requestBody: {\n contentType: 'multipart/form-data',\n schema: attachmentUploadBodySchema,\n },\n responses: [\n { status: 200, description: 'Attachment stored successfully', schema: uploadResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Payload validation error', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Attachment violates field constraints', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Removes an uploaded attachment and deletes the stored asset.',\n query: attachmentDeleteQuerySchema,\n responses: [\n { status: 200, description: 'Attachment deleted', schema: z.object({ ok: z.literal(true) }) },\n { status: 404, description: 'Attachment not found', schema: errorSchema },\n ],\n errors: [\n { status: 400, description: 'Missing attachment identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Owner policy denies access', schema: errorSchema },\n { status: 404, description: 'Attachment or owner is unavailable', schema: errorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["metadata", "record"]
|
|
7
7
|
}
|