@open-mercato/core 0.6.6-develop.6543.1.49fadb4c31 → 0.6.6
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 +9 -23
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/cli.js +1 -2
- package/dist/modules/api_keys/cli.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +7 -9
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +17 -21
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +2 -6
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/cli.js +2 -3
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +0 -22
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +2 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -7
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +7 -11
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/cli.js +3 -4
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js +1 -9
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
- package/dist/modules/dictionaries/commands/entries.js +14 -7
- package/dist/modules/dictionaries/commands/entries.js.map +2 -2
- package/dist/modules/dictionaries/commands/entry-operations.js +16 -7
- package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +3 -9
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +41 -56
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +3 -10
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/cli.js +1 -2
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/query_index/api/status.js +8 -2
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +0 -44
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +27 -2
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +2 -6
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +0 -1
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +0 -9
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +0 -44
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +38 -59
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/lib/shipments/snapshots.js +1 -27
- package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -1
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +1 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/find-definition.js +18 -2
- package/dist/modules/workflows/lib/find-definition.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +3 -2
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +4 -8
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +3 -5
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +2 -1
- package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +5 -13
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +11 -9
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260715120000.js +68 -0
- package/dist/modules/workflows/migrations/Migration20260715120000.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js +24 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js.map +7 -0
- package/dist/modules/workflows/workflows.js +2 -0
- package/dist/modules/workflows/workflows.js.map +2 -2
- package/package.json +8 -9
- package/src/modules/api_keys/api/keys/route.ts +9 -27
- package/src/modules/api_keys/cli.ts +3 -2
- package/src/modules/attachments/api/file/[id]/route.ts +7 -11
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -11
- package/src/modules/attachments/api/route.ts +21 -26
- package/src/modules/auth/api/users/route.ts +2 -11
- package/src/modules/auth/cli.ts +2 -3
- package/src/modules/configs/i18n/de.json +1 -5
- package/src/modules/configs/i18n/en.json +1 -5
- package/src/modules/configs/i18n/es.json +1 -5
- package/src/modules/configs/i18n/pl.json +1 -5
- package/src/modules/configs/lib/upgrade-actions.ts +0 -24
- package/src/modules/customer_accounts/AGENTS.md +6 -8
- package/src/modules/customer_accounts/widgets/injection-table.ts +2 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -11
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +7 -13
- package/src/modules/dashboards/api/roles/widgets/route.ts +3 -22
- package/src/modules/dashboards/api/users/widgets/route.ts +3 -22
- package/src/modules/dashboards/cli.ts +9 -4
- package/src/modules/dashboards/lib/widgetAssignmentScope.ts +0 -18
- package/src/modules/dictionaries/commands/entries.ts +16 -7
- package/src/modules/dictionaries/commands/entry-operations.ts +18 -8
- package/src/modules/dictionaries/commands/factory.ts +2 -8
- package/src/modules/dictionaries/fields/dictionary.tsx +32 -55
- package/src/modules/dictionaries/i18n/de.json +0 -2
- package/src/modules/dictionaries/i18n/en.json +0 -2
- package/src/modules/dictionaries/i18n/es.json +0 -2
- package/src/modules/dictionaries/i18n/pl.json +0 -2
- package/src/modules/directory/utils/organizationScope.ts +2 -36
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +49 -51
- package/src/modules/entities/cli.ts +1 -2
- package/src/modules/entities/i18n/de.json +0 -34
- package/src/modules/entities/i18n/en.json +0 -34
- package/src/modules/entities/i18n/es.json +0 -34
- package/src/modules/entities/i18n/pl.json +0 -34
- package/src/modules/query_index/api/status.ts +8 -2
- package/src/modules/sales/commands/documents.ts +0 -65
- package/src/modules/sales/commands/returns.ts +32 -2
- package/src/modules/sales/components/documents/AddressesSection.tsx +2 -6
- package/src/modules/sales/components/documents/ItemsSection.tsx +0 -5
- package/src/modules/sales/components/documents/LineItemDialog.tsx +0 -10
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +0 -46
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +40 -65
- package/src/modules/sales/i18n/de.json +0 -4
- package/src/modules/sales/i18n/en.json +0 -4
- package/src/modules/sales/i18n/es.json +0 -4
- package/src/modules/sales/i18n/pl.json +0 -4
- package/src/modules/sales/lib/shipments/snapshots.ts +0 -30
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +12 -1
- package/src/modules/workflows/lib/activity-executor.ts +22 -2
- package/src/modules/workflows/lib/find-definition.ts +53 -1
- package/src/modules/workflows/lib/signal-handler.ts +5 -4
- package/src/modules/workflows/lib/step-handler.ts +4 -8
- package/src/modules/workflows/lib/task-handler.ts +2 -4
- package/src/modules/workflows/lib/timer-handler.ts +3 -2
- package/src/modules/workflows/lib/transition-handler.ts +5 -13
- package/src/modules/workflows/lib/workflow-executor.ts +11 -8
- package/src/modules/workflows/migrations/Migration20260715120000.ts +114 -0
- package/src/modules/workflows/migrations/Migration20260716120000.ts +57 -0
- package/src/modules/workflows/workflows.ts +2 -0
- package/dist/modules/attachments/lib/reconcileOrganization.js +0 -110
- package/dist/modules/attachments/lib/reconcileOrganization.js.map +0 -7
- package/dist/modules/attachments/lib/requestScope.js +0 -10
- package/dist/modules/attachments/lib/requestScope.js.map +0 -7
- package/dist/modules/customers/components/detail/useDealsAccess.js +0 -16
- package/dist/modules/customers/components/detail/useDealsAccess.js.map +0 -7
- package/src/modules/attachments/lib/reconcileOrganization.ts +0 -188
- package/src/modules/attachments/lib/requestScope.ts +0 -27
- package/src/modules/customers/components/detail/useDealsAccess.ts +0 -19
package/.turbo/turbo-build.log
CHANGED
|
@@ -9,8 +9,6 @@ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
|
9
9
|
import { enforceTenantSelection, resolveIsSuperAdmin } from "@open-mercato/core/modules/auth/lib/tenantAccess";
|
|
10
10
|
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
|
|
11
11
|
import { assertActorCanGrantRoles } from "@open-mercato/core/modules/auth/lib/grantChecks";
|
|
12
|
-
import { isOrganizationAccessAllowed } from "@open-mercato/shared/lib/auth/organizationAccess";
|
|
13
|
-
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
14
12
|
const listQuerySchema = z.object({
|
|
15
13
|
page: z.string().optional(),
|
|
16
14
|
pageSize: z.string().optional(),
|
|
@@ -275,30 +273,18 @@ const crud = makeCrudRoute({
|
|
|
275
273
|
const { translate } = await resolveTranslations();
|
|
276
274
|
if (!auth?.tenantId) throw json({ error: translate("api_keys.errors.tenantRequired", "Tenant context required") }, { status: 400 });
|
|
277
275
|
const em = ctx.container.resolve("em");
|
|
276
|
+
const record = await em.findOne(ApiKey, { id, deletedAt: null });
|
|
277
|
+
if (!record) throw json({ error: translate("api_keys.errors.notFound", "Not found") }, { status: 404 });
|
|
278
278
|
const scopedCtx = ctx;
|
|
279
279
|
const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
id,
|
|
283
|
-
tenantId: auth.tenantId,
|
|
284
|
-
deletedAt: null
|
|
285
|
-
};
|
|
286
|
-
if (!isSuperAdmin && Array.isArray(allowedIds)) {
|
|
287
|
-
recordFilter.organizationId = { $in: allowedIds };
|
|
280
|
+
if (!isSuperAdmin && record.tenantId && record.tenantId !== auth.tenantId) {
|
|
281
|
+
throw json({ error: translate("api_keys.errors.forbidden", "Forbidden") }, { status: 403 });
|
|
288
282
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
{ tenantId: auth.tenantId, organizationId: null }
|
|
295
|
-
);
|
|
296
|
-
if (!record || record.tenantId !== auth.tenantId || !isOrganizationAccessAllowed({
|
|
297
|
-
isSuperAdmin,
|
|
298
|
-
allowedOrganizationIds: allowedIds,
|
|
299
|
-
targetOrganizationId: record.organizationId ?? null
|
|
300
|
-
})) {
|
|
301
|
-
throw json({ error: translate("api_keys.errors.notFound", "Not found") }, { status: 404 });
|
|
283
|
+
const allowedIds = ctx.organizationScope?.allowedIds ?? null;
|
|
284
|
+
if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {
|
|
285
|
+
if (!allowedIds.includes(record.organizationId)) {
|
|
286
|
+
throw json({ error: translate("api_keys.errors.organizationOutOfScope", "Organization out of scope") }, { status: 403 });
|
|
287
|
+
}
|
|
302
288
|
}
|
|
303
289
|
scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null;
|
|
304
290
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/api_keys/api/keys/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '../../data/entities'\nimport { createApiKeySchema } from '../../data/validators'\nimport { generateApiKeySecret, hashApiKey } from '../../services/apiKeyService'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\nimport { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\ntype ApiKeyCrudCtx = CrudCtx & {\n __apiKeySecret?: { secret: string; prefix: string }\n __apiKeyHash?: string\n __apiKeyRoleIds?: string[]\n __apiKeyRoles?: Role[]\n __apiKeyOrganizationId?: string | null\n __apiKeyTenantId?: string | null\n}\n\ntype ApiKeyEntityWithMeta = ApiKey & {\n __apiKeySecret?: string\n __apiKeyRoles?: Role[]\n}\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n create: {\n schema: createApiKeySchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const keyHash = scopedCtx.__apiKeyHash\n if (!secretData || !keyHash) throw new Error('API key secret not prepared')\n const roleIds = Array.isArray(scopedCtx.__apiKeyRoleIds) ? scopedCtx.__apiKeyRoleIds : []\n const organizationId = scopedCtx.__apiKeyOrganizationId ?? null\n const tenantId = scopedCtx.__apiKeyTenantId ?? null\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId,\n organizationId,\n keyHash,\n keyPrefix: secretData.prefix,\n rolesJson: roleIds,\n createdBy: ctx.auth?.sub ?? null,\n expiresAt: input.expiresAt ?? null,\n }\n },\n response: (entity) => {\n const meta = entity as ApiKeyEntityWithMeta\n const secret = meta.__apiKeySecret\n const roles = meta.__apiKeyRoles\n return {\n id: String(entity.id),\n name: entity.name,\n keyPrefix: entity.keyPrefix,\n secret,\n tenantId: entity.tenantId ?? null,\n organizationId: entity.organizationId ?? null,\n roles: Array.isArray(roles)\n ? roles.map((role) => ({ id: String(role.id), name: role.name ?? null }))\n : (Array.isArray(entity.rolesJson) ? entity.rolesJson.map((id: string) => ({ id, name: null })) : []),\n }\n },\n },\n del: { idFrom: 'query' },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\n }\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { keyPrefix: { $ilike: pattern } },\n ],\n })\n }\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n if (!items.length) {\n throw json({ items: [], total, page, pageSize, totalPages: Math.ceil(total / pageSize) })\n }\n\n const roleIdSet = new Set<string>()\n const orgIdSet = new Set<string>()\n for (const item of items) {\n if (Array.isArray(item.rolesJson)) {\n for (const roleId of item.rolesJson) roleIdSet.add(String(roleId))\n }\n if (item.organizationId) orgIdSet.add(String(item.organizationId))\n }\n\n const roleIdArray = Array.from(roleIdSet)\n const organizationIdArray = Array.from(orgIdSet)\n const roleFilter: FilterQuery<Role> = { id: { $in: roleIdArray } }\n const organizationFilter: FilterQuery<Organization> = { id: { $in: organizationIdArray } }\n const [roles, organizations] = await Promise.all([\n roleIdArray.length ? em.find(Role, roleFilter) : [],\n organizationIdArray.length ? em.find(Organization, organizationFilter) : [],\n ])\n const roleMap = new Map(roles.map((role) => [String(role.id), role.name ?? null]))\n const orgMap = new Map(organizations.map((org) => [String(org.id), org.name ?? null]))\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n keyPrefix: item.keyPrefix,\n organizationId: item.organizationId ?? null,\n organizationName: item.organizationId ? orgMap.get(String(item.organizationId)) ?? null : null,\n createdAt: item.createdAt,\n lastUsedAt: item.lastUsedAt ?? null,\n expiresAt: item.expiresAt ?? null,\n roles: Array.isArray(item.rolesJson)\n ? item.rolesJson.map((id) => ({ id, name: roleMap.get(String(id)) ?? null }))\n : [],\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n beforeCreate: async (input, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId') ? input.tenantId : auth.tenantId\n const scopedCtx = ctx as ApiKeyCrudCtx\n const targetTenantId = await enforceTenantSelection(scopedCtx, requestedTenant)\n scopedCtx.__apiKeyTenantId = targetTenantId\n\n const secretData = generateApiKeySecret()\n scopedCtx.__apiKeySecret = secretData\n scopedCtx.__apiKeyHash = await hashApiKey(secretData.secret)\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const roleTokens = Array.isArray(input.roles) ? input.roles.filter((value) => typeof value === 'string' && value.trim().length > 0) : []\n const roleEntities: Role[] = []\n const roleIds: string[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const rawTenantId = targetTenantId ?? auth.tenantId ?? null\n const effectiveTenantId = typeof rawTenantId === 'string' && rawTenantId.trim().length > 0 ? rawTenantId.trim() : null\n const normalizedEffectiveTenantId = effectiveTenantId ? effectiveTenantId.toLowerCase() : null\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const nameFilter: FilterQuery<Role> = { name: value, deletedAt: null }\n if (normalizedEffectiveTenantId) {\n nameFilter.$or = [\n { tenantId: effectiveTenantId },\n { tenantId: null },\n ]\n } else {\n nameFilter.tenantId = null\n }\n const candidates = await em.find(Role, nameFilter, { limit: 5 })\n if (normalizedEffectiveTenantId) {\n role =\n candidates.find((candidate) => {\n if (!candidate.tenantId) return false\n return String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n }) ??\n candidates.find((candidate) => candidate.tenantId === null) ??\n null\n } else {\n role = candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n role = candidates[0] ?? null\n }\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId) : null\n const normalizedRoleTenantId = roleTenantId ? roleTenantId.toLowerCase() : null\n if (normalizedRoleTenantId && normalizedEffectiveTenantId && normalizedRoleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n roleIds.push(String(role.id))\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n scopedCtx.__apiKeyRoles = roleEntities\n scopedCtx.__apiKeyRoleIds = roleIds\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? auth.orgId ?? null\n if (organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = organizationId ?? null\n\n return { ...input, organizationId }\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const roles = scopedCtx.__apiKeyRoles\n if (secretData) (entity as ApiKeyEntityWithMeta).__apiKeySecret = secretData.secret\n if (roles) (entity as ApiKeyEntityWithMeta).__apiKeyRoles = roles\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${entity.id}`)\n } catch {}\n },\n beforeDelete: async (id, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const em = (ctx.container.resolve('em') as EntityManager)\n const scopedCtx = ctx as ApiKeyCrudCtx\n const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx)\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const recordFilter: FilterQuery<ApiKey> = {\n id,\n tenantId: auth.tenantId,\n deletedAt: null,\n }\n if (!isSuperAdmin && Array.isArray(allowedIds)) {\n recordFilter.organizationId = { $in: allowedIds }\n }\n const record = await findOneWithDecryption(\n em,\n ApiKey,\n recordFilter,\n undefined,\n { tenantId: auth.tenantId, organizationId: null },\n )\n if (\n !record ||\n record.tenantId !== auth.tenantId ||\n !isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: record.organizationId ?? null,\n })\n ) {\n throw json({ error: translate('api_keys.errors.notFound', 'Not found') }, { status: 404 })\n }\n scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null\n },\n afterDelete: async (id, ctx) => {\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${id}`)\n } catch {}\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage API keys',\n description:\n 'Provides list, creation, and deletion capabilities for API keys scoped to the authenticated tenant and organization.',\n methods: {\n GET: {\n summary: 'List API keys',\n description:\n 'Returns paginated API keys visible to the current user, including per-key role assignments and organization context.',\n query: listQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Collection of API keys',\n schema: apiKeyCollectionResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden by organization scope', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create API key',\n description:\n 'Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.',\n requestBody: {\n contentType: 'application/json',\n schema: createApiKeySchema,\n description: 'API key definition including optional scope and role assignments.',\n },\n responses: [\n {\n status: 201,\n description: 'API key created successfully',\n schema: apiKeyCreateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing tenant context', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete API key',\n description:\n 'Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.',\n query: z.object({\n id: z.string().uuid().describe('API key identifier to delete'),\n }),\n responses: [\n { status: 200, description: 'Key deleted successfully', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing or invalid identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n { status: 404, description: 'Key not found within scope', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '../../data/entities'\nimport { createApiKeySchema } from '../../data/validators'\nimport { generateApiKeySecret, hashApiKey } from '../../services/apiKeyService'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'\n\ntype ApiKeyCrudCtx = CrudCtx & {\n __apiKeySecret?: { secret: string; prefix: string }\n __apiKeyHash?: string\n __apiKeyRoleIds?: string[]\n __apiKeyRoles?: Role[]\n __apiKeyOrganizationId?: string | null\n __apiKeyTenantId?: string | null\n}\n\ntype ApiKeyEntityWithMeta = ApiKey & {\n __apiKeySecret?: string\n __apiKeyRoles?: Role[]\n}\n\nconst listQuerySchema = z.object({\n page: z.string().optional(),\n pageSize: z.string().optional(),\n search: z.string().optional(),\n})\n\nconst apiKeyRoleSchema = z.object({\n id: z.string().describe('Role identifier or alias assigned to the key'),\n name: z.string().nullable().describe('Display name of the mapped role, if available'),\n})\n\nconst apiKeyListItemSchema = z.object({\n id: z.string().describe('API key identifier'),\n name: z.string().describe('Friendly label used to identify the key'),\n description: z.string().nullable().describe('Optional free-form description'),\n keyPrefix: z.string().describe('Public prefix exposed to clients'),\n organizationId: z.string().uuid().nullable().describe('Organization scope of the key'),\n organizationName: z.string().nullable().describe('Resolved organization display name'),\n createdAt: z.string().describe('Creation timestamp (ISO 8601)'),\n lastUsedAt: z.string().nullable().describe('Last time the key was observed in use (ISO 8601)'),\n expiresAt: z.string().nullable().describe('When the key expires (ISO 8601)'),\n roles: z.array(apiKeyRoleSchema).describe('Effective roles applied when this key authenticates'),\n})\n\nconst apiKeyCollectionResponseSchema = z.object({\n items: z.array(apiKeyListItemSchema),\n total: z.number().int().nonnegative(),\n page: z.number().int().positive(),\n pageSize: z.number().int().positive(),\n totalPages: z.number().int().nonnegative(),\n})\n\nconst apiKeyCreateResponseSchema = z.object({\n id: z.string().describe('Newly created API key identifier'),\n name: z.string(),\n keyPrefix: z.string(),\n secret: z.string().describe('Full API key value. Shown once for secure persistence.').optional(),\n tenantId: z.string().uuid().nullable(),\n organizationId: z.string().uuid().nullable(),\n roles: z.array(apiKeyRoleSchema),\n})\n\nconst deleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst errorSchema = z.object({\n error: z.string(),\n})\n\nfunction json(payload: unknown, init: ResponseInit = { status: 200 }) {\n return new Response(JSON.stringify(payload), {\n ...init,\n headers: { 'content-type': 'application/json', ...(init.headers || {}) },\n })\n}\n\nconst crud = makeCrudRoute<\n z.infer<typeof createApiKeySchema>,\n never,\n z.infer<typeof listQuerySchema>\n>({\n metadata: {\n GET: { requireAuth: true, requireFeatures: ['api_keys.view'] },\n POST: { requireAuth: true, requireFeatures: ['api_keys.create'] },\n DELETE: { requireAuth: true, requireFeatures: ['api_keys.delete'] },\n },\n orm: { entity: ApiKey, orgField: null },\n list: { schema: listQuerySchema },\n create: {\n schema: createApiKeySchema,\n mapToEntity: (input, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const keyHash = scopedCtx.__apiKeyHash\n if (!secretData || !keyHash) throw new Error('API key secret not prepared')\n const roleIds = Array.isArray(scopedCtx.__apiKeyRoleIds) ? scopedCtx.__apiKeyRoleIds : []\n const organizationId = scopedCtx.__apiKeyOrganizationId ?? null\n const tenantId = scopedCtx.__apiKeyTenantId ?? null\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId,\n organizationId,\n keyHash,\n keyPrefix: secretData.prefix,\n rolesJson: roleIds,\n createdBy: ctx.auth?.sub ?? null,\n expiresAt: input.expiresAt ?? null,\n }\n },\n response: (entity) => {\n const meta = entity as ApiKeyEntityWithMeta\n const secret = meta.__apiKeySecret\n const roles = meta.__apiKeyRoles\n return {\n id: String(entity.id),\n name: entity.name,\n keyPrefix: entity.keyPrefix,\n secret,\n tenantId: entity.tenantId ?? null,\n organizationId: entity.organizationId ?? null,\n roles: Array.isArray(roles)\n ? roles.map((role) => ({ id: String(role.id), name: role.name ?? null }))\n : (Array.isArray(entity.rolesJson) ? entity.rolesJson.map((id: string) => ({ id, name: null })) : []),\n }\n },\n },\n del: { idFrom: 'query' },\n hooks: {\n beforeList: async (query, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const page = Math.max(parseInt(query.page ?? '1', 10) || 1, 1)\n const pageSize = Math.min(Math.max(parseInt(query.pageSize ?? '20', 10) || 20, 1), 200)\n const search = (query.search ?? '').trim().toLowerCase()\n\n const organizationIds = Array.isArray(ctx.organizationIds) ? ctx.organizationIds : null\n if (organizationIds && organizationIds.length === 0) {\n throw json({ items: [], total: 0, page, pageSize, totalPages: 0 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const qb = em.createQueryBuilder(ApiKey, 'k')\n qb.where({ deletedAt: null })\n qb.andWhere({ tenantId: auth.tenantId })\n if (organizationIds && organizationIds.length > 0) {\n qb.andWhere({ organizationId: { $in: organizationIds } })\n } else if (auth.orgId) {\n qb.andWhere({ organizationId: auth.orgId })\n }\n if (search) {\n const pattern = `%${escapeLikePattern(search)}%`\n qb.andWhere({\n $or: [\n { name: { $ilike: pattern } },\n { keyPrefix: { $ilike: pattern } },\n ],\n })\n }\n qb.orderBy({ createdAt: 'desc' })\n qb.limit(pageSize).offset((page - 1) * pageSize)\n const [items, total] = await qb.getResultAndCount()\n\n if (!items.length) {\n throw json({ items: [], total, page, pageSize, totalPages: Math.ceil(total / pageSize) })\n }\n\n const roleIdSet = new Set<string>()\n const orgIdSet = new Set<string>()\n for (const item of items) {\n if (Array.isArray(item.rolesJson)) {\n for (const roleId of item.rolesJson) roleIdSet.add(String(roleId))\n }\n if (item.organizationId) orgIdSet.add(String(item.organizationId))\n }\n\n const roleIdArray = Array.from(roleIdSet)\n const organizationIdArray = Array.from(orgIdSet)\n const roleFilter: FilterQuery<Role> = { id: { $in: roleIdArray } }\n const organizationFilter: FilterQuery<Organization> = { id: { $in: organizationIdArray } }\n const [roles, organizations] = await Promise.all([\n roleIdArray.length ? em.find(Role, roleFilter) : [],\n organizationIdArray.length ? em.find(Organization, organizationFilter) : [],\n ])\n const roleMap = new Map(roles.map((role) => [String(role.id), role.name ?? null]))\n const orgMap = new Map(organizations.map((org) => [String(org.id), org.name ?? null]))\n\n const payload = {\n items: items.map((item) => ({\n id: item.id,\n name: item.name,\n description: item.description ?? null,\n keyPrefix: item.keyPrefix,\n organizationId: item.organizationId ?? null,\n organizationName: item.organizationId ? orgMap.get(String(item.organizationId)) ?? null : null,\n createdAt: item.createdAt,\n lastUsedAt: item.lastUsedAt ?? null,\n expiresAt: item.expiresAt ?? null,\n roles: Array.isArray(item.rolesJson)\n ? item.rolesJson.map((id) => ({ id, name: roleMap.get(String(id)) ?? null }))\n : [],\n })),\n total,\n page,\n pageSize,\n totalPages: Math.ceil(total / pageSize),\n }\n\n throw json(payload)\n },\n beforeCreate: async (input, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId') ? input.tenantId : auth.tenantId\n const scopedCtx = ctx as ApiKeyCrudCtx\n const targetTenantId = await enforceTenantSelection(scopedCtx, requestedTenant)\n scopedCtx.__apiKeyTenantId = targetTenantId\n\n const secretData = generateApiKeySecret()\n scopedCtx.__apiKeySecret = secretData\n scopedCtx.__apiKeyHash = await hashApiKey(secretData.secret)\n\n const em = (ctx.container.resolve('em') as EntityManager)\n const roleTokens = Array.isArray(input.roles) ? input.roles.filter((value) => typeof value === 'string' && value.trim().length > 0) : []\n const roleEntities: Role[] = []\n const roleIds: string[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const rawTenantId = targetTenantId ?? auth.tenantId ?? null\n const effectiveTenantId = typeof rawTenantId === 'string' && rawTenantId.trim().length > 0 ? rawTenantId.trim() : null\n const normalizedEffectiveTenantId = effectiveTenantId ? effectiveTenantId.toLowerCase() : null\n let role: Role | null = null\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {\n role = await em.findOne(Role, { id: value, deletedAt: null })\n }\n if (!role) {\n const nameFilter: FilterQuery<Role> = { name: value, deletedAt: null }\n if (normalizedEffectiveTenantId) {\n nameFilter.$or = [\n { tenantId: effectiveTenantId },\n { tenantId: null },\n ]\n } else {\n nameFilter.tenantId = null\n }\n const candidates = await em.find(Role, nameFilter, { limit: 5 })\n if (normalizedEffectiveTenantId) {\n role =\n candidates.find((candidate) => {\n if (!candidate.tenantId) return false\n return String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n }) ??\n candidates.find((candidate) => candidate.tenantId === null) ??\n null\n } else {\n role = candidates.find((candidate) => candidate.tenantId === null) ?? null\n }\n if (!role) {\n role = candidates[0] ?? null\n }\n }\n if (!role) {\n throw json({ error: translate('api_keys.errors.roleNotFound', `Role ${value} not found`, { identifier: value }) }, { status: 400 })\n }\n const roleTenantId = role.tenantId ? String(role.tenantId) : null\n const normalizedRoleTenantId = roleTenantId ? roleTenantId.toLowerCase() : null\n if (normalizedRoleTenantId && normalizedEffectiveTenantId && normalizedRoleTenantId !== normalizedEffectiveTenantId) {\n throw json({ error: translate('api_keys.errors.roleWrongTenant', `Role ${role.name} belongs to another tenant`, { role: role.name ?? value }) }, { status: 400 })\n }\n roleEntities.push(role)\n roleIds.push(String(role.id))\n }\n await assertActorCanGrantRoles({\n em,\n rbacService: ctx.container.resolve('rbacService') as RbacService,\n actorUserId: auth.sub,\n tenantId: targetTenantId,\n organizationId: auth.orgId ?? null,\n roles: roleEntities,\n })\n scopedCtx.__apiKeyRoles = roleEntities\n scopedCtx.__apiKeyRoleIds = roleIds\n\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? auth.orgId ?? null\n if (organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = organizationId ?? null\n\n return { ...input, organizationId }\n },\n afterCreate: async (entity, ctx) => {\n const scopedCtx = ctx as ApiKeyCrudCtx\n const secretData = scopedCtx.__apiKeySecret\n const roles = scopedCtx.__apiKeyRoles\n if (secretData) (entity as ApiKeyEntityWithMeta).__apiKeySecret = secretData.secret\n if (roles) (entity as ApiKeyEntityWithMeta).__apiKeyRoles = roles\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${entity.id}`)\n } catch {}\n },\n beforeDelete: async (id, ctx) => {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n const em = (ctx.container.resolve('em') as EntityManager)\n const record = await em.findOne(ApiKey, { id, deletedAt: null })\n if (!record) throw json({ error: translate('api_keys.errors.notFound', 'Not found') }, { status: 404 })\n const scopedCtx = ctx as ApiKeyCrudCtx\n const isSuperAdmin = await resolveIsSuperAdmin(scopedCtx)\n if (!isSuperAdmin && record.tenantId && record.tenantId !== auth.tenantId) {\n throw json({ error: translate('api_keys.errors.forbidden', 'Forbidden') }, { status: 403 })\n }\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n if (record.organizationId && Array.isArray(allowedIds) && allowedIds.length > 0) {\n if (!allowedIds.includes(record.organizationId)) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n }\n scopedCtx.__apiKeyOrganizationId = record.organizationId ?? null\n },\n afterDelete: async (id, ctx) => {\n try {\n const rbac = (ctx.container.resolve('rbacService') as RbacService)\n await rbac.invalidateUserCache(`api_key:${id}`)\n } catch {}\n },\n },\n})\n\nexport const metadata = crud.metadata\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const DELETE = crud.DELETE\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Manage API keys',\n description:\n 'Provides list, creation, and deletion capabilities for API keys scoped to the authenticated tenant and organization.',\n methods: {\n GET: {\n summary: 'List API keys',\n description:\n 'Returns paginated API keys visible to the current user, including per-key role assignments and organization context.',\n query: listQuerySchema,\n responses: [\n {\n status: 200,\n description: 'Collection of API keys',\n schema: apiKeyCollectionResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Tenant context missing', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden by organization scope', schema: errorSchema },\n ],\n },\n POST: {\n summary: 'Create API key',\n description:\n 'Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.',\n requestBody: {\n contentType: 'application/json',\n schema: createApiKeySchema,\n description: 'API key definition including optional scope and role assignments.',\n },\n responses: [\n {\n status: 201,\n description: 'API key created successfully',\n schema: apiKeyCreateResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid payload or missing tenant context', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete API key',\n description:\n 'Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.',\n query: z.object({\n id: z.string().uuid().describe('API key identifier to delete'),\n }),\n responses: [\n { status: 200, description: 'Key deleted successfully', schema: deleteResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Missing or invalid identifier', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Organization outside allowed scope', schema: errorSchema },\n { status: 404, description: 'Key not found within scope', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,6 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
2
2
|
import { createApiKey } from "./services/apiKeyService.js";
|
|
3
3
|
import { Organization } from "@open-mercato/core/modules/directory/data/entities";
|
|
4
4
|
import { Role } from "@open-mercato/core/modules/auth/data/entities";
|
|
5
|
-
import { parseCommaSeparatedList } from "@open-mercato/shared/lib/string";
|
|
6
5
|
function parseArgs(rest) {
|
|
7
6
|
const args = {};
|
|
8
7
|
for (let i = 0; i < rest.length; i += 2) {
|
|
@@ -40,7 +39,7 @@ const addApiKey = {
|
|
|
40
39
|
console.error("Tenant context is required. Provide --tenantId or select an organization belonging to a tenant.");
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
|
-
const roleInputs =
|
|
42
|
+
const roleInputs = rolesCsv ? rolesCsv.split(",").map((value) => value.trim()).filter(Boolean) : [];
|
|
44
43
|
const roleIds = [];
|
|
45
44
|
if (roleInputs.length) {
|
|
46
45
|
for (const token of roleInputs) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/api_keys/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from './services/apiKeyService'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createApiKey } from './services/apiKeyService'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { Role } from '@open-mercato/core/modules/auth/data/entities'\n\nfunction parseArgs(rest: string[]): Record<string, string> {\n const args: Record<string, string> = {}\n for (let i = 0; i < rest.length; i += 2) {\n const key = rest[i]?.replace(/^--/, '')\n const value = rest[i + 1]\n if (key) args[key] = value ?? ''\n }\n return args\n}\n\nconst addApiKey: ModuleCli = {\n command: 'add',\n async run(rest) {\n const args = parseArgs(rest)\n const name = args.name || args.label\n const organizationId = args.organizationId || args.orgId || args.org\n let tenantId: string | null = args.tenantId || args.tenant || null\n const rolesCsv = args.roles || ''\n\n if (!name) {\n console.error('Usage: mercato api_keys add --name <name> [--organizationId <uuid>] [--tenantId <uuid>] [--roles roleA,roleB]')\n return\n }\n\n const { resolve } = await createRequestContainer()\n const em = resolve('em') as any\n const rbac = resolve('rbacService') as any\n\n let orgRecord: any = null\n if (organizationId) {\n orgRecord = await em.findOne(Organization, { id: organizationId })\n if (!orgRecord) {\n console.error('Organization not found:', organizationId)\n return\n }\n tenantId = tenantId ?? (orgRecord.tenant ? String(orgRecord.tenant.id) : null)\n }\n\n if (!tenantId) {\n console.error('Tenant context is required. Provide --tenantId or select an organization belonging to a tenant.')\n return\n }\n\n const roleInputs = rolesCsv\n ? rolesCsv.split(',').map((value) => value.trim()).filter(Boolean)\n : []\n const roleIds: string[] = []\n if (roleInputs.length) {\n for (const token of roleInputs) {\n let role = await em.findOne(Role, { id: token })\n if (!role) {\n role = await em.findOne(Role, { name: token })\n }\n if (!role) {\n console.error(`Role not found: ${token}`)\n return\n }\n roleIds.push(String(role.id))\n }\n }\n\n const { record, secret } = await createApiKey(em, {\n name,\n description: args.description || null,\n organizationId: organizationId || null,\n tenantId,\n roles: roleIds,\n createdBy: null,\n expiresAt: args.expiresAt ? new Date(args.expiresAt) : null,\n }, { rbac })\n\n console.log('API key created:')\n console.log(' id:', record.id)\n console.log(' name:', record.name)\n console.log(' tenantId:', record.tenantId)\n console.log(' organizationId:', record.organizationId ?? 'null')\n if (roleIds.length) console.log(' roles:', roleInputs.join(', '))\n console.log(' keyPrefix:', record.keyPrefix)\n console.log('')\n console.log('Secret (store immediately):')\n console.log(secret)\n },\n}\n\nexport default [addApiKey]\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,YAAY;AAErB,SAAS,UAAU,MAAwC;AACzD,QAAM,OAA+B,CAAC;AACtC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;AACvC,UAAM,MAAM,KAAK,CAAC,GAAG,QAAQ,OAAO,EAAE;AACtC,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,IAAK,MAAK,GAAG,IAAI,SAAS;AAAA,EAChC;AACA,SAAO;AACT;AAEA,MAAM,YAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,MAAM,IAAI,MAAM;AACd,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,UAAM,iBAAiB,KAAK,kBAAkB,KAAK,SAAS,KAAK;AACjE,QAAI,WAA0B,KAAK,YAAY,KAAK,UAAU;AAC9D,UAAM,WAAW,KAAK,SAAS;AAE/B,QAAI,CAAC,MAAM;AACT,cAAQ,MAAM,+GAA+G;AAC7H;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,UAAM,KAAK,QAAQ,IAAI;AACvB,UAAM,OAAO,QAAQ,aAAa;AAElC,QAAI,YAAiB;AACrB,QAAI,gBAAgB;AAClB,kBAAY,MAAM,GAAG,QAAQ,cAAc,EAAE,IAAI,eAAe,CAAC;AACjE,UAAI,CAAC,WAAW;AACd,gBAAQ,MAAM,2BAA2B,cAAc;AACvD;AAAA,MACF;AACA,iBAAW,aAAa,UAAU,SAAS,OAAO,UAAU,OAAO,EAAE,IAAI;AAAA,IAC3E;AAEA,QAAI,CAAC,UAAU;AACb,cAAQ,MAAM,iGAAiG;AAC/G;AAAA,IACF;AAEA,UAAM,aAAa,WACf,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO,IAC/D,CAAC;AACL,UAAM,UAAoB,CAAC;AAC3B,QAAI,WAAW,QAAQ;AACrB,iBAAW,SAAS,YAAY;AAC9B,YAAI,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,MAAM,CAAC;AAC/C,YAAI,CAAC,MAAM;AACT,iBAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,MAAM,MAAM,CAAC;AAAA,QAC/C;AACA,YAAI,CAAC,MAAM;AACT,kBAAQ,MAAM,mBAAmB,KAAK,EAAE;AACxC;AAAA,QACF;AACA,gBAAQ,KAAK,OAAO,KAAK,EAAE,CAAC;AAAA,MAC9B;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,OAAO,IAAI,MAAM,aAAa,IAAI;AAAA,MAChD;AAAA,MACA,aAAa,KAAK,eAAe;AAAA,MACjC,gBAAgB,kBAAkB;AAAA,MAClC;AAAA,MACA,OAAO;AAAA,MACP,WAAW;AAAA,MACX,WAAW,KAAK,YAAY,IAAI,KAAK,KAAK,SAAS,IAAI;AAAA,IACzD,GAAG,EAAE,KAAK,CAAC;AAEX,YAAQ,IAAI,kBAAkB;AAC9B,YAAQ,IAAI,SAAS,OAAO,EAAE;AAC9B,YAAQ,IAAI,WAAW,OAAO,IAAI;AAClC,YAAQ,IAAI,eAAe,OAAO,QAAQ;AAC1C,YAAQ,IAAI,qBAAqB,OAAO,kBAAkB,MAAM;AAChE,QAAI,QAAQ,OAAQ,SAAQ,IAAI,YAAY,WAAW,KAAK,IAAI,CAAC;AACjE,YAAQ,IAAI,gBAAgB,OAAO,SAAS;AAC5C,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,6BAA6B;AACzC,YAAQ,IAAI,MAAM;AAAA,EACpB;AACF;AAEA,IAAO,cAAQ,CAAC,SAAS;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
canRenderInlineAttachment
|
|
14
14
|
} from "@open-mercato/core/modules/attachments/lib/security";
|
|
15
15
|
import { StorageDriverFactory } from "../../../lib/drivers/index.js";
|
|
16
|
-
import { resolveAttachmentOrganizationId } from "@open-mercato/core/modules/attachments/lib/requestScope";
|
|
17
16
|
const metadata = {
|
|
18
17
|
GET: { requireAuth: false }
|
|
19
18
|
};
|
|
@@ -26,14 +25,13 @@ async function GET(req, context) {
|
|
|
26
25
|
);
|
|
27
26
|
}
|
|
28
27
|
const auth = await getAuthFromRequest(req);
|
|
29
|
-
const
|
|
30
|
-
const em =
|
|
31
|
-
const storageDriverFactory =
|
|
32
|
-
const scopedAuth = auth ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) } : auth;
|
|
28
|
+
const { resolve } = await createRequestContainer();
|
|
29
|
+
const em = resolve("em");
|
|
30
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
33
31
|
const findFilter = { id };
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
32
|
+
if (auth && !isSuperAdminAuth(auth)) {
|
|
33
|
+
if (auth.tenantId) findFilter.tenantId = auth.tenantId;
|
|
34
|
+
if (auth.orgId) findFilter.organizationId = auth.orgId;
|
|
37
35
|
}
|
|
38
36
|
const attachment = await em.findOne(Attachment, findFilter);
|
|
39
37
|
if (!attachment) {
|
|
@@ -51,7 +49,7 @@ async function GET(req, context) {
|
|
|
51
49
|
{ status: 500 }
|
|
52
50
|
);
|
|
53
51
|
}
|
|
54
|
-
const access = checkAttachmentAccess(
|
|
52
|
+
const access = checkAttachmentAccess(auth, attachment, partition);
|
|
55
53
|
if (!access.ok) {
|
|
56
54
|
const message = access.status === 401 ? "Unauthorized" : "Forbidden";
|
|
57
55
|
return NextResponse.json({ error: message }, { status: access.status });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/attachments/api/file/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextRequest, NextResponse } from \"next/server\";\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 {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAE1C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,SAAS,uBAAuB,wBAAwB;AACxD,SAAS,SAAS;AAClB,SAAS,gBAAgB,6BAA6B;AACtD;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import { NextRequest, NextResponse } from \"next/server\";\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 {\n Attachment,\n AttachmentPartition,\n} from \"@open-mercato/core/modules/attachments/data/entities\";\nimport type { EntityManager } from \"@mikro-orm/postgresql\";\nimport { checkAttachmentAccess, isSuperAdminAuth } from \"@open-mercato/core/modules/attachments/lib/access\";\nimport { z } from \"zod\";\nimport { attachmentsTag, attachmentErrorSchema } from \"../../openapi\";\nimport {\n buildAttachmentContentDisposition,\n canRenderInlineAttachment,\n} from \"@open-mercato/core/modules/attachments/lib/security\";\nimport { StorageDriverFactory } from '../../../lib/drivers';\n\nexport const metadata = {\n GET: { requireAuth: false },\n};\n\nexport async function GET(\n req: NextRequest,\n context: { params: Promise<{ id: string }> },\n) {\n const { id } = await context.params;\n if (!id) {\n return NextResponse.json(\n { error: \"Attachment id is required\" },\n { status: 400 },\n );\n }\n const auth = await getAuthFromRequest(req);\n const { resolve } = await createRequestContainer();\n const em = resolve(\"em\") as EntityManager;\n const storageDriverFactory =\n (resolve(\"storageDriverFactory\") as StorageDriverFactory | null) ??\n new StorageDriverFactory(em);\n\n const findFilter: Record<string, unknown> = { id };\n if (auth && !isSuperAdminAuth(auth)) {\n if (auth.tenantId) findFilter.tenantId = auth.tenantId;\n if (auth.orgId) findFilter.organizationId = auth.orgId;\n }\n const attachment = await em.findOne(Attachment, findFilter);\n if (!attachment) {\n return NextResponse.json(\n { error: \"Attachment not found\" },\n { status: 404 },\n );\n }\n const partition = await em.findOne(AttachmentPartition, {\n code: attachment.partitionCode,\n });\n if (!partition) {\n return NextResponse.json(\n { error: \"Partition misconfigured\" },\n { status: 500 },\n );\n }\n\n const access = checkAttachmentAccess(auth, 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 let buffer: Buffer;\n try {\n const result = await driver.read(attachment.partitionCode, attachment.storagePath);\n buffer = result.buffer;\n } catch {\n return NextResponse.json({ error: \"File not available\" }, { status: 404 });\n }\n\n const url = new URL(req.url);\n const forceDownload = url.searchParams.get(\"download\") === \"1\";\n const renderInline = !forceDownload && canRenderInlineAttachment(attachment.mimeType);\n const headers: Record<string, string> = {\n \"Cache-Control\": partition.isPublic\n ? \"public, max-age=86400\"\n : \"private, max-age=60\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"Content-Type\": renderInline\n ? attachment.mimeType || \"application/octet-stream\"\n : \"application/octet-stream\",\n \"Content-Disposition\": buildAttachmentContentDisposition(\n attachment.fileName,\n renderInline ? \"inline\" : \"attachment\",\n ),\n \"X-Content-Type-Options\": \"nosniff\",\n };\n if (attachment.fileSize > 0) {\n headers[\"Content-Length\"] = String(attachment.fileSize);\n }\n\n const responseBody = new Uint8Array(buffer);\n\n return new NextResponse(responseBody, { status: 200, headers });\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: attachmentsTag,\n summary: \"Download attachment file\",\n methods: {\n GET: {\n summary: \"Download or serve attachment file\",\n description:\n \"Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.\",\n responses: [\n {\n status: 200,\n description: \"File content with appropriate MIME type\",\n schema: z.any().describe(\"Binary file content\"),\n },\n ],\n errors: [\n {\n status: 400,\n description: \"Missing attachment ID\",\n schema: attachmentErrorSchema,\n },\n {\n status: 401,\n description:\n \"Unauthorized - authentication required for private partitions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 403,\n description: \"Forbidden - insufficient permissions\",\n schema: attachmentErrorSchema,\n },\n {\n status: 404,\n description: \"Attachment or file not found\",\n schema: attachmentErrorSchema,\n },\n {\n status: 500,\n description: \"Partition misconfigured\",\n schema: attachmentErrorSchema,\n },\n ],\n },\n },\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
validateImageMagicBytes
|
|
19
19
|
} from "@open-mercato/core/modules/attachments/lib/imageSafety";
|
|
20
20
|
import { StorageDriverFactory } from "../../../../lib/drivers/index.js";
|
|
21
|
-
import { resolveAttachmentOrganizationId } from "@open-mercato/core/modules/attachments/lib/requestScope";
|
|
22
21
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
23
22
|
const logger = createLogger("attachments").child({ component: "image" });
|
|
24
23
|
const querySchema = z.object({
|
|
@@ -42,14 +41,13 @@ async function GET(req, context) {
|
|
|
42
41
|
return NextResponse.json({ error: "Invalid size parameters" }, { status: 400 });
|
|
43
42
|
}
|
|
44
43
|
const { width, height, cropType } = parsedQuery.data;
|
|
45
|
-
const
|
|
46
|
-
const em =
|
|
47
|
-
const storageDriverFactory =
|
|
48
|
-
const scopedAuth = auth ? { ...auth, orgId: await resolveAttachmentOrganizationId(container, auth, req) } : auth;
|
|
44
|
+
const { resolve } = await createRequestContainer();
|
|
45
|
+
const em = resolve("em");
|
|
46
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
49
47
|
const findFilter = { id };
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
48
|
+
if (auth && !isSuperAdminAuth(auth)) {
|
|
49
|
+
if (auth.tenantId) findFilter.tenantId = auth.tenantId;
|
|
50
|
+
if (auth.orgId) findFilter.organizationId = auth.orgId;
|
|
53
51
|
}
|
|
54
52
|
const attachment = await em.findOne(Attachment, findFilter);
|
|
55
53
|
if (!attachment) {
|
|
@@ -62,7 +60,7 @@ async function GET(req, context) {
|
|
|
62
60
|
if (!partition) {
|
|
63
61
|
return NextResponse.json({ error: "Partition misconfigured" }, { status: 500 });
|
|
64
62
|
}
|
|
65
|
-
const access = checkAttachmentAccess(
|
|
63
|
+
const access = checkAttachmentAccess(auth, attachment, partition);
|
|
66
64
|
if (!access.ok) {
|
|
67
65
|
const message = access.status === 401 ? "Unauthorized" : "Forbidden";
|
|
68
66
|
return NextResponse.json({ error: message }, { status: access.status });
|
|
@@ -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 {
|
|
5
|
-
"mappings": "AAAA,SAAsB,oBAAoB;AAC1C,OAAO,WAAW;AAClB,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,YAAY,2BAA2B;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uBAAuB,wBAAwB;AAExD,SAAS,gBAAkC,6BAA6B;AACxE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,
|
|
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 { 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 { resolve } = await createRequestContainer()\n const em = resolve('em') as EntityManager\n const storageDriverFactory =\n (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)\n\n const findFilter: Record<string, unknown> = { id }\n if (auth && !isSuperAdminAuth(auth)) {\n if (auth.tenantId) findFilter.tenantId = auth.tenantId\n if (auth.orgId) findFilter.organizationId = auth.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(auth, 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: sharp.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
|
}
|
|
@@ -12,7 +12,6 @@ import { StorageDriverFactory } from "../lib/drivers/index.js";
|
|
|
12
12
|
import { OcrService, shouldUseLlmOcr } from "../lib/ocrService.js";
|
|
13
13
|
import { clearAttachmentThumbnailCache } from "../lib/thumbnailCache.js";
|
|
14
14
|
import { assertAttachmentScopeInvariant } from "../lib/access.js";
|
|
15
|
-
import { resolveAttachmentOrganizationId } from "../lib/requestScope.js";
|
|
16
15
|
import {
|
|
17
16
|
mergeAttachmentMetadata,
|
|
18
17
|
normalizeAttachmentAssignments,
|
|
@@ -176,11 +175,10 @@ async function GET(req) {
|
|
|
176
175
|
return NextResponse.json({ error: "entityId and recordId are required" }, { status: 400 });
|
|
177
176
|
}
|
|
178
177
|
const { entityId, recordId, page, pageSize } = parsedQuery.data;
|
|
179
|
-
const
|
|
180
|
-
const em =
|
|
181
|
-
const orgId = await resolveAttachmentOrganizationId(container, auth, req);
|
|
178
|
+
const { resolve } = await createRequestContainer();
|
|
179
|
+
const em = resolve("em");
|
|
182
180
|
const filter = { entityId, recordId, tenantId: auth.tenantId };
|
|
183
|
-
if (orgId) filter.organizationId = orgId;
|
|
181
|
+
if (auth.orgId) filter.organizationId = auth.orgId;
|
|
184
182
|
const orderBy = { createdAt: "DESC" };
|
|
185
183
|
const usePaging = typeof page === "number" && typeof pageSize === "number";
|
|
186
184
|
const total = usePaging ? await em.count(Attachment, filter) : null;
|
|
@@ -201,7 +199,7 @@ async function GET(req) {
|
|
|
201
199
|
},
|
|
202
200
|
{
|
|
203
201
|
tenantId: auth.tenantId ?? null,
|
|
204
|
-
organizationId: orgId ?? null
|
|
202
|
+
organizationId: auth.orgId ?? null
|
|
205
203
|
}
|
|
206
204
|
);
|
|
207
205
|
return NextResponse.json({
|
|
@@ -243,6 +241,7 @@ async function POST(req) {
|
|
|
243
241
|
}, { status: 400 });
|
|
244
242
|
}
|
|
245
243
|
const tenantId = auth.tenantId;
|
|
244
|
+
const orgId = auth.orgId;
|
|
246
245
|
const contentType = req.headers.get("content-type") || "";
|
|
247
246
|
if (!contentType.toLowerCase().includes("multipart/form-data")) {
|
|
248
247
|
return NextResponse.json({ error: "Expected multipart/form-data" }, { status: 400 });
|
|
@@ -262,12 +261,10 @@ async function POST(req) {
|
|
|
262
261
|
const partitionOverride = typeof partitionOverrideRaw === "string" && partitionOverrideRaw.trim().length > 0 ? sanitizePartitionCode(partitionOverrideRaw) : null;
|
|
263
262
|
const tags = parseFormTags(form.get("tags"));
|
|
264
263
|
const assignmentsFromForm = parseFormAssignments(form.get("assignments"));
|
|
265
|
-
const
|
|
266
|
-
const em =
|
|
267
|
-
const dataEngine =
|
|
268
|
-
const storageDriverFactory =
|
|
269
|
-
const orgId = await resolveAttachmentOrganizationId(container, auth, req);
|
|
270
|
-
if (!orgId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
264
|
+
const { resolve } = await createRequestContainer();
|
|
265
|
+
const em = resolve("em");
|
|
266
|
+
const dataEngine = resolve("dataEngine");
|
|
267
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
271
268
|
await ensureDefaultPartitions(em);
|
|
272
269
|
let partitionFromField = null;
|
|
273
270
|
let fieldMaxAttachmentSizeMb = null;
|
|
@@ -387,12 +384,12 @@ async function POST(req) {
|
|
|
387
384
|
}
|
|
388
385
|
const metadata2 = mergeAttachmentMetadata(null, { assignments, tags });
|
|
389
386
|
const attachmentId = randomUUID();
|
|
390
|
-
assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: orgId });
|
|
387
|
+
assertAttachmentScopeInvariant({ tenantId: auth.tenantId, organizationId: auth.orgId });
|
|
391
388
|
const att = em.create(Attachment, {
|
|
392
389
|
id: attachmentId,
|
|
393
390
|
entityId,
|
|
394
391
|
recordId,
|
|
395
|
-
organizationId: orgId,
|
|
392
|
+
organizationId: auth.orgId,
|
|
396
393
|
tenantId: auth.tenantId,
|
|
397
394
|
fileName: safeName,
|
|
398
395
|
mimeType: fileMimeType,
|
|
@@ -485,13 +482,12 @@ async function DELETE(req) {
|
|
|
485
482
|
const url = new URL(req.url);
|
|
486
483
|
const id = url.searchParams.get("id") || "";
|
|
487
484
|
if (!id) return NextResponse.json({ error: "Attachment id is required" }, { status: 400 });
|
|
488
|
-
const
|
|
489
|
-
const em =
|
|
490
|
-
const dataEngine =
|
|
491
|
-
const storageDriverFactory =
|
|
492
|
-
const orgId = await resolveAttachmentOrganizationId(container, auth, req);
|
|
485
|
+
const { resolve } = await createRequestContainer();
|
|
486
|
+
const em = resolve("em");
|
|
487
|
+
const dataEngine = resolve("dataEngine");
|
|
488
|
+
const storageDriverFactory = resolve("storageDriverFactory") ?? new StorageDriverFactory(em);
|
|
493
489
|
const deleteFilter = { id, tenantId: auth.tenantId };
|
|
494
|
-
if (orgId) deleteFilter.organizationId = orgId;
|
|
490
|
+
if (auth.orgId) deleteFilter.organizationId = auth.orgId;
|
|
495
491
|
const record = await em.findOne(Attachment, deleteFilter);
|
|
496
492
|
if (!record) return NextResponse.json({ error: "Attachment not found" }, { status: 404 });
|
|
497
493
|
await em.remove(record).flush();
|
|
@@ -501,7 +497,7 @@ async function DELETE(req) {
|
|
|
501
497
|
if (record.storagePath) {
|
|
502
498
|
const delDriver = await storageDriverFactory.resolveForPartition(record.partitionCode, {
|
|
503
499
|
tenantId: record.tenantId ?? auth.tenantId,
|
|
504
|
-
organizationId: record.organizationId ?? orgId ?? ""
|
|
500
|
+
organizationId: record.organizationId ?? auth.orgId ?? ""
|
|
505
501
|
});
|
|
506
502
|
await delDriver.delete(record.partitionCode, record.storagePath);
|
|
507
503
|
}
|