@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa
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/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- 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 +2 -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/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- 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/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.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/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- 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-handler.js +16 -8
- package/dist/modules/workflows/lib/task-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/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.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/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- 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/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -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/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { LockMode } from '@mikro-orm/core'
|
|
2
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { prepareAttachmentMutation } from '../lib/access-mutation'
|
|
4
|
+
import { throwAttachmentAccessError } from '../lib/access-errors'
|
|
5
|
+
import { createAttachmentAccessContext } from '../lib/access-runner'
|
|
6
|
+
import { assertAttachmentOwnerAccess, requiresAttachmentAccessScan, scanAuthorizedAttachments } from '../lib/access-query'
|
|
7
|
+
import { withAttachmentAccessErrors } from '../lib/access-errors'
|
|
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'
|
|
@@ -5,6 +12,7 @@ import { z } from 'zod'
|
|
|
5
12
|
import { sql } from 'kysely'
|
|
6
13
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
7
14
|
import { buildAttachmentFileUrl, buildAttachmentImageUrl, slugifyAttachmentFileName } from '../lib/imageUrls'
|
|
15
|
+
import { ensureAttachmentPartitionProtection } from '../lib/access-protection'
|
|
8
16
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode, sanitizePartitionCode } from '../lib/partitions'
|
|
9
17
|
import { Attachment, AttachmentPartition } from '../data/entities'
|
|
10
18
|
import { extractAttachmentContent } from '../lib/textExtraction'
|
|
@@ -12,7 +20,7 @@ import { requestOcrProcessing } from '../lib/ocrQueue'
|
|
|
12
20
|
import { StorageDriverFactory } from '../lib/drivers'
|
|
13
21
|
import { OcrService, shouldUseLlmOcr } from '../lib/ocrService'
|
|
14
22
|
import { clearAttachmentThumbnailCache } from '../lib/thumbnailCache'
|
|
15
|
-
import {
|
|
23
|
+
import { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../lib/storageReferences'
|
|
16
24
|
import { assertAttachmentScopeInvariant } from '../lib/access'
|
|
17
25
|
import { resolveAttachmentRequestScope } from '../lib/requestScope'
|
|
18
26
|
import {
|
|
@@ -193,7 +201,7 @@ function parseFormAssignments(value: FormDataEntryValue | null): AttachmentAssig
|
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
|
|
204
|
+
async function listRecordAttachments(req: Request) {
|
|
197
205
|
const auth = await getAuthFromRequest(req)
|
|
198
206
|
if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
199
207
|
const url = new URL(req.url)
|
|
@@ -217,29 +225,29 @@ export async function GET(req: Request) {
|
|
|
217
225
|
if (orgId) filter.organizationId = orgId
|
|
218
226
|
const orderBy: Record<string, 'ASC' | 'DESC'> = { createdAt: 'DESC', id: 'DESC' }
|
|
219
227
|
const usePaging = typeof page === 'number' && typeof pageSize === 'number'
|
|
220
|
-
const total = usePaging ? await em.count(Attachment, filter) : null
|
|
221
228
|
const currentPage = usePaging ? Math.max(1, page) : null
|
|
222
229
|
const currentPageSize = usePaging ? pageSize : null
|
|
223
|
-
const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null
|
|
224
230
|
const pageOffset = usePaging ? (currentPage! - 1) * currentPageSize! : undefined
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
231
|
+
const partitions = await em.find(AttachmentPartition, {})
|
|
232
|
+
const protectedScan = requiresAttachmentAccessScan(partitions)
|
|
233
|
+
const scope = { tenantId: auth.tenantId ?? null, organizationId: orgId ?? null }
|
|
234
|
+
let items: Attachment[]
|
|
235
|
+
let total: number | null
|
|
236
|
+
if (protectedScan) {
|
|
237
|
+
const scanned = await scanAuthorizedAttachments({
|
|
238
|
+
auth: { ...auth, orgId }, context: createAttachmentAccessContext(container), partitions,
|
|
239
|
+
offset: pageOffset, limit: currentPageSize ?? ATTACHMENT_LIST_MAX_PAGE_SIZE,
|
|
240
|
+
loadBatch: (offset, limit) => findWithDecryption(em, Attachment, filter, { orderBy, offset, limit }, scope),
|
|
241
|
+
})
|
|
242
|
+
items = scanned.records
|
|
243
|
+
total = usePaging ? scanned.total : null
|
|
244
|
+
} else {
|
|
245
|
+
total = usePaging ? await em.count(Attachment, filter) : null
|
|
246
|
+
items = await findWithDecryption(em, Attachment, filter, {
|
|
247
|
+
orderBy, ...(usePaging ? { limit: currentPageSize!, offset: pageOffset } : { limit: ATTACHMENT_LIST_MAX_PAGE_SIZE }),
|
|
248
|
+
}, scope)
|
|
249
|
+
}
|
|
250
|
+
const totalPages = usePaging && total !== null ? Math.max(1, Math.ceil(total / currentPageSize!)) : null
|
|
243
251
|
return NextResponse.json({
|
|
244
252
|
items: items.map((a: any) => {
|
|
245
253
|
const metadata = readAttachmentMetadata(a.storageMetadata)
|
|
@@ -269,9 +277,11 @@ export async function GET(req: Request) {
|
|
|
269
277
|
totalPages,
|
|
270
278
|
}
|
|
271
279
|
: {}),
|
|
272
|
-
})
|
|
280
|
+
}, { headers: protectedScan ? { 'Cache-Control': 'private, no-store' } : undefined })
|
|
273
281
|
}
|
|
274
282
|
|
|
283
|
+
export const GET = withAttachmentAccessErrors(listRecordAttachments)
|
|
284
|
+
|
|
275
285
|
export async function POST(req: Request) {
|
|
276
286
|
const { t } = await resolveTranslations()
|
|
277
287
|
const auth = await getAuthFromRequest(req)
|
|
@@ -458,6 +468,7 @@ export async function POST(req: Request) {
|
|
|
458
468
|
if (!partition) {
|
|
459
469
|
return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })
|
|
460
470
|
}
|
|
471
|
+
await ensureAttachmentPartitionProtection(em, partition)
|
|
461
472
|
const requestedPublicOverride =
|
|
462
473
|
typeof partitionOverride === 'string' &&
|
|
463
474
|
partitionOverride.length > 0 &&
|
|
@@ -703,7 +714,7 @@ async function readTenantAttachmentUsageBytes(em: EntityManager, tenantId: strin
|
|
|
703
714
|
}
|
|
704
715
|
}
|
|
705
716
|
|
|
706
|
-
|
|
717
|
+
async function deleteAttachment(req: Request) {
|
|
707
718
|
const auth = await getAuthFromRequest(req)
|
|
708
719
|
if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
709
720
|
const url = new URL(req.url)
|
|
@@ -724,20 +735,34 @@ export async function DELETE(req: Request) {
|
|
|
724
735
|
const orgId = deleteScope.organizationId
|
|
725
736
|
const deleteFilter: Record<string, unknown> = { id, tenantId: auth.tenantId! }
|
|
726
737
|
if (orgId) deleteFilter.organizationId = orgId
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
738
|
+
const scopedAuth = { ...auth, orgId }
|
|
739
|
+
const outcome = await em.transactional(async (tx) => {
|
|
740
|
+
const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {
|
|
741
|
+
tenantId: auth.tenantId, organizationId: orgId,
|
|
742
|
+
})
|
|
743
|
+
if (!candidate) return throwAttachmentAccessError(404)
|
|
744
|
+
const referenceIds = await lockAttachmentStorageReferences(tx, candidate)
|
|
745
|
+
const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)
|
|
746
|
+
if (!removal.removed) return throwAttachmentAccessError(404)
|
|
747
|
+
const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {
|
|
748
|
+
lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,
|
|
749
|
+
}, { tenantId: auth.tenantId, organizationId: orgId })
|
|
750
|
+
if (!record) return throwAttachmentAccessError(404)
|
|
751
|
+
const guard = await prepareAttachmentMutation({ container, req, auth: scopedAuth, recordId: record.id, operation: 'delete' })
|
|
752
|
+
await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth: scopedAuth, attachment: record, action: 'delete' })
|
|
753
|
+
const driver = record.storagePath ? await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
754
|
+
tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? orgId ?? '',
|
|
755
|
+
}) : null
|
|
756
|
+
tx.remove(record)
|
|
757
|
+
await tx.flush()
|
|
758
|
+
return { record, driver, guard, releaseStorage: removal.releaseStorage }
|
|
759
|
+
})
|
|
760
|
+
const { record } = outcome
|
|
731
761
|
await clearAttachmentThumbnailCache(record.partitionCode, record.id).catch((error) => {
|
|
732
762
|
logger.error('Failed to cleanup cached thumbnails', { err: error })
|
|
733
763
|
})
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
tenantId: record.tenantId ?? auth.tenantId!,
|
|
737
|
-
organizationId: record.organizationId ?? orgId ?? '',
|
|
738
|
-
})
|
|
739
|
-
await delDriver.delete(record.partitionCode, record.storagePath)
|
|
740
|
-
}
|
|
764
|
+
if (outcome.driver && outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)
|
|
765
|
+
await outcome.guard.runAfterSuccess()
|
|
741
766
|
if (dataEngine) {
|
|
742
767
|
await emitCrudSideEffects({
|
|
743
768
|
dataEngine,
|
|
@@ -756,6 +781,8 @@ export async function DELETE(req: Request) {
|
|
|
756
781
|
return NextResponse.json({ ok: true })
|
|
757
782
|
}
|
|
758
783
|
|
|
784
|
+
export const DELETE = withAttachmentAccessErrors(deleteAttachment)
|
|
785
|
+
|
|
759
786
|
export const openApi: OpenApiRouteDoc = {
|
|
760
787
|
summary: 'Manage entity attachments',
|
|
761
788
|
description: 'Upload and list attachments associated with module entities and records.',
|
|
@@ -770,6 +797,9 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
770
797
|
errors: [
|
|
771
798
|
{ status: 400, description: 'Missing entity or record identifiers', schema: errorSchema },
|
|
772
799
|
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
800
|
+
{ status: 403, description: 'Owner policy denies access', schema: errorSchema },
|
|
801
|
+
{ status: 404, description: 'Attachment or owner is unavailable', schema: errorSchema },
|
|
802
|
+
{ status: 504, description: 'Owner authorization timed out', schema: errorSchema },
|
|
773
803
|
],
|
|
774
804
|
},
|
|
775
805
|
POST: {
|
|
@@ -799,6 +829,9 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
799
829
|
errors: [
|
|
800
830
|
{ status: 400, description: 'Missing attachment identifier', schema: errorSchema },
|
|
801
831
|
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
832
|
+
{ status: 403, description: 'Owner policy denies access', schema: errorSchema },
|
|
833
|
+
{ status: 404, description: 'Attachment or owner is unavailable', schema: errorSchema },
|
|
834
|
+
{ status: 504, description: 'Owner authorization timed out', schema: errorSchema },
|
|
802
835
|
],
|
|
803
836
|
},
|
|
804
837
|
},
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { LockMode } from '@mikro-orm/core'
|
|
3
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { Attachment } from '../../data/entities'
|
|
5
|
+
import { createAttachmentAccessContext } from '../../lib/access-runner'
|
|
6
|
+
import { assertAttachmentOwnerAccess } from '../../lib/access-query'
|
|
7
|
+
import { withAttachmentAccessErrors, throwAttachmentAccessError } from '../../lib/access-errors'
|
|
8
|
+
import { prepareAttachmentMutation } from '../../lib/access-mutation'
|
|
9
|
+
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
1
10
|
import { NextResponse } from 'next/server'
|
|
2
11
|
import { z } from 'zod'
|
|
3
12
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
@@ -22,60 +31,72 @@ export const metadata = {
|
|
|
22
31
|
POST: { requireAuth: true, requireFeatures: ['attachments.manage'] },
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
async function transferAttachments(req: Request) {
|
|
26
35
|
const auth = await getAuthFromRequest(req)
|
|
27
36
|
if (!auth || !auth.tenantId || !auth.orgId) {
|
|
28
37
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
29
38
|
}
|
|
30
|
-
const json = await req
|
|
39
|
+
const json = await readJsonSafe(req, null)
|
|
31
40
|
const parsed = transferSchema.safeParse(json)
|
|
32
41
|
if (!parsed.success) {
|
|
33
42
|
return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })
|
|
34
43
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
AttachmentEntity = mod.Attachment
|
|
42
|
-
} catch {
|
|
43
|
-
return NextResponse.json({ error: 'Attachment model missing' }, { status: 500 })
|
|
44
|
-
}
|
|
44
|
+
const container = await createRequestContainer()
|
|
45
|
+
const em = container.resolve<EntityManager>('em')
|
|
46
|
+
const guard = await prepareAttachmentMutation({ container, req, auth, operation: 'update', payload: parsed.data })
|
|
47
|
+
const effective = transferSchema.safeParse(guard.modifiedPayload ?? parsed.data)
|
|
48
|
+
if (!effective.success) return throwAttachmentAccessError(403)
|
|
49
|
+
const { attachmentIds, entityId, fromRecordId } = effective.data
|
|
45
50
|
const filters: Record<string, unknown> = {
|
|
46
|
-
id: { $in: attachmentIds },
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
organizationId: auth.orgId,
|
|
50
|
-
}
|
|
51
|
-
if (fromRecordId) {
|
|
52
|
-
filters.recordId = fromRecordId
|
|
53
|
-
}
|
|
54
|
-
const records = await em.find(AttachmentEntity, filters)
|
|
55
|
-
if (!records.length) {
|
|
56
|
-
return NextResponse.json({ error: 'Attachments not found' }, { status: 404 })
|
|
51
|
+
id: { $in: [...new Set(attachmentIds)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) }, entityId,
|
|
52
|
+
tenantId: auth.tenantId, organizationId: auth.orgId,
|
|
53
|
+
...(fromRecordId ? { recordId: fromRecordId } : {}),
|
|
57
54
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
const accessContext = createAttachmentAccessContext(container)
|
|
56
|
+
const outcome = await em.transactional(async (tx) => {
|
|
57
|
+
const records = await findWithDecryption(tx, Attachment, filters, {
|
|
58
|
+
orderBy: { id: 'asc' }, lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,
|
|
59
|
+
}, { tenantId: auth.tenantId, organizationId: auth.orgId })
|
|
60
|
+
if (records.length !== new Set(attachmentIds).size) return throwAttachmentAccessError(404)
|
|
61
|
+
const changes = []
|
|
62
|
+
for (const record of records) {
|
|
63
|
+
const recordPayload = { ...effective.data, attachmentIds: [record.id] }
|
|
64
|
+
const recordGuard = await prepareAttachmentMutation({
|
|
65
|
+
container, req, auth, recordId: record.id, operation: 'update', payload: recordPayload,
|
|
66
|
+
})
|
|
67
|
+
const recordEffective = transferSchema.safeParse(recordGuard.modifiedPayload ?? recordPayload)
|
|
68
|
+
if (!recordEffective.success
|
|
69
|
+
|| recordEffective.data.attachmentIds.length !== 1
|
|
70
|
+
|| recordEffective.data.attachmentIds[0] !== record.id
|
|
71
|
+
|| recordEffective.data.entityId !== record.entityId
|
|
72
|
+
|| (recordEffective.data.fromRecordId && recordEffective.data.fromRecordId !== record.recordId)) {
|
|
73
|
+
return throwAttachmentAccessError(403)
|
|
74
|
+
}
|
|
75
|
+
const destinationId = recordEffective.data.toRecordId
|
|
76
|
+
const metadata = readAttachmentMetadata(record.storageMetadata)
|
|
77
|
+
const assignments = metadata.assignments?.map((assignment) => (
|
|
78
|
+
assignment.type === entityId && assignment.id === (fromRecordId ?? record.recordId)
|
|
79
|
+
? { ...assignment, id: destinationId } : assignment
|
|
80
|
+
)) ?? []
|
|
81
|
+
const storageMetadata = mergeAttachmentMetadata(record.storageMetadata, { assignments })
|
|
82
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })
|
|
83
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, recordId: destinationId, storageMetadata }, action: 'reassign' })
|
|
84
|
+
changes.push({ record, storageMetadata, destinationId, guard: recordGuard })
|
|
85
|
+
}
|
|
86
|
+
for (const { record, storageMetadata, destinationId } of changes) {
|
|
87
|
+
record.recordId = destinationId
|
|
88
|
+
record.storageMetadata = storageMetadata
|
|
89
|
+
}
|
|
90
|
+
await tx.flush()
|
|
91
|
+
return { updated: records.length, guards: changes.map((change) => change.guard) }
|
|
92
|
+
})
|
|
93
|
+
await guard.runAfterSuccess()
|
|
94
|
+
for (const recordGuard of outcome.guards) await recordGuard.runAfterSuccess()
|
|
95
|
+
return NextResponse.json({ ok: true, updated: outcome.updated })
|
|
77
96
|
}
|
|
78
97
|
|
|
98
|
+
export const POST = withAttachmentAccessErrors(transferAttachments)
|
|
99
|
+
|
|
79
100
|
export const openApi: OpenApiRouteDoc = {
|
|
80
101
|
tag: attachmentsTag,
|
|
81
102
|
summary: 'Transfer attachments between records',
|
|
@@ -95,6 +116,8 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
95
116
|
{ status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },
|
|
96
117
|
{ status: 404, description: 'Attachments not found', schema: attachmentErrorSchema },
|
|
97
118
|
{ status: 500, description: 'Attachment model missing', schema: attachmentErrorSchema },
|
|
119
|
+
{ status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },
|
|
120
|
+
{ status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },
|
|
98
121
|
],
|
|
99
122
|
},
|
|
100
123
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BigIntType, OptionalProps } from '@mikro-orm/core'
|
|
2
2
|
import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
3
3
|
import { resolveDefaultAttachmentOcrEnabled } from '../lib/ocrConfig'
|
|
4
|
+
import type { AttachmentAccessRequirement } from '../lib/access-types'
|
|
4
5
|
|
|
5
6
|
@Entity({ tableName: 'attachment_partitions' })
|
|
6
7
|
@Unique({ name: 'attachment_partitions_code_unique', properties: ['code'] })
|
|
@@ -25,6 +26,9 @@ export class AttachmentPartition {
|
|
|
25
26
|
@Property({ name: 'config_json', type: 'json', nullable: true })
|
|
26
27
|
configJson?: Record<string, unknown> | null
|
|
27
28
|
|
|
29
|
+
@Property({ name: 'access_resolver_requirements', type: 'jsonb', nullable: true })
|
|
30
|
+
accessResolverRequirements?: AttachmentAccessRequirement[] | null
|
|
31
|
+
|
|
28
32
|
@Property({ name: 'is_public', type: 'boolean', default: false })
|
|
29
33
|
isPublic: boolean = false
|
|
30
34
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { AttachmentAccessResolver } from '../lib/access-types'
|
|
3
|
+
|
|
4
|
+
export const attachmentAccessActionSchema = z.enum(['read', 'render', 'metadata', 'reassign', 'delete', 'export'])
|
|
5
|
+
export const attachmentAccessSubjectFeaturesSchema = z.array(z.string())
|
|
6
|
+
export const attachmentAccessSelectorSchema = z.string().min(1).max(200).regex(/^(?:\*|[^*\s]+(?:\.\*)?)$/)
|
|
7
|
+
const resolverIdSchema = z.string().min(3).max(200).regex(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_.-]+$/)
|
|
8
|
+
|
|
9
|
+
export const attachmentAccessRequirementSchema = z.object({
|
|
10
|
+
resolverId: resolverIdSchema,
|
|
11
|
+
targetEntity: attachmentAccessSelectorSchema,
|
|
12
|
+
}).strict()
|
|
13
|
+
|
|
14
|
+
export const protectedAttachmentTargetSchema = attachmentAccessRequirementSchema.extend({
|
|
15
|
+
targetPartition: attachmentAccessSelectorSchema,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const attachmentAccessResolverSchema = z.object({
|
|
19
|
+
id: resolverIdSchema,
|
|
20
|
+
targetPartition: attachmentAccessSelectorSchema,
|
|
21
|
+
targetEntity: attachmentAccessSelectorSchema.optional(),
|
|
22
|
+
actions: z.array(attachmentAccessActionSchema).min(1).optional(),
|
|
23
|
+
priority: z.number().finite().optional(),
|
|
24
|
+
timeoutMs: z.number().int().min(1).max(15_000).optional(),
|
|
25
|
+
resolve: z.custom<AttachmentAccessResolver['resolve']>((value) => typeof value === 'function'),
|
|
26
|
+
}).strict()
|
|
27
|
+
|
|
28
|
+
export const attachmentAccessRequirementsSchema = z.array(attachmentAccessRequirementSchema)
|
|
29
|
+
export const attachmentAccessDecisionSchema = z.discriminatedUnion('ok', [
|
|
30
|
+
z.object({ ok: z.literal(true) }).strict(),
|
|
31
|
+
z.object({
|
|
32
|
+
ok: z.literal(false),
|
|
33
|
+
status: z.union([z.literal(401), z.literal(403), z.literal(404), z.literal(504)]),
|
|
34
|
+
reason: z.string().min(1).max(200).regex(/^[a-zA-Z0-9_.:-]+$/),
|
|
35
|
+
}).strict(),
|
|
36
|
+
])
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAttachmentAccessContext } from './lib/access-runner'
|
|
1
2
|
import { asFunction, asValue } from 'awilix'
|
|
2
3
|
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
4
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
@@ -9,6 +10,7 @@ import { AttachmentTargetAccessService } from './lib/target-access-service'
|
|
|
9
10
|
import { ScopedAttachmentUploadService } from './lib/scoped-upload-service'
|
|
10
11
|
|
|
11
12
|
export function register(container: AppContainer) {
|
|
13
|
+
const accessContext = () => createAttachmentAccessContext(container)
|
|
12
14
|
container.register({
|
|
13
15
|
attachmentQuotaRecoveryScheduler: asValue(scheduleAttachmentQuotaRecovery),
|
|
14
16
|
attachmentQuotaService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>
|
|
@@ -17,7 +19,7 @@ export function register(container: AppContainer) {
|
|
|
17
19
|
.scoped()
|
|
18
20
|
.proxy(),
|
|
19
21
|
attachmentTargetAccessService: asFunction(({ em }: { em: ConstructorParameters<typeof StorageDriverFactory>[0] }) =>
|
|
20
|
-
new AttachmentTargetAccessService(em),
|
|
22
|
+
new AttachmentTargetAccessService(em, accessContext),
|
|
21
23
|
)
|
|
22
24
|
.scoped()
|
|
23
25
|
.proxy(),
|
|
@@ -59,6 +61,7 @@ export function register(container: AppContainer) {
|
|
|
59
61
|
cradle.em,
|
|
60
62
|
cradle.storageDriverFactory,
|
|
61
63
|
() => cradle.attachmentScopedUploadService ?? null,
|
|
64
|
+
accessContext,
|
|
62
65
|
))
|
|
63
66
|
.scoped()
|
|
64
67
|
.proxy(),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GeneratorPlugin } from '@open-mercato/shared/modules/generators'
|
|
2
|
+
|
|
3
|
+
const accessPlugin: GeneratorPlugin = {
|
|
4
|
+
id: 'attachments.access-resolvers',
|
|
5
|
+
conventionFile: 'data/attachment-access.ts',
|
|
6
|
+
importPrefix: 'ATTACHMENT_ACCESS',
|
|
7
|
+
configExpr: (importName, moduleId) =>
|
|
8
|
+
`{ moduleId: '${moduleId}', resolvers: (${importName}.attachmentAccessResolvers ?? []), protectedTargets: (${importName}.protectedAttachmentTargets ?? []) }`,
|
|
9
|
+
outputFileName: 'attachment-access.generated.ts',
|
|
10
|
+
buildOutput: ({ importSection, entriesLiteral }) => `// AUTO-GENERATED by mercato generate registry
|
|
11
|
+
import type { AttachmentAccessResolverEntry } from '@open-mercato/core/modules/attachments'
|
|
12
|
+
${importSection}
|
|
13
|
+
|
|
14
|
+
export const attachmentAccessEntries: AttachmentAccessResolverEntry[] = [
|
|
15
|
+
${entriesLiteral ? ` ${entriesLiteral}\n` : ''}]
|
|
16
|
+
`,
|
|
17
|
+
bootstrapRegistration: {
|
|
18
|
+
entriesExportName: 'attachmentAccessEntries',
|
|
19
|
+
registrationImports: [
|
|
20
|
+
"import { registerAttachmentAccessResolvers } from '@open-mercato/core/modules/attachments/lib/access-registry'",
|
|
21
|
+
],
|
|
22
|
+
buildCall: (entries) => `registerAttachmentAccessResolvers(${entries})`,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const generatorPlugins: GeneratorPlugin[] = [accessPlugin]
|
|
27
|
+
export default generatorPlugins
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"attachments.access.forbidden": "Zugriff verweigert",
|
|
3
|
+
"attachments.access.notFound": "Anhang nicht gefunden.",
|
|
4
|
+
"attachments.access.timeout": "Zeitüberschreitung bei der Zugriffsprüfung des Anhangs. Bitte versuchen Sie es erneut.",
|
|
5
|
+
"attachments.access.unauthorized": "Nicht autorisiert",
|
|
2
6
|
"attachments.errors.activeContentBlocked": "Das Hochladen aktiver Inhalte ist nicht erlaubt.",
|
|
3
7
|
"attachments.errors.dangerousExecutable": "Ausführbare Dateitypen sind als Anhänge nicht erlaubt.",
|
|
4
8
|
"attachments.errors.maxUploadSize": "Der Anhang überschreitet die maximal zulässige Upload-Größe.",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"attachments.access.forbidden": "Forbidden",
|
|
3
|
+
"attachments.access.notFound": "Attachment not found.",
|
|
4
|
+
"attachments.access.timeout": "Attachment authorization timed out. Please try again.",
|
|
5
|
+
"attachments.access.unauthorized": "Unauthorized",
|
|
2
6
|
"attachments.errors.activeContentBlocked": "Active content uploads are not allowed.",
|
|
3
7
|
"attachments.errors.dangerousExecutable": "Executable file types are not allowed as attachments.",
|
|
4
8
|
"attachments.errors.maxUploadSize": "Attachment exceeds the maximum upload size.",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"attachments.access.forbidden": "Acceso denegado",
|
|
3
|
+
"attachments.access.notFound": "No se encontró el archivo adjunto.",
|
|
4
|
+
"attachments.access.timeout": "Se agotó el tiempo de comprobación de acceso al archivo adjunto. Inténtalo de nuevo.",
|
|
5
|
+
"attachments.access.unauthorized": "No autorizado",
|
|
2
6
|
"attachments.errors.activeContentBlocked": "No se permite la carga de contenido activo.",
|
|
3
7
|
"attachments.errors.dangerousExecutable": "No se permiten archivos ejecutables como adjuntos.",
|
|
4
8
|
"attachments.errors.maxUploadSize": "El archivo adjunto supera el tamaño máximo de carga permitido.",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"attachments.access.forbidden": "접근이 거부되었습니다",
|
|
3
|
+
"attachments.access.notFound": "첨부 파일을 찾을 수 없습니다.",
|
|
4
|
+
"attachments.access.timeout": "첨부 파일 접근 권한 확인 시간이 초과되었습니다. 다시 시도해 주세요.",
|
|
5
|
+
"attachments.access.unauthorized": "인증이 필요합니다",
|
|
2
6
|
"attachments.errors.activeContentBlocked": "활성 콘텐츠 업로드는 허용되지 않습니다.",
|
|
3
7
|
"attachments.errors.dangerousExecutable": "실행 파일 형식은 첨부 파일로 허용되지 않습니다.",
|
|
4
8
|
"attachments.errors.maxUploadSize": "첨부 파일이 최대 업로드 크기를 초과합니다.",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"attachments.access.forbidden": "Brak dostępu",
|
|
3
|
+
"attachments.access.notFound": "Nie znaleziono załącznika.",
|
|
4
|
+
"attachments.access.timeout": "Przekroczono czas sprawdzania dostępu do załącznika. Spróbuj ponownie.",
|
|
5
|
+
"attachments.access.unauthorized": "Brak autoryzacji",
|
|
2
6
|
"attachments.errors.activeContentBlocked": "Przesyłanie treści aktywnych nie jest dozwolone.",
|
|
3
7
|
"attachments.errors.dangerousExecutable": "Pliki wykonywalne nie są dozwolone jako załączniki.",
|
|
4
8
|
"attachments.errors.maxUploadSize": "Załącznik przekracza maksymalny dopuszczalny rozmiar przesyłania.",
|
|
@@ -19,3 +19,21 @@ export {
|
|
|
19
19
|
type ReadScopedAttachmentResult,
|
|
20
20
|
type ReleaseScopedAttachmentInput,
|
|
21
21
|
} from './lib/attachment-service'
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
AttachmentAccessAction,
|
|
25
|
+
AttachmentAccessDecision,
|
|
26
|
+
AttachmentAccessInput,
|
|
27
|
+
AttachmentAccessPartition,
|
|
28
|
+
AttachmentAccessRecord,
|
|
29
|
+
AttachmentAccessRequirement,
|
|
30
|
+
AttachmentAccessResolver,
|
|
31
|
+
AttachmentAccessResolverEntry,
|
|
32
|
+
AttachmentAccessSubject,
|
|
33
|
+
AttachmentAccessTarget,
|
|
34
|
+
ProtectedAttachmentTarget,
|
|
35
|
+
} from './lib/access-types'
|
|
36
|
+
export { registerAttachmentAccessResolvers } from './lib/access-registry'
|
|
37
|
+
export { syncAttachmentAccessProtection } from './lib/access-protection'
|
|
38
|
+
export { createAttachmentAccessContext, evaluateAttachmentAccess } from './lib/access-runner'
|
|
39
|
+
export type { AttachmentAccessContext, AttachmentAccessResult } from './lib/access-runner'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
2
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
3
|
+
import type { AttachmentAccessDecision } from './access-types'
|
|
4
|
+
|
|
5
|
+
export async function attachmentAccessErrorBody(status: number): Promise<{ error: string }> {
|
|
6
|
+
const { t } = await resolveTranslations()
|
|
7
|
+
const key = status === 401 ? 'unauthorized' : status === 404 ? 'notFound' : status === 504 ? 'timeout' : 'forbidden'
|
|
8
|
+
return { error: t(`attachments.access.${key}`) }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function attachmentAccessErrorResponse(decision: Extract<AttachmentAccessDecision, { ok: false }>): Promise<Response> {
|
|
12
|
+
return Response.json(await attachmentAccessErrorBody(decision.status), {
|
|
13
|
+
status: decision.status, headers: { 'Cache-Control': 'private, no-store' },
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function throwAttachmentAccessError(status: number): Promise<never> {
|
|
18
|
+
throw new CrudHttpError(status, await attachmentAccessErrorBody(status))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function withAttachmentAccessErrors<Arguments extends unknown[]>(handler: (...args: Arguments) => Promise<Response>) {
|
|
22
|
+
return async (...args: Arguments): Promise<Response> => {
|
|
23
|
+
try {
|
|
24
|
+
return await handler(...args)
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (isCrudHttpError(error)) {
|
|
27
|
+
return Response.json(error.body, { status: error.status, headers: { 'Cache-Control': 'private, no-store' } })
|
|
28
|
+
}
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
3
|
+
import { throwAttachmentAccessError } from './access-errors'
|
|
4
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
6
|
+
|
|
7
|
+
export async function prepareAttachmentMutation(input: {
|
|
8
|
+
container: AwilixContainer
|
|
9
|
+
req: Request
|
|
10
|
+
auth: NonNullable<AuthContext>
|
|
11
|
+
recordId?: string
|
|
12
|
+
operation: 'update' | 'delete'
|
|
13
|
+
payload?: Record<string, unknown>
|
|
14
|
+
}) {
|
|
15
|
+
if (!input.auth.tenantId) return throwAttachmentAccessError(401)
|
|
16
|
+
const guard = await runRouteMutationGuards({
|
|
17
|
+
container: input.container, req: input.req,
|
|
18
|
+
auth: { tenantId: input.auth.tenantId, organizationId: input.auth.orgId ?? null, userId: input.auth.sub },
|
|
19
|
+
input: {
|
|
20
|
+
resourceKind: 'attachments:attachment', resourceId: input.recordId,
|
|
21
|
+
operation: input.operation, mutationPayload: input.payload,
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
if (!guard.ok) throw new CrudHttpError(guard.errorStatus, guard.errorBody)
|
|
25
|
+
return guard
|
|
26
|
+
}
|