@open-mercato/core 0.10.1-develop.7841.1.af87ac614d → 0.10.1-develop.7857.1.259928eb85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/api_keys/api/keys/route.js +15 -1
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +6 -1
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +6 -1
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +16 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +11 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/cli.js +2 -0
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +11 -0
- package/dist/modules/attachments/data/validators.js.map +2 -2
- package/dist/modules/attachments/index.js +10 -0
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-query.js +5 -1
- package/dist/modules/attachments/lib/access-query.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/driverFactory.js +54 -33
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/index.js +11 -1
- package/dist/modules/attachments/lib/drivers/index.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/storageValidation.js +85 -0
- package/dist/modules/attachments/lib/drivers/storageValidation.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +3 -1
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/storageErrors.js +22 -0
- package/dist/modules/attachments/lib/storageErrors.js.map +7 -0
- package/dist/modules/auth/api/users/route.js +16 -1
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +13 -2
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +4 -2
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +4 -9
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +4 -9
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/components/detail/TaskDialog.js +2 -0
- package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +23 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +6 -9
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -3
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/wms/api/listEnrichers.js +3 -1
- package/dist/modules/wms/api/listEnrichers.js.map +2 -2
- package/dist/modules/wms/index.js +1 -1
- package/dist/modules/wms/index.js.map +2 -2
- package/jest.config.cjs +1 -1
- package/package.json +8 -8
- package/src/modules/api_keys/api/keys/route.ts +15 -1
- package/src/modules/api_keys/i18n/de.json +1 -0
- package/src/modules/api_keys/i18n/en.json +1 -0
- package/src/modules/api_keys/i18n/es.json +1 -0
- package/src/modules/api_keys/i18n/ko.json +1 -0
- package/src/modules/api_keys/i18n/pl.json +1 -0
- package/src/modules/attachments/api/file/[id]/route.ts +7 -1
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -1
- package/src/modules/attachments/api/library/[id]/route.ts +14 -1
- package/src/modules/attachments/api/route.ts +11 -3
- package/src/modules/attachments/cli.ts +2 -0
- package/src/modules/attachments/data/validators.ts +11 -0
- package/src/modules/attachments/i18n/de.json +1 -0
- package/src/modules/attachments/i18n/en.json +1 -0
- package/src/modules/attachments/i18n/es.json +1 -0
- package/src/modules/attachments/i18n/ko.json +1 -0
- package/src/modules/attachments/i18n/pl.json +1 -0
- package/src/modules/attachments/index.ts +12 -0
- package/src/modules/attachments/lib/access-query.ts +5 -1
- package/src/modules/attachments/lib/createFromBuffer.ts +2 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +79 -55
- package/src/modules/attachments/lib/drivers/index.ts +13 -0
- package/src/modules/attachments/lib/drivers/storageValidation.ts +114 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +3 -1
- package/src/modules/attachments/lib/storageErrors.ts +21 -0
- package/src/modules/auth/api/users/route.ts +18 -1
- package/src/modules/auth/backend/users/page.tsx +14 -2
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/catalog/commands/products.ts +4 -1
- package/src/modules/customer_accounts/AGENTS.md +1 -1
- package/src/modules/customers/api/companies/[id]/route.ts +6 -13
- package/src/modules/customers/api/people/[id]/route.ts +6 -13
- package/src/modules/customers/components/detail/TaskDialog.tsx +2 -0
- package/src/modules/customers/lib/todoCompatibility.ts +28 -0
- package/src/modules/directory/api/organization-branding/route.ts +5 -8
- package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
- package/src/modules/sales/AGENTS.md +3 -3
- package/src/modules/wms/api/listEnrichers.ts +5 -2
- package/src/modules/wms/index.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/wms/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'wms',\n title: 'Warehouse Management System',\n version: '0.1.0',\n description: 'Warehouse topology, inventory balances, reservations, and movement ledger.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n requires: ['catalog', 'sales', 'feature_toggles'],\n}\n\nexport { features } from './acl'\n"],
|
|
5
|
-
"mappings": "AACA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU,CAAC,WAAW,SAAS,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'wms',\n title: 'Warehouse Management System',\n version: '0.1.0',\n description: 'Warehouse topology, inventory balances, reservations, and movement ledger.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n requires: ['catalog', 'customers', 'sales', 'feature_toggles'],\n}\n\nexport { features } from './acl'\n"],
|
|
5
|
+
"mappings": "AACA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU,CAAC,WAAW,aAAa,SAAS,iBAAiB;AAC/D;AAEA,SAAS,gBAAgB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/jest.config.cjs
CHANGED
|
@@ -41,7 +41,7 @@ module.exports = {
|
|
|
41
41
|
},
|
|
42
42
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
43
43
|
transformIgnorePatterns: [
|
|
44
|
-
'node_modules/(?!(
|
|
44
|
+
'node_modules/(?!(sanitize-html|htmlparser2|domhandler|domutils|dom-serializer|domelementtype|entities|@mikro-orm|kysely|ai|@ai-sdk|ai-sdk-ollama|@workflow|@standard-schema|@tanstack/react-table|@tanstack/table-core|@tanstack/react-store|@tanstack/store)/)',
|
|
45
45
|
],
|
|
46
46
|
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
47
47
|
passWithNoTests: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7857.1.259928eb85",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
"pdf-lib": "^1.17.1",
|
|
259
259
|
"pdfjs-dist": "^6.2.108",
|
|
260
260
|
"resend": "^6.24.0",
|
|
261
|
-
"sanitize-html": "2.
|
|
261
|
+
"sanitize-html": "2.18.1",
|
|
262
262
|
"semver": "^7.8.5",
|
|
263
263
|
"sharp": "^0.35.3",
|
|
264
264
|
"svix": "^1.99.1",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7857.1.259928eb85",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7857.1.259928eb85",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7857.1.259928eb85",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7857.1.259928eb85",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7857.1.259928eb85",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7857.1.259928eb85",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -4,7 +4,7 @@ import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
|
4
4
|
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
5
5
|
import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
|
-
import { Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
7
|
+
import { Role, User } from '@open-mercato/core/modules/auth/data/entities'
|
|
8
8
|
import { Organization } from '@open-mercato/core/modules/directory/data/entities'
|
|
9
9
|
import { ApiKey } from '../../data/entities'
|
|
10
10
|
import { createApiKeySchema } from '../../data/validators'
|
|
@@ -13,6 +13,7 @@ import { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/
|
|
|
13
13
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
14
14
|
import { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'
|
|
15
15
|
import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
|
|
16
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
16
17
|
import type { CreateApiKeyCommandInput } from '../../commands/keys'
|
|
17
18
|
|
|
18
19
|
const listQuerySchema = z.object({
|
|
@@ -138,6 +139,19 @@ async function prepareCreateCommandInput(
|
|
|
138
139
|
})) {
|
|
139
140
|
throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })
|
|
140
141
|
}
|
|
142
|
+
if (organizationId) {
|
|
143
|
+
const creator = await findOneWithDecryption(
|
|
144
|
+
em,
|
|
145
|
+
User,
|
|
146
|
+
{ id: auth.sub, tenantId: targetTenantId, deletedAt: null },
|
|
147
|
+
{ fields: ['organizationId'] },
|
|
148
|
+
{ tenantId: targetTenantId, organizationId: null },
|
|
149
|
+
)
|
|
150
|
+
if (creator?.organizationId?.toLowerCase() !== organizationId.toLowerCase()) {
|
|
151
|
+
const message = translate('api_keys.errors.organizationMustMatchCreator')
|
|
152
|
+
throw json({ error: message, fieldErrors: { organizationId: message } }, { status: 400 })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
141
155
|
|
|
142
156
|
return {
|
|
143
157
|
name: input.name,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"api_keys.copy.warning": "Bewahre diesen Schlüssel sicher auf – er kann später nicht erneut abgerufen werden.",
|
|
6
6
|
"api_keys.errors.forbidden": "Verboten",
|
|
7
7
|
"api_keys.errors.notFound": "Nicht gefunden",
|
|
8
|
+
"api_keys.errors.organizationMustMatchCreator": "Ein organisationsgebundener API-Schlüssel muss die Organisation Ihres Kontos verwenden. Wählen Sie diese Organisation aus, um einen nutzbaren Schlüssel zu erstellen.",
|
|
8
9
|
"api_keys.errors.organizationOutOfScope": "Organisation außerhalb des Geltungsbereichs",
|
|
9
10
|
"api_keys.errors.roleNotFound": "Rolle {identifier} nicht gefunden",
|
|
10
11
|
"api_keys.errors.roleWrongTenant": "Rolle {role} gehört zu einem anderen Mandanten",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"api_keys.copy.warning": "Keep this key safe — it cannot be retrieved later.",
|
|
6
6
|
"api_keys.errors.forbidden": "Forbidden",
|
|
7
7
|
"api_keys.errors.notFound": "Not found",
|
|
8
|
+
"api_keys.errors.organizationMustMatchCreator": "An organization-scoped API key must use your account’s organization. Select that organization to create a usable key.",
|
|
8
9
|
"api_keys.errors.organizationOutOfScope": "Organization out of scope",
|
|
9
10
|
"api_keys.errors.roleNotFound": "Role {identifier} not found",
|
|
10
11
|
"api_keys.errors.roleWrongTenant": "Role {role} belongs to another tenant",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"api_keys.copy.warning": "Mantén esta clave a salvo; no se puede recuperar más tarde.",
|
|
6
6
|
"api_keys.errors.forbidden": "Prohibido",
|
|
7
7
|
"api_keys.errors.notFound": "No encontrado",
|
|
8
|
+
"api_keys.errors.organizationMustMatchCreator": "Una clave API limitada a una organización debe usar la organización de tu cuenta. Selecciona esa organización para crear una clave utilizable.",
|
|
8
9
|
"api_keys.errors.organizationOutOfScope": "Organización fuera del ámbito",
|
|
9
10
|
"api_keys.errors.roleNotFound": "Rol {identifier} no encontrado",
|
|
10
11
|
"api_keys.errors.roleWrongTenant": "El rol {role} pertenece a otro inquilino",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"api_keys.copy.warning": "이 키를 안전하게 보관하세요 — 나중에 다시 확인할 수 없습니다.",
|
|
6
6
|
"api_keys.errors.forbidden": "접근이 거부되었습니다",
|
|
7
7
|
"api_keys.errors.notFound": "찾을 수 없습니다",
|
|
8
|
+
"api_keys.errors.organizationMustMatchCreator": "조직 범위 API 키는 계정의 조직을 사용해야 합니다. 사용 가능한 키를 만들려면 해당 조직을 선택하세요.",
|
|
8
9
|
"api_keys.errors.organizationOutOfScope": "조직이 범위를 벗어났습니다",
|
|
9
10
|
"api_keys.errors.roleNotFound": "역할 {identifier}을(를) 찾을 수 없습니다",
|
|
10
11
|
"api_keys.errors.roleWrongTenant": "역할 {role}은(는) 다른 테넌트에 속합니다",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"api_keys.copy.warning": "Zachowaj ten klucz w bezpiecznym miejscu – nie można go później odzyskać.",
|
|
6
6
|
"api_keys.errors.forbidden": "Brak uprawnień",
|
|
7
7
|
"api_keys.errors.notFound": "Nie znaleziono",
|
|
8
|
+
"api_keys.errors.organizationMustMatchCreator": "Klucz API przypisany do organizacji musi używać organizacji Twojego konta. Wybierz tę organizację, aby utworzyć działający klucz.",
|
|
8
9
|
"api_keys.errors.organizationOutOfScope": "Organizacja poza zakresem",
|
|
9
10
|
"api_keys.errors.roleNotFound": "Nie znaleziono roli {identifier}",
|
|
10
11
|
"api_keys.errors.roleWrongTenant": "Rola {role} należy do innego najemcy",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'
|
|
2
|
+
import { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'
|
|
1
3
|
import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../lib/access-runner'
|
|
2
4
|
import { attachmentAccessErrorResponse } from '../../../lib/access-errors'
|
|
3
5
|
import { NextRequest, NextResponse } from "next/server";
|
|
@@ -23,7 +25,7 @@ export const metadata = {
|
|
|
23
25
|
GET: { requireAuth: false },
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
async function readAttachmentFile(
|
|
27
29
|
req: NextRequest,
|
|
28
30
|
context: { params: Promise<{ id: string }> },
|
|
29
31
|
) {
|
|
@@ -64,6 +66,7 @@ export async function GET(
|
|
|
64
66
|
const partition = await em.findOne(AttachmentPartition, {
|
|
65
67
|
code: attachment.partitionCode,
|
|
66
68
|
});
|
|
69
|
+
assertAttachmentStoragePartitionExists(Boolean(partition))
|
|
67
70
|
if (!partition) {
|
|
68
71
|
return NextResponse.json(
|
|
69
72
|
{ error: "Partition misconfigured" },
|
|
@@ -115,6 +118,8 @@ export async function GET(
|
|
|
115
118
|
return new NextResponse(responseBody, { status: 200, headers });
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
export const GET = withAttachmentStorageErrors(readAttachmentFile)
|
|
122
|
+
|
|
118
123
|
export const openApi: OpenApiRouteDoc = {
|
|
119
124
|
tag: attachmentsTag,
|
|
120
125
|
summary: "Download attachment file",
|
|
@@ -131,6 +136,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
131
136
|
},
|
|
132
137
|
],
|
|
133
138
|
errors: [
|
|
139
|
+
{ status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },
|
|
134
140
|
{
|
|
135
141
|
status: 400,
|
|
136
142
|
description: "Missing attachment ID",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'
|
|
2
|
+
import { assertAttachmentStoragePartitionExists } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'
|
|
1
3
|
import { createAttachmentAccessContext, evaluateAttachmentAccess } from '../../../../lib/access-runner'
|
|
2
4
|
import { attachmentAccessErrorResponse } from '../../../../lib/access-errors'
|
|
3
5
|
import { NextRequest, NextResponse } from 'next/server'
|
|
@@ -37,7 +39,7 @@ export const metadata = {
|
|
|
37
39
|
GET: { requireAuth: false },
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
async function readAttachmentImage(
|
|
41
43
|
req: NextRequest,
|
|
42
44
|
context: { params: Promise<{ id: string; slug?: string[] | undefined }> }
|
|
43
45
|
) {
|
|
@@ -74,6 +76,7 @@ export async function GET(
|
|
|
74
76
|
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
75
77
|
}
|
|
76
78
|
const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })
|
|
79
|
+
assertAttachmentStoragePartitionExists(Boolean(partition))
|
|
77
80
|
if (!partition) {
|
|
78
81
|
return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })
|
|
79
82
|
}
|
|
@@ -147,6 +150,8 @@ export async function GET(
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
export const GET = withAttachmentStorageErrors(readAttachmentImage)
|
|
154
|
+
|
|
150
155
|
export const openApi: OpenApiRouteDoc = {
|
|
151
156
|
tag: attachmentsTag,
|
|
152
157
|
summary: 'Serve resized images',
|
|
@@ -162,6 +167,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
162
167
|
},
|
|
163
168
|
],
|
|
164
169
|
errors: [
|
|
170
|
+
{ status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },
|
|
165
171
|
{ status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },
|
|
166
172
|
{ status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },
|
|
167
173
|
{ status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'
|
|
2
|
+
import { getAttachmentStoragePolicy } from '../../../lib/drivers/storageValidation'
|
|
1
3
|
import { LockMode } from '@mikro-orm/core'
|
|
2
4
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
5
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
@@ -256,6 +258,16 @@ async function deleteAttachment(req: NextRequest, ctx: RouteContext) {
|
|
|
256
258
|
tenantId: auth.tenantId,
|
|
257
259
|
organizationId: auth.orgId,
|
|
258
260
|
}
|
|
261
|
+
if (getAttachmentStoragePolicy() === 'strict') {
|
|
262
|
+
const preflightRecord = await findOneWithDecryption(em, Attachment, deleteFilter, undefined, {
|
|
263
|
+
tenantId: auth.tenantId, organizationId: auth.orgId,
|
|
264
|
+
})
|
|
265
|
+
if (!preflightRecord) return throwAttachmentAccessError(404)
|
|
266
|
+
await assertAttachmentOwnerAccess({ em, context: createAttachmentAccessContext(container), auth, attachment: preflightRecord, action: 'delete' })
|
|
267
|
+
await storageDriverFactory.resolveForPartition(preflightRecord.partitionCode, {
|
|
268
|
+
tenantId: preflightRecord.tenantId ?? auth.tenantId, organizationId: preflightRecord.organizationId ?? auth.orgId,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
259
271
|
const outcome = await em.transactional(async (tx) => {
|
|
260
272
|
const candidate = await findOneWithDecryption(tx, Attachment, deleteFilter, undefined, {
|
|
261
273
|
tenantId: auth.tenantId, organizationId: auth.orgId,
|
|
@@ -301,7 +313,7 @@ async function deleteAttachment(req: NextRequest, ctx: RouteContext) {
|
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
export const PATCH = withAttachmentAccessErrors(updateAttachment)
|
|
304
|
-
export const DELETE = withAttachmentAccessErrors(deleteAttachment)
|
|
316
|
+
export const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment))
|
|
305
317
|
|
|
306
318
|
export const openApi: OpenApiRouteDoc = {
|
|
307
319
|
tag: attachmentsTag,
|
|
@@ -347,6 +359,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
347
359
|
{ status: 200, description: 'Attachment deleted successfully', schema: z.object({ ok: z.literal(true) }) },
|
|
348
360
|
],
|
|
349
361
|
errors: [
|
|
362
|
+
{ status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },
|
|
350
363
|
{ status: 400, description: 'Invalid attachment ID', schema: attachmentErrorSchema },
|
|
351
364
|
{ status: 401, description: 'Unauthorized', schema: attachmentErrorSchema },
|
|
352
365
|
{ status: 404, description: 'Attachment not found', schema: attachmentErrorSchema },
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { attachmentErrorSchema } from './openapi'
|
|
2
|
+
import { withAttachmentStorageErrors } from '@open-mercato/core/modules/attachments/lib/storageErrors'
|
|
3
|
+
import { assertAttachmentStoragePartitionExists, getAttachmentStoragePolicy } from '@open-mercato/core/modules/attachments/lib/drivers/storageValidation'
|
|
1
4
|
import { LockMode } from '@mikro-orm/core'
|
|
2
5
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
6
|
import { prepareAttachmentMutation } from '../lib/access-mutation'
|
|
@@ -282,7 +285,7 @@ async function listRecordAttachments(req: Request) {
|
|
|
282
285
|
|
|
283
286
|
export const GET = withAttachmentAccessErrors(listRecordAttachments)
|
|
284
287
|
|
|
285
|
-
|
|
288
|
+
async function uploadAttachment(req: Request) {
|
|
286
289
|
const { t } = await resolveTranslations()
|
|
287
290
|
const auth = await getAuthFromRequest(req)
|
|
288
291
|
if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
@@ -384,7 +387,7 @@ export async function POST(req: Request) {
|
|
|
384
387
|
if (uploadScope.denied) return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
385
388
|
const orgId = uploadScope.organizationId
|
|
386
389
|
if (!orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
387
|
-
await ensureDefaultPartitions(em)
|
|
390
|
+
if (getAttachmentStoragePolicy() === 'legacy') await ensureDefaultPartitions(em)
|
|
388
391
|
// Optional per-field validations
|
|
389
392
|
let partitionFromField: string | null = null
|
|
390
393
|
let fieldMaxAttachmentSizeMb: number | null = null
|
|
@@ -457,6 +460,7 @@ export async function POST(req: Request) {
|
|
|
457
460
|
let partition: AttachmentPartition | null = null
|
|
458
461
|
for (const code of partitionCodeCandidates) {
|
|
459
462
|
const record = await em.findOne(AttachmentPartition, { code })
|
|
463
|
+
assertAttachmentStoragePartitionExists(Boolean(record))
|
|
460
464
|
if (record) {
|
|
461
465
|
partition = record
|
|
462
466
|
break
|
|
@@ -466,6 +470,7 @@ export async function POST(req: Request) {
|
|
|
466
470
|
partition = await em.findOne(AttachmentPartition, { code: defaultPartitionCode })
|
|
467
471
|
}
|
|
468
472
|
if (!partition) {
|
|
473
|
+
assertAttachmentStoragePartitionExists(false)
|
|
469
474
|
return NextResponse.json({ error: 'Storage partition is not configured.' }, { status: 400 })
|
|
470
475
|
}
|
|
471
476
|
await ensureAttachmentPartitionProtection(em, partition)
|
|
@@ -781,7 +786,8 @@ async function deleteAttachment(req: Request) {
|
|
|
781
786
|
return NextResponse.json({ ok: true })
|
|
782
787
|
}
|
|
783
788
|
|
|
784
|
-
export const
|
|
789
|
+
export const POST = withAttachmentAccessErrors(withAttachmentStorageErrors(uploadAttachment))
|
|
790
|
+
export const DELETE = withAttachmentAccessErrors(withAttachmentStorageErrors(deleteAttachment))
|
|
785
791
|
|
|
786
792
|
export const openApi: OpenApiRouteDoc = {
|
|
787
793
|
summary: 'Manage entity attachments',
|
|
@@ -813,6 +819,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
813
819
|
{ status: 200, description: 'Attachment stored successfully', schema: uploadResponseSchema },
|
|
814
820
|
],
|
|
815
821
|
errors: [
|
|
822
|
+
{ status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },
|
|
816
823
|
{ status: 400, description: 'Payload validation error', schema: errorSchema },
|
|
817
824
|
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
818
825
|
{ status: 403, description: 'Attachment violates field constraints', schema: errorSchema },
|
|
@@ -827,6 +834,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
827
834
|
{ status: 404, description: 'Attachment not found', schema: errorSchema },
|
|
828
835
|
],
|
|
829
836
|
errors: [
|
|
837
|
+
{ status: 503, description: 'Attachment storage configuration rejected by policy', schema: attachmentErrorSchema },
|
|
830
838
|
{ status: 400, description: 'Missing attachment identifier', schema: errorSchema },
|
|
831
839
|
{ status: 401, description: 'Unauthorized', schema: errorSchema },
|
|
832
840
|
{ status: 403, description: 'Owner policy denies access', schema: errorSchema },
|
|
@@ -3,6 +3,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
3
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
4
|
import { Attachment } from './data/entities'
|
|
5
5
|
import type { StorageDriverFactory } from './lib/drivers'
|
|
6
|
+
import { isAttachmentStorageConfigurationError } from './lib/drivers/storageValidation'
|
|
6
7
|
import { removeAttachmentRecord } from './lib/storageReferences'
|
|
7
8
|
|
|
8
9
|
type ParsedArgs = Record<string, string | boolean>
|
|
@@ -93,6 +94,7 @@ const deleteAttachments: ModuleCli = {
|
|
|
93
94
|
console.log(`Not found: ${missing.join(', ')}`)
|
|
94
95
|
}
|
|
95
96
|
} catch (err) {
|
|
97
|
+
if (isAttachmentStorageConfigurationError(err)) throw err
|
|
96
98
|
console.error('[attachments] delete command failed:', err)
|
|
97
99
|
}
|
|
98
100
|
},
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import type { AttachmentAccessResolver } from '../lib/access-types'
|
|
3
3
|
|
|
4
|
+
export const attachmentStorageConfigurationSchema = z.custom<Record<string, unknown>>((value) => {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
6
|
+
const prototype = Object.getPrototypeOf(value)
|
|
7
|
+
return prototype === Object.prototype || prototype === null
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const attachmentStorageScopeSchema = z.object({
|
|
11
|
+
tenantId: z.string(),
|
|
12
|
+
organizationId: z.string(),
|
|
13
|
+
}).refine((scope) => Boolean(scope.tenantId.trim()) === Boolean(scope.organizationId.trim()))
|
|
14
|
+
|
|
4
15
|
export const attachmentAccessActionSchema = z.enum(['read', 'render', 'metadata', 'reassign', 'delete', 'export'])
|
|
5
16
|
export const attachmentAccessSubjectFeaturesSchema = z.array(z.string())
|
|
6
17
|
export const attachmentAccessSelectorSchema = z.string().min(1).max(200).regex(/^(?:\*|[^*\s]+(?:\.\*)?)$/)
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"attachments.errors.quotaRecoveryUnsupported": "Der Speichertreiber kann nicht an der Kontingent-Wiederherstellung teilnehmen.",
|
|
14
14
|
"attachments.errors.quotaUnavailable": "Die Speicherkontingent-Abrechnung ist nicht verfügbar.",
|
|
15
15
|
"attachments.errors.selectOrganization": "Wählen Sie eine bestimmte Organisation aus, bevor Sie einen Anhang hochladen.",
|
|
16
|
+
"attachments.errors.storageConfigurationInvalid": "Die Speicherkonfiguration für Anhänge ist nicht verfügbar.",
|
|
16
17
|
"attachments.errors.storageFailed": "Die Anhangsdatei konnte nicht im Speicher abgelegt werden.",
|
|
17
18
|
"attachments.errors.storagePathExists": "Der Zielspeicherpfad existiert bereits.",
|
|
18
19
|
"attachments.errors.uploadFailed": "Das Hochladen des Anhangs ist fehlgeschlagen.",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"attachments.errors.quotaRecoveryUnsupported": "Storage driver cannot participate in quota recovery.",
|
|
14
14
|
"attachments.errors.quotaUnavailable": "Storage quota accounting is unavailable.",
|
|
15
15
|
"attachments.errors.selectOrganization": "Select a specific organization before uploading an attachment.",
|
|
16
|
+
"attachments.errors.storageConfigurationInvalid": "Attachment storage configuration is unavailable.",
|
|
16
17
|
"attachments.errors.storageFailed": "Failed to store the attachment file.",
|
|
17
18
|
"attachments.errors.storagePathExists": "The target storage path already exists.",
|
|
18
19
|
"attachments.errors.uploadFailed": "Attachment upload failed.",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"attachments.errors.quotaRecoveryUnsupported": "El controlador de almacenamiento no puede participar en la recuperación de cuota.",
|
|
14
14
|
"attachments.errors.quotaUnavailable": "La contabilidad de la cuota de almacenamiento no está disponible.",
|
|
15
15
|
"attachments.errors.selectOrganization": "Selecciona una organización específica antes de subir un adjunto.",
|
|
16
|
+
"attachments.errors.storageConfigurationInvalid": "La configuración del almacenamiento de archivos adjuntos no está disponible.",
|
|
16
17
|
"attachments.errors.storageFailed": "No se pudo almacenar el archivo adjunto.",
|
|
17
18
|
"attachments.errors.storagePathExists": "La ruta de almacenamiento de destino ya existe.",
|
|
18
19
|
"attachments.errors.uploadFailed": "Error al subir el archivo adjunto.",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"attachments.errors.quotaRecoveryUnsupported": "저장 용량 복구가 지원되지 않습니다.",
|
|
14
14
|
"attachments.errors.quotaUnavailable": "첨부파일 저장 용량 정보를 확인할 수 없습니다.",
|
|
15
15
|
"attachments.errors.selectOrganization": "첨부파일을 업로드하기 전에 특정 조직을 선택하세요.",
|
|
16
|
+
"attachments.errors.storageConfigurationInvalid": "첨부 파일 저장소 구성을 사용할 수 없습니다.",
|
|
16
17
|
"attachments.errors.storageFailed": "첨부 파일을 저장소에 기록하지 못했습니다.",
|
|
17
18
|
"attachments.errors.storagePathExists": "대상 저장 경로가 이미 존재합니다.",
|
|
18
19
|
"attachments.errors.uploadFailed": "첨부 파일 업로드에 실패했습니다.",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"attachments.errors.quotaRecoveryUnsupported": "Sterownik przechowywania nie może uczestniczyć w odzyskiwaniu limitu.",
|
|
14
14
|
"attachments.errors.quotaUnavailable": "Rozliczanie limitu przestrzeni dyskowej jest niedostępne.",
|
|
15
15
|
"attachments.errors.selectOrganization": "Wybierz konkretną organizację przed przesłaniem załącznika.",
|
|
16
|
+
"attachments.errors.storageConfigurationInvalid": "Konfiguracja magazynu załączników jest niedostępna.",
|
|
16
17
|
"attachments.errors.storageFailed": "Nie udało się zapisać pliku załącznika w magazynie.",
|
|
17
18
|
"attachments.errors.storagePathExists": "Docelowa ścieżka przechowywania już istnieje.",
|
|
18
19
|
"attachments.errors.uploadFailed": "Przesyłanie załącznika nie powiodło się.",
|
|
@@ -20,6 +20,18 @@ export {
|
|
|
20
20
|
type ReleaseScopedAttachmentInput,
|
|
21
21
|
} from './lib/attachment-service'
|
|
22
22
|
|
|
23
|
+
export {
|
|
24
|
+
AttachmentStorageConfigurationError,
|
|
25
|
+
getAttachmentStoragePolicy,
|
|
26
|
+
isAttachmentStorageConfigurationError,
|
|
27
|
+
registerStorageDriverValidator,
|
|
28
|
+
type AttachmentStoragePolicy,
|
|
29
|
+
type AttachmentStorageScope,
|
|
30
|
+
type AttachmentStorageValidationStage,
|
|
31
|
+
type AttachmentStorageValidationContext,
|
|
32
|
+
type AttachmentStorageValidator,
|
|
33
|
+
type AttachmentStorageConfigurationReason,
|
|
34
|
+
} from './lib/drivers/storageValidation'
|
|
23
35
|
export type {
|
|
24
36
|
AttachmentAccessAction,
|
|
25
37
|
AttachmentAccessDecision,
|
|
@@ -9,6 +9,7 @@ import { evaluateAttachmentAccess, type AttachmentAccessContext } from './access
|
|
|
9
9
|
import type { AttachmentAccessAction, AttachmentAccessRecord } from './access-types'
|
|
10
10
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
11
11
|
import { attachmentAccessErrorBody, throwAttachmentAccessError } from './access-errors'
|
|
12
|
+
import { assertAttachmentStoragePartitionExists } from './drivers/storageValidation'
|
|
12
13
|
import { readAttachmentMetadata } from './metadata'
|
|
13
14
|
|
|
14
15
|
export function requiresAttachmentAccessScan(partitions: readonly AttachmentPartition[]): boolean {
|
|
@@ -31,7 +32,10 @@ export async function assertAttachmentOwnerAccess(input: {
|
|
|
31
32
|
const partition = await findOneWithDecryption(input.em, AttachmentPartition, { code: input.attachment.partitionCode }, undefined, {
|
|
32
33
|
tenantId: input.attachment.tenantId, organizationId: input.attachment.organizationId,
|
|
33
34
|
})
|
|
34
|
-
if (!partition)
|
|
35
|
+
if (!partition) {
|
|
36
|
+
if (input.action === 'delete') assertAttachmentStoragePartitionExists(false)
|
|
37
|
+
await throwAttachmentAccessError(404)
|
|
38
|
+
}
|
|
35
39
|
if (input.persistProtection) await ensureAttachmentPartitionProtection(input.em, partition!)
|
|
36
40
|
const result = await evaluateAttachmentAccess({ ...input, partition: partition!, requireAuthForPublic: true })
|
|
37
41
|
if (!result.ok) await throwAttachmentAccessError(result.status)
|
|
@@ -3,6 +3,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
3
3
|
import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
4
4
|
import { Attachment, AttachmentPartition } from '../data/entities'
|
|
5
5
|
import { StorageDriverFactory } from './drivers'
|
|
6
|
+
import { assertAttachmentStoragePartitionExists } from './drivers/storageValidation'
|
|
6
7
|
import { ensureDefaultPartitions, resolveDefaultPartitionCode } from './partitions'
|
|
7
8
|
import { buildAttachmentFileUrl } from './imageUrls'
|
|
8
9
|
import { mergeAttachmentMetadata, upsertAssignment } from './metadata'
|
|
@@ -54,6 +55,7 @@ export async function createAttachmentFromBuffer(input: CreateAttachmentFromBuff
|
|
|
54
55
|
|
|
55
56
|
let partition = await em.findOne(AttachmentPartition, { code })
|
|
56
57
|
if (!partition) {
|
|
58
|
+
assertAttachmentStoragePartitionExists(false)
|
|
57
59
|
await ensureDefaultPartitions(em)
|
|
58
60
|
partition = await em.findOne(AttachmentPartition, { code })
|
|
59
61
|
}
|