@open-mercato/core 0.6.4-develop.4095.1.9c790dc021 → 0.6.4-develop.4106.1.12c205a613
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/dist/modules/currencies/acl.js +10 -5
- package/dist/modules/currencies/acl.js.map +2 -2
- package/dist/modules/directory/acl.js +12 -2
- package/dist/modules/directory/acl.js.map +2 -2
- package/dist/modules/directory/api/tenants/route.js +48 -34
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/entities/acl.js +12 -2
- package/dist/modules/entities/acl.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +20 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +15 -25
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/query_index/acl.js +12 -2
- package/dist/modules/query_index/acl.js.map +2 -2
- package/dist/modules/resources/acl.js +6 -1
- package/dist/modules/resources/acl.js.map +2 -2
- package/dist/modules/sync_excel/acl.js +6 -1
- package/dist/modules/sync_excel/acl.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/acl.ts +5 -0
- package/src/modules/directory/acl.ts +12 -2
- package/src/modules/directory/api/tenants/route.ts +55 -41
- package/src/modules/entities/acl.ts +12 -2
- package/src/modules/integrations/api/[id]/credentials/route.ts +21 -3
- package/src/modules/integrations/lib/credentials-service.ts +15 -33
- package/src/modules/query_index/acl.ts +12 -2
- package/src/modules/resources/acl.ts +6 -1
- package/src/modules/sync_excel/acl.ts +6 -1
|
@@ -7,27 +7,32 @@ const features = [
|
|
|
7
7
|
{
|
|
8
8
|
id: "currencies.manage",
|
|
9
9
|
title: "Manage currencies",
|
|
10
|
-
module: "currencies"
|
|
10
|
+
module: "currencies",
|
|
11
|
+
dependsOn: ["currencies.view"]
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
id: "currencies.rates.view",
|
|
14
15
|
title: "View exchange rates",
|
|
15
|
-
module: "currencies"
|
|
16
|
+
module: "currencies",
|
|
17
|
+
dependsOn: ["currencies.view"]
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
20
|
id: "currencies.rates.manage",
|
|
19
21
|
title: "Manage exchange rates",
|
|
20
|
-
module: "currencies"
|
|
22
|
+
module: "currencies",
|
|
23
|
+
dependsOn: ["currencies.rates.view"]
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
26
|
id: "currencies.fetch.view",
|
|
24
27
|
title: "View currency fetch configuration",
|
|
25
|
-
module: "currencies"
|
|
28
|
+
module: "currencies",
|
|
29
|
+
dependsOn: ["currencies.view"]
|
|
26
30
|
},
|
|
27
31
|
{
|
|
28
32
|
id: "currencies.fetch.manage",
|
|
29
33
|
title: "Manage currency fetch configuration",
|
|
30
|
-
module: "currencies"
|
|
34
|
+
module: "currencies",
|
|
35
|
+
dependsOn: ["currencies.fetch.view"]
|
|
31
36
|
}
|
|
32
37
|
];
|
|
33
38
|
var acl_default = features;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/currencies/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n {\n id: 'currencies.view',\n title: 'View currencies',\n module: 'currencies',\n },\n {\n id: 'currencies.manage',\n title: 'Manage currencies',\n module: 'currencies',\n },\n {\n id: 'currencies.rates.view',\n title: 'View exchange rates',\n module: 'currencies',\n },\n {\n id: 'currencies.rates.manage',\n title: 'Manage exchange rates',\n module: 'currencies',\n },\n {\n id: 'currencies.fetch.view',\n title: 'View currency fetch configuration',\n module: 'currencies',\n },\n {\n id: 'currencies.fetch.manage',\n title: 'Manage currency fetch configuration',\n module: 'currencies',\n },\n]\n\nexport default features\n"],
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,
|
|
4
|
+
"sourcesContent": ["export const features = [\n {\n id: 'currencies.view',\n title: 'View currencies',\n module: 'currencies',\n },\n {\n id: 'currencies.manage',\n title: 'Manage currencies',\n module: 'currencies',\n dependsOn: ['currencies.view'],\n },\n {\n id: 'currencies.rates.view',\n title: 'View exchange rates',\n module: 'currencies',\n dependsOn: ['currencies.view'],\n },\n {\n id: 'currencies.rates.manage',\n title: 'Manage exchange rates',\n module: 'currencies',\n dependsOn: ['currencies.rates.view'],\n },\n {\n id: 'currencies.fetch.view',\n title: 'View currency fetch configuration',\n module: 'currencies',\n dependsOn: ['currencies.view'],\n },\n {\n id: 'currencies.fetch.manage',\n title: 'Manage currency fetch configuration',\n module: 'currencies',\n dependsOn: ['currencies.fetch.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "directory.tenants.view", title: "View tenants", module: "directory" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "directory.tenants.manage",
|
|
5
|
+
title: "Manage tenants",
|
|
6
|
+
module: "directory",
|
|
7
|
+
dependsOn: ["directory.tenants.view"]
|
|
8
|
+
},
|
|
4
9
|
{ id: "directory.organizations.view", title: "View organizations", module: "directory" },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: "directory.organizations.manage",
|
|
12
|
+
title: "Manage organizations",
|
|
13
|
+
module: "directory",
|
|
14
|
+
dependsOn: ["directory.organizations.view"]
|
|
15
|
+
}
|
|
6
16
|
];
|
|
7
17
|
var acl_default = features;
|
|
8
18
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/directory/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'directory.tenants.view', title: 'View tenants', module: 'directory' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACpB,EAAE,IAAI,0BAA0B,OAAO,gBAAgB,QAAQ,YAAY;AAAA,EAC3E,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'directory.tenants.view', title: 'View tenants', module: 'directory' },\n {\n id: 'directory.tenants.manage',\n title: 'Manage tenants',\n module: 'directory',\n dependsOn: ['directory.tenants.view'],\n },\n { id: 'directory.organizations.view', title: 'View organizations', module: 'directory' },\n {\n id: 'directory.organizations.manage',\n title: 'Manage organizations',\n module: 'directory',\n dependsOn: ['directory.organizations.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACpB,EAAE,IAAI,0BAA0B,OAAO,gBAAgB,QAAQ,YAAY;AAAA,EAC3E;AAAA,IACI,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,wBAAwB;AAAA,EACxC;AAAA,EACA,EAAE,IAAI,gCAAgC,OAAO,sBAAsB,QAAQ,YAAY;AAAA,EACvF;AAAA,IACI,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,8BAA8B;AAAA,EAC9C;AACJ;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -110,22 +110,6 @@ async function GET(req) {
|
|
|
110
110
|
} else {
|
|
111
111
|
orderBy.name = "ASC";
|
|
112
112
|
}
|
|
113
|
-
const all = await em.find(Tenant, where, { orderBy });
|
|
114
|
-
const recordIds = all.map((tenant) => String(tenant.id));
|
|
115
|
-
const tenantIdByRecord = {};
|
|
116
|
-
const organizationIdByRecord = {};
|
|
117
|
-
for (const rid of recordIds) {
|
|
118
|
-
tenantIdByRecord[rid] = null;
|
|
119
|
-
organizationIdByRecord[rid] = null;
|
|
120
|
-
}
|
|
121
|
-
const cfValues = recordIds.length ? await loadCustomFieldValues({
|
|
122
|
-
em,
|
|
123
|
-
entityId: E.directory.tenant,
|
|
124
|
-
recordIds,
|
|
125
|
-
tenantIdByRecord,
|
|
126
|
-
organizationIdByRecord,
|
|
127
|
-
tenantFallbacks: auth.tenantId ? [auth.tenantId] : []
|
|
128
|
-
}) : {};
|
|
129
113
|
const rawQuery = Array.from(url.searchParams.keys()).reduce((acc, key) => {
|
|
130
114
|
const values = url.searchParams.getAll(key);
|
|
131
115
|
if (!values.length) return acc;
|
|
@@ -142,26 +126,56 @@ async function GET(req) {
|
|
|
142
126
|
const normalizedKey = rawKey.startsWith("cf:") ? rawKey.slice(3) : rawKey.replace(/^cf_/, "");
|
|
143
127
|
return [normalizedKey, condition];
|
|
144
128
|
});
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
129
|
+
const loadCfValues = (tenants) => {
|
|
130
|
+
const recordIds = tenants.map((tenant) => String(tenant.id));
|
|
131
|
+
if (!recordIds.length) return Promise.resolve({});
|
|
132
|
+
const tenantIdByRecord = {};
|
|
133
|
+
const organizationIdByRecord = {};
|
|
134
|
+
for (const rid of recordIds) {
|
|
135
|
+
tenantIdByRecord[rid] = null;
|
|
136
|
+
organizationIdByRecord[rid] = null;
|
|
137
|
+
}
|
|
138
|
+
return loadCustomFieldValues({
|
|
139
|
+
em,
|
|
140
|
+
entityId: E.directory.tenant,
|
|
141
|
+
recordIds,
|
|
142
|
+
tenantIdByRecord,
|
|
143
|
+
organizationIdByRecord,
|
|
144
|
+
tenantFallbacks: auth.tenantId ? [auth.tenantId] : []
|
|
159
145
|
});
|
|
160
|
-
}
|
|
161
|
-
const total = filtered.length;
|
|
146
|
+
};
|
|
162
147
|
const start = (page - 1) * pageSize;
|
|
163
|
-
|
|
164
|
-
|
|
148
|
+
let pageTenants;
|
|
149
|
+
let total;
|
|
150
|
+
let cfValues;
|
|
151
|
+
if (cfFilterEntries.length) {
|
|
152
|
+
const all = await em.find(Tenant, where, { orderBy });
|
|
153
|
+
cfValues = await loadCfValues(all);
|
|
154
|
+
const filtered = all.filter((tenant) => {
|
|
155
|
+
const rid = String(tenant.id);
|
|
156
|
+
const payload = cfValues[rid] ?? {};
|
|
157
|
+
return cfFilterEntries.every(([key, expected]) => {
|
|
158
|
+
const value = payload[`cf_${key}`];
|
|
159
|
+
if (expected && typeof expected === "object" && !Array.isArray(expected)) {
|
|
160
|
+
const maybeIn = expected.$in;
|
|
161
|
+
if (Array.isArray(maybeIn)) {
|
|
162
|
+
if (value === void 0 || value === null) return false;
|
|
163
|
+
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val));
|
|
164
|
+
return maybeIn.includes(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return matchesValue(value, expected);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
total = filtered.length;
|
|
171
|
+
pageTenants = filtered.slice(start, start + pageSize);
|
|
172
|
+
} else {
|
|
173
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start });
|
|
174
|
+
total = count;
|
|
175
|
+
pageTenants = rows;
|
|
176
|
+
cfValues = await loadCfValues(rows);
|
|
177
|
+
}
|
|
178
|
+
const items = pageTenants.map((tenant) => {
|
|
165
179
|
const rid = String(tenant.id);
|
|
166
180
|
const cf = cfValues[rid] ?? {};
|
|
167
181
|
return toRow(tenant, cf);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/directory/api/tenants/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { tenantCrudEvents, tenantCrudIndexer } from '@open-mercato/core/modules/directory/commands/tenants'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { directoryTag, directoryErrorSchema, directoryOkSchema, tenantListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst listQuerySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n}).passthrough()\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n} & Record<string, unknown>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.tenants.view'] },\n POST: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Tenant,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: tenantCrudEvents,\n indexer: tenantCrudIndexer,\n actions: {\n create: {\n commandId: 'directory.tenants.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'directory.tenants.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'directory.tenants.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({\n id: String(tenant.id),\n name: String(tenant.name),\n isActive: !!tenant.isActive,\n createdAt: tenant.createdAt ? tenant.createdAt.toISOString() : null,\n updatedAt: tenant.updatedAt ? tenant.updatedAt.toISOString() : null,\n ...cf,\n})\n\nconst matchesValue = (current: unknown, expected: unknown): boolean => {\n if (Array.isArray(current)) return current.some((item) => item === expected)\n return current === expected\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid query parameters', details: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isActive } = parsed.data\n const where: FilterQuery<Tenant> = { deletedAt: null }\n if (id) where.id = id\n if (search) {\n Object.assign(where, { name: { $ilike: `%${escapeLikePattern(search)}%` } })\n }\n const active = parseBooleanToken(isActive)\n if (active !== null) where.isActive = active\n\n const fieldMap: Record<string, string> = { name: 'name', createdAt: 'createdAt', updatedAt: 'updatedAt' }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'name'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.name = 'ASC'\n }\n\n const all = await em.find(Tenant, where, { orderBy })\n const recordIds = all.map((tenant) => String(tenant.id))\n\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const rid of recordIds) {\n tenantIdByRecord[rid] = null\n organizationIdByRecord[rid] = null\n }\n\n const cfValues = recordIds.length\n ? await loadCustomFieldValues({\n em,\n entityId: E.directory.tenant,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],\n })\n : {}\n\n const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {\n const values = url.searchParams.getAll(key)\n if (!values.length) return acc\n acc[key] = values.length === 1 ? values[0] : values\n return acc\n }, {})\n\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId: E.directory.tenant,\n query: rawQuery,\n em,\n tenantId: auth.tenantId ?? null,\n })\n const cfFilterEntries = Object.entries(cfFilters).map(([rawKey, condition]) => {\n const normalizedKey = rawKey.startsWith('cf:') ? rawKey.slice(3) : rawKey.replace(/^cf_/, '')\n return [normalizedKey, condition] as const\n })\n\n const filtered = cfFilterEntries.length\n ? all.filter((tenant) => {\n const rid = String(tenant.id)\n const payload = cfValues[rid] ?? {}\n return cfFilterEntries.every(([key, expected]) => {\n const value = payload[`cf_${key}`]\n if (expected && typeof expected === 'object' && !Array.isArray(expected)) {\n const maybeIn = (expected as { $in?: unknown[] }).$in\n if (Array.isArray(maybeIn)) {\n if (value === undefined || value === null) return false\n if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))\n return maybeIn.includes(value)\n }\n }\n return matchesValue(value, expected)\n })\n })\n : all\n\n const total = filtered.length\n const start = (page - 1) * pageSize\n const paged = filtered.slice(start, start + pageSize)\n const items = paged.map((tenant) => {\n const rid = String(tenant.id)\n const cf = cfValues[rid] ?? {}\n return toRow(tenant, cf)\n })\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'directory.tenant',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: rawQuery,\n accessType: id ? 'read:item' : undefined,\n })\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst tenantCreateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantDeleteRequestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantGetDoc: OpenApiMethodDoc = {\n summary: 'List tenants',\n description: 'Returns tenants visible to the current user with optional search and pagination.',\n tags: [directoryTag],\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Paged list of tenants.', schema: tenantListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPostDoc: OpenApiMethodDoc = {\n summary: 'Create tenant',\n description: 'Creates a new tenant and returns its identifier.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantCreateSchema,\n description: 'Tenant name and optional activation flag.',\n },\n responses: [\n { status: 201, description: 'Tenant created.', schema: tenantCreateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPutDoc: OpenApiMethodDoc = {\n summary: 'Update tenant',\n description: 'Updates tenant properties such as name or activation state.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantUpdateSchema,\n description: 'Tenant identifier with fields to update.',\n },\n responses: [\n { status: 200, description: 'Tenant updated.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete tenant',\n description: 'Soft deletes the tenant identified by id.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantDeleteRequestSchema,\n description: 'Identifier of the tenant to remove.',\n },\n responses: [\n { status: 200, description: 'Tenant removed.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: directoryTag,\n summary: 'Manage tenants',\n methods: {\n GET: tenantGetDoc,\n POST: tenantPostDoc,\n PUT: tenantPutDoc,\n DELETE: tenantDeleteDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { Tenant } from '@open-mercato/core/modules/directory/data/entities'\nimport { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'\nimport { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { E } from '#generated/entities.ids.generated'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport { tenantCrudEvents, tenantCrudIndexer } from '@open-mercato/core/modules/directory/commands/tenants'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { directoryTag, directoryErrorSchema, directoryOkSchema, tenantListResponseSchema } from '../openapi'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\nconst listQuerySchema = z.object({\n id: z.string().uuid().optional(),\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n sortField: z.enum(['name', 'createdAt', 'updatedAt']).optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n isActive: z.enum(['true', 'false']).optional(),\n}).passthrough()\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n} & Record<string, unknown>\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['directory.tenants.view'] },\n POST: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['directory.tenants.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\ntype CrudInput = Record<string, unknown>\n\nconst crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({\n metadata: routeMetadata,\n orm: {\n entity: Tenant,\n idField: 'id',\n orgField: null,\n tenantField: null,\n softDeleteField: 'deletedAt',\n },\n events: tenantCrudEvents,\n indexer: tenantCrudIndexer,\n actions: {\n create: {\n commandId: 'directory.tenants.create',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: ({ result }) => ({ id: String(result.id) }),\n status: 201,\n },\n update: {\n commandId: 'directory.tenants.update',\n schema: rawBodySchema,\n mapInput: ({ parsed }) => parsed,\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'directory.tenants.delete',\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({\n id: String(tenant.id),\n name: String(tenant.name),\n isActive: !!tenant.isActive,\n createdAt: tenant.createdAt ? tenant.createdAt.toISOString() : null,\n updatedAt: tenant.updatedAt ? tenant.updatedAt.toISOString() : null,\n ...cf,\n})\n\nconst matchesValue = (current: unknown, expected: unknown): boolean => {\n if (Array.isArray(current)) return current.some((item) => item === expected)\n return current === expected\n}\n\nexport async function GET(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })\n }\n\n const url = new URL(req.url)\n const parsed = listQuerySchema.safeParse({\n id: url.searchParams.get('id') ?? undefined,\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sortField: url.searchParams.get('sortField') ?? undefined,\n sortDir: url.searchParams.get('sortDir') ?? undefined,\n isActive: url.searchParams.get('isActive') ?? undefined,\n })\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid query parameters', details: parsed.error.flatten() },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n\n const { id, page, pageSize, search, sortField, sortDir, isActive } = parsed.data\n const where: FilterQuery<Tenant> = { deletedAt: null }\n if (id) where.id = id\n if (search) {\n Object.assign(where, { name: { $ilike: `%${escapeLikePattern(search)}%` } })\n }\n const active = parseBooleanToken(isActive)\n if (active !== null) where.isActive = active\n\n const fieldMap: Record<string, string> = { name: 'name', createdAt: 'createdAt', updatedAt: 'updatedAt' }\n const orderBy: Record<string, 'ASC' | 'DESC'> = {}\n if (sortField) {\n const mapped = fieldMap[sortField] || 'name'\n orderBy[mapped] = sortDir === 'desc' ? 'DESC' : 'ASC'\n } else {\n orderBy.name = 'ASC'\n }\n\n const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {\n const values = url.searchParams.getAll(key)\n if (!values.length) return acc\n acc[key] = values.length === 1 ? values[0] : values\n return acc\n }, {})\n\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityId: E.directory.tenant,\n query: rawQuery,\n em,\n tenantId: auth.tenantId ?? null,\n })\n const cfFilterEntries = Object.entries(cfFilters).map(([rawKey, condition]) => {\n const normalizedKey = rawKey.startsWith('cf:') ? rawKey.slice(3) : rawKey.replace(/^cf_/, '')\n return [normalizedKey, condition] as const\n })\n\n const loadCfValues = (tenants: Tenant[]): Promise<Record<string, Record<string, unknown>>> => {\n const recordIds = tenants.map((tenant) => String(tenant.id))\n if (!recordIds.length) return Promise.resolve({})\n const tenantIdByRecord: Record<string, string | null> = {}\n const organizationIdByRecord: Record<string, string | null> = {}\n for (const rid of recordIds) {\n tenantIdByRecord[rid] = null\n organizationIdByRecord[rid] = null\n }\n return loadCustomFieldValues({\n em,\n entityId: E.directory.tenant,\n recordIds,\n tenantIdByRecord,\n organizationIdByRecord,\n tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],\n })\n }\n\n const start = (page - 1) * pageSize\n\n let pageTenants: Tenant[]\n let total: number\n let cfValues: Record<string, Record<string, unknown>>\n\n if (cfFilterEntries.length) {\n // Custom-field filters are matched in JS against separately-loaded values,\n // so the full result set must be fetched before filtering and paging.\n const all = await em.find(Tenant, where, { orderBy })\n cfValues = await loadCfValues(all)\n const filtered = all.filter((tenant) => {\n const rid = String(tenant.id)\n const payload = cfValues[rid] ?? {}\n return cfFilterEntries.every(([key, expected]) => {\n const value = payload[`cf_${key}`]\n if (expected && typeof expected === 'object' && !Array.isArray(expected)) {\n const maybeIn = (expected as { $in?: unknown[] }).$in\n if (Array.isArray(maybeIn)) {\n if (value === undefined || value === null) return false\n if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))\n return maybeIn.includes(value)\n }\n }\n return matchesValue(value, expected)\n })\n })\n total = filtered.length\n pageTenants = filtered.slice(start, start + pageSize)\n } else {\n // No JS-side filtering applies, so pagination is pushed to the database\n // instead of fetching the whole table and slicing in memory.\n const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })\n total = count\n pageTenants = rows\n cfValues = await loadCfValues(rows)\n }\n\n const items = pageTenants.map((tenant) => {\n const rid = String(tenant.id)\n const cf = cfValues[rid] ?? {}\n return toRow(tenant, cf)\n })\n const totalPages = Math.max(1, Math.ceil(total / pageSize))\n\n await logCrudAccess({\n container,\n auth,\n request: req,\n items,\n idField: 'id',\n resourceKind: 'directory.tenant',\n organizationId: null,\n tenantId: auth.tenantId ?? null,\n query: rawQuery,\n accessType: id ? 'read:item' : undefined,\n })\n\n return NextResponse.json({ items, total, page, pageSize, totalPages })\n}\n\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst tenantCreateResponseSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantDeleteRequestSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst tenantGetDoc: OpenApiMethodDoc = {\n summary: 'List tenants',\n description: 'Returns tenants visible to the current user with optional search and pagination.',\n tags: [directoryTag],\n query: listQuerySchema,\n responses: [\n { status: 200, description: 'Paged list of tenants.', schema: tenantListResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPostDoc: OpenApiMethodDoc = {\n summary: 'Create tenant',\n description: 'Creates a new tenant and returns its identifier.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantCreateSchema,\n description: 'Tenant name and optional activation flag.',\n },\n responses: [\n { status: 201, description: 'Tenant created.', schema: tenantCreateResponseSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantPutDoc: OpenApiMethodDoc = {\n summary: 'Update tenant',\n description: 'Updates tenant properties such as name or activation state.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantUpdateSchema,\n description: 'Tenant identifier with fields to update.',\n },\n responses: [\n { status: 200, description: 'Tenant updated.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nconst tenantDeleteDoc: OpenApiMethodDoc = {\n summary: 'Delete tenant',\n description: 'Soft deletes the tenant identified by id.',\n tags: [directoryTag],\n requestBody: {\n contentType: 'application/json',\n schema: tenantDeleteRequestSchema,\n description: 'Identifier of the tenant to remove.',\n },\n responses: [\n { status: 200, description: 'Tenant removed.', schema: directoryOkSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed', schema: directoryErrorSchema },\n { status: 401, description: 'Authentication required', schema: directoryErrorSchema },\n { status: 403, description: 'Missing directory.tenants.manage feature', schema: directoryErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: directoryTag,\n summary: 'Manage tenants',\n methods: {\n GET: tenantGetDoc,\n POST: tenantPostDoc,\n PUT: tenantPutDoc,\n DELETE: tenantDeleteDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "entities.definitions.view", title: "View custom field definitions", module: "entities" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "entities.definitions.manage",
|
|
5
|
+
title: "Manage custom field definitions",
|
|
6
|
+
module: "entities",
|
|
7
|
+
dependsOn: ["entities.definitions.view"]
|
|
8
|
+
},
|
|
4
9
|
{ id: "entities.records.view", title: "View records", module: "entities" },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: "entities.records.manage",
|
|
12
|
+
title: "Manage records",
|
|
13
|
+
module: "entities",
|
|
14
|
+
dependsOn: ["entities.records.view"]
|
|
15
|
+
}
|
|
6
16
|
];
|
|
7
17
|
var acl_default = features;
|
|
8
18
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/entities/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'entities.definitions.view', title: 'View custom field definitions', module: 'entities' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,6BAA6B,OAAO,iCAAiC,QAAQ,WAAW;AAAA,EAC9F,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'entities.definitions.view', title: 'View custom field definitions', module: 'entities' },\n {\n id: 'entities.definitions.manage',\n title: 'Manage custom field definitions',\n module: 'entities',\n dependsOn: ['entities.definitions.view'],\n },\n { id: 'entities.records.view', title: 'View records', module: 'entities' },\n {\n id: 'entities.records.manage',\n title: 'Manage records',\n module: 'entities',\n dependsOn: ['entities.records.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,6BAA6B,OAAO,iCAAiC,QAAQ,WAAW;AAAA,EAC9F;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,2BAA2B;AAAA,EACzC;AAAA,EACA,EAAE,IAAI,yBAAyB,OAAO,gBAAgB,QAAQ,WAAW;AAAA,EACzE;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,uBAAuB;AAAA,EACrC;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,9 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
5
5
|
import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
|
|
6
6
|
import { emitIntegrationsEvent } from "../../../events.js";
|
|
7
7
|
import { saveCredentialsSchema } from "../../../data/validators.js";
|
|
8
|
+
import {
|
|
9
|
+
isCredentialsEncryptionUnavailableError
|
|
10
|
+
} from "../../../lib/credentials-service.js";
|
|
8
11
|
import {
|
|
9
12
|
resolveUserFeatures,
|
|
10
13
|
runIntegrationMutationGuardAfterSuccess,
|
|
@@ -43,7 +46,15 @@ async function GET(req, ctx) {
|
|
|
43
46
|
const container = await createRequestContainer();
|
|
44
47
|
const credentialsService = container.resolve("integrationCredentialsService");
|
|
45
48
|
const scope = { organizationId: auth.orgId, tenantId: auth.tenantId };
|
|
46
|
-
|
|
49
|
+
let values;
|
|
50
|
+
try {
|
|
51
|
+
values = await credentialsService.resolve(integration.id, scope);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
54
|
+
return NextResponse.json({ error: "Integration credentials encryption is unavailable" }, { status: 503 });
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
47
58
|
return NextResponse.json({
|
|
48
59
|
integrationId: integration.id,
|
|
49
60
|
schema: credentialsService.getSchema(integration.id),
|
|
@@ -99,7 +110,14 @@ async function PUT(req, ctx) {
|
|
|
99
110
|
}
|
|
100
111
|
const credentialsService = container.resolve("integrationCredentialsService");
|
|
101
112
|
const scope = { organizationId: auth.orgId, tenantId: auth.tenantId };
|
|
102
|
-
|
|
113
|
+
try {
|
|
114
|
+
await credentialsService.save(integration.id, payloadData.credentials, scope);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
117
|
+
return NextResponse.json({ error: "Integration credentials encryption is unavailable" }, { status: 503 });
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
103
121
|
await emitIntegrationsEvent("integrations.credentials.updated", {
|
|
104
122
|
integrationId: integration.id,
|
|
105
123
|
tenantId: auth.tenantId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/integrations/api/%5Bid%5D/credentials/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport { emitIntegrationsEvent } from '../../../events'\nimport { saveCredentialsSchema } from '../../../data/validators'\nimport type
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,6BAA6B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport { emitIntegrationsEvent } from '../../../events'\nimport { saveCredentialsSchema } from '../../../data/validators'\nimport {\n isCredentialsEncryptionUnavailableError,\n type CredentialsService,\n} from '../../../lib/credentials-service'\nimport {\n resolveUserFeatures,\n runIntegrationMutationGuardAfterSuccess,\n runIntegrationMutationGuards,\n} from '../../guards'\n\nconst idParamsSchema = z.object({ id: z.string().min(1) })\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['integrations.credentials.manage'] },\n}\n\nexport const openApi = {\n tags: ['Integrations'],\n summary: 'Get or save integration credentials',\n}\n\nfunction resolveParams(ctx: { params?: Promise<{ id?: string }> | { id?: string } }): Promise<{ id?: string } | undefined> | { id?: string } | undefined {\n if (!ctx.params) return undefined\n if (typeof (ctx.params as Promise<unknown>).then === 'function') {\n return ctx.params as Promise<{ id?: string }>\n }\n return ctx.params as { id?: string }\n}\n\nexport async function GET(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const container = await createRequestContainer()\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n\n let values: Record<string, unknown> | null\n try {\n values = await credentialsService.resolve(integration.id, scope)\n } catch (error) {\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n return NextResponse.json({\n integrationId: integration.id,\n schema: credentialsService.getSchema(integration.id),\n credentials: values ?? {},\n })\n}\n\nexport async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = await resolveParams(ctx)\n const parsedParams = idParamsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid integration id' }, { status: 400 })\n }\n\n const integration = getIntegration(parsedParams.data.id)\n if (!integration) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const payload = await req.json().catch(() => null)\n const parsedBody = saveCredentialsSchema.safeParse(payload)\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid credentials payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runIntegrationMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsedBody.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n let payloadData = parsedBody.data\n if (guardResult.modifiedPayload) {\n const mergedPayload = { ...parsedBody.data, ...guardResult.modifiedPayload }\n const reparsed = saveCredentialsSchema.safeParse(mergedPayload)\n if (!reparsed.success) {\n return NextResponse.json({ error: 'Invalid credentials payload after guard transform', details: reparsed.error.flatten() }, { status: 422 })\n }\n payloadData = reparsed.data\n }\n\n const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService\n const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }\n\n try {\n await credentialsService.save(integration.id, payloadData.credentials, scope)\n } catch (error) {\n if (isCredentialsEncryptionUnavailableError(error)) {\n return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })\n }\n throw error\n }\n\n await emitIntegrationsEvent('integrations.credentials.updated', {\n integrationId: integration.id,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n })\n\n await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: 'integrations.integration',\n resourceId: integration.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n\n return NextResponse.json({ ok: true })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import { decryptWithAesGcm, encryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
|
|
3
2
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
3
|
import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
|
|
@@ -11,7 +10,16 @@ import {
|
|
|
11
10
|
import { EncryptionMap } from "../../entities/data/entities.js";
|
|
12
11
|
import { IntegrationCredentials } from "../data/entities.js";
|
|
13
12
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = "__om_encrypted_credentials_blob_v1";
|
|
14
|
-
const
|
|
13
|
+
const CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE = "Integration credentials encryption key is unavailable. Configure Vault KMS or TENANT_DATA_ENCRYPTION_FALLBACK_KEY.";
|
|
14
|
+
class CredentialsEncryptionUnavailableError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE);
|
|
17
|
+
this.name = "CredentialsEncryptionUnavailableError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isCredentialsEncryptionUnavailableError(error) {
|
|
21
|
+
return error instanceof CredentialsEncryptionUnavailableError;
|
|
22
|
+
}
|
|
15
23
|
function isRecordValue(value) {
|
|
16
24
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
17
25
|
}
|
|
@@ -21,26 +29,6 @@ function normalizeCredentialsRecord(value) {
|
|
|
21
29
|
const parsed = parseDecryptedFieldValue(value);
|
|
22
30
|
return isRecordValue(parsed) ? parsed : {};
|
|
23
31
|
}
|
|
24
|
-
function resolveFallbackEncryptionSecret() {
|
|
25
|
-
const candidates = [
|
|
26
|
-
process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,
|
|
27
|
-
process.env.TENANT_DATA_ENCRYPTION_KEY,
|
|
28
|
-
process.env.AUTH_SECRET,
|
|
29
|
-
process.env.NEXTAUTH_SECRET
|
|
30
|
-
];
|
|
31
|
-
for (const value of candidates) {
|
|
32
|
-
const normalized = value?.trim();
|
|
33
|
-
if (normalized) return normalized;
|
|
34
|
-
}
|
|
35
|
-
if (process.env.NODE_ENV !== "production") return "om-dev-tenant-encryption";
|
|
36
|
-
console.warn(
|
|
37
|
-
"[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately."
|
|
38
|
-
);
|
|
39
|
-
return "om-emergency-fallback-rotate-me";
|
|
40
|
-
}
|
|
41
|
-
function deriveDekFromSecret(secret, tenantId) {
|
|
42
|
-
return crypto.createHash("sha256").update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`).digest().toString("base64");
|
|
43
|
-
}
|
|
44
32
|
function createCredentialsService(em) {
|
|
45
33
|
const credentialsEncryptionSpec = [{ field: "credentials" }];
|
|
46
34
|
async function ensureCredentialsEncryptionMap(scope) {
|
|
@@ -78,7 +66,7 @@ function createCredentialsService(em) {
|
|
|
78
66
|
if (existing?.key) return existing.key;
|
|
79
67
|
const created = await kms.createTenantDek(scope.tenantId);
|
|
80
68
|
if (created?.key) return created.key;
|
|
81
|
-
|
|
69
|
+
throw new CredentialsEncryptionUnavailableError();
|
|
82
70
|
}
|
|
83
71
|
async function encryptCredentialsBlob(credentials, scope) {
|
|
84
72
|
const dek = await resolveCredentialsDek(scope);
|
|
@@ -124,8 +112,8 @@ function createCredentialsService(em) {
|
|
|
124
112
|
return this.getRaw(definition.bundleId, scope);
|
|
125
113
|
},
|
|
126
114
|
async save(integrationId, credentials, scope) {
|
|
127
|
-
await ensureCredentialsEncryptionMap(scope);
|
|
128
115
|
const encryptedCredentials = await encryptCredentialsBlob(credentials, scope);
|
|
116
|
+
await ensureCredentialsEncryptionMap(scope);
|
|
129
117
|
const row = await findOneWithDecryption(
|
|
130
118
|
em,
|
|
131
119
|
IntegrationCredentials,
|
|
@@ -169,6 +157,8 @@ function createCredentialsService(em) {
|
|
|
169
157
|
};
|
|
170
158
|
}
|
|
171
159
|
export {
|
|
172
|
-
|
|
160
|
+
CredentialsEncryptionUnavailableError,
|
|
161
|
+
createCredentialsService,
|
|
162
|
+
isCredentialsEncryptionUnavailableError
|
|
173
163
|
};
|
|
174
164
|
//# sourceMappingURL=credentials-service.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/integrations/lib/credentials-service.ts"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport {\n getBundle,\n getIntegration,\n resolveIntegrationCredentialsSchema,\n type IntegrationScope,\n} from '@open-mercato/shared/modules/integrations/types'\nimport { EncryptionMap } from '../../entities/data/entities'\nimport { IntegrationCredentials } from '../data/entities'\n\nconst ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'\nconst CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE =\n 'Integration credentials encryption key is unavailable. Configure Vault KMS or TENANT_DATA_ENCRYPTION_FALLBACK_KEY.'\n\nexport class CredentialsEncryptionUnavailableError extends Error {\n constructor() {\n super(CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE)\n this.name = 'CredentialsEncryptionUnavailableError'\n }\n}\n\nexport function isCredentialsEncryptionUnavailableError(error: unknown): error is CredentialsEncryptionUnavailableError {\n return error instanceof CredentialsEncryptionUnavailableError\n}\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction normalizeCredentialsRecord(value: unknown): Record<string, unknown> {\n if (isRecordValue(value)) return value\n if (typeof value !== 'string') return {}\n\n const parsed = parseDecryptedFieldValue(value)\n return isRecordValue(parsed) ? parsed : {}\n}\n\nexport function createCredentialsService(em: EntityManager) {\n const credentialsEncryptionSpec = [{ field: 'credentials' }]\n\n async function ensureCredentialsEncryptionMap(scope: IntegrationScope): Promise<void> {\n const existing = await findOneWithDecryption(\n em,\n EncryptionMap,\n {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!existing) {\n const created = em.create(EncryptionMap, {\n entityId: 'integrations:integration_credentials',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n fieldsJson: credentialsEncryptionSpec,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(created)\n return\n }\n\n existing.fieldsJson = credentialsEncryptionSpec\n existing.isActive = true\n }\n\n async function resolveCredentialsDek(scope: IntegrationScope): Promise<string> {\n const kms = createKmsService()\n const existing = await kms.getTenantDek(scope.tenantId)\n if (existing?.key) return existing.key\n\n const created = await kms.createTenantDek(scope.tenantId)\n if (created?.key) return created.key\n\n throw new CredentialsEncryptionUnavailableError()\n }\n\n async function encryptCredentialsBlob(\n credentials: Record<string, unknown>,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const dek = await resolveCredentialsDek(scope)\n const payload = encryptWithAesGcm(JSON.stringify(credentials), dek)\n return { [ENCRYPTED_CREDENTIALS_BLOB_KEY]: payload.value }\n }\n\n async function decryptCredentialsBlob(\n credentialsInput: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const credentials = normalizeCredentialsRecord(credentialsInput)\n const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY]\n if (typeof encrypted !== 'string' || !encrypted) return credentials\n\n const dek = await resolveCredentialsDek(scope)\n const decryptedRaw = decryptWithAesGcm(encrypted, dek)\n if (!decryptedRaw) return {}\n\n try {\n const parsed = JSON.parse(decryptedRaw) as unknown\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n\n return {\n async getRaw(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!row) return null\n return decryptCredentialsBlob(row.credentials, scope)\n },\n\n async resolve(integrationId: string, scope: IntegrationScope): Promise<Record<string, unknown> | null> {\n const direct = await this.getRaw(integrationId, scope)\n if (direct) return direct\n\n const definition = getIntegration(integrationId)\n if (!definition?.bundleId) return null\n return this.getRaw(definition.bundleId, scope)\n },\n\n async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {\n const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)\n await ensureCredentialsEncryptionMap(scope)\n\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (row) {\n row.credentials = encryptedCredentials\n await em.flush()\n return\n }\n\n const created = em.create(IntegrationCredentials, {\n integrationId,\n credentials: encryptedCredentials,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n await em.persist(created).flush()\n },\n\n async saveField(\n integrationId: string,\n fieldKey: string,\n value: unknown,\n scope: IntegrationScope,\n ): Promise<Record<string, unknown>> {\n const current = (await this.getRaw(integrationId, scope)) ?? {}\n const updated = { ...current, [fieldKey]: value }\n await this.save(integrationId, updated, scope)\n return updated\n },\n\n getSchema(integrationId: string) {\n const definition = getIntegration(integrationId)\n if (!definition) return undefined\n\n if (definition.bundleId) {\n const bundle = getBundle(definition.bundleId)\n return bundle?.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n }\n\n return definition.credentials ?? resolveIntegrationCredentialsSchema(integrationId)\n },\n }\n}\n\nexport type CredentialsService = ReturnType<typeof createCredentialsService>\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "query_index.status.view", title: "View index status", module: "query_index" },
|
|
3
|
-
{
|
|
4
|
-
|
|
3
|
+
{
|
|
4
|
+
id: "query_index.reindex",
|
|
5
|
+
title: "Trigger reindex",
|
|
6
|
+
module: "query_index",
|
|
7
|
+
dependsOn: ["query_index.status.view"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "query_index.purge",
|
|
11
|
+
title: "Purge index",
|
|
12
|
+
module: "query_index",
|
|
13
|
+
dependsOn: ["query_index.status.view"]
|
|
14
|
+
}
|
|
5
15
|
];
|
|
6
16
|
var acl_default = features;
|
|
7
17
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/query_index/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'query_index.status.view', title: 'View index status', module: 'query_index' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,qBAAqB,QAAQ,cAAc;AAAA,EACnF,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'query_index.status.view', title: 'View index status', module: 'query_index' },\n {\n id: 'query_index.reindex',\n title: 'Trigger reindex',\n module: 'query_index',\n dependsOn: ['query_index.status.view'],\n },\n {\n id: 'query_index.purge',\n title: 'Purge index',\n module: 'query_index',\n dependsOn: ['query_index.status.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,2BAA2B,OAAO,qBAAqB,QAAQ,cAAc;AAAA,EACnF;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,yBAAyB;AAAA,EACvC;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "resources.view", title: "View resources", module: "resources" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "resources.manage_resources",
|
|
5
|
+
title: "Manage resources",
|
|
6
|
+
module: "resources",
|
|
7
|
+
dependsOn: ["resources.view"]
|
|
8
|
+
}
|
|
4
9
|
];
|
|
5
10
|
var acl_default = features;
|
|
6
11
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/resources/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'resources.view', title: 'View resources', module: 'resources' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'resources.view', title: 'View resources', module: 'resources' },\n {\n id: 'resources.manage_resources',\n title: 'Manage resources',\n module: 'resources',\n dependsOn: ['resources.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,kBAAkB,OAAO,kBAAkB,QAAQ,YAAY;AAAA,EACrE;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,gBAAgB;AAAA,EAC9B;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const features = [
|
|
2
2
|
{ id: "sync_excel.view", title: "View sync_excel uploads and previews", module: "sync_excel" },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: "sync_excel.run",
|
|
5
|
+
title: "Upload CSV files and prepare imports",
|
|
6
|
+
module: "sync_excel",
|
|
7
|
+
dependsOn: ["sync_excel.view"]
|
|
8
|
+
}
|
|
4
9
|
];
|
|
5
10
|
var acl_default = features;
|
|
6
11
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sync_excel/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },\n {
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,wCAAwC,QAAQ,aAAa;AAAA,EAC7F,
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },\n {\n id: 'sync_excel.run',\n title: 'Upload CSV files and prepare imports',\n module: 'sync_excel',\n dependsOn: ['sync_excel.view'],\n },\n]\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,mBAAmB,OAAO,wCAAwC,QAAQ,aAAa;AAAA,EAC7F;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW,CAAC,iBAAiB;AAAA,EAC/B;AACF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4106.1.12c205a613",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4106.1.12c205a613",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4106.1.12c205a613",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4106.1.12c205a613",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -8,26 +8,31 @@ export const features = [
|
|
|
8
8
|
id: 'currencies.manage',
|
|
9
9
|
title: 'Manage currencies',
|
|
10
10
|
module: 'currencies',
|
|
11
|
+
dependsOn: ['currencies.view'],
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
id: 'currencies.rates.view',
|
|
14
15
|
title: 'View exchange rates',
|
|
15
16
|
module: 'currencies',
|
|
17
|
+
dependsOn: ['currencies.view'],
|
|
16
18
|
},
|
|
17
19
|
{
|
|
18
20
|
id: 'currencies.rates.manage',
|
|
19
21
|
title: 'Manage exchange rates',
|
|
20
22
|
module: 'currencies',
|
|
23
|
+
dependsOn: ['currencies.rates.view'],
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
26
|
id: 'currencies.fetch.view',
|
|
24
27
|
title: 'View currency fetch configuration',
|
|
25
28
|
module: 'currencies',
|
|
29
|
+
dependsOn: ['currencies.view'],
|
|
26
30
|
},
|
|
27
31
|
{
|
|
28
32
|
id: 'currencies.fetch.manage',
|
|
29
33
|
title: 'Manage currency fetch configuration',
|
|
30
34
|
module: 'currencies',
|
|
35
|
+
dependsOn: ['currencies.fetch.view'],
|
|
31
36
|
},
|
|
32
37
|
]
|
|
33
38
|
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'directory.tenants.view', title: 'View tenants', module: 'directory' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'directory.tenants.manage',
|
|
5
|
+
title: 'Manage tenants',
|
|
6
|
+
module: 'directory',
|
|
7
|
+
dependsOn: ['directory.tenants.view'],
|
|
8
|
+
},
|
|
4
9
|
{ id: 'directory.organizations.view', title: 'View organizations', module: 'directory' },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: 'directory.organizations.manage',
|
|
12
|
+
title: 'Manage organizations',
|
|
13
|
+
module: 'directory',
|
|
14
|
+
dependsOn: ['directory.organizations.view'],
|
|
15
|
+
},
|
|
6
16
|
]
|
|
7
17
|
|
|
8
18
|
export default features
|
|
@@ -136,27 +136,6 @@ export async function GET(req: Request) {
|
|
|
136
136
|
orderBy.name = 'ASC'
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const all = await em.find(Tenant, where, { orderBy })
|
|
140
|
-
const recordIds = all.map((tenant) => String(tenant.id))
|
|
141
|
-
|
|
142
|
-
const tenantIdByRecord: Record<string, string | null> = {}
|
|
143
|
-
const organizationIdByRecord: Record<string, string | null> = {}
|
|
144
|
-
for (const rid of recordIds) {
|
|
145
|
-
tenantIdByRecord[rid] = null
|
|
146
|
-
organizationIdByRecord[rid] = null
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const cfValues = recordIds.length
|
|
150
|
-
? await loadCustomFieldValues({
|
|
151
|
-
em,
|
|
152
|
-
entityId: E.directory.tenant,
|
|
153
|
-
recordIds,
|
|
154
|
-
tenantIdByRecord,
|
|
155
|
-
organizationIdByRecord,
|
|
156
|
-
tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],
|
|
157
|
-
})
|
|
158
|
-
: {}
|
|
159
|
-
|
|
160
139
|
const rawQuery = Array.from(url.searchParams.keys()).reduce<Record<string, unknown>>((acc, key) => {
|
|
161
140
|
const values = url.searchParams.getAll(key)
|
|
162
141
|
if (!values.length) return acc
|
|
@@ -175,29 +154,64 @@ export async function GET(req: Request) {
|
|
|
175
154
|
return [normalizedKey, condition] as const
|
|
176
155
|
})
|
|
177
156
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
157
|
+
const loadCfValues = (tenants: Tenant[]): Promise<Record<string, Record<string, unknown>>> => {
|
|
158
|
+
const recordIds = tenants.map((tenant) => String(tenant.id))
|
|
159
|
+
if (!recordIds.length) return Promise.resolve({})
|
|
160
|
+
const tenantIdByRecord: Record<string, string | null> = {}
|
|
161
|
+
const organizationIdByRecord: Record<string, string | null> = {}
|
|
162
|
+
for (const rid of recordIds) {
|
|
163
|
+
tenantIdByRecord[rid] = null
|
|
164
|
+
organizationIdByRecord[rid] = null
|
|
165
|
+
}
|
|
166
|
+
return loadCustomFieldValues({
|
|
167
|
+
em,
|
|
168
|
+
entityId: E.directory.tenant,
|
|
169
|
+
recordIds,
|
|
170
|
+
tenantIdByRecord,
|
|
171
|
+
organizationIdByRecord,
|
|
172
|
+
tenantFallbacks: auth.tenantId ? [auth.tenantId] : [],
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const start = (page - 1) * pageSize
|
|
177
|
+
|
|
178
|
+
let pageTenants: Tenant[]
|
|
179
|
+
let total: number
|
|
180
|
+
let cfValues: Record<string, Record<string, unknown>>
|
|
181
|
+
|
|
182
|
+
if (cfFilterEntries.length) {
|
|
183
|
+
// Custom-field filters are matched in JS against separately-loaded values,
|
|
184
|
+
// so the full result set must be fetched before filtering and paging.
|
|
185
|
+
const all = await em.find(Tenant, where, { orderBy })
|
|
186
|
+
cfValues = await loadCfValues(all)
|
|
187
|
+
const filtered = all.filter((tenant) => {
|
|
188
|
+
const rid = String(tenant.id)
|
|
189
|
+
const payload = cfValues[rid] ?? {}
|
|
190
|
+
return cfFilterEntries.every(([key, expected]) => {
|
|
191
|
+
const value = payload[`cf_${key}`]
|
|
192
|
+
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
193
|
+
const maybeIn = (expected as { $in?: unknown[] }).$in
|
|
194
|
+
if (Array.isArray(maybeIn)) {
|
|
195
|
+
if (value === undefined || value === null) return false
|
|
196
|
+
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))
|
|
197
|
+
return maybeIn.includes(value)
|
|
191
198
|
}
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
}
|
|
200
|
+
return matchesValue(value, expected)
|
|
194
201
|
})
|
|
195
|
-
|
|
202
|
+
})
|
|
203
|
+
total = filtered.length
|
|
204
|
+
pageTenants = filtered.slice(start, start + pageSize)
|
|
205
|
+
} else {
|
|
206
|
+
// No JS-side filtering applies, so pagination is pushed to the database
|
|
207
|
+
// instead of fetching the whole table and slicing in memory.
|
|
208
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })
|
|
209
|
+
total = count
|
|
210
|
+
pageTenants = rows
|
|
211
|
+
cfValues = await loadCfValues(rows)
|
|
212
|
+
}
|
|
196
213
|
|
|
197
|
-
const
|
|
198
|
-
const start = (page - 1) * pageSize
|
|
199
|
-
const paged = filtered.slice(start, start + pageSize)
|
|
200
|
-
const items = paged.map((tenant) => {
|
|
214
|
+
const items = pageTenants.map((tenant) => {
|
|
201
215
|
const rid = String(tenant.id)
|
|
202
216
|
const cf = cfValues[rid] ?? {}
|
|
203
217
|
return toRow(tenant, cf)
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'entities.definitions.view', title: 'View custom field definitions', module: 'entities' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'entities.definitions.manage',
|
|
5
|
+
title: 'Manage custom field definitions',
|
|
6
|
+
module: 'entities',
|
|
7
|
+
dependsOn: ['entities.definitions.view'],
|
|
8
|
+
},
|
|
4
9
|
{ id: 'entities.records.view', title: 'View records', module: 'entities' },
|
|
5
|
-
{
|
|
10
|
+
{
|
|
11
|
+
id: 'entities.records.manage',
|
|
12
|
+
title: 'Manage records',
|
|
13
|
+
module: 'entities',
|
|
14
|
+
dependsOn: ['entities.records.view'],
|
|
15
|
+
},
|
|
6
16
|
]
|
|
7
17
|
|
|
8
18
|
export default features
|
|
@@ -5,7 +5,10 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
6
|
import { emitIntegrationsEvent } from '../../../events'
|
|
7
7
|
import { saveCredentialsSchema } from '../../../data/validators'
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
isCredentialsEncryptionUnavailableError,
|
|
10
|
+
type CredentialsService,
|
|
11
|
+
} from '../../../lib/credentials-service'
|
|
9
12
|
import {
|
|
10
13
|
resolveUserFeatures,
|
|
11
14
|
runIntegrationMutationGuardAfterSuccess,
|
|
@@ -53,7 +56,15 @@ export async function GET(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
53
56
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
54
57
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
let values: Record<string, unknown> | null
|
|
60
|
+
try {
|
|
61
|
+
values = await credentialsService.resolve(integration.id, scope)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
64
|
+
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
65
|
+
}
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
57
68
|
|
|
58
69
|
return NextResponse.json({
|
|
59
70
|
integrationId: integration.id,
|
|
@@ -118,7 +129,14 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
118
129
|
const credentialsService = container.resolve('integrationCredentialsService') as CredentialsService
|
|
119
130
|
const scope = { organizationId: auth.orgId as string, tenantId: auth.tenantId }
|
|
120
131
|
|
|
121
|
-
|
|
132
|
+
try {
|
|
133
|
+
await credentialsService.save(integration.id, payloadData.credentials, scope)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (isCredentialsEncryptionUnavailableError(error)) {
|
|
136
|
+
return NextResponse.json({ error: 'Integration credentials encryption is unavailable' }, { status: 503 })
|
|
137
|
+
}
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
122
140
|
|
|
123
141
|
await emitIntegrationsEvent('integrations.credentials.updated', {
|
|
124
142
|
integrationId: integration.id,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
2
|
import { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
|
|
4
3
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -14,7 +13,19 @@ import { EncryptionMap } from '../../entities/data/entities'
|
|
|
14
13
|
import { IntegrationCredentials } from '../data/entities'
|
|
15
14
|
|
|
16
15
|
const ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'
|
|
17
|
-
const
|
|
16
|
+
const CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE =
|
|
17
|
+
'Integration credentials encryption key is unavailable. Configure Vault KMS or TENANT_DATA_ENCRYPTION_FALLBACK_KEY.'
|
|
18
|
+
|
|
19
|
+
export class CredentialsEncryptionUnavailableError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(CREDENTIALS_ENCRYPTION_UNAVAILABLE_MESSAGE)
|
|
22
|
+
this.name = 'CredentialsEncryptionUnavailableError'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isCredentialsEncryptionUnavailableError(error: unknown): error is CredentialsEncryptionUnavailableError {
|
|
27
|
+
return error instanceof CredentialsEncryptionUnavailableError
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
function isRecordValue(value: unknown): value is Record<string, unknown> {
|
|
20
31
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -28,35 +39,6 @@ function normalizeCredentialsRecord(value: unknown): Record<string, unknown> {
|
|
|
28
39
|
return isRecordValue(parsed) ? parsed : {}
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
function resolveFallbackEncryptionSecret(): string {
|
|
32
|
-
const candidates = [
|
|
33
|
-
process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,
|
|
34
|
-
process.env.TENANT_DATA_ENCRYPTION_KEY,
|
|
35
|
-
process.env.AUTH_SECRET,
|
|
36
|
-
process.env.NEXTAUTH_SECRET,
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
for (const value of candidates) {
|
|
40
|
-
const normalized = value?.trim()
|
|
41
|
-
if (normalized) return normalized
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (process.env.NODE_ENV !== 'production') return 'om-dev-tenant-encryption'
|
|
45
|
-
|
|
46
|
-
console.warn(
|
|
47
|
-
'[integrations.credentials] No encryption secret configured; using emergency fallback secret. Configure TENANT_DATA_ENCRYPTION_FALLBACK_KEY immediately.',
|
|
48
|
-
)
|
|
49
|
-
return 'om-emergency-fallback-rotate-me'
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function deriveDekFromSecret(secret: string, tenantId: string): string {
|
|
53
|
-
return crypto
|
|
54
|
-
.createHash('sha256')
|
|
55
|
-
.update(`${DERIVED_KEY_CONTEXT}:${tenantId}:${secret}`)
|
|
56
|
-
.digest()
|
|
57
|
-
.toString('base64')
|
|
58
|
-
}
|
|
59
|
-
|
|
60
42
|
export function createCredentialsService(em: EntityManager) {
|
|
61
43
|
const credentialsEncryptionSpec = [{ field: 'credentials' }]
|
|
62
44
|
|
|
@@ -100,7 +82,7 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
100
82
|
const created = await kms.createTenantDek(scope.tenantId)
|
|
101
83
|
if (created?.key) return created.key
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
throw new CredentialsEncryptionUnavailableError()
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
async function encryptCredentialsBlob(
|
|
@@ -162,8 +144,8 @@ export function createCredentialsService(em: EntityManager) {
|
|
|
162
144
|
},
|
|
163
145
|
|
|
164
146
|
async save(integrationId: string, credentials: Record<string, unknown>, scope: IntegrationScope): Promise<void> {
|
|
165
|
-
await ensureCredentialsEncryptionMap(scope)
|
|
166
147
|
const encryptedCredentials = await encryptCredentialsBlob(credentials, scope)
|
|
148
|
+
await ensureCredentialsEncryptionMap(scope)
|
|
167
149
|
|
|
168
150
|
const row = await findOneWithDecryption(
|
|
169
151
|
em,
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'query_index.status.view', title: 'View index status', module: 'query_index' },
|
|
3
|
-
{
|
|
4
|
-
|
|
3
|
+
{
|
|
4
|
+
id: 'query_index.reindex',
|
|
5
|
+
title: 'Trigger reindex',
|
|
6
|
+
module: 'query_index',
|
|
7
|
+
dependsOn: ['query_index.status.view'],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: 'query_index.purge',
|
|
11
|
+
title: 'Purge index',
|
|
12
|
+
module: 'query_index',
|
|
13
|
+
dependsOn: ['query_index.status.view'],
|
|
14
|
+
},
|
|
5
15
|
]
|
|
6
16
|
|
|
7
17
|
export default features
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'resources.view', title: 'View resources', module: 'resources' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'resources.manage_resources',
|
|
5
|
+
title: 'Manage resources',
|
|
6
|
+
module: 'resources',
|
|
7
|
+
dependsOn: ['resources.view'],
|
|
8
|
+
},
|
|
4
9
|
]
|
|
5
10
|
|
|
6
11
|
export default features
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const features = [
|
|
2
2
|
{ id: 'sync_excel.view', title: 'View sync_excel uploads and previews', module: 'sync_excel' },
|
|
3
|
-
{
|
|
3
|
+
{
|
|
4
|
+
id: 'sync_excel.run',
|
|
5
|
+
title: 'Upload CSV files and prepare imports',
|
|
6
|
+
module: 'sync_excel',
|
|
7
|
+
dependsOn: ['sync_excel.view'],
|
|
8
|
+
},
|
|
4
9
|
]
|
|
5
10
|
|
|
6
11
|
export default features
|