@open-mercato/core 0.6.7-develop.6674.1.add2064cad → 0.6.7-develop.6676.1.7dad6df292
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/audit_logs/api/audit-logs/access/route.js +5 -1
- package/dist/modules/audit_logs/api/audit-logs/access/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +5 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js +5 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/readScope.js +12 -0
- package/dist/modules/audit_logs/api/audit-logs/readScope.js.map +7 -0
- package/dist/modules/directory/api/organization-switcher/route.js +84 -20
- package/dist/modules/directory/api/organization-switcher/route.js.map +2 -2
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +5 -1
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +11 -3
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/api/audit-logs/access/route.ts +5 -0
- package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +5 -0
- package/src/modules/audit_logs/api/audit-logs/actions/route.ts +5 -0
- package/src/modules/audit_logs/api/audit-logs/readScope.ts +28 -0
- package/src/modules/directory/api/organization-switcher/route.ts +97 -26
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +4 -1
- package/src/modules/directory/utils/organizationScope.ts +9 -3
|
@@ -6,12 +6,19 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
|
|
|
6
6
|
import { computeHierarchyForOrganizations, type ComputedHierarchy } from '@open-mercato/core/modules/directory/lib/hierarchy'
|
|
7
7
|
import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
|
|
8
8
|
import {
|
|
9
|
+
buildOrgScopeTenantCacheTag,
|
|
9
10
|
getSelectedOrganizationFromRequest,
|
|
10
11
|
getSelectedTenantFromRequest,
|
|
11
12
|
resolveOrganizationScope,
|
|
12
13
|
} from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
14
|
+
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
13
15
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
directoryTag,
|
|
18
|
+
directoryErrorSchema,
|
|
19
|
+
directoryIdSchema,
|
|
20
|
+
organizationSwitcherResponseSchema,
|
|
21
|
+
} from '../openapi'
|
|
15
22
|
import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
16
23
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
17
24
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
@@ -19,6 +26,21 @@ import type { FilterQuery } from '@mikro-orm/core'
|
|
|
19
26
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
20
27
|
|
|
21
28
|
const logger = createLogger('directory').child({ component: 'organization-switcher' })
|
|
29
|
+
const ORGANIZATION_SWITCHER_CACHE_TTL_MS = 60_000
|
|
30
|
+
const organizationSwitcherCachePayloadSchema = organizationSwitcherResponseSchema.or(
|
|
31
|
+
organizationSwitcherResponseSchema.pick({
|
|
32
|
+
items: true,
|
|
33
|
+
selectedId: true,
|
|
34
|
+
canManage: true,
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
function buildSelectionCacheKeyPart(rawSelected: string | null): string | null {
|
|
39
|
+
if (isAllOrganizationsSelection(rawSelected)) return 'mode:all'
|
|
40
|
+
if (rawSelected === null) return 'mode:none'
|
|
41
|
+
const parsedOrganizationId = directoryIdSchema.safeParse(rawSelected)
|
|
42
|
+
return parsedOrganizationId.success ? `org:${parsedOrganizationId.data.toLowerCase()}` : null
|
|
43
|
+
}
|
|
22
44
|
|
|
23
45
|
type OrganizationMenuNode = {
|
|
24
46
|
id: string
|
|
@@ -141,6 +163,49 @@ export async function GET(req: NextRequest) {
|
|
|
141
163
|
})
|
|
142
164
|
}
|
|
143
165
|
|
|
166
|
+
const rawSelected = getSelectedOrganizationFromRequest(req)
|
|
167
|
+
const requestedAll = isAllOrganizationsSelection(rawSelected)
|
|
168
|
+
const selectedKeyPart = buildSelectionCacheKeyPart(rawSelected)
|
|
169
|
+
const cacheKey = selectedKeyPart
|
|
170
|
+
? `org-switcher:v1:${auth.sub}:${tenantId}:${selectedKeyPart}`
|
|
171
|
+
: null
|
|
172
|
+
let cache: CacheStrategy | null = null
|
|
173
|
+
if (!actorIsSuperAdmin && cacheKey) {
|
|
174
|
+
try {
|
|
175
|
+
cache = container.resolve<CacheStrategy>('cache')
|
|
176
|
+
} catch {
|
|
177
|
+
cache = null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const logAccess = async (payload: { items: unknown[]; selectedId: string | null }) => {
|
|
182
|
+
await logCrudAccess({
|
|
183
|
+
container,
|
|
184
|
+
auth,
|
|
185
|
+
request: req,
|
|
186
|
+
items: payload.items,
|
|
187
|
+
idField: 'id',
|
|
188
|
+
resourceKind: 'directory.organization_switcher',
|
|
189
|
+
organizationId: payload.selectedId,
|
|
190
|
+
tenantId,
|
|
191
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (cache && cacheKey) {
|
|
196
|
+
const activeCache = cache
|
|
197
|
+
try {
|
|
198
|
+
const cached = await runWithCacheTenant(tenantId, () => activeCache.get(cacheKey))
|
|
199
|
+
const parsedCached = organizationSwitcherCachePayloadSchema.safeParse(cached)
|
|
200
|
+
if (parsedCached.success) {
|
|
201
|
+
await logAccess(parsedCached.data)
|
|
202
|
+
return NextResponse.json(parsedCached.data)
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
logger.warn('Failed to read organization switcher cache', { err })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
144
209
|
const scopedOrgId = actorTenantId && actorTenantId === tenantId ? auth.orgId ?? null : null
|
|
145
210
|
const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: scopedOrgId })
|
|
146
211
|
const aclIsSuperAdmin = acl?.isSuperAdmin === true
|
|
@@ -163,9 +228,7 @@ export async function GET(req: NextRequest) {
|
|
|
163
228
|
{ orderBy: { name: 'ASC' } },
|
|
164
229
|
)
|
|
165
230
|
const hierarchy = computeHierarchyForOrganizations(orgEntities, tenantId)
|
|
166
|
-
const rawSelected = getSelectedOrganizationFromRequest(req)
|
|
167
231
|
let hasSelectionCookie = rawSelected !== null
|
|
168
|
-
const requestedAll = isAllOrganizationsSelection(rawSelected)
|
|
169
232
|
const scope = await resolveOrganizationScope({
|
|
170
233
|
em,
|
|
171
234
|
rbac,
|
|
@@ -194,32 +257,40 @@ export async function GET(req: NextRequest) {
|
|
|
194
257
|
}
|
|
195
258
|
|
|
196
259
|
const showMenu = menuData.nodes.length > 0 || hasManageFeature || effectiveIsSuperAdmin
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
260
|
+
const response = showMenu
|
|
261
|
+
? {
|
|
262
|
+
items: menuData.nodes,
|
|
263
|
+
selectedId,
|
|
264
|
+
canManage: !!hasManageFeature,
|
|
265
|
+
canViewAllOrganizations: accessible === null,
|
|
266
|
+
tenantId,
|
|
267
|
+
tenants: tenantRecords,
|
|
268
|
+
isSuperAdmin: effectiveIsSuperAdmin,
|
|
269
|
+
}
|
|
270
|
+
: {
|
|
271
|
+
items: [],
|
|
272
|
+
selectedId: null,
|
|
273
|
+
canManage: false,
|
|
274
|
+
}
|
|
200
275
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
276
|
+
if (cache && cacheKey) {
|
|
277
|
+
const activeCache = cache
|
|
278
|
+
try {
|
|
279
|
+
await runWithCacheTenant(tenantId, () =>
|
|
280
|
+
activeCache.set(cacheKey, response, {
|
|
281
|
+
ttl: ORGANIZATION_SWITCHER_CACHE_TTL_MS,
|
|
282
|
+
tags: [
|
|
283
|
+
buildOrgScopeTenantCacheTag(tenantId),
|
|
284
|
+
`rbac:user:${auth.sub}`,
|
|
285
|
+
],
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
} catch (err) {
|
|
289
|
+
logger.warn('Failed to write organization switcher cache', { err })
|
|
290
|
+
}
|
|
210
291
|
}
|
|
211
292
|
|
|
212
|
-
await
|
|
213
|
-
container,
|
|
214
|
-
auth,
|
|
215
|
-
request: req,
|
|
216
|
-
items: response.items,
|
|
217
|
-
idField: 'id',
|
|
218
|
-
resourceKind: 'directory.organization_switcher',
|
|
219
|
-
organizationId: response.selectedId,
|
|
220
|
-
tenantId,
|
|
221
|
-
query: Object.fromEntries(url.searchParams.entries()),
|
|
222
|
-
})
|
|
293
|
+
await logAccess(response)
|
|
223
294
|
|
|
224
295
|
return NextResponse.json(response)
|
|
225
296
|
} catch (err) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// for races where the event fires after a request reads the cache.
|
|
9
9
|
|
|
10
10
|
import { buildOrgScopeTenantCacheTag } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
11
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
11
12
|
|
|
12
13
|
type CacheService = {
|
|
13
14
|
deleteByTags(tags: string[]): Promise<number>
|
|
@@ -34,7 +35,9 @@ export default async function handle(
|
|
|
34
35
|
}
|
|
35
36
|
if (!cache) return
|
|
36
37
|
try {
|
|
37
|
-
await
|
|
38
|
+
await runWithCacheTenant(tenantId, () =>
|
|
39
|
+
cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),
|
|
40
|
+
)
|
|
38
41
|
} catch {
|
|
39
42
|
// best-effort; TTL is the backstop.
|
|
40
43
|
}
|
|
@@ -5,7 +5,7 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
|
|
|
5
5
|
import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
8
|
-
import type
|
|
8
|
+
import { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
9
9
|
import { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'
|
|
10
10
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
11
11
|
|
|
@@ -104,11 +104,15 @@ function resolveCacheFromContainer(container: AwilixContainer | null | undefined
|
|
|
104
104
|
export async function invalidateOrganizationScopeCacheForUser(
|
|
105
105
|
container: AwilixContainer,
|
|
106
106
|
userId: string,
|
|
107
|
+
tenantId?: string | null,
|
|
107
108
|
): Promise<void> {
|
|
108
109
|
const cache = resolveCacheFromContainer(container)
|
|
109
110
|
if (!cache?.deleteByTags) return
|
|
110
111
|
try {
|
|
111
|
-
|
|
112
|
+
const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId
|
|
113
|
+
await runWithCacheTenant(cacheTenantId, () =>
|
|
114
|
+
cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),
|
|
115
|
+
)
|
|
112
116
|
} catch (err) {
|
|
113
117
|
logger.warn('Cache invalidate user failed', { err })
|
|
114
118
|
}
|
|
@@ -121,7 +125,9 @@ export async function invalidateOrganizationScopeCacheForTenant(
|
|
|
121
125
|
const cache = resolveCacheFromContainer(container)
|
|
122
126
|
if (!cache?.deleteByTags) return
|
|
123
127
|
try {
|
|
124
|
-
await
|
|
128
|
+
await runWithCacheTenant(tenantId, () =>
|
|
129
|
+
cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),
|
|
130
|
+
)
|
|
125
131
|
} catch (err) {
|
|
126
132
|
logger.warn('Cache invalidate tenant failed', { err })
|
|
127
133
|
}
|