@open-mercato/core 0.6.7-develop.6592.1.7f69fa2454 → 0.6.7-develop.6594.1.5581f765f4
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/custom_entity/index.js +2 -0
- package/dist/generated/entities/custom_entity/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/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/lib/imageSafety.js.map +2 -2
- package/dist/modules/auth/api/features.js +14 -0
- package/dist/modules/auth/api/features.js.map +2 -2
- package/dist/modules/entities/api/entities.js +20 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +89 -0
- package/dist/modules/entities/api/entity-settings.js.map +7 -0
- package/dist/modules/entities/api/records.js +46 -25
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +13 -4
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/create/page.js +27 -2
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/data/entities.js +4 -0
- package/dist/modules/entities/data/entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +17 -4
- package/dist/modules/entities/lib/entityAcl.js.map +3 -3
- package/dist/modules/entities/lib/install-from-ce.js +2 -0
- package/dist/modules/entities/lib/install-from-ce.js.map +2 -2
- package/dist/modules/entities/lib/recordFeatures.js +24 -0
- package/dist/modules/entities/lib/recordFeatures.js.map +7 -0
- package/dist/modules/entities/lib/register.js +3 -0
- package/dist/modules/entities/lib/register.js.map +2 -2
- package/dist/modules/entities/lib/restrictedEntityFeatures.js +62 -0
- package/dist/modules/entities/lib/restrictedEntityFeatures.js.map +7 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js +13 -0
- package/dist/modules/entities/migrations/Migration20260716120000.js.map +7 -0
- package/generated/entities/custom_entity/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +8 -7
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +2 -2
- package/src/modules/attachments/lib/imageSafety.ts +2 -2
- package/src/modules/auth/api/features.ts +17 -0
- package/src/modules/entities/api/entities.ts +28 -0
- package/src/modules/entities/api/entity-settings.ts +103 -0
- package/src/modules/entities/api/records.ts +70 -29
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +12 -3
- package/src/modules/entities/backend/entities/user/create/page.tsx +33 -1
- package/src/modules/entities/data/entities.ts +6 -0
- package/src/modules/entities/i18n/de.json +3 -0
- package/src/modules/entities/i18n/en.json +3 -0
- package/src/modules/entities/i18n/es.json +3 -0
- package/src/modules/entities/i18n/pl.json +3 -0
- package/src/modules/entities/lib/entityAcl.ts +27 -5
- package/src/modules/entities/lib/install-from-ce.ts +2 -0
- package/src/modules/entities/lib/recordFeatures.ts +48 -0
- package/src/modules/entities/lib/register.ts +4 -0
- package/src/modules/entities/lib/restrictedEntityFeatures.ts +98 -0
- package/src/modules/entities/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/entities/migrations/Migration20260716120000.ts +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/attachments/api/image/%5Bid%5D/%5B%5B...slug%5D%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentOrganizationId } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions:
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,OAAO,
|
|
4
|
+
"sourcesContent": ["import { NextRequest, NextResponse } from 'next/server'\nimport sharp, { type ResizeOptions } from 'sharp'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Attachment, AttachmentPartition } from '@open-mercato/core/modules/attachments/data/entities'\nimport {\n buildThumbnailCacheKey,\n readThumbnailCache,\n writeThumbnailCache,\n} from '@open-mercato/core/modules/attachments/lib/thumbnailCache'\nimport { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'\nimport { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'\nimport {\n MAX_IMAGE_SOURCE_PIXELS,\n validateImageDimensions,\n validateImageMagicBytes,\n} from '@open-mercato/core/modules/attachments/lib/imageSafety'\nimport { StorageDriverFactory } from '../../../../lib/drivers'\nimport { resolveAttachmentOrganizationId } from '@open-mercato/core/modules/attachments/lib/requestScope'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('attachments').child({ component: 'image' })\n\nconst querySchema = z.object({\n width: z.coerce.number().int().min(1).max(4000).optional(),\n height: z.coerce.number().int().min(1).max(4000).optional(),\n cropType: z.enum(['cover', 'contain']).optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: false },\n}\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string; slug?: string[] | undefined }> }\n) {\n const auth = await getAuthFromRequest(req)\n const { id } = await context.params\n if (!id) {\n return NextResponse.json({ error: 'Attachment id is required' }, { status: 400 })\n }\n const parsedQuery = querySchema.safeParse(\n Object.fromEntries(new URL(req.url).searchParams.entries())\n )\n if (!parsedQuery.success) {\n return NextResponse.json({ error: 'Invalid size parameters' }, { status: 400 })\n }\n const { width, height, cropType } = parsedQuery.data\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const storageDriverFactory =\n (container.resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const scopedAuth = auth\n ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) }\n : auth\n const findFilter: Record<string, unknown> = { id }\n if (scopedAuth && !isSuperAdminAuth(scopedAuth)) {\n if (scopedAuth.tenantId) findFilter.tenantId = scopedAuth.tenantId\n if (scopedAuth.orgId) findFilter.organizationId = scopedAuth.orgId\n }\n const attachment = await em.findOne(Attachment, findFilter)\n if (!attachment) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n if (!canRenderInlineAttachment(attachment.mimeType)) {\n return NextResponse.json({ error: 'Unsupported media type' }, { status: 400 })\n }\n const partition = await em.findOne(AttachmentPartition, { code: attachment.partitionCode })\n if (!partition) {\n return NextResponse.json({ error: 'Partition misconfigured' }, { status: 500 })\n }\n const access = checkAttachmentAccess(scopedAuth, attachment, partition)\n if (!access.ok) {\n const message = access.status === 401 ? 'Unauthorized' : 'Forbidden'\n return NextResponse.json({ error: message }, { status: access.status })\n }\n\n const driver = await storageDriverFactory.resolveForPartition(attachment.partitionCode, {\n tenantId: attachment.tenantId ?? '',\n organizationId: attachment.organizationId ?? '',\n })\n const cacheKey = buildThumbnailCacheKey(width, height, cropType)\n try {\n let buffer: Buffer | null = null\n if (cacheKey) {\n buffer = await readThumbnailCache(attachment.partitionCode, attachment.id, cacheKey)\n }\n if (!buffer) {\n const { buffer: input } = await driver.read(attachment.partitionCode, attachment.storagePath)\n const magicBytesValidation = validateImageMagicBytes(input, attachment.mimeType)\n if (!magicBytesValidation.ok) {\n return NextResponse.json({ error: magicBytesValidation.error }, { status: magicBytesValidation.status })\n }\n\n const dimensionsValidation = await validateImageDimensions(input)\n if (!dimensionsValidation.ok) {\n return NextResponse.json({ error: dimensionsValidation.error }, { status: dimensionsValidation.status })\n }\n\n let transformer = sharp(input, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n })\n if (width || height) {\n const resizeOptions: ResizeOptions = {\n width: width || undefined,\n height: height || undefined,\n fit: cropType === 'contain' ? 'contain' : 'cover',\n }\n if (cropType === 'contain') {\n resizeOptions.background = { r: 0, g: 0, b: 0, alpha: 0 }\n }\n transformer = transformer.resize(resizeOptions)\n }\n buffer = await transformer.toBuffer()\n if (cacheKey) {\n void writeThumbnailCache(attachment.partitionCode, attachment.id, cacheKey, buffer).catch((cacheError) => {\n logger.error('Thumbnail cache write failed', { err: cacheError })\n })\n }\n }\n if (!buffer) {\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n const responseBody = new Uint8Array(buffer)\n\n return new NextResponse(responseBody, {\n headers: {\n 'Content-Type': attachment.mimeType || 'image/jpeg',\n 'Cache-Control': partition.isPublic ? 'public, max-age=3600' : 'private, max-age=60',\n 'X-Content-Type-Options': 'nosniff',\n },\n })\n } catch (error) {\n logger.error('Image read failed', { err: error })\n return NextResponse.json({ error: 'Failed to render image' }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: 'Serve resized images',\n methods: {\n GET: {\n summary: 'Serve image with optional resizing',\n description: 'Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).',\n responses: [\n {\n status: 200,\n description: 'Binary image content (Content-Type: image/jpeg, image/png, etc.)',\n schema: z.any().describe('Binary image content - actual Content-Type header set to image MIME type, not application/json'),\n },\n ],\n errors: [\n { status: 400, description: 'Invalid parameters, missing ID, or non-image attachment', schema: attachmentErrorSchema },\n { status: 401, description: 'Unauthorized - authentication required for private partitions', schema: attachmentErrorSchema },\n { status: 403, description: 'Forbidden - insufficient permissions', schema: attachmentErrorSchema },\n { status: 404, description: 'Image not found', schema: attachmentErrorSchema },\n { status: 500, description: 'Partition misconfigured or image rendering failed', schema: attachmentErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/imageSafety.ts"],
|
|
4
|
-
"sourcesContent": ["import sharp from 'sharp'\n\nexport const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024\nexport const MAX_IMAGE_SOURCE_PIXELS = 40_000_000\n\nconst allowedImageMimeTypes = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n])\n\nconst mimeAliases = new Map([\n ['image/jpg', 'image/jpeg'],\n ['image/pjpeg', 'image/jpeg'],\n ['image/x-png', 'image/png'],\n])\n\nexport type ImageSafetyResult =\n | { ok: true; mimeType: string }\n | { ok: false; status: 400 | 413; error: string }\n\nfunction normalizeMimeType(mimeType: string | null | undefined): string | null {\n if (typeof mimeType !== 'string') return null\n const normalized = mimeType.split(';')[0]?.trim().toLowerCase() ?? ''\n if (!normalized) return null\n return mimeAliases.get(normalized) ?? normalized\n}\n\nexport function detectImageMimeType(buffer: Buffer): string | null {\n if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return 'image/jpeg'\n }\n\n if (\n buffer.length >= 8 &&\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47 &&\n buffer[4] === 0x0d &&\n buffer[5] === 0x0a &&\n buffer[6] === 0x1a &&\n buffer[7] === 0x0a\n ) {\n return 'image/png'\n }\n\n if (\n buffer.length >= 6 &&\n buffer[0] === 0x47 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x38 &&\n (buffer[4] === 0x37 || buffer[4] === 0x39) &&\n buffer[5] === 0x61\n ) {\n return 'image/gif'\n }\n\n if (\n buffer.length >= 12 &&\n buffer.toString('ascii', 0, 4) === 'RIFF' &&\n buffer.toString('ascii', 8, 12) === 'WEBP'\n ) {\n return 'image/webp'\n }\n\n return null\n}\n\nexport function validateImageMagicBytes(buffer: Buffer, declaredMimeType: string | null | undefined): ImageSafetyResult {\n if (buffer.length > MAX_IMAGE_SOURCE_BYTES) {\n return { ok: false, status: 413, error: 'Image exceeds source byte limit' }\n }\n\n const detectedMimeType = detectImageMimeType(buffer)\n if (!detectedMimeType || !allowedImageMimeTypes.has(detectedMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported image format' }\n }\n\n const normalizedDeclaredMimeType = normalizeMimeType(declaredMimeType)\n if (!normalizedDeclaredMimeType || !allowedImageMimeTypes.has(normalizedDeclaredMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported media type' }\n }\n\n if (normalizedDeclaredMimeType !== detectedMimeType) {\n return { ok: false, status: 400, error: 'Image MIME type does not match file content' }\n }\n\n return { ok: true, mimeType: detectedMimeType }\n}\n\nexport async function validateImageDimensions(buffer: Buffer): Promise<ImageSafetyResult> {\n let metadata:
|
|
5
|
-
"mappings": "AAAA,OAAO,
|
|
4
|
+
"sourcesContent": ["import sharp, { type Metadata } from 'sharp'\n\nexport const MAX_IMAGE_SOURCE_BYTES = 25 * 1024 * 1024\nexport const MAX_IMAGE_SOURCE_PIXELS = 40_000_000\n\nconst allowedImageMimeTypes = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n])\n\nconst mimeAliases = new Map([\n ['image/jpg', 'image/jpeg'],\n ['image/pjpeg', 'image/jpeg'],\n ['image/x-png', 'image/png'],\n])\n\nexport type ImageSafetyResult =\n | { ok: true; mimeType: string }\n | { ok: false; status: 400 | 413; error: string }\n\nfunction normalizeMimeType(mimeType: string | null | undefined): string | null {\n if (typeof mimeType !== 'string') return null\n const normalized = mimeType.split(';')[0]?.trim().toLowerCase() ?? ''\n if (!normalized) return null\n return mimeAliases.get(normalized) ?? normalized\n}\n\nexport function detectImageMimeType(buffer: Buffer): string | null {\n if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {\n return 'image/jpeg'\n }\n\n if (\n buffer.length >= 8 &&\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47 &&\n buffer[4] === 0x0d &&\n buffer[5] === 0x0a &&\n buffer[6] === 0x1a &&\n buffer[7] === 0x0a\n ) {\n return 'image/png'\n }\n\n if (\n buffer.length >= 6 &&\n buffer[0] === 0x47 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x38 &&\n (buffer[4] === 0x37 || buffer[4] === 0x39) &&\n buffer[5] === 0x61\n ) {\n return 'image/gif'\n }\n\n if (\n buffer.length >= 12 &&\n buffer.toString('ascii', 0, 4) === 'RIFF' &&\n buffer.toString('ascii', 8, 12) === 'WEBP'\n ) {\n return 'image/webp'\n }\n\n return null\n}\n\nexport function validateImageMagicBytes(buffer: Buffer, declaredMimeType: string | null | undefined): ImageSafetyResult {\n if (buffer.length > MAX_IMAGE_SOURCE_BYTES) {\n return { ok: false, status: 413, error: 'Image exceeds source byte limit' }\n }\n\n const detectedMimeType = detectImageMimeType(buffer)\n if (!detectedMimeType || !allowedImageMimeTypes.has(detectedMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported image format' }\n }\n\n const normalizedDeclaredMimeType = normalizeMimeType(declaredMimeType)\n if (!normalizedDeclaredMimeType || !allowedImageMimeTypes.has(normalizedDeclaredMimeType)) {\n return { ok: false, status: 400, error: 'Unsupported media type' }\n }\n\n if (normalizedDeclaredMimeType !== detectedMimeType) {\n return { ok: false, status: 400, error: 'Image MIME type does not match file content' }\n }\n\n return { ok: true, mimeType: detectedMimeType }\n}\n\nexport async function validateImageDimensions(buffer: Buffer): Promise<ImageSafetyResult> {\n let metadata: Metadata\n try {\n metadata = await sharp(buffer, {\n failOn: 'error',\n limitInputPixels: MAX_IMAGE_SOURCE_PIXELS,\n }).metadata()\n } catch {\n return { ok: false, status: 400, error: 'Invalid image content' }\n }\n\n const width = metadata.width ?? 0\n const height = metadata.height ?? 0\n if (width <= 0 || height <= 0) {\n return { ok: false, status: 400, error: 'Invalid image dimensions' }\n }\n\n if (width * height > MAX_IMAGE_SOURCE_PIXELS) {\n return { ok: false, status: 413, error: 'Image exceeds pixel limit' }\n }\n\n return { ok: true, mimeType: metadata.format ? `image/${metadata.format}` : 'image/jpeg' }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
|
+
import { synthesizeRestrictedEntityFeatures } from "@open-mercato/core/modules/entities/lib/restrictedEntityFeatures";
|
|
5
7
|
const metadata = {
|
|
6
8
|
GET: { requireAuth: true, requireFeatures: ["auth.acl.manage"] }
|
|
7
9
|
};
|
|
@@ -33,6 +35,18 @@ async function GET(req) {
|
|
|
33
35
|
return base;
|
|
34
36
|
})
|
|
35
37
|
);
|
|
38
|
+
try {
|
|
39
|
+
const { resolve } = await createRequestContainer();
|
|
40
|
+
const em = resolve("em");
|
|
41
|
+
const synthesized = await synthesizeRestrictedEntityFeatures(em, auth.tenantId ?? null);
|
|
42
|
+
for (const item of synthesized) {
|
|
43
|
+
const deps = normalizeDependsOn(item.dependsOn);
|
|
44
|
+
const base = { id: item.id, title: item.title, module: item.module };
|
|
45
|
+
if (deps) base.dependsOn = deps;
|
|
46
|
+
items.push(base);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
36
50
|
const byId = /* @__PURE__ */ new Map();
|
|
37
51
|
for (const it of items) if (!byId.has(it.id)) byId.set(it.id, it);
|
|
38
52
|
const list = Array.from(byId.values()).sort((a, b) => a.module.localeCompare(b.module) || a.id.localeCompare(b.id));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/auth/api/features.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\ntype FeatureItem = {\n id: string\n title: string\n module: string\n dependsOn?: string[]\n}\n\nfunction normalizeDependsOn(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined\n const out: string[] = []\n for (const entry of value) {\n if (typeof entry !== 'string') continue\n const trimmed = entry.trim()\n if (!trimmed) continue\n out.push(trimmed)\n }\n if (out.length === 0) return undefined\n return Array.from(new Set(out))\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const modules = getModules()\n const items: FeatureItem[] = (modules || []).flatMap((m: any) =>\n (m.features || []).map((f: any) => {\n const deps = normalizeDependsOn(f?.dependsOn)\n const base: FeatureItem = {\n id: String(f.id),\n title: String(f.title || f.id),\n module: String(f.module || m.id),\n }\n if (deps) base.dependsOn = deps\n return base\n })\n )\n // Deduplicate by id (keep first occurrence)\n const byId = new Map<string, FeatureItem>()\n for (const it of items) if (!byId.has(it.id)) byId.set(it.id, it)\n const list = Array.from(byId.values()).sort((a, b) => a.module.localeCompare(b.module) || a.id.localeCompare(b.id))\n\n // Build module info map\n const moduleInfo = new Map<string, { id: string; title: string }>()\n for (const m of modules) {\n if (m.id) {\n moduleInfo.set(m.id, { id: m.id, title: (m.info as any)?.title || m.id })\n }\n }\n\n return NextResponse.json({ items: list, modules: Array.from(moduleInfo.values()) })\n}\n\nconst featureItemSchema = z.object({\n id: z.string(),\n title: z.string(),\n module: z.string(),\n dependsOn: z.array(z.string()).optional(),\n})\n\nconst featureModuleSchema = z.object({\n id: z.string(),\n title: z.string(),\n})\n\nconst featuresResponseSchema = z.object({\n items: z.array(featureItemSchema),\n modules: z.array(featureModuleSchema),\n})\n\nconst featuresMethodDoc: OpenApiMethodDoc = {\n summary: 'List declared feature flags',\n description: 'Returns all static features contributed by the enabled modules along with their module source.',\n tags: ['Authentication & Accounts'],\n responses: [\n {\n status: 200,\n description: 'Aggregated feature catalog',\n schema: featuresResponseSchema,\n },\n ],\n errors: [\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List declared feature flags',\n methods: {\n GET: featuresMethodDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { getModules } from '@open-mercato/shared/lib/i18n/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { synthesizeRestrictedEntityFeatures } from '@open-mercato/core/modules/entities/lib/restrictedEntityFeatures'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['auth.acl.manage'] },\n}\n\ntype FeatureItem = {\n id: string\n title: string\n module: string\n dependsOn?: string[]\n}\n\nfunction normalizeDependsOn(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined\n const out: string[] = []\n for (const entry of value) {\n if (typeof entry !== 'string') continue\n const trimmed = entry.trim()\n if (!trimmed) continue\n out.push(trimmed)\n }\n if (out.length === 0) return undefined\n return Array.from(new Set(out))\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const modules = getModules()\n const items: FeatureItem[] = (modules || []).flatMap((m: any) =>\n (m.features || []).map((f: any) => {\n const deps = normalizeDependsOn(f?.dependsOn)\n const base: FeatureItem = {\n id: String(f.id),\n title: String(f.title || f.id),\n module: String(f.module || m.id),\n }\n if (deps) base.dependsOn = deps\n return base\n })\n )\n // Append synthesized per-entity features for the tenant's restricted custom\n // entities so they can be granted in the ACL editor. Tenant-scoped; never\n // throws (falls back to the static catalog on any failure).\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const synthesized = await synthesizeRestrictedEntityFeatures(em, auth.tenantId ?? null)\n for (const item of synthesized) {\n const deps = normalizeDependsOn(item.dependsOn)\n const base: FeatureItem = { id: item.id, title: item.title, module: item.module }\n if (deps) base.dependsOn = deps\n items.push(base)\n }\n } catch {}\n\n // Deduplicate by id (keep first occurrence)\n const byId = new Map<string, FeatureItem>()\n for (const it of items) if (!byId.has(it.id)) byId.set(it.id, it)\n const list = Array.from(byId.values()).sort((a, b) => a.module.localeCompare(b.module) || a.id.localeCompare(b.id))\n\n // Build module info map\n const moduleInfo = new Map<string, { id: string; title: string }>()\n for (const m of modules) {\n if (m.id) {\n moduleInfo.set(m.id, { id: m.id, title: (m.info as any)?.title || m.id })\n }\n }\n\n return NextResponse.json({ items: list, modules: Array.from(moduleInfo.values()) })\n}\n\nconst featureItemSchema = z.object({\n id: z.string(),\n title: z.string(),\n module: z.string(),\n dependsOn: z.array(z.string()).optional(),\n})\n\nconst featureModuleSchema = z.object({\n id: z.string(),\n title: z.string(),\n})\n\nconst featuresResponseSchema = z.object({\n items: z.array(featureItemSchema),\n modules: z.array(featureModuleSchema),\n})\n\nconst featuresMethodDoc: OpenApiMethodDoc = {\n summary: 'List declared feature flags',\n description: 'Returns all static features contributed by the enabled modules along with their module source.',\n tags: ['Authentication & Accounts'],\n responses: [\n {\n status: 200,\n description: 'Aggregated feature catalog',\n schema: featuresResponseSchema,\n },\n ],\n errors: [\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'List declared feature flags',\n methods: {\n GET: featuresMethodDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -55,6 +55,7 @@ async function GET(req) {
|
|
|
55
55
|
labelField: c.labelField ?? void 0,
|
|
56
56
|
defaultEditor: c.defaultEditor ?? void 0,
|
|
57
57
|
showInSidebar: c.showInSidebar ?? false,
|
|
58
|
+
accessRestricted: c.accessRestricted ?? false,
|
|
58
59
|
updatedAt: c.updatedAt instanceof Date ? c.updatedAt.toISOString() : c.updatedAt ?? void 0
|
|
59
60
|
}));
|
|
60
61
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -97,6 +98,7 @@ async function POST(req) {
|
|
|
97
98
|
return NextResponse.json({ error: "Validation failed", details: parsed.error.flatten() }, { status: 400 });
|
|
98
99
|
}
|
|
99
100
|
const input = parsed.data;
|
|
101
|
+
const accessRestrictedExplicit = body != null && typeof body === "object" && Object.prototype.hasOwnProperty.call(body, "accessRestricted");
|
|
100
102
|
const container = await createRequestContainer();
|
|
101
103
|
const { resolve } = container;
|
|
102
104
|
const em = resolve("em");
|
|
@@ -133,6 +135,7 @@ async function POST(req) {
|
|
|
133
135
|
mutationPayload: input
|
|
134
136
|
});
|
|
135
137
|
if (guard.blockedResponse) return guard.blockedResponse;
|
|
138
|
+
const isCreate = !ent;
|
|
136
139
|
if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: /* @__PURE__ */ new Date() });
|
|
137
140
|
ent.label = input.label;
|
|
138
141
|
ent.description = input.description ?? null;
|
|
@@ -140,6 +143,22 @@ async function POST(req) {
|
|
|
140
143
|
ent.labelField = input.labelField ?? ent.labelField ?? null;
|
|
141
144
|
ent.defaultEditor = input.defaultEditor ?? ent.defaultEditor ?? null;
|
|
142
145
|
ent.showInSidebar = input.showInSidebar ?? ent.showInSidebar ?? false;
|
|
146
|
+
if (accessRestrictedExplicit) {
|
|
147
|
+
ent.accessRestricted = input.accessRestricted === true;
|
|
148
|
+
} else if (isCreate) {
|
|
149
|
+
let policyDefault = false;
|
|
150
|
+
try {
|
|
151
|
+
const moduleConfigService = resolve("moduleConfigService");
|
|
152
|
+
policyDefault = await moduleConfigService.getValue("entities", "newEntitiesRestrictedByDefault", {
|
|
153
|
+
defaultValue: false,
|
|
154
|
+
scope: { tenantId: auth.tenantId ?? null }
|
|
155
|
+
}) === true;
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
ent.accessRestricted = policyDefault;
|
|
159
|
+
} else {
|
|
160
|
+
ent.accessRestricted = ent.accessRestricted ?? false;
|
|
161
|
+
}
|
|
143
162
|
ent.updatedAt = /* @__PURE__ */ new Date();
|
|
144
163
|
em.persist(ent);
|
|
145
164
|
await em.flush();
|
|
@@ -203,6 +222,7 @@ const entitySummarySchema = z.object({
|
|
|
203
222
|
labelField: z.string().optional(),
|
|
204
223
|
defaultEditor: z.string().optional(),
|
|
205
224
|
showInSidebar: z.boolean().optional(),
|
|
225
|
+
accessRestricted: z.boolean().optional(),
|
|
206
226
|
updatedAt: z.string().optional(),
|
|
207
227
|
count: z.number()
|
|
208
228
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/entities/api/entities.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomEntity, CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n beginEntitiesMutationGuard,\n ENTITY_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\n\nconst CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n\n // Generated entities from code\n const AllEntities = getEntityIds()\n const generated: { entityId: string; source: 'code'; label: string }[] = []\n for (const modId of Object.keys(AllEntities)) {\n const entities = (AllEntities as any)[modId] as Record<string, string>\n for (const k of Object.keys(entities)) {\n const id = entities[k]\n if (!isSystemEntitySelectable(id)) continue\n generated.push({ entityId: id, source: 'code', label: id })\n }\n }\n\n // Custom user-defined entities (global/org/tenant scoped)\n const where: any = { isActive: true }\n where.$and = [\n { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] },\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n const customs = await em.find(CustomEntity as any, where as any, { orderBy: { entityId: 'asc' } as any })\n // Resolve overlay precedence: prefer organization/tenant-specific over global\n const customByEntityId = new Map<string, any>()\n for (const c of customs as any[]) {\n const specificity = (c.organizationId ? 2 : 0) + (c.tenantId ? 1 : 0)\n const prev = customByEntityId.get(c.entityId)\n const prevSpec = prev ? ((prev.organizationId ? 2 : 0) + (prev.tenantId ? 1 : 0)) : -1\n if (!prev || specificity > prevSpec) customByEntityId.set(c.entityId, c)\n }\n\n const custom = Array.from(customByEntityId.values())\n .filter((c) => isSystemEntitySelectable(c.entityId))\n .map((c) => ({\n entityId: c.entityId,\n source: 'custom' as const,\n label: c.label,\n description: c.description ?? undefined,\n labelField: (c as any).labelField ?? undefined,\n defaultEditor: (c as any).defaultEditor ?? undefined,\n showInSidebar: (c as any).showInSidebar ?? false,\n updatedAt: c.updatedAt instanceof Date ? c.updatedAt.toISOString() : (c.updatedAt ?? undefined),\n }))\n\n const byId = new Map<string, any>()\n for (const g of generated) byId.set(g.entityId, g)\n for (const cu of custom) {\n const existing = byId.get(cu.entityId)\n byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source })\n }\n\n // Count field definitions scoped to current tenant/org (same scoping as custom entities)\n const defsWhere: any = { isActive: true }\n defsWhere.$and = [\n //{ $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { tenantId: auth.tenantId ?? undefined as any },\n ]\n const defs = await em.find(CustomFieldDef as any, defsWhere as any)\n // Count distinct field names (keys) per entityId\n const keySets = new Map<string, Set<string>>()\n for (const d of defs as any[]) {\n const eid = String(d.entityId)\n const k = String(d.key)\n if (!isSystemEntitySelectable(eid)) continue\n const set = keySets.get(eid) || new Set<string>()\n set.add(k)\n keySets.set(eid, set)\n }\n const counts: Record<string, number> = {}\n for (const [eid, set] of keySets.entries()) counts[eid] = set.size\n\n const items = Array.from(byId.values()).map((it: any) => ({ ...it, count: counts[it.entityId] || 0 }))\n return NextResponse.json({ items })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = upsertCustomEntitySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const input = parsed.data\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n // A registration for a module-declared, table-backed system entity would flip\n // query-engine classification to doc storage for the whole entity type (#2939's\n // failure mode via another door) \u2014 refuse to create one.\n if (isOrmBackedSystemEntityId(em, input.entityId)) {\n return NextResponse.json(\n { error: 'System entities cannot be registered as custom entities', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId: input.entityId },\n { status: 400 },\n )\n }\n\n const where: any = { entityId: input.entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let ent = await em.findOne(CustomEntity, where)\n if (ent) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n current: ent.updatedAt ?? null,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n }\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent ? ent.id : input.entityId,\n operation: ent ? 'update' : 'create',\n mutationPayload: input as unknown as Record<string, unknown>,\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })\n ent.label = input.label\n ent.description = input.description ?? null\n ent.isActive = input.isActive ?? true\n ent.labelField = input.labelField ?? ent.labelField ?? null\n ent.defaultEditor = input.defaultEditor ?? ent.defaultEditor ?? null\n ent.showInSidebar = input.showInSidebar ?? ent.showInSidebar ?? false\n ent.updatedAt = new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true, item: { id: ent.id, entityId: ent.entityId, label: ent.label, description: ent.description ?? undefined } })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const entityId = body?.entityId\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n const ent = await em.findOne(CustomEntity, where)\n if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n operation: 'delete',\n mutationPayload: { entityId },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n ent.isActive = false\n ent.updatedAt = new Date()\n ent.deletedAt = ent.deletedAt ?? new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true })\n}\n\nconst entitySummarySchema = z.object({\n entityId: z.string(),\n source: z.enum(['code', 'custom']),\n label: z.string(),\n description: z.string().optional(),\n labelField: z.string().optional(),\n defaultEditor: z.string().optional(),\n showInSidebar: z.boolean().optional(),\n updatedAt: z.string().optional(),\n count: z.number(),\n})\n\nconst entityListResponseSchema = z.object({\n items: z.array(entitySummarySchema),\n})\n\nconst deleteEntityRequestSchema = z.object({\n entityId: z.string(),\n})\n\nconst upsertCustomEntityResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string().uuid(),\n entityId: z.string(),\n label: z.string(),\n description: z.string().optional(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage custom entities',\n methods: {\n GET: {\n summary: 'List available entities',\n description: 'Returns generated and custom entities scoped to the caller with field counts per entity.',\n responses: [\n {\n status: 200,\n description: 'List of entities',\n schema: entityListResponseSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Upsert custom entity',\n description: 'Creates or updates a tenant/org scoped custom entity definition.',\n requestBody: {\n contentType: 'application/json',\n schema: upsertCustomEntitySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity saved',\n schema: upsertCustomEntityResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n DELETE: {\n summary: 'Soft delete custom entity',\n description: 'Marks the specified custom entity inactive within the current scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteEntityRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity deleted',\n schema: z.object({ ok: z.boolean() }),\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Entity not found in scope',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomEntity, CustomFieldDef } from '@open-mercato/core/modules/entities/data/entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { upsertCustomEntitySchema } from '@open-mercato/core/modules/entities/data/validators'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { isSystemEntitySelectable } from '@open-mercato/shared/lib/entities/system-entities'\nimport { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n beginEntitiesMutationGuard,\n ENTITY_DEFINITION_RESOURCE_KIND,\n} from './definitions.mutation-guard'\n\nconst CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND = ENTITY_DEFINITION_RESOURCE_KIND\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || (!auth.orgId && !auth.isSuperAdmin)) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n\n // Generated entities from code\n const AllEntities = getEntityIds()\n const generated: { entityId: string; source: 'code'; label: string }[] = []\n for (const modId of Object.keys(AllEntities)) {\n const entities = (AllEntities as any)[modId] as Record<string, string>\n for (const k of Object.keys(entities)) {\n const id = entities[k]\n if (!isSystemEntitySelectable(id)) continue\n generated.push({ entityId: id, source: 'code', label: id })\n }\n }\n\n // Custom user-defined entities (global/org/tenant scoped)\n const where: any = { isActive: true }\n where.$and = [\n { $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] },\n { $or: [ { tenantId: auth.tenantId ?? undefined as any }, { tenantId: null } ] },\n ]\n const customs = await em.find(CustomEntity as any, where as any, { orderBy: { entityId: 'asc' } as any })\n // Resolve overlay precedence: prefer organization/tenant-specific over global\n const customByEntityId = new Map<string, any>()\n for (const c of customs as any[]) {\n const specificity = (c.organizationId ? 2 : 0) + (c.tenantId ? 1 : 0)\n const prev = customByEntityId.get(c.entityId)\n const prevSpec = prev ? ((prev.organizationId ? 2 : 0) + (prev.tenantId ? 1 : 0)) : -1\n if (!prev || specificity > prevSpec) customByEntityId.set(c.entityId, c)\n }\n\n const custom = Array.from(customByEntityId.values())\n .filter((c) => isSystemEntitySelectable(c.entityId))\n .map((c) => ({\n entityId: c.entityId,\n source: 'custom' as const,\n label: c.label,\n description: c.description ?? undefined,\n labelField: (c as any).labelField ?? undefined,\n defaultEditor: (c as any).defaultEditor ?? undefined,\n showInSidebar: (c as any).showInSidebar ?? false,\n accessRestricted: (c as any).accessRestricted ?? false,\n updatedAt: c.updatedAt instanceof Date ? c.updatedAt.toISOString() : (c.updatedAt ?? undefined),\n }))\n\n const byId = new Map<string, any>()\n for (const g of generated) byId.set(g.entityId, g)\n for (const cu of custom) {\n const existing = byId.get(cu.entityId)\n byId.set(cu.entityId, { ...existing, ...cu, source: existing?.source ?? cu.source })\n }\n\n // Count field definitions scoped to current tenant/org (same scoping as custom entities)\n const defsWhere: any = { isActive: true }\n defsWhere.$and = [\n //{ $or: [ { organizationId: auth.orgId ?? undefined as any }, { organizationId: null } ] }, // the entities and custom fields are defined per tenant\n { tenantId: auth.tenantId ?? undefined as any },\n ]\n const defs = await em.find(CustomFieldDef as any, defsWhere as any)\n // Count distinct field names (keys) per entityId\n const keySets = new Map<string, Set<string>>()\n for (const d of defs as any[]) {\n const eid = String(d.entityId)\n const k = String(d.key)\n if (!isSystemEntitySelectable(eid)) continue\n const set = keySets.get(eid) || new Set<string>()\n set.add(k)\n keySets.set(eid, set)\n }\n const counts: Record<string, number> = {}\n for (const [eid, set] of keySets.entries()) counts[eid] = set.size\n\n const items = Array.from(byId.values()).map((it: any) => ({ ...it, count: counts[it.entityId] || 0 }))\n return NextResponse.json({ items })\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = upsertCustomEntitySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const input = parsed.data\n // Distinguish \"caller did not send the field\" (apply tenant default-restricted\n // policy on create) from \"caller explicitly chose false\".\n const accessRestrictedExplicit = body != null && typeof body === 'object'\n && Object.prototype.hasOwnProperty.call(body, 'accessRestricted')\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n // A registration for a module-declared, table-backed system entity would flip\n // query-engine classification to doc storage for the whole entity type (#2939's\n // failure mode via another door) \u2014 refuse to create one.\n if (isOrmBackedSystemEntityId(em, input.entityId)) {\n return NextResponse.json(\n { error: 'System entities cannot be registered as custom entities', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId: input.entityId },\n { status: 400 },\n )\n }\n\n const where: any = { entityId: input.entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let ent = await em.findOne(CustomEntity, where)\n if (ent) {\n try {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: CUSTOM_ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n current: ent.updatedAt ?? null,\n request: req,\n })\n } catch (lockError) {\n if (isCrudHttpError(lockError)) {\n return NextResponse.json(lockError.body, { status: lockError.status })\n }\n throw lockError\n }\n }\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent ? ent.id : input.entityId,\n operation: ent ? 'update' : 'create',\n mutationPayload: input as unknown as Record<string, unknown>,\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n const isCreate = !ent\n if (!ent) ent = em.create(CustomEntity, { ...where, createdAt: new Date() })\n ent.label = input.label\n ent.description = input.description ?? null\n ent.isActive = input.isActive ?? true\n ent.labelField = input.labelField ?? ent.labelField ?? null\n ent.defaultEditor = input.defaultEditor ?? ent.defaultEditor ?? null\n ent.showInSidebar = input.showInSidebar ?? ent.showInSidebar ?? false\n // Never silently flip a security control on a partial upsert. `access_restricted`\n // changes ONLY when the caller explicitly sends the field. On create without it,\n // fall back to the tenant default-restricted policy; on update without it, keep\n // the entity's current value (a metadata-only update must not un-restrict).\n if (accessRestrictedExplicit) {\n ent.accessRestricted = input.accessRestricted === true\n } else if (isCreate) {\n let policyDefault = false\n try {\n const moduleConfigService = resolve('moduleConfigService') as {\n getValue: (m: string, n: string, o?: { defaultValue?: unknown; scope?: { tenantId?: string | null } }) => Promise<unknown>\n }\n policyDefault = (await moduleConfigService.getValue('entities', 'newEntitiesRestrictedByDefault', {\n defaultValue: false,\n scope: { tenantId: auth.tenantId ?? null },\n })) === true\n } catch {}\n ent.accessRestricted = policyDefault\n } else {\n ent.accessRestricted = ent.accessRestricted ?? false\n }\n ent.updatedAt = new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true, item: { id: ent.id, entityId: ent.entityId, label: ent.label, description: ent.description ?? undefined } })\n}\n\nexport async function DELETE(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const entityId = body?.entityId\n if (!entityId) return NextResponse.json({ error: 'entityId is required' }, { status: 400 })\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n\n const where: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n const ent = await em.findOne(CustomEntity, where)\n if (!ent) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n\n const guard = await beginEntitiesMutationGuard({\n container,\n auth,\n req,\n resourceKind: ENTITY_DEFINITION_RESOURCE_KIND,\n resourceId: ent.id,\n operation: 'delete',\n mutationPayload: { entityId },\n })\n if (guard.blockedResponse) return guard.blockedResponse\n\n ent.isActive = false\n ent.updatedAt = new Date()\n ent.deletedAt = ent.deletedAt ?? new Date()\n em.persist(ent)\n await em.flush()\n await guard.runAfterSuccess()\n // Invalidate sidebar/nav cache for tenant scope (also when tenantId is null)\n try {\n const cache = (await createRequestContainer()).resolve('cache') as any\n if (cache) {\n await cache.deleteByTags([`nav:entities:${auth.tenantId || 'null'}`])\n }\n } catch {}\n return NextResponse.json({ ok: true })\n}\n\nconst entitySummarySchema = z.object({\n entityId: z.string(),\n source: z.enum(['code', 'custom']),\n label: z.string(),\n description: z.string().optional(),\n labelField: z.string().optional(),\n defaultEditor: z.string().optional(),\n showInSidebar: z.boolean().optional(),\n accessRestricted: z.boolean().optional(),\n updatedAt: z.string().optional(),\n count: z.number(),\n})\n\nconst entityListResponseSchema = z.object({\n items: z.array(entitySummarySchema),\n})\n\nconst deleteEntityRequestSchema = z.object({\n entityId: z.string(),\n})\n\nconst upsertCustomEntityResponseSchema = z.object({\n ok: z.literal(true),\n item: z.object({\n id: z.string().uuid(),\n entityId: z.string(),\n label: z.string(),\n description: z.string().optional(),\n }),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Manage custom entities',\n methods: {\n GET: {\n summary: 'List available entities',\n description: 'Returns generated and custom entities scoped to the caller with field counts per entity.',\n responses: [\n {\n status: 200,\n description: 'List of entities',\n schema: entityListResponseSchema,\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n POST: {\n summary: 'Upsert custom entity',\n description: 'Creates or updates a tenant/org scoped custom entity definition.',\n requestBody: {\n contentType: 'application/json',\n schema: upsertCustomEntitySchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity saved',\n schema: upsertCustomEntityResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n DELETE: {\n summary: 'Soft delete custom entity',\n description: 'Marks the specified custom entity inactive within the current scope.',\n requestBody: {\n contentType: 'application/json',\n schema: deleteEntityRequestSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Entity deleted',\n schema: z.object({ ok: z.boolean() }),\n },\n {\n status: 400,\n description: 'Missing entity id',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 404,\n description: 'Entity not found in scope',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
const CONFIG_MODULE = "entities";
|
|
6
|
+
const NEW_ENTITIES_RESTRICTED_KEY = "newEntitiesRestrictedByDefault";
|
|
7
|
+
const metadata = {
|
|
8
|
+
GET: { requireAuth: true, requireFeatures: ["entities.definitions.view"] },
|
|
9
|
+
PUT: { requireAuth: true, requireFeatures: ["entities.definitions.manage"] }
|
|
10
|
+
};
|
|
11
|
+
async function readPolicy(tenantId) {
|
|
12
|
+
try {
|
|
13
|
+
const { resolve } = await createRequestContainer();
|
|
14
|
+
const moduleConfigService = resolve("moduleConfigService");
|
|
15
|
+
const value = await moduleConfigService.getValue(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {
|
|
16
|
+
defaultValue: false,
|
|
17
|
+
scope: { tenantId }
|
|
18
|
+
});
|
|
19
|
+
return value === true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function GET(req) {
|
|
25
|
+
const auth = await getAuthFromRequest(req);
|
|
26
|
+
if (!auth || !auth.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
27
|
+
const newEntitiesRestrictedByDefault = await readPolicy(auth.tenantId ?? null);
|
|
28
|
+
return NextResponse.json({ newEntitiesRestrictedByDefault });
|
|
29
|
+
}
|
|
30
|
+
const putBodySchema = z.object({
|
|
31
|
+
newEntitiesRestrictedByDefault: z.boolean()
|
|
32
|
+
});
|
|
33
|
+
async function PUT(req) {
|
|
34
|
+
const auth = await getAuthFromRequest(req);
|
|
35
|
+
if (!auth || !auth.tenantId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await req.json();
|
|
39
|
+
} catch {
|
|
40
|
+
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
const parsed = putBodySchema.safeParse(body);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
return NextResponse.json({ error: "Validation failed", details: parsed.error.flatten() }, { status: 400 });
|
|
45
|
+
}
|
|
46
|
+
const { resolve } = await createRequestContainer();
|
|
47
|
+
const moduleConfigService = resolve("moduleConfigService");
|
|
48
|
+
await moduleConfigService.setValue(
|
|
49
|
+
CONFIG_MODULE,
|
|
50
|
+
NEW_ENTITIES_RESTRICTED_KEY,
|
|
51
|
+
parsed.data.newEntitiesRestrictedByDefault,
|
|
52
|
+
{ tenantId: auth.tenantId ?? null }
|
|
53
|
+
);
|
|
54
|
+
return NextResponse.json({ ok: true, newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault });
|
|
55
|
+
}
|
|
56
|
+
const settingsResponseSchema = z.object({
|
|
57
|
+
newEntitiesRestrictedByDefault: z.boolean()
|
|
58
|
+
});
|
|
59
|
+
const openApi = {
|
|
60
|
+
tag: "Entities",
|
|
61
|
+
summary: "Custom entity workspace settings",
|
|
62
|
+
methods: {
|
|
63
|
+
GET: {
|
|
64
|
+
summary: "Get custom entity settings",
|
|
65
|
+
description: "Returns the tenant-scoped default-restricted policy for new custom entities.",
|
|
66
|
+
responses: [
|
|
67
|
+
{ status: 200, description: "Current settings", schema: settingsResponseSchema },
|
|
68
|
+
{ status: 401, description: "Missing authentication", schema: z.object({ error: z.string() }) }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
PUT: {
|
|
72
|
+
summary: "Update custom entity settings",
|
|
73
|
+
description: "Sets the tenant-scoped default-restricted policy for new custom entities.",
|
|
74
|
+
requestBody: { schema: putBodySchema },
|
|
75
|
+
responses: [
|
|
76
|
+
{ status: 200, description: "Updated settings", schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean() }) },
|
|
77
|
+
{ status: 400, description: "Invalid payload", schema: z.object({ error: z.string() }) },
|
|
78
|
+
{ status: 401, description: "Missing authentication", schema: z.object({ error: z.string() }) }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
export {
|
|
84
|
+
GET,
|
|
85
|
+
PUT,
|
|
86
|
+
metadata,
|
|
87
|
+
openApi
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=entity-settings.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/entities/api/entity-settings.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\n// Tenant-scoped policy: when true, new custom entities are created with\n// `access_restricted = true` unless the create request explicitly sets the flag.\nconst CONFIG_MODULE = 'entities'\nconst NEW_ENTITIES_RESTRICTED_KEY = 'newEntitiesRestrictedByDefault'\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['entities.definitions.view'] },\n PUT: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\ntype ModuleConfigServiceLike = {\n getValue: (\n moduleId: string,\n name: string,\n options?: { defaultValue?: unknown; scope?: { tenantId?: string | null } },\n ) => Promise<unknown>\n setValue: (\n moduleId: string,\n name: string,\n value: unknown,\n scope?: { tenantId?: string | null },\n ) => Promise<unknown>\n}\n\nasync function readPolicy(tenantId: string | null): Promise<boolean> {\n try {\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigServiceLike\n const value = await moduleConfigService.getValue(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {\n defaultValue: false,\n scope: { tenantId },\n })\n return value === true\n } catch {\n return false\n }\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const newEntitiesRestrictedByDefault = await readPolicy(auth.tenantId ?? null)\n return NextResponse.json({ newEntitiesRestrictedByDefault })\n}\n\nconst putBodySchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n})\n\nexport async function PUT(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: unknown\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n const parsed = putBodySchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n }\n const { resolve } = await createRequestContainer()\n const moduleConfigService = resolve('moduleConfigService') as ModuleConfigServiceLike\n await moduleConfigService.setValue(\n CONFIG_MODULE,\n NEW_ENTITIES_RESTRICTED_KEY,\n parsed.data.newEntitiesRestrictedByDefault,\n { tenantId: auth.tenantId ?? null },\n )\n return NextResponse.json({ ok: true, newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault })\n}\n\nconst settingsResponseSchema = z.object({\n newEntitiesRestrictedByDefault: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Custom entity workspace settings',\n methods: {\n GET: {\n summary: 'Get custom entity settings',\n description: 'Returns the tenant-scoped default-restricted policy for new custom entities.',\n responses: [\n { status: 200, description: 'Current settings', schema: settingsResponseSchema },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update custom entity settings',\n description: 'Sets the tenant-scoped default-restricted policy for new custom entities.',\n requestBody: { schema: putBodySchema },\n responses: [\n { status: 200, description: 'Updated settings', schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean() }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -13,36 +13,57 @@ import { getModules } from "@open-mercato/shared/lib/i18n/server";
|
|
|
13
13
|
import { assertEntityAclForRequest } from "../lib/entityAcl.js";
|
|
14
14
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
15
|
const logger = createLogger("entities").child({ component: "records" });
|
|
16
|
-
let
|
|
17
|
-
function
|
|
18
|
-
if (
|
|
16
|
+
let declaredCustomEntityRestricted = null;
|
|
17
|
+
function loadDeclaredCustomEntities() {
|
|
18
|
+
if (declaredCustomEntityRestricted === null) {
|
|
19
19
|
try {
|
|
20
20
|
const mods = getModules();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
if (spec?.id) ids.add(spec.id);
|
|
26
|
-
}
|
|
21
|
+
const map = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const mod of mods ?? []) {
|
|
23
|
+
for (const spec of mod?.customEntities ?? []) {
|
|
24
|
+
if (spec?.id) map.set(spec.id, spec.accessRestricted === true);
|
|
27
25
|
}
|
|
28
|
-
declaredCustomEntityIds = ids;
|
|
29
26
|
}
|
|
27
|
+
declaredCustomEntityRestricted = map;
|
|
30
28
|
} catch {
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
|
-
return
|
|
31
|
+
return declaredCustomEntityRestricted ?? /* @__PURE__ */ new Map();
|
|
32
|
+
}
|
|
33
|
+
function isDeclaredCustomEntity(entityId) {
|
|
34
|
+
return loadDeclaredCustomEntities().has(entityId);
|
|
35
|
+
}
|
|
36
|
+
async function findScopedCustomEntity(em, CustomEntity, entityId, scope) {
|
|
37
|
+
const { tenantId, organizationId } = scope;
|
|
38
|
+
const candidates = [
|
|
39
|
+
{ entityId, organizationId, tenantId },
|
|
40
|
+
{ entityId, organizationId: null, tenantId },
|
|
41
|
+
{ entityId, organizationId: null, tenantId: null }
|
|
42
|
+
];
|
|
43
|
+
const seen = /* @__PURE__ */ new Set();
|
|
44
|
+
for (const where of candidates) {
|
|
45
|
+
const key = JSON.stringify(where);
|
|
46
|
+
if (seen.has(key)) continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
const row = await em.findOne(CustomEntity, where);
|
|
49
|
+
if (row) return row;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
34
52
|
}
|
|
35
53
|
const CUSTOM_ENTITY_RECORD_RESOURCE_KIND = "entities.record";
|
|
36
|
-
async function classifyRecordsEntity(em, entityId) {
|
|
37
|
-
if (isOrmBackedSystemEntityId(em, entityId)) return "system";
|
|
38
|
-
|
|
54
|
+
async function classifyRecordsEntity(em, entityId, scope) {
|
|
55
|
+
if (isOrmBackedSystemEntityId(em, entityId)) return { kind: "system", restricted: false };
|
|
56
|
+
const declared = loadDeclaredCustomEntities();
|
|
57
|
+
if (declared.has(entityId)) return { kind: "custom", restricted: declared.get(entityId) === true };
|
|
39
58
|
try {
|
|
40
59
|
const { CustomEntity } = await import("../data/entities.js");
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
60
|
+
const scoped = await findScopedCustomEntity(em, CustomEntity, entityId, scope);
|
|
61
|
+
if (scoped) return { kind: "custom", restricted: scoped.accessRestricted === true };
|
|
62
|
+
const anyRow = await em.findOne(CustomEntity, { entityId });
|
|
63
|
+
if (anyRow) return { kind: "custom", restricted: false };
|
|
43
64
|
} catch {
|
|
44
65
|
}
|
|
45
|
-
return "unknown";
|
|
66
|
+
return { kind: "unknown", restricted: false };
|
|
46
67
|
}
|
|
47
68
|
function systemEntityRecordsRejection(entityId) {
|
|
48
69
|
return NextResponse.json(
|
|
@@ -125,10 +146,10 @@ async function GET(req) {
|
|
|
125
146
|
const rbac = resolve("rbacService");
|
|
126
147
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) });
|
|
127
148
|
let organizationIds = scope.filterIds;
|
|
128
|
-
const entityKind = await classifyRecordsEntity(em, entityId);
|
|
149
|
+
const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null });
|
|
129
150
|
if (entityKind === "system") return systemEntityRecordsRejection(entityId);
|
|
130
151
|
const isCustomEntity = entityKind === "custom";
|
|
131
|
-
await assertEntityAclForRequest({ auth, entityId, action: "view", isCustomEntity, rbac });
|
|
152
|
+
await assertEntityAclForRequest({ auth, entityId, action: "view", isCustomEntity, isRestricted, rbac });
|
|
132
153
|
if (organizationIds && organizationIds.length === 0) {
|
|
133
154
|
return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 });
|
|
134
155
|
}
|
|
@@ -324,10 +345,10 @@ async function POST(req) {
|
|
|
324
345
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) });
|
|
325
346
|
const targetOrgId = scope.selectedId ?? auth.orgId;
|
|
326
347
|
if (!targetOrgId) return NextResponse.json({ error: "Organization context is required" }, { status: 400 });
|
|
327
|
-
const entityKind = await classifyRecordsEntity(em, entityId);
|
|
348
|
+
const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null });
|
|
328
349
|
if (entityKind === "system") return systemEntityRecordsRejection(entityId);
|
|
329
350
|
const isCustomEntity = entityKind === "custom";
|
|
330
|
-
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, rbac });
|
|
351
|
+
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, isRestricted, rbac });
|
|
331
352
|
for (const reservedKey of ["id", "created_at", "createdAt", "updated_at", "updatedAt", "deleted_at", "deletedAt"]) {
|
|
332
353
|
delete values[reservedKey];
|
|
333
354
|
}
|
|
@@ -389,10 +410,10 @@ async function PUT(req) {
|
|
|
389
410
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) });
|
|
390
411
|
const targetOrgId = scope.selectedId ?? auth.orgId;
|
|
391
412
|
if (!targetOrgId) return NextResponse.json({ error: "Organization context is required" }, { status: 400 });
|
|
392
|
-
const entityKind = await classifyRecordsEntity(em, entityId);
|
|
413
|
+
const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null });
|
|
393
414
|
if (entityKind === "system") return systemEntityRecordsRejection(entityId);
|
|
394
415
|
const isCustomEntity = entityKind === "custom";
|
|
395
|
-
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, rbac });
|
|
416
|
+
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, isRestricted, rbac });
|
|
396
417
|
for (const reservedKey of ["id", "created_at", "createdAt", "updated_at", "updatedAt", "deleted_at", "deletedAt"]) {
|
|
397
418
|
delete values[reservedKey];
|
|
398
419
|
}
|
|
@@ -479,10 +500,10 @@ async function DELETE(req) {
|
|
|
479
500
|
const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) });
|
|
480
501
|
const targetOrgId = scope.selectedId ?? auth.orgId;
|
|
481
502
|
if (!targetOrgId) return NextResponse.json({ error: "Organization context is required" }, { status: 400 });
|
|
482
|
-
const entityKind = await classifyRecordsEntity(em, entityId);
|
|
503
|
+
const { kind: entityKind, restricted: isRestricted } = await classifyRecordsEntity(em, entityId, { tenantId: auth.tenantId ?? null, organizationId: scope.selectedId ?? auth.orgId ?? null });
|
|
483
504
|
if (entityKind === "system") return systemEntityRecordsRejection(entityId);
|
|
484
505
|
const isCustomEntity = entityKind === "custom";
|
|
485
|
-
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, rbac });
|
|
506
|
+
await assertEntityAclForRequest({ auth, entityId, action: "manage", isCustomEntity, isRestricted, rbac });
|
|
486
507
|
try {
|
|
487
508
|
const currentUpdatedAt = await readCustomEntityRecordUpdatedAt(em, {
|
|
488
509
|
entityType: entityId,
|