@open-mercato/core 0.10.1-develop.7803.1.e99e862b4e → 0.10.1-develop.7807.1.9b2b8953e9
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 +8 -4
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/progress/api/jobs/[id]/route.js +19 -4
- package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/api_keys/api/keys/route.ts +12 -4
- package/src/modules/data_sync/AGENTS.md +3 -3
- package/src/modules/progress/api/jobs/[id]/route.ts +29 -10
package/.turbo/turbo-build.log
CHANGED
|
@@ -159,10 +159,14 @@ const crud = makeCrudRoute({
|
|
|
159
159
|
const qb = em.createQueryBuilder(ApiKey, "k");
|
|
160
160
|
qb.where({ deletedAt: null });
|
|
161
161
|
qb.andWhere({ tenantId: auth.tenantId });
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
const organizationConstraint = organizationIds?.length ? { organizationId: { $in: organizationIds } } : auth.orgId ? { organizationId: auth.orgId } : null;
|
|
163
|
+
if (organizationConstraint) {
|
|
164
|
+
const tenantWideAccess = isOrganizationAccessAllowed({
|
|
165
|
+
targetOrganizationId: null,
|
|
166
|
+
allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],
|
|
167
|
+
isSuperAdmin: await resolveIsSuperAdmin(ctx)
|
|
168
|
+
});
|
|
169
|
+
qb.andWhere(tenantWideAccess ? { $or: [{ organizationId: null }, organizationConstraint] } : organizationConstraint);
|
|
166
170
|
}
|
|
167
171
|
if (search) {
|
|
168
172
|
const pattern = `%${escapeLikePattern(search)}%`;
|
|
@@ -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 { 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 type { CreateApiKeyCommandInput } from '../../commands/keys'\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\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\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 candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\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).toLowerCase() : null\n if (roleTenantId && roleTenantId !== 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 }\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\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\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 del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\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 },\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 { 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 type { CreateApiKeyCommandInput } from '../../commands/keys'\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\nasync function prepareCreateCommandInput(\n input: z.infer<typeof createApiKeySchema>,\n ctx: CrudCtx,\n): Promise<CreateApiKeyCommandInput> {\n const auth = ctx.auth\n const { translate } = await resolveTranslations()\n if (!auth?.tenantId) {\n throw json({ error: translate('api_keys.errors.tenantRequired', 'Tenant context required') }, { status: 400 })\n }\n\n const requestedTenant = Object.prototype.hasOwnProperty.call(input, 'tenantId')\n ? input.tenantId\n : auth.tenantId\n const targetTenantId = await enforceTenantSelection(ctx, requestedTenant)\n const em = ctx.container.resolve('em') as EntityManager\n const roleTokens = input.roles.filter((value) => value.trim().length > 0)\n const roleEntities: Role[] = []\n for (const token of roleTokens) {\n const value = token.trim()\n const effectiveTenantId = targetTenantId ?? auth.tenantId\n const normalizedEffectiveTenantId = effectiveTenantId.toLowerCase()\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 candidates = await em.find(Role, {\n name: value,\n deletedAt: null,\n $or: [{ tenantId: effectiveTenantId }, { tenantId: null }],\n } as FilterQuery<Role>, { limit: 5 })\n role = candidates.find((candidate) => (\n candidate.tenantId !== null\n && String(candidate.tenantId).toLowerCase() === normalizedEffectiveTenantId\n )) ?? candidates.find((candidate) => candidate.tenantId === null) ?? null\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).toLowerCase() : null\n if (roleTenantId && roleTenantId !== 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 }\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\n const allowedIds = ctx.organizationScope?.allowedIds ?? null\n const organizationId = Object.prototype.hasOwnProperty.call(input, 'organizationId')\n ? input.organizationId ?? null\n : ctx.selectedOrganizationId ?? auth.orgId ?? null\n const isSuperAdmin = await resolveIsSuperAdmin(ctx)\n if (!isOrganizationAccessAllowed({\n isSuperAdmin,\n allowedOrganizationIds: allowedIds,\n targetOrganizationId: organizationId,\n })) {\n throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })\n }\n\n return {\n name: input.name,\n description: input.description ?? null,\n tenantId: targetTenantId,\n organizationId,\n roleIds: roleEntities.map((role) => String(role.id)),\n expiresAt: input.expiresAt ?? null,\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 del: { idFrom: 'query' },\n actions: {\n create: {\n commandId: 'api_keys.keys.create',\n schema: createApiKeySchema,\n mapInput: ({ parsed, ctx }) => prepareCreateCommandInput(parsed, ctx),\n response: ({ result }) => result,\n },\n delete: {\n commandId: 'api_keys.keys.delete',\n mapInput: ({ raw }) => ({ id: String(raw.query?.id ?? '') }),\n response: () => ({ success: true }),\n },\n },\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 const organizationConstraint = organizationIds?.length\n ? { organizationId: { $in: organizationIds } }\n : auth.orgId ? { organizationId: auth.orgId } : null\n if (organizationConstraint) {\n const tenantWideAccess = isOrganizationAccessAllowed({\n targetOrganizationId: null,\n allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],\n isSuperAdmin: await resolveIsSuperAdmin(ctx),\n })\n qb.andWhere(tenantWideAccess\n ? { $or: [{ organizationId: null }, organizationConstraint] }\n : organizationConstraint)\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 },\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
|
}
|
|
@@ -13,6 +13,17 @@ const routeMetadata = {
|
|
|
13
13
|
DELETE: { requireAuth: true, requireFeatures: ["progress.cancel"] }
|
|
14
14
|
};
|
|
15
15
|
const metadata = routeMetadata;
|
|
16
|
+
async function canMutateProgressJob(container, auth, job) {
|
|
17
|
+
if (job.startedByUserId === auth.sub) return true;
|
|
18
|
+
const rbacService = container.resolve("rbacService");
|
|
19
|
+
return rbacService.userHasAllFeatures(auth.sub, ["progress.manage"], {
|
|
20
|
+
tenantId: auth.tenantId ?? null,
|
|
21
|
+
organizationId: job.organizationId ?? null
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function cannotCancelJobResponse() {
|
|
25
|
+
return NextResponse.json({ error: "Cannot cancel this job" }, { status: 400 });
|
|
26
|
+
}
|
|
16
27
|
async function GET(req, { params }) {
|
|
17
28
|
const auth = await getAuthFromRequest(req);
|
|
18
29
|
if (!auth || !auth.tenantId) {
|
|
@@ -83,7 +94,7 @@ async function PUT(req, { params }) {
|
|
|
83
94
|
userId: auth.sub
|
|
84
95
|
};
|
|
85
96
|
const existing = await progressService.getJob(params.id, progressContext);
|
|
86
|
-
if (!existing) {
|
|
97
|
+
if (!existing || !await canMutateProgressJob(container, auth, existing)) {
|
|
87
98
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
88
99
|
}
|
|
89
100
|
const job = await progressService.updateProgress(params.id, parsed.data, progressContext);
|
|
@@ -107,10 +118,14 @@ async function DELETE(req, { params }) {
|
|
|
107
118
|
userId: auth.sub
|
|
108
119
|
};
|
|
109
120
|
try {
|
|
121
|
+
const existing = await progressService.getJob(params.id, progressContext);
|
|
122
|
+
if (!existing || !await canMutateProgressJob(container, auth, existing)) {
|
|
123
|
+
return cannotCancelJobResponse();
|
|
124
|
+
}
|
|
110
125
|
await progressService.cancelJob(params.id, progressContext);
|
|
111
126
|
return NextResponse.json({ ok: true });
|
|
112
127
|
} catch {
|
|
113
|
-
return
|
|
128
|
+
return cannotCancelJobResponse();
|
|
114
129
|
}
|
|
115
130
|
}
|
|
116
131
|
const openApi = {
|
|
@@ -125,7 +140,7 @@ const openApi = {
|
|
|
125
140
|
},
|
|
126
141
|
PUT: {
|
|
127
142
|
summary: "Update progress job",
|
|
128
|
-
description: "Updates progress metrics and heartbeat for a running job.",
|
|
143
|
+
description: "Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user's job requires progress.manage.",
|
|
129
144
|
tags: ["Progress"],
|
|
130
145
|
responses: {
|
|
131
146
|
200: { description: "Progress updated" },
|
|
@@ -135,7 +150,7 @@ const openApi = {
|
|
|
135
150
|
},
|
|
136
151
|
DELETE: {
|
|
137
152
|
summary: "Cancel progress job",
|
|
138
|
-
description: "Requests cancellation of a running or pending job.",
|
|
153
|
+
description: "Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user's job requires progress.manage.",
|
|
139
154
|
tags: ["Progress"],
|
|
140
155
|
responses: {
|
|
141
156
|
200: { description: "Cancellation requested" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/progress/api/jobs/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { updateProgressSchema } from '../../../data/validators'\nimport type { ProgressService } from '../../../lib/progressService'\nimport {\n hasReadableProgressScope,\n hasWritableProgressScope,\n resolveProgressRequestScope,\n} from '../../requestScope'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['progress.view'] },\n PUT: { requireAuth: true, requireFeatures: ['progress.update'] },\n DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },\n}\n\nexport const metadata = routeMetadata\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasReadableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const job = await progressService.getJob(params.id, {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n })\n\n if (!job) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n id: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n etaSeconds: job.etaSeconds,\n cancellable: job.cancellable,\n meta: job.meta,\n resultSummary: job.resultSummary,\n errorMessage: job.errorMessage,\n startedByUserId: job.startedByUserId,\n startedAt: job.startedAt?.toISOString() ?? null,\n heartbeatAt: job.heartbeatAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n parentJobId: job.parentJobId,\n partitionIndex: job.partitionIndex,\n partitionCount: job.partitionCount,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json()\n const parsed = updateProgressSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const job = await progressService.updateProgress(params.id, parsed.data, progressContext)\n\n return NextResponse.json({ ok: true, progressPercent: job.progressPercent })\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { AwilixContainer } from 'awilix'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { updateProgressSchema } from '../../../data/validators'\nimport type { ProgressJob } from '../../../data/entities'\nimport type { ProgressService } from '../../../lib/progressService'\nimport {\n hasReadableProgressScope,\n hasWritableProgressScope,\n resolveProgressRequestScope,\n} from '../../requestScope'\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['progress.view'] },\n PUT: { requireAuth: true, requireFeatures: ['progress.update'] },\n DELETE: { requireAuth: true, requireFeatures: ['progress.cancel'] },\n}\n\nexport const metadata = routeMetadata\n\nasync function canMutateProgressJob(\n container: AwilixContainer,\n auth: NonNullable<AuthContext>,\n job: Pick<ProgressJob, 'startedByUserId' | 'organizationId'>,\n): Promise<boolean> {\n if (job.startedByUserId === auth.sub) return true\n const rbacService = container.resolve('rbacService') as RbacService\n return rbacService.userHasAllFeatures(auth.sub, ['progress.manage'], {\n tenantId: auth.tenantId ?? null,\n organizationId: job.organizationId ?? null,\n })\n}\n\nfunction cannotCancelJobResponse() {\n return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })\n}\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasReadableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const job = await progressService.getJob(params.id, {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n })\n\n if (!job) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n return NextResponse.json({\n id: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount,\n etaSeconds: job.etaSeconds,\n cancellable: job.cancellable,\n meta: job.meta,\n resultSummary: job.resultSummary,\n errorMessage: job.errorMessage,\n startedByUserId: job.startedByUserId,\n startedAt: job.startedAt?.toISOString() ?? null,\n heartbeatAt: job.heartbeatAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n parentJobId: job.parentJobId,\n partitionIndex: job.partitionIndex,\n partitionCount: job.partitionCount,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n tenantId: job.tenantId,\n organizationId: job.organizationId,\n })\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const body = await req.json()\n const parsed = updateProgressSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid input', details: parsed.error.flatten() }, { status: 400 })\n }\n\n const container = await createRequestContainer()\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressService = container.resolve('progressService') as ProgressService\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing || !(await canMutateProgressJob(container, auth, existing))) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const job = await progressService.updateProgress(params.id, parsed.data, progressContext)\n\n return NextResponse.json({ ok: true, progressPercent: job.progressPercent })\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const progressService = container.resolve('progressService') as ProgressService\n const scope = await resolveProgressRequestScope(container, auth, req)\n if (!hasWritableProgressScope(scope)) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n const progressContext = {\n tenantId: scope.tenantId,\n organizationId: scope.selectedId,\n organizationIds: scope.filterIds,\n userId: auth.sub,\n }\n try {\n const existing = await progressService.getJob(params.id, progressContext)\n if (!existing || !(await canMutateProgressJob(container, auth, existing))) {\n return cannotCancelJobResponse()\n }\n await progressService.cancelJob(params.id, progressContext)\n return NextResponse.json({ ok: true })\n } catch {\n return cannotCancelJobResponse()\n }\n}\n\nexport const openApi = {\n GET: {\n summary: 'Get progress job details',\n description: 'Returns full details of a specific progress job by ID.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress job details' },\n 404: { description: 'Job not found' },\n },\n },\n PUT: {\n summary: 'Update progress job',\n description: 'Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user\\'s job requires progress.manage.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Progress updated' },\n 400: { description: 'Invalid input' },\n 404: { description: 'Job not found' },\n },\n },\n DELETE: {\n summary: 'Cancel progress job',\n description: 'Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user\\'s job requires progress.manage.',\n tags: ['Progress'],\n responses: {\n 200: { description: 'Cancellation requested' },\n 400: { description: 'Cannot cancel this job' },\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7807.1.9b2b8953e9",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -193,10 +193,18 @@ const crud = makeCrudRoute<
|
|
|
193
193
|
const qb = em.createQueryBuilder(ApiKey, 'k')
|
|
194
194
|
qb.where({ deletedAt: null })
|
|
195
195
|
qb.andWhere({ tenantId: auth.tenantId })
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
const organizationConstraint = organizationIds?.length
|
|
197
|
+
? { organizationId: { $in: organizationIds } }
|
|
198
|
+
: auth.orgId ? { organizationId: auth.orgId } : null
|
|
199
|
+
if (organizationConstraint) {
|
|
200
|
+
const tenantWideAccess = isOrganizationAccessAllowed({
|
|
201
|
+
targetOrganizationId: null,
|
|
202
|
+
allowedOrganizationIds: ctx.organizationScope ? ctx.organizationScope.allowedIds : [],
|
|
203
|
+
isSuperAdmin: await resolveIsSuperAdmin(ctx),
|
|
204
|
+
})
|
|
205
|
+
qb.andWhere(tenantWideAccess
|
|
206
|
+
? { $or: [{ organizationId: null }, organizationConstraint] }
|
|
207
|
+
: organizationConstraint)
|
|
200
208
|
}
|
|
201
209
|
if (search) {
|
|
202
210
|
const pattern = `%${escapeLikePattern(search)}%`
|
|
@@ -238,9 +238,9 @@ and both belong to the adapter to state.
|
|
|
238
238
|
Unless the caller passes `createProgressJob: false`, every run started through
|
|
239
239
|
`startDataSyncRun` (`lib/start-run.ts`) — the run route, Retry, the scheduled
|
|
240
240
|
worker and provider-owned routes — creates a `ProgressJob` that `ProgressTopBar`
|
|
241
|
-
shows to every user with `progress.view
|
|
242
|
-
`progress.cancel
|
|
243
|
-
job per entity type:
|
|
241
|
+
shows to every user with `progress.view`; its Cancel works for the user who
|
|
242
|
+
started the run (`progress.cancel`) or anyone holding `progress.manage`. An
|
|
243
|
+
adapter may describe that job per entity type:
|
|
244
244
|
|
|
245
245
|
```typescript
|
|
246
246
|
describeProgressJob: ({ entityType }) =>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
2
|
+
import { getAuthFromRequest, type AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import type { AwilixContainer } from 'awilix'
|
|
5
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
4
6
|
import { updateProgressSchema } from '../../../data/validators'
|
|
7
|
+
import type { ProgressJob } from '../../../data/entities'
|
|
5
8
|
import type { ProgressService } from '../../../lib/progressService'
|
|
6
9
|
import {
|
|
7
10
|
hasReadableProgressScope,
|
|
@@ -17,6 +20,23 @@ const routeMetadata = {
|
|
|
17
20
|
|
|
18
21
|
export const metadata = routeMetadata
|
|
19
22
|
|
|
23
|
+
async function canMutateProgressJob(
|
|
24
|
+
container: AwilixContainer,
|
|
25
|
+
auth: NonNullable<AuthContext>,
|
|
26
|
+
job: Pick<ProgressJob, 'startedByUserId' | 'organizationId'>,
|
|
27
|
+
): Promise<boolean> {
|
|
28
|
+
if (job.startedByUserId === auth.sub) return true
|
|
29
|
+
const rbacService = container.resolve('rbacService') as RbacService
|
|
30
|
+
return rbacService.userHasAllFeatures(auth.sub, ['progress.manage'], {
|
|
31
|
+
tenantId: auth.tenantId ?? null,
|
|
32
|
+
organizationId: job.organizationId ?? null,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cannotCancelJobResponse() {
|
|
37
|
+
return NextResponse.json({ error: 'Cannot cancel this job' }, { status: 400 })
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
21
41
|
const auth = await getAuthFromRequest(req)
|
|
22
42
|
if (!auth || !auth.tenantId) {
|
|
@@ -93,7 +113,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
|
|
|
93
113
|
userId: auth.sub,
|
|
94
114
|
}
|
|
95
115
|
const existing = await progressService.getJob(params.id, progressContext)
|
|
96
|
-
if (!existing) {
|
|
116
|
+
if (!existing || !(await canMutateProgressJob(container, auth, existing))) {
|
|
97
117
|
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
98
118
|
}
|
|
99
119
|
|
|
@@ -120,16 +140,15 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
|
|
|
120
140
|
organizationIds: scope.filterIds,
|
|
121
141
|
userId: auth.sub,
|
|
122
142
|
}
|
|
123
|
-
// No existence pre-check here on purpose. `cancelJob` already loads the job
|
|
124
|
-
// through the SAME scoped filter and throws when it resolves to nothing, so a
|
|
125
|
-
// job outside the caller's organization scope is rejected exactly like one that
|
|
126
|
-
// does not exist — and the route keeps its documented answer for an unknown id
|
|
127
|
-
// (400 "Cannot cancel this job", TC-PROG-009) instead of a 404.
|
|
128
143
|
try {
|
|
144
|
+
const existing = await progressService.getJob(params.id, progressContext)
|
|
145
|
+
if (!existing || !(await canMutateProgressJob(container, auth, existing))) {
|
|
146
|
+
return cannotCancelJobResponse()
|
|
147
|
+
}
|
|
129
148
|
await progressService.cancelJob(params.id, progressContext)
|
|
130
149
|
return NextResponse.json({ ok: true })
|
|
131
150
|
} catch {
|
|
132
|
-
return
|
|
151
|
+
return cannotCancelJobResponse()
|
|
133
152
|
}
|
|
134
153
|
}
|
|
135
154
|
|
|
@@ -145,7 +164,7 @@ export const openApi = {
|
|
|
145
164
|
},
|
|
146
165
|
PUT: {
|
|
147
166
|
summary: 'Update progress job',
|
|
148
|
-
description: 'Updates progress metrics and heartbeat for a running job.',
|
|
167
|
+
description: 'Updates progress metrics and heartbeat for a running job. Callers can update jobs they started; updating another user\'s job requires progress.manage.',
|
|
149
168
|
tags: ['Progress'],
|
|
150
169
|
responses: {
|
|
151
170
|
200: { description: 'Progress updated' },
|
|
@@ -155,7 +174,7 @@ export const openApi = {
|
|
|
155
174
|
},
|
|
156
175
|
DELETE: {
|
|
157
176
|
summary: 'Cancel progress job',
|
|
158
|
-
description: 'Requests cancellation of a running or pending job.',
|
|
177
|
+
description: 'Requests cancellation of a running or pending job. Callers can cancel jobs they started; cancelling another user\'s job requires progress.manage.',
|
|
159
178
|
tags: ['Progress'],
|
|
160
179
|
responses: {
|
|
161
180
|
200: { description: 'Cancellation requested' },
|