@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,3 +1,11 @@
|
|
|
1
|
+
import { LockMode } from "@mikro-orm/core";
|
|
2
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
4
|
+
import { prepareAttachmentMutation } from "../../../lib/access-mutation.js";
|
|
5
|
+
import { throwAttachmentAccessError } from "../../../lib/access-errors.js";
|
|
6
|
+
import { createAttachmentAccessContext } from "../../../lib/access-runner.js";
|
|
7
|
+
import { assertAttachmentOwnerAccess } from "../../../lib/access-query.js";
|
|
8
|
+
import { withAttachmentAccessErrors } from "../../../lib/access-errors.js";
|
|
1
9
|
import { NextResponse } from "next/server";
|
|
2
10
|
import { z } from "zod";
|
|
3
11
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -9,7 +17,7 @@ import {
|
|
|
9
17
|
normalizeAttachmentTags,
|
|
10
18
|
readAttachmentMetadata
|
|
11
19
|
} from "../../../lib/metadata.js";
|
|
12
|
-
import {
|
|
20
|
+
import { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from "../../../lib/storageReferences.js";
|
|
13
21
|
import { splitCustomFieldPayload, loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
|
|
14
22
|
import { emitCrudSideEffects, setCustomFieldsIfAny } from "@open-mercato/shared/lib/commands/helpers";
|
|
15
23
|
import { normalizeCustomFieldResponse } from "@open-mercato/shared/lib/custom-fields/normalize";
|
|
@@ -21,8 +29,6 @@ import {
|
|
|
21
29
|
attachmentDetailResponseSchema,
|
|
22
30
|
attachmentErrorSchema
|
|
23
31
|
} from "../../openapi.js";
|
|
24
|
-
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
25
|
-
const logger = createLogger("attachments").child({ component: "library" });
|
|
26
32
|
const updateSchema = z.object({
|
|
27
33
|
tags: z.array(z.string()).optional(),
|
|
28
34
|
assignments: z.array(
|
|
@@ -51,7 +57,7 @@ async function resolveAttachmentId(ctx) {
|
|
|
51
57
|
return null;
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
|
-
async function
|
|
60
|
+
async function getAttachment(req, ctx) {
|
|
55
61
|
const auth = await getAuthFromRequest(req);
|
|
56
62
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) {
|
|
57
63
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -60,7 +66,8 @@ async function GET(req, ctx) {
|
|
|
60
66
|
if (!attachmentId) {
|
|
61
67
|
return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
|
|
62
68
|
}
|
|
63
|
-
const
|
|
69
|
+
const container = await createRequestContainer();
|
|
70
|
+
const { resolve } = container;
|
|
64
71
|
const em = resolve("em");
|
|
65
72
|
let queryEngine = null;
|
|
66
73
|
try {
|
|
@@ -79,6 +86,7 @@ async function GET(req, ctx) {
|
|
|
79
86
|
if (!record) {
|
|
80
87
|
return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
|
|
81
88
|
}
|
|
89
|
+
await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: "metadata" });
|
|
82
90
|
const metadata2 = readAttachmentMetadata(record.storageMetadata);
|
|
83
91
|
const partition = record.partitionCode ? await em.findOne(AttachmentPartition, { code: record.partitionCode }) : null;
|
|
84
92
|
const customFieldValues = await loadCustomFieldValues({
|
|
@@ -110,9 +118,10 @@ async function GET(req, ctx) {
|
|
|
110
118
|
content: record.content && record.content.trim() ? record.content : null,
|
|
111
119
|
customFields
|
|
112
120
|
}
|
|
113
|
-
});
|
|
121
|
+
}, { headers: { "Cache-Control": "private, no-store" } });
|
|
114
122
|
}
|
|
115
|
-
|
|
123
|
+
const GET = withAttachmentAccessErrors(getAttachment);
|
|
124
|
+
async function updateAttachment(req, ctx) {
|
|
116
125
|
const auth = await getAuthFromRequest(req);
|
|
117
126
|
if (!auth || !auth.tenantId || !auth.orgId) {
|
|
118
127
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -121,13 +130,14 @@ async function PATCH(req, ctx) {
|
|
|
121
130
|
if (!attachmentId) {
|
|
122
131
|
return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
|
|
123
132
|
}
|
|
124
|
-
const rawBody = await req
|
|
133
|
+
const rawBody = await readJsonSafe(req, null);
|
|
125
134
|
const { base, custom } = splitCustomFieldPayload(rawBody);
|
|
126
135
|
const parsed = updateSchema.safeParse(base);
|
|
127
136
|
if (!parsed.success) {
|
|
128
137
|
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
|
|
129
138
|
}
|
|
130
|
-
const
|
|
139
|
+
const container = await createRequestContainer();
|
|
140
|
+
const { resolve } = container;
|
|
131
141
|
const em = resolve("em");
|
|
132
142
|
let queryEngine = null;
|
|
133
143
|
try {
|
|
@@ -141,32 +151,47 @@ async function PATCH(req, ctx) {
|
|
|
141
151
|
tenantId: auth.tenantId,
|
|
142
152
|
organizationId: auth.orgId
|
|
143
153
|
};
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
entityId: E.attachments.attachment,
|
|
159
|
-
recordId: record.id,
|
|
160
|
-
tenantId: record.tenantId ?? auth.tenantId ?? null,
|
|
161
|
-
organizationId: record.organizationId ?? auth.orgId ?? null,
|
|
162
|
-
values: custom
|
|
163
|
-
});
|
|
164
|
-
}
|
|
154
|
+
const accessContext = createAttachmentAccessContext(container);
|
|
155
|
+
const outcome = await em.transactional(async (tx) => {
|
|
156
|
+
const record2 = await findOneWithDecryption(tx, Attachment, patchFilter, {
|
|
157
|
+
lockMode: LockMode.PESSIMISTIC_WRITE,
|
|
158
|
+
refresh: true
|
|
159
|
+
}, { tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
160
|
+
if (!record2) return throwAttachmentAccessError(404);
|
|
161
|
+
const guard = await prepareAttachmentMutation({
|
|
162
|
+
container,
|
|
163
|
+
req,
|
|
164
|
+
auth,
|
|
165
|
+
recordId: record2.id,
|
|
166
|
+
operation: "update",
|
|
167
|
+
payload: { ...parsed.data, customFields: custom }
|
|
165
168
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom });
|
|
170
|
+
const effectiveParsed = updateSchema.safeParse(effective.base);
|
|
171
|
+
if (!effectiveParsed.success) return throwAttachmentAccessError(403);
|
|
172
|
+
const patch = {};
|
|
173
|
+
if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags);
|
|
174
|
+
if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments);
|
|
175
|
+
const nextMetadata = mergeAttachmentMetadata(record2.storageMetadata, patch);
|
|
176
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record2, action: "reassign", persistProtection: true });
|
|
177
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record2, storageMetadata: nextMetadata }, action: "reassign" });
|
|
178
|
+
await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record2, storageMetadata: nextMetadata }, action: "metadata" });
|
|
179
|
+
record2.storageMetadata = nextMetadata;
|
|
180
|
+
await tx.flush();
|
|
181
|
+
if (dataEngine && Object.keys(effective.custom).length) {
|
|
182
|
+
await setCustomFieldsIfAny({
|
|
183
|
+
dataEngine,
|
|
184
|
+
entityId: E.attachments.attachment,
|
|
185
|
+
recordId: record2.id,
|
|
186
|
+
tenantId: record2.tenantId ?? auth.tenantId,
|
|
187
|
+
organizationId: record2.organizationId ?? auth.orgId,
|
|
188
|
+
values: effective.custom
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { record: record2, custom: effective.custom, guard };
|
|
192
|
+
});
|
|
193
|
+
const { record } = outcome;
|
|
194
|
+
await outcome.guard.runAfterSuccess();
|
|
170
195
|
if (dataEngine) {
|
|
171
196
|
await emitCrudSideEffects({
|
|
172
197
|
dataEngine,
|
|
@@ -196,11 +221,11 @@ async function PATCH(req, ctx) {
|
|
|
196
221
|
id: record.id,
|
|
197
222
|
tags: metadata2.tags ?? [],
|
|
198
223
|
assignments: enrichedAssignments,
|
|
199
|
-
customFields: normalizeCustomFieldResponse(custom ?? null)
|
|
224
|
+
customFields: normalizeCustomFieldResponse(outcome.custom ?? null)
|
|
200
225
|
}
|
|
201
226
|
});
|
|
202
227
|
}
|
|
203
|
-
async function
|
|
228
|
+
async function deleteAttachment(req, ctx) {
|
|
204
229
|
const auth = await getAuthFromRequest(req);
|
|
205
230
|
if (!auth || !auth.tenantId || !auth.orgId) {
|
|
206
231
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -209,7 +234,8 @@ async function DELETE(req, ctx) {
|
|
|
209
234
|
if (!attachmentId) {
|
|
210
235
|
return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
|
|
211
236
|
}
|
|
212
|
-
const
|
|
237
|
+
const container = await createRequestContainer();
|
|
238
|
+
const { resolve } = container;
|
|
213
239
|
const em = resolve("em");
|
|
214
240
|
const dataEngine = resolve("dataEngine");
|
|
215
241
|
const storageDriverFactory = resolve("storageDriverFactory");
|
|
@@ -218,23 +244,33 @@ async function DELETE(req, ctx) {
|
|
|
218
244
|
tenantId: auth.tenantId,
|
|
219
245
|
organizationId: auth.orgId
|
|
220
246
|
};
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
const outcome = await em.transactional(async (tx) => {
|
|
248
|
+
const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, void 0, {
|
|
249
|
+
tenantId: auth.tenantId,
|
|
250
|
+
organizationId: auth.orgId
|
|
251
|
+
});
|
|
252
|
+
if (!candidate) return throwAttachmentAccessError(404);
|
|
253
|
+
const referenceIds = await lockAttachmentStorageReferences(tx, candidate);
|
|
254
|
+
const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id);
|
|
255
|
+
if (!removal.removed) return throwAttachmentAccessError(404);
|
|
256
|
+
const record2 = await findOneWithDecryption(tx, Attachment, deleteFilter, {
|
|
257
|
+
lockMode: LockMode.PESSIMISTIC_WRITE,
|
|
258
|
+
refresh: true
|
|
259
|
+
}, { tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
260
|
+
if (!record2) return throwAttachmentAccessError(404);
|
|
261
|
+
const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record2.id, operation: "delete" });
|
|
262
|
+
await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record2, action: "delete" });
|
|
263
|
+
const driver = await storageDriverFactory.resolveForPartition(record2.partitionCode, {
|
|
264
|
+
tenantId: record2.tenantId ?? auth.tenantId,
|
|
265
|
+
organizationId: record2.organizationId ?? auth.orgId
|
|
266
|
+
});
|
|
267
|
+
tx.remove(record2);
|
|
268
|
+
await tx.flush();
|
|
269
|
+
return { record: record2, driver, guard, releaseStorage: removal.releaseStorage };
|
|
228
270
|
});
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!removal.removed) {
|
|
233
|
-
return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
|
|
234
|
-
}
|
|
235
|
-
if (removal.releaseStorage) {
|
|
236
|
-
await deleteDriver.delete(recordPartitionCode, recordStoragePath);
|
|
237
|
-
}
|
|
271
|
+
const { record } = outcome;
|
|
272
|
+
if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath);
|
|
273
|
+
await outcome.guard.runAfterSuccess();
|
|
238
274
|
if (dataEngine) {
|
|
239
275
|
await emitCrudSideEffects({
|
|
240
276
|
dataEngine,
|
|
@@ -252,6 +288,8 @@ async function DELETE(req, ctx) {
|
|
|
252
288
|
}
|
|
253
289
|
return NextResponse.json({ ok: true });
|
|
254
290
|
}
|
|
291
|
+
const PATCH = withAttachmentAccessErrors(updateAttachment);
|
|
292
|
+
const DELETE = withAttachmentAccessErrors(deleteAttachment);
|
|
255
293
|
const openApi = {
|
|
256
294
|
tag: attachmentsTag,
|
|
257
295
|
summary: "Attachment detail management",
|
|
@@ -265,7 +303,9 @@ const openApi = {
|
|
|
265
303
|
errors: [
|
|
266
304
|
{ status: 400, description: "Invalid attachment ID", schema: attachmentErrorSchema },
|
|
267
305
|
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
268
|
-
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema }
|
|
306
|
+
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
|
|
307
|
+
{ status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
|
|
308
|
+
{ status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
|
|
269
309
|
]
|
|
270
310
|
},
|
|
271
311
|
PATCH: {
|
|
@@ -282,7 +322,9 @@ const openApi = {
|
|
|
282
322
|
{ status: 400, description: "Invalid payload or attachment ID", schema: attachmentErrorSchema },
|
|
283
323
|
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
284
324
|
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
|
|
285
|
-
{ status: 500, description: "Failed to save attributes", schema: attachmentErrorSchema }
|
|
325
|
+
{ status: 500, description: "Failed to save attributes", schema: attachmentErrorSchema },
|
|
326
|
+
{ status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
|
|
327
|
+
{ status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
|
|
286
328
|
]
|
|
287
329
|
},
|
|
288
330
|
DELETE: {
|
|
@@ -294,7 +336,9 @@ const openApi = {
|
|
|
294
336
|
errors: [
|
|
295
337
|
{ status: 400, description: "Invalid attachment ID", schema: attachmentErrorSchema },
|
|
296
338
|
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
297
|
-
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema }
|
|
339
|
+
{ status: 404, description: "Attachment not found", schema: attachmentErrorSchema },
|
|
340
|
+
{ status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
|
|
341
|
+
{ status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
|
|
298
342
|
]
|
|
299
343
|
}
|
|
300
344
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/attachments/api/library/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { removeAttachmentRecord } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'library' })\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nexport async function GET(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n })\n}\n\nexport async function PATCH(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await req.json().catch(() => null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const record = await em.findOne(Attachment, patchFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n const patch: Record<string, unknown> = {}\n if (parsed.data.tags) patch.tags = normalizeAttachmentTags(parsed.data.tags)\n if (parsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(parsed.data.assignments)\n record.storageMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n // Commit the metadata mutation and the custom-field write atomically so a\n // custom-field failure cannot leave the attachment metadata partially updated.\n try {\n await em.transactional(async (tx) => {\n await tx.flush()\n if (dataEngine && custom && Object.keys(custom).length) {\n await setCustomFieldsIfAny({\n dataEngine,\n entityId: E.attachments.attachment,\n recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n values: custom,\n })\n }\n })\n } catch (error) {\n logger.error('Failed to persist custom attributes', { err: error })\n return NextResponse.json({ error: 'Failed to save attachment attributes.' }, { status: 500 })\n }\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(custom ?? null),\n },\n })\n}\n\nexport async function DELETE(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const record = await em.findOne(Attachment, deleteFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n const deleteDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId,\n organizationId: record.organizationId ?? auth.orgId,\n })\n // Commit the DB row removal before deleting the irreversible storage file so a\n // failed commit cannot leave a dangling record whose backing file is already gone.\n const recordPartitionCode = record.partitionCode\n const recordStoragePath = record.storagePath\n const removal = await removeAttachmentRecord(em, record)\n if (!removal.removed) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n if (removal.releaseStorage) {\n await deleteDriver.delete(recordPartitionCode, recordStoragePath)\n }\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,YAAY,2BAA2B;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,
|
|
6
|
-
"names": ["metadata"]
|
|
4
|
+
"sourcesContent": ["import { LockMode } from '@mikro-orm/core'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { prepareAttachmentMutation } from '../../../lib/access-mutation'\nimport { throwAttachmentAccessError } from '../../../lib/access-errors'\nimport { createAttachmentAccessContext } from '../../../lib/access-runner'\nimport { assertAttachmentOwnerAccess } from '../../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../../lib/access-errors'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../../../data/entities'\nimport {\n mergeAttachmentMetadata,\n normalizeAttachmentAssignments,\n normalizeAttachmentTags,\n readAttachmentMetadata,\n} from '../../../lib/metadata'\nimport type { StorageDriverFactory } from '../../../lib/drivers'\nimport { lockAttachmentStorageReferences, resolveAttachmentStorageRelease } from '../../../lib/storageReferences'\nimport { splitCustomFieldPayload, loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { emitCrudSideEffects, setCustomFieldsIfAny } from '@open-mercato/shared/lib/commands/helpers'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { E } from '#generated/entities.ids.generated'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { attachmentCrudEvents, attachmentCrudIndexer } from '../../../lib/crud'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../../lib/assignmentDetails'\nimport {\n attachmentsTag,\n attachmentDetailResponseSchema,\n attachmentErrorSchema,\n} from '../../openapi'\n\nconst updateSchema = z.object({\n tags: z.array(z.string()).optional(),\n assignments: z\n .array(\n z.object({\n type: z.string().min(1),\n id: z.string().min(1),\n href: z.string().nullable().optional(),\n label: z.string().nullable().optional(),\n }),\n )\n .optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n PATCH: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['attachments.manage'] },\n}\n\ntype RouteParams = { id: string }\ntype RouteContext = { params: Promise<RouteParams> }\n\nasync function resolveAttachmentId(ctx: RouteContext): Promise<string | null> {\n const params = ctx?.params\n try {\n const { id } = await params\n if (typeof id === 'string' && id.trim().length) {\n return id\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function getAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const findFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n }\n if (auth.orgId) {\n findFilter.organizationId = auth.orgId\n }\n const record = await em.findOne(Attachment, findFilter)\n if (!record) {\n return NextResponse.json({ error: 'Attachment not found' }, { status: 404 })\n }\n await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'metadata' })\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const partition = record.partitionCode\n ? await em.findOne(AttachmentPartition, { code: record.partitionCode })\n : null\n const customFieldValues = await loadCustomFieldValues({\n em,\n entityId: E.attachments.attachment,\n recordIds: [record.id],\n tenantIdByRecord: { [record.id]: record.tenantId ?? auth.tenantId ?? null },\n organizationIdByRecord: { [record.id]: record.organizationId ?? auth.orgId ?? null },\n tenantFallbacks: [auth.tenantId ?? null].filter((value): value is string => !!value),\n })\n const customFields = normalizeCustomFieldResponse(customFieldValues[record.id])\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n item: {\n id: record.id,\n fileName: record.fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partition?.title ?? null,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n content: record.content && record.content.trim() ? record.content : null,\n customFields,\n },\n }, { headers: { 'Cache-Control': 'private, no-store' } })\n}\n\nexport const GET = withAttachmentAccessErrors(getAttachment)\n\nasync function updateAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const rawBody = await readJsonSafe(req, null)\n const { base, custom } = splitCustomFieldPayload(rawBody)\n const parsed = updateSchema.safeParse(base)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const dataEngine = resolve('dataEngine') as DataEngine\n const patchFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const accessContext = createAttachmentAccessContext(container)\n const outcome = await em.transactional(async (tx) => {\n const record = await findOneWithDecryption(tx, Attachment, patchFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({\n container, req, auth, recordId: record.id, operation: 'update',\n payload: { ...parsed.data, customFields: custom },\n })\n const effective = splitCustomFieldPayload(guard.modifiedPayload ?? { ...parsed.data, customFields: custom })\n const effectiveParsed = updateSchema.safeParse(effective.base)\n if (!effectiveParsed.success) return throwAttachmentAccessError(403)\n const patch: Record<string, unknown> = {}\n if (effectiveParsed.data.tags) patch.tags = normalizeAttachmentTags(effectiveParsed.data.tags)\n if (effectiveParsed.data.assignments) patch.assignments = normalizeAttachmentAssignments(effectiveParsed.data.assignments)\n const nextMetadata = mergeAttachmentMetadata(record.storageMetadata, patch)\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: record, action: 'reassign', persistProtection: true })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'reassign' })\n await assertAttachmentOwnerAccess({ em: tx, context: accessContext, auth, attachment: { ...record, storageMetadata: nextMetadata }, action: 'metadata' })\n record.storageMetadata = nextMetadata\n await tx.flush()\n if (dataEngine && Object.keys(effective.custom).length) {\n await setCustomFieldsIfAny({ dataEngine, entityId: E.attachments.attachment, recordId: record.id,\n tenantId: record.tenantId ?? auth.tenantId, organizationId: record.organizationId ?? auth.orgId,\n values: effective.custom,\n })\n }\n return { record, custom: effective.custom, guard }\n })\n const { record } = outcome\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const assignments = metadata.assignments ?? []\n const enrichments = await resolveAssignmentEnrichments(assignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedAssignments = applyAssignmentEnrichments(assignments, enrichments)\n return NextResponse.json({\n ok: true,\n item: {\n id: record.id,\n tags: metadata.tags ?? [],\n assignments: enrichedAssignments,\n customFields: normalizeCustomFieldResponse(outcome.custom ?? null),\n },\n })\n}\n\nasync function deleteAttachment(req: NextRequest, ctx: RouteContext) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const attachmentId = await resolveAttachmentId(ctx)\n if (!attachmentId) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n const dataEngine = resolve('dataEngine') as DataEngine\n const storageDriverFactory = resolve('storageDriverFactory') as StorageDriverFactory\n const deleteFilter: Record<string, unknown> = {\n id: attachmentId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n }\n const outcome = await em.transactional(async (tx) => {\n const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {\n tenantId: auth.tenantId, organizationId: auth.orgId,\n })\n if (!candidate) return throwAttachmentAccessError(404)\n const referenceIds = await lockAttachmentStorageReferences(tx, candidate)\n const removal = resolveAttachmentStorageRelease(referenceIds, candidate.id)\n if (!removal.removed) return throwAttachmentAccessError(404)\n const record = await findOneWithDecryption(tx, Attachment, deleteFilter, {\n lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true,\n }, { tenantId: auth.tenantId, organizationId: auth.orgId })\n if (!record) return throwAttachmentAccessError(404)\n const guard = await prepareAttachmentMutation({ container, req, auth, recordId: record.id, operation: 'delete' })\n await assertAttachmentOwnerAccess({ em: tx, context: createAttachmentAccessContext(container), auth, attachment: record, action: 'delete' })\n const driver = await storageDriverFactory.resolveForPartition(record.partitionCode, {\n tenantId: record.tenantId ?? auth.tenantId!, organizationId: record.organizationId ?? auth.orgId!,\n })\n tx.remove(record)\n await tx.flush()\n return { record, driver, guard, releaseStorage: removal.releaseStorage }\n })\n const { record } = outcome\n if (outcome.releaseStorage) await outcome.driver.delete(record.partitionCode, record.storagePath)\n await outcome.guard.runAfterSuccess()\n\n if (dataEngine) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n organizationId: record.organizationId ?? auth.orgId ?? null,\n tenantId: record.tenantId ?? auth.tenantId ?? null,\n },\n events: attachmentCrudEvents,\n indexer: attachmentCrudIndexer,\n })\n await dataEngine.flushOrmEntityChanges()\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport const PATCH = withAttachmentAccessErrors(updateAttachment)\nexport const DELETE = withAttachmentAccessErrors(deleteAttachment)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment detail management',\n methods: {\n GET: {\n summary: 'Get attachment details',\n description: 'Returns complete details of an attachment including metadata, tags, assignments, and custom fields.',\n responses: [\n { status: 200, description: 'Attachment details', schema: attachmentDetailResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n PATCH: {\n summary: 'Update attachment metadata',\n description: 'Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Attachment updated successfully', schema: z.object({ ok: z.literal(true), item: z.any() }) },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Failed to save attributes', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete attachment',\n description: 'Permanently deletes an attachment file from storage and database. Emits CRUD side effects.',\n responses: [\n { status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },\n ],\n errors: [\n { status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["metadata", "record"]
|
|
7
7
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createAttachmentAccessContext } from "../../lib/access-runner.js";
|
|
2
|
+
import { requiresAttachmentAccessScan, scanAuthorizedAttachments } from "../../lib/access-query.js";
|
|
3
|
+
import { withAttachmentAccessErrors } from "../../lib/access-errors.js";
|
|
1
4
|
import { NextResponse } from "next/server";
|
|
2
5
|
import { z } from "zod";
|
|
3
6
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
@@ -44,7 +47,7 @@ function formatDateValue(value) {
|
|
|
44
47
|
};
|
|
45
48
|
return toDate().toISOString();
|
|
46
49
|
}
|
|
47
|
-
async function
|
|
50
|
+
async function listAttachments(req) {
|
|
48
51
|
const auth = await getAuthFromRequest(req);
|
|
49
52
|
if (!auth || !auth.tenantId || !auth.orgId && !auth.isSuperAdmin) {
|
|
50
53
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -57,7 +60,8 @@ async function GET(req) {
|
|
|
57
60
|
const { page, pageSize, search, partition, tags, sortField, sortDir } = parsed.data;
|
|
58
61
|
const tagList = buildTagFilter(tags);
|
|
59
62
|
const offset = (page - 1) * pageSize;
|
|
60
|
-
const
|
|
63
|
+
const container = await createRequestContainer();
|
|
64
|
+
const { resolve } = container;
|
|
61
65
|
const em = resolve("em");
|
|
62
66
|
await ensureDefaultPartitions(em);
|
|
63
67
|
let queryEngine = null;
|
|
@@ -88,14 +92,42 @@ async function GET(req) {
|
|
|
88
92
|
createdAt: "a.created_at"
|
|
89
93
|
};
|
|
90
94
|
const orderColumn = orderMap[sortField ?? "createdAt"] ?? "a.created_at";
|
|
91
|
-
qb.orderBy({ [orderColumn]: sortDir === "asc" ? "asc" : "desc" });
|
|
95
|
+
qb.orderBy({ [orderColumn]: sortDir === "asc" ? "asc" : "desc", "a.id": "asc" });
|
|
92
96
|
qb.limit(pageSize).offset(offset);
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const partitions = await em.find(AttachmentPartition, {}, { orderBy: { title: "asc" } });
|
|
98
|
+
const protectedScan = requiresAttachmentAccessScan(partitions);
|
|
99
|
+
const accessContext = createAttachmentAccessContext(container);
|
|
100
|
+
const deadline = Date.now() + 15e3;
|
|
101
|
+
let records;
|
|
102
|
+
let total;
|
|
103
|
+
let authorizedTags = null;
|
|
104
|
+
if (protectedScan) {
|
|
105
|
+
const scanned = await scanAuthorizedAttachments({
|
|
106
|
+
auth,
|
|
107
|
+
context: accessContext,
|
|
108
|
+
partitions,
|
|
109
|
+
offset,
|
|
110
|
+
limit: pageSize,
|
|
111
|
+
deadline,
|
|
112
|
+
loadBatch: (batchOffset, limit) => qb.clone().limit(limit).offset(batchOffset).getResultList()
|
|
113
|
+
});
|
|
114
|
+
records = scanned.records;
|
|
115
|
+
total = scanned.total;
|
|
116
|
+
const facetQuery = em.createQueryBuilder(Attachment, "a").where(baseFilter).orderBy({ "a.id": "asc" });
|
|
117
|
+
const facets = await scanAuthorizedAttachments({
|
|
118
|
+
auth,
|
|
119
|
+
context: accessContext,
|
|
120
|
+
partitions,
|
|
121
|
+
limit: 0,
|
|
122
|
+
collectTags: true,
|
|
123
|
+
deadline,
|
|
124
|
+
loadBatch: (batchOffset, limit) => facetQuery.clone().limit(limit).offset(batchOffset).getResultList()
|
|
125
|
+
});
|
|
126
|
+
authorizedTags = facets.tags;
|
|
127
|
+
} else {
|
|
128
|
+
;
|
|
129
|
+
[records, total] = await Promise.all([qb.getResultList(), countQb.getCount("a.id", true)]);
|
|
130
|
+
}
|
|
99
131
|
const partitionTitleByCode = partitions.reduce((acc, entry) => {
|
|
100
132
|
if (entry.code) acc[entry.code] = entry.title ?? entry.code;
|
|
101
133
|
return acc;
|
|
@@ -135,13 +167,16 @@ async function GET(req) {
|
|
|
135
167
|
assignments: applyAssignmentEnrichments(item.assignments ?? [], enrichments)
|
|
136
168
|
})) : items;
|
|
137
169
|
const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize));
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
tagQuery =
|
|
170
|
+
let availableTags = authorizedTags ?? [];
|
|
171
|
+
if (!protectedScan) {
|
|
172
|
+
const db = em.getKysely();
|
|
173
|
+
let tagQuery = db.selectFrom("attachments").select(sql`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as("tag")).where("tenant_id", "=", auth.tenantId);
|
|
174
|
+
if (auth.orgId) {
|
|
175
|
+
tagQuery = tagQuery.where("organization_id", "=", auth.orgId);
|
|
176
|
+
}
|
|
177
|
+
const tagRows = await tagQuery.orderBy("tag", "asc").execute();
|
|
178
|
+
availableTags = tagRows.map((row) => typeof row.tag === "string" ? row.tag.trim() : "").filter((tag) => tag.length > 0);
|
|
142
179
|
}
|
|
143
|
-
const tagRows = await tagQuery.orderBy("tag", "asc").execute();
|
|
144
|
-
const availableTags = tagRows.map((row) => typeof row.tag === "string" ? row.tag.trim() : "").filter((tag) => tag.length > 0);
|
|
145
180
|
return NextResponse.json({
|
|
146
181
|
items: enrichedItems,
|
|
147
182
|
page,
|
|
@@ -155,8 +190,9 @@ async function GET(req) {
|
|
|
155
190
|
description: entry.description ?? null,
|
|
156
191
|
isPublic: entry.isPublic ?? false
|
|
157
192
|
}))
|
|
158
|
-
});
|
|
193
|
+
}, { headers: protectedScan ? { "Cache-Control": "private, no-store" } : void 0 });
|
|
159
194
|
}
|
|
195
|
+
const GET = withAttachmentAccessErrors(listAttachments);
|
|
160
196
|
const openApi = {
|
|
161
197
|
tag: attachmentsTag,
|
|
162
198
|
summary: "Attachment library management",
|
|
@@ -170,7 +206,10 @@ const openApi = {
|
|
|
170
206
|
],
|
|
171
207
|
errors: [
|
|
172
208
|
{ status: 400, description: "Invalid query parameters", schema: attachmentErrorSchema },
|
|
173
|
-
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema }
|
|
209
|
+
{ status: 401, description: "Unauthorized", schema: attachmentErrorSchema },
|
|
210
|
+
{ status: 403, description: "Owner policy denies access", schema: attachmentErrorSchema },
|
|
211
|
+
{ status: 404, description: "Attachment or owner is unavailable", schema: attachmentErrorSchema },
|
|
212
|
+
{ status: 504, description: "Owner authorization timed out", schema: attachmentErrorSchema }
|
|
174
213
|
]
|
|
175
214
|
}
|
|
176
215
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/attachments/api/library/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 type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '../../lib/imageUrls'\nimport { readAttachmentMetadata } from '../../lib/metadata'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { ensureDefaultPartitions } from '../../lib/partitions'\nimport {\n attachmentsTag,\n attachmentListQuerySchema as openApiListQuerySchema,\n attachmentListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n partition: z.string().optional(),\n tags: z.string().optional(),\n sortField: z.enum(['fileName', 'fileSize', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n}\n\nfunction buildTagFilter(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((tag) => tag.trim())\n .filter((tag) => tag.length > 0)\n}\n\nfunction formatDateValue(value: unknown): string {\n const toDate = (): Date => {\n if (value instanceof Date) return value\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed\n }\n const fallback = new Date(value as any)\n if (!Number.isNaN(fallback.getTime())) return fallback\n return new Date()\n }\n return toDate().toISOString()\n}\n\
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,WAAW;AACpB,SAAS,YAAY,2BAA2B;AAChD,SAAS,yBAAyB,iCAAiC;AACnE,SAAS,8BAA8B;AAEvC,SAAS,4BAA4B,oCAAoC;AACzE,SAAS,sBAAsB;AAC/B,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,EACA,6BAA6B;AAAA,EAC7B;AAAA,EACA;AAAA,OACK;AAEP,MAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,WAAW,EAAE,KAAK,CAAC,YAAY,YAAY,WAAW,CAAC,EAAE,SAAS;AAAA,EAClE,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAC5C,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AAClE;AAEA,SAAS,eAAe,KAAwB;AAC9C,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,SAAO,IACJ,MAAM,GAAG,EACT,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,EACvB,OAAO,CAAC,QAAQ,IAAI,SAAS,CAAC;AACnC;AAEA,SAAS,gBAAgB,OAAwB;AAC/C,QAAM,SAAS,MAAY;AACzB,QAAI,iBAAiB,KAAM,QAAO;AAClC,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,SAAS,IAAI,KAAK,KAAK;AAC7B,UAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,CAAC,EAAG,QAAO;AAAA,IAC9C;AACA,UAAM,WAAW,IAAI,KAAK,KAAY;AACtC,QAAI,CAAC,OAAO,MAAM,SAAS,QAAQ,CAAC,EAAG,QAAO;AAC9C,WAAO,oBAAI,KAAK;AAAA,EAClB;AACA,SAAO,OAAO,EAAE,YAAY;AAC9B;AAEA,
|
|
4
|
+
"sourcesContent": ["import { createAttachmentAccessContext } from '../../lib/access-runner'\nimport { requiresAttachmentAccessScan, scanAuthorizedAttachments } from '../../lib/access-query'\nimport { withAttachmentAccessErrors } from '../../lib/access-errors'\nimport { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { Attachment, AttachmentPartition } from '../../data/entities'\nimport { buildAttachmentImageUrl, slugifyAttachmentFileName } from '../../lib/imageUrls'\nimport { readAttachmentMetadata } from '../../lib/metadata'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'\nimport { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'\nimport { ensureDefaultPartitions } from '../../lib/partitions'\nimport {\n attachmentsTag,\n attachmentListQuerySchema as openApiListQuerySchema,\n attachmentListResponseSchema,\n attachmentErrorSchema,\n} from '../openapi'\n\nconst listQuerySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n partition: z.string().optional(),\n tags: z.string().optional(),\n sortField: z.enum(['fileName', 'fileSize', 'createdAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['attachments.view'] },\n}\n\nfunction buildTagFilter(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((tag) => tag.trim())\n .filter((tag) => tag.length > 0)\n}\n\nfunction formatDateValue(value: unknown): string {\n const toDate = (): Date => {\n if (value instanceof Date) return value\n if (typeof value === 'string') {\n const parsed = new Date(value)\n if (!Number.isNaN(parsed.getTime())) return parsed\n }\n const fallback = new Date(value as any)\n if (!Number.isNaN(fallback.getTime())) return fallback\n return new Date()\n }\n return toDate().toISOString()\n}\n\nasync function listAttachments(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse(Object.fromEntries(url.searchParams.entries()))\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid query' }, { status: 400 })\n }\n\n const { page, pageSize, search, partition, tags, sortField, sortDir } = parsed.data\n const tagList = buildTagFilter(tags)\n const offset = (page - 1) * pageSize\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as EntityManager\n await ensureDefaultPartitions(em)\n let queryEngine: QueryEngine | null = null\n try {\n queryEngine = resolve('queryEngine') as QueryEngine\n } catch {\n queryEngine = null\n }\n const qb = em.createQueryBuilder(Attachment, 'a')\n const baseFilter: Record<string, unknown> = { tenantId: auth.tenantId }\n if (auth.orgId) {\n baseFilter.organizationId = auth.orgId\n }\n qb.where(baseFilter)\n if (search && search.trim().length > 0) {\n qb.andWhere({ fileName: { $ilike: buildIlikeTerm(search.trim()) } })\n }\n if (partition && partition.trim().length > 0) {\n qb.andWhere({ partitionCode: partition.trim() })\n }\n if (tagList.length > 0) {\n qb.andWhere(`coalesce(a.storage_metadata->'tags', '[]'::jsonb) @> ?::jsonb`, [JSON.stringify(tagList)])\n }\n const countQb = qb.clone()\n const orderMap: Record<string, string> = {\n fileName: 'a.file_name',\n fileSize: 'a.file_size',\n createdAt: 'a.created_at',\n }\n const orderColumn = orderMap[sortField ?? 'createdAt'] ?? 'a.created_at'\n qb.orderBy({ [orderColumn]: sortDir === 'asc' ? 'asc' : 'desc', 'a.id': 'asc' })\n qb.limit(pageSize).offset(offset)\n\n const partitions = await em.find(AttachmentPartition, {}, { orderBy: { title: 'asc' } })\n const protectedScan = requiresAttachmentAccessScan(partitions)\n const accessContext = createAttachmentAccessContext(container)\n const deadline = Date.now() + 15_000\n let records: Attachment[]\n let total: number\n let authorizedTags: string[] | null = null\n if (protectedScan) {\n const scanned = await scanAuthorizedAttachments({\n auth, context: accessContext, partitions, offset, limit: pageSize, deadline,\n loadBatch: (batchOffset, limit) => qb.clone().limit(limit).offset(batchOffset).getResultList(),\n })\n records = scanned.records\n total = scanned.total\n const facetQuery = em.createQueryBuilder(Attachment, 'a').where(baseFilter).orderBy({ 'a.id': 'asc' })\n const facets = await scanAuthorizedAttachments({\n auth, context: accessContext, partitions, limit: 0, collectTags: true, deadline,\n loadBatch: (batchOffset, limit) => facetQuery.clone().limit(limit).offset(batchOffset).getResultList(),\n })\n authorizedTags = facets.tags\n } else {\n ;[records, total] = await Promise.all([qb.getResultList(), countQb.getCount('a.id', true)])\n }\n const partitionTitleByCode = partitions.reduce<Record<string, string>>((acc, entry) => {\n if (entry.code) acc[entry.code] = entry.title ?? entry.code\n return acc\n }, {})\n const items = records.map((record) => {\n const metadata = readAttachmentMetadata(record.storageMetadata)\n const fileName = record.fileName || ''\n const isImage = typeof record.mimeType === 'string' && record.mimeType.toLowerCase().startsWith('image/')\n const thumbnailUrl = isImage\n ? buildAttachmentImageUrl(record.id, {\n width: 200,\n height: 200,\n slug: slugifyAttachmentFileName(fileName),\n })\n : undefined\n return {\n id: record.id,\n fileName,\n fileSize: record.fileSize,\n mimeType: record.mimeType,\n partitionCode: record.partitionCode,\n partitionTitle: partitionTitleByCode[record.partitionCode] ?? null,\n url: record.url,\n createdAt: formatDateValue(record.createdAt),\n tags: metadata.tags ?? [],\n assignments: metadata.assignments ?? [],\n thumbnailUrl,\n content: record.content && record.content.trim() ? record.content : null,\n }\n })\n\n const allAssignments = items.flatMap((item) => item.assignments ?? [])\n const enrichments = await resolveAssignmentEnrichments(allAssignments, {\n queryEngine,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const enrichedItems = enrichments.size\n ? items.map((item) => ({\n ...item,\n assignments: applyAssignmentEnrichments(item.assignments ?? [], enrichments),\n }))\n : items\n\n const totalPages = Math.max(1, Math.ceil(Number(total) / pageSize))\n let availableTags = authorizedTags ?? []\n if (!protectedScan) {\n const db = em.getKysely<any>() as any\n let tagQuery = db\n .selectFrom('attachments')\n .select(sql<string>`distinct jsonb_array_elements_text(coalesce(storage_metadata->'tags', '[]'::jsonb))`.as('tag'))\n .where('tenant_id', '=', auth.tenantId)\n if (auth.orgId) {\n tagQuery = tagQuery.where('organization_id', '=', auth.orgId)\n }\n const tagRows = await tagQuery.orderBy('tag', 'asc').execute() as Array<{ tag?: string | null }>\n availableTags = tagRows\n .map((row) => (typeof row.tag === 'string' ? row.tag.trim() : ''))\n .filter((tag) => tag.length > 0)\n }\n\n return NextResponse.json({\n items: enrichedItems,\n page,\n pageSize,\n total,\n totalPages,\n availableTags,\n partitions: partitions.map((entry) => ({\n code: entry.code,\n title: entry.title,\n description: entry.description ?? null,\n isPublic: entry.isPublic ?? false,\n })),\n }, { headers: protectedScan ? { 'Cache-Control': 'private, no-store' } : undefined })\n}\n\nexport const GET = withAttachmentAccessErrors(listAttachments)\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Attachment library management',\n methods: {\n GET: {\n summary: 'List attachments',\n description: 'Returns paginated list of attachments with optional filtering by search term, partition, and tags. Includes available tags and partitions.',\n query: openApiListQuerySchema,\n responses: [\n { status: 200, description: 'Attachments list with pagination and metadata', schema: attachmentListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },\n { status: 403, description: 'Owner policy denies access', schema: attachmentErrorSchema },\n { status: 404, description: 'Attachment or owner is unavailable', schema: attachmentErrorSchema },\n { status: 504, description: 'Owner authorization timed out', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["metadata"]
|
|
7
7
|
}
|
|
@@ -6,6 +6,7 @@ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/
|
|
|
6
6
|
import { Attachment, AttachmentPartition } from "../../data/entities.js";
|
|
7
7
|
import { ensureDefaultPartitions, DEFAULT_ATTACHMENT_PARTITIONS, sanitizePartitionCode, isPartitionSettingsLocked } from "../../lib/partitions.js";
|
|
8
8
|
import { resolvePartitionEnvKey } from "../../lib/partitionEnv.js";
|
|
9
|
+
import { getDeclaredAttachmentAccessRequirements } from "../../lib/access-protection.js";
|
|
9
10
|
import { resolveDefaultAttachmentOcrEnabled } from "../../lib/ocrConfig.js";
|
|
10
11
|
import {
|
|
11
12
|
attachmentsTag,
|
|
@@ -113,6 +114,7 @@ async function POST(req) {
|
|
|
113
114
|
}
|
|
114
115
|
const entry = em.create(AttachmentPartition, {
|
|
115
116
|
code,
|
|
117
|
+
accessResolverRequirements: getDeclaredAttachmentAccessRequirements(code),
|
|
116
118
|
title: parsed.data.title.trim(),
|
|
117
119
|
description: parsed.data.description?.trim() ?? null,
|
|
118
120
|
storageDriver: parsed.data.storageDriver ?? "local",
|