plugin-cluster-manager 1.1.10 → 1.1.13

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.
Files changed (119) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/client.js +1 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
  6. package/dist/client-v2/index.js +10 -0
  7. package/dist/externalVersion.js +6 -5
  8. package/dist/locale/en-US.json +138 -28
  9. package/dist/locale/vi-VN.json +139 -28
  10. package/dist/locale/zh-CN.json +140 -28
  11. package/dist/server/actions/cache-monitor.js +301 -0
  12. package/dist/server/actions/cluster-nodes.js +391 -11
  13. package/dist/server/actions/doctor.js +1246 -0
  14. package/dist/server/actions/orchestrator.js +37 -0
  15. package/dist/server/actions/queue-mappings.js +107 -0
  16. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  17. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  18. package/dist/server/collections/worker-queue-mappings.js +106 -0
  19. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  20. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  21. package/dist/server/orchestrator/PackageManager.js +21 -24
  22. package/dist/server/orchestrator/docker-adapter.js +49 -27
  23. package/dist/server/plugin.js +71 -16
  24. package/dist/server/queue-scanner.js +141 -0
  25. package/dist/server/utils/node.js +30 -2
  26. package/dist/server/utils/versionManager.js +91 -0
  27. package/package.json +9 -5
  28. package/server.js +1 -0
  29. package/src/client/AclCacheManager.tsx +292 -287
  30. package/src/client/CacheMonitor.tsx +166 -179
  31. package/src/client/ClusterManagerLayout.tsx +54 -42
  32. package/src/client/ClusterNodes.tsx +698 -418
  33. package/src/client/ContainerOrchestrator.tsx +184 -102
  34. package/src/client/Doctor.tsx +559 -0
  35. package/src/client/NginxCacheManager.tsx +415 -0
  36. package/src/client/PluginOperations.tsx +234 -234
  37. package/src/client/QueueAssignment.tsx +355 -0
  38. package/src/client/TaskManager.tsx +194 -187
  39. package/src/client/WorkflowExecutions.tsx +243 -238
  40. package/src/client/index.tsx +22 -14
  41. package/src/client/utils/clientSafeCache.ts +41 -0
  42. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  43. package/src/client-v2/plugin.tsx +24 -0
  44. package/src/locale/en-US.json +138 -28
  45. package/src/locale/vi-VN.json +139 -28
  46. package/src/locale/zh-CN.json +140 -28
  47. package/src/server/__tests__/doctor.test.ts +53 -0
  48. package/src/server/actions/acl-cache.ts +272 -272
  49. package/src/server/actions/cache-monitor.ts +453 -116
  50. package/src/server/actions/cluster-nodes.ts +878 -378
  51. package/src/server/actions/doctor.ts +1536 -0
  52. package/src/server/actions/orchestrator.ts +54 -2
  53. package/src/server/actions/queue-mappings.ts +94 -0
  54. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  55. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  56. package/src/server/collections/worker-queue-mappings.ts +85 -0
  57. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  58. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  59. package/src/server/orchestrator/PackageManager.ts +20 -24
  60. package/src/server/orchestrator/docker-adapter.ts +74 -37
  61. package/src/server/plugin.ts +347 -270
  62. package/src/server/queue-scanner.ts +154 -0
  63. package/src/server/utils/node.ts +48 -0
  64. package/src/server/utils/versionManager.ts +69 -0
  65. package/dist/client/AclCacheManager.d.ts +0 -2
  66. package/dist/client/CacheMonitor.d.ts +0 -2
  67. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  68. package/dist/client/ClusterNodes.d.ts +0 -2
  69. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  70. package/dist/client/EventQueueMonitor.d.ts +0 -2
  71. package/dist/client/LockMonitor.d.ts +0 -2
  72. package/dist/client/PackageInstaller.d.ts +0 -2
  73. package/dist/client/PluginOperations.d.ts +0 -2
  74. package/dist/client/RedisMonitor.d.ts +0 -2
  75. package/dist/client/TaskManager.d.ts +0 -2
  76. package/dist/client/WorkflowExecutions.d.ts +0 -2
  77. package/dist/client/index.d.ts +0 -5
  78. package/dist/client/utils.d.ts +0 -12
  79. package/dist/index.d.ts +0 -2
  80. package/dist/server/actions/acl-cache.d.ts +0 -53
  81. package/dist/server/actions/cache-monitor.d.ts +0 -23
  82. package/dist/server/actions/cluster-nodes.d.ts +0 -49
  83. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  84. package/dist/server/actions/lock-monitor.d.ts +0 -19
  85. package/dist/server/actions/orchestrator.d.ts +0 -58
  86. package/dist/server/actions/package-manager.d.ts +0 -6
  87. package/dist/server/actions/plugin-operations.d.ts +0 -6
  88. package/dist/server/actions/redis-monitor.d.ts +0 -12
  89. package/dist/server/actions/tasks.d.ts +0 -7
  90. package/dist/server/actions/workflow-executions.d.ts +0 -7
  91. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  92. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  93. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  94. package/dist/server/collections/app.d.ts +0 -8
  95. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  96. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  97. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  98. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  99. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  100. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  101. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  102. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  103. package/dist/server/collections/cluster-manager.d.ts +0 -22
  104. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  105. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  106. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  107. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  108. package/dist/server/collections/worker-packages.d.ts +0 -22
  109. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  110. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  111. package/dist/server/orchestrator/index.d.ts +0 -4
  112. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  113. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  114. package/dist/server/orchestrator/types.d.ts +0 -84
  115. package/dist/server/plugin.d.ts +0 -26
  116. package/dist/server/utils/node.d.ts +0 -6
  117. package/dist/server/utils/redis.d.ts +0 -29
  118. package/dist/shared/packages.d.ts +0 -23
  119. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -1,272 +1,272 @@
1
- import { Context } from '@nocobase/actions';
2
- import { scanKeys, deleteKeysChunked } from '../utils/redis';
3
-
4
- /**
5
- * In-memory ACL stats counter.
6
- * Tracks total checks, cache hits/misses per role:resource:action.
7
- */
8
- export interface AclCacheStats {
9
- totalChecks: number;
10
- cacheHits: number;
11
- cacheMisses: number;
12
- startedAt: string;
13
- detailByRole: Record<string, { checks: number; hits: number; misses: number }>;
14
- }
15
-
16
- const stats: AclCacheStats = {
17
- totalChecks: 0,
18
- cacheHits: 0,
19
- cacheMisses: 0,
20
- startedAt: new Date().toISOString(),
21
- detailByRole: {},
22
- };
23
-
24
- const ACL_CACHE_PREFIX = 'acl:can:';
25
- const ACL_CACHE_TTL = 60 * 5; // 5 minutes default
26
-
27
- function getCacheKey(role: string, resource: string, action: string): string {
28
- return `${ACL_CACHE_PREFIX}${role}:${resource}:${action}`;
29
- }
30
-
31
- function recordStat(role: string, hit: boolean) {
32
- stats.totalChecks++;
33
- if (hit) {
34
- stats.cacheHits++;
35
- } else {
36
- stats.cacheMisses++;
37
- }
38
- if (!stats.detailByRole[role]) {
39
- stats.detailByRole[role] = { checks: 0, hits: 0, misses: 0 };
40
- }
41
- stats.detailByRole[role].checks++;
42
- if (hit) {
43
- stats.detailByRole[role].hits++;
44
- } else {
45
- stats.detailByRole[role].misses++;
46
- }
47
- }
48
-
49
- /**
50
- * Middleware that caches the permission object computed by the ACL middleware.
51
- * Install via: app.acl.use(aclCacheMiddleware, { tag: 'aclCache', before: 'core', after: 'allow-manager' })
52
- *
53
- * FIX: Previously monkey-patched ctx.app.acl.can per-request, which is a race condition
54
- * because acl is a shared singleton across concurrent requests. Now we use a post-check
55
- * approach that reads from cache first and writes after the ACL middleware runs, without
56
- * ever replacing the shared acl.can method.
57
- */
58
- export function createAclCacheMiddleware(app: any) {
59
- return async function aclCacheMiddleware(ctx: Context, next: () => Promise<void>) {
60
- const cache = app.cache;
61
- if (!cache) {
62
- return next();
63
- }
64
-
65
- const role = ctx.state?.currentRole;
66
- const resourceName = ctx.permission?.resourceName || ctx.action?.resourceName;
67
- const actionName = ctx.permission?.actionName || ctx.action?.actionName;
68
-
69
- if (!role || !resourceName || !actionName) {
70
- return next();
71
- }
72
-
73
- const cacheKey = getCacheKey(role, resourceName, actionName);
74
-
75
- // Try reading from cache first
76
- try {
77
- const cached = await cache.get(cacheKey);
78
- if (cached !== undefined && cached !== null) {
79
- recordStat(role, true);
80
- if (cached === '__DENIED__') {
81
- ctx.throw(403, 'No permissions');
82
- return;
83
- }
84
-
85
- // Replace the permission object computed for this request with the cached value.
86
- ctx.permission = ctx.permission || {};
87
- ctx.permission.can = JSON.parse(cached);
88
- return next();
89
- }
90
- } catch {
91
- // Cache read failed, proceed normally
92
- }
93
-
94
- recordStat(role, false);
95
-
96
- // Let the rest of the ACL middleware run normally. Cache explicit denials too,
97
- // but never turn a cached denial into a skipped permission check.
98
- try {
99
- await next();
100
- } catch (error: any) {
101
- if (error?.status === 403 || error?.statusCode === 403) {
102
- cache.set(cacheKey, '__DENIED__', ACL_CACHE_TTL).catch(() => {});
103
- }
104
- throw error;
105
- }
106
-
107
- // After ACL ran, cache the permission result for future requests
108
- // This is safe because we read ctx.permission AFTER next() completes —
109
- // no monkey-patching of shared singletons.
110
- try {
111
- const result = ctx.permission?.can as any;
112
- const valueToCache = JSON.stringify(result !== undefined && result !== null ? result : true);
113
- cache.set(cacheKey, valueToCache, ACL_CACHE_TTL).catch(() => {});
114
- } catch {
115
- // Ignore cache write errors
116
- }
117
- };
118
- }
119
-
120
- export const aclCacheActions = {
121
- /**
122
- * GET /clusterManagerAclCache:stats
123
- * Returns ACL cache hit/miss statistics
124
- */
125
- async stats(ctx: Context, next: () => Promise<void>) {
126
- const hitRate =
127
- stats.totalChecks > 0 ? Math.round((stats.cacheHits / stats.totalChecks) * 10000) / 100 : 0;
128
-
129
- // Count cached keys using SCAN (production-safe)
130
- let cachedKeys = 0;
131
- try {
132
- const redis = (ctx.app as any).redisConnectionManager?.getConnection();
133
- if (redis) {
134
- const keys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
135
- cachedKeys = keys.length;
136
- }
137
- } catch {
138
- // Redis not available
139
- }
140
-
141
- ctx.body = {
142
- ...stats,
143
- hitRate,
144
- cachedKeys,
145
- ttlSeconds: ACL_CACHE_TTL,
146
- _note: 'Stats are per-node. In HA clusters each node tracks its own counters independently.',
147
- };
148
- await next();
149
- },
150
-
151
- /**
152
- * GET /clusterManagerAclCache:listKeys
153
- * Lists all cached ACL permission keys
154
- */
155
- async listKeys(ctx: Context, next: () => Promise<void>) {
156
- const keys: { key: string; role: string; resource: string; action: string }[] = [];
157
-
158
- try {
159
- const redis = (ctx.app as any).redisConnectionManager?.getConnection();
160
- if (redis) {
161
- const rawKeys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
162
- for (const key of rawKeys) {
163
- const parts = key.replace(ACL_CACHE_PREFIX, '').split(':');
164
- keys.push({
165
- key,
166
- role: parts[0] || '',
167
- resource: parts[1] || '',
168
- action: parts[2] || '',
169
- });
170
- }
171
- }
172
- } catch {
173
- // Redis not available
174
- }
175
-
176
- ctx.body = { data: keys, meta: { count: keys.length } };
177
- await next();
178
- },
179
-
180
- /**
181
- * POST /clusterManagerAclCache:clear
182
- * Clear all ACL cache entries and reset stats
183
- */
184
- async clear(ctx: Context, next: () => Promise<void>) {
185
- const user = ctx.state?.currentUser?.nickname || ctx.state?.currentUser?.id || 'unknown';
186
- ctx.app.logger.info(`[cluster-manager] Clearing all ACL cache by user ${user}`);
187
- let deletedCount = 0;
188
-
189
- try {
190
- const redis = (ctx.app as any).redisConnectionManager?.getConnection();
191
- if (redis) {
192
- const rawKeys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
193
- if (rawKeys.length > 0) {
194
- deletedCount = await deleteKeysChunked(redis, rawKeys);
195
- }
196
- }
197
- } catch {
198
- // Fallback: clear through app cache
199
- try {
200
- await ctx.app.cache.reset?.();
201
- } catch {
202
- // Ignore
203
- }
204
- }
205
-
206
- // Also clear role caches
207
- try {
208
- const redis = (ctx.app as any).redisConnectionManager?.getConnection();
209
- if (redis) {
210
- const roleKeys = await scanKeys(redis, 'roles:*');
211
- if (roleKeys.length > 0) {
212
- deletedCount += await deleteKeysChunked(redis, roleKeys);
213
- }
214
- // Also clear system settings cache
215
- try {
216
- await redis.sendCommand(['DEL', 'app:systemSettings']);
217
- deletedCount++;
218
- } catch {
219
- // Ignore if clear fails
220
- }
221
- }
222
- } catch {
223
- // Redis not available
224
- }
225
-
226
- ctx.body = { success: true, deletedCount };
227
- await next();
228
- },
229
-
230
- /**
231
- * POST /clusterManagerAclCache:resetStats
232
- * Reset the in-memory ACL stats counters
233
- */
234
- async resetStats(ctx: Context, next: () => Promise<void>) {
235
- stats.totalChecks = 0;
236
- stats.cacheHits = 0;
237
- stats.cacheMisses = 0;
238
- stats.startedAt = new Date().toISOString();
239
- stats.detailByRole = {};
240
-
241
- ctx.body = { success: true };
242
- await next();
243
- },
244
-
245
- /**
246
- * POST /clusterManagerAclCache:clearRole
247
- * Clear ACL cache entries for a specific role
248
- */
249
- async clearRole(ctx: Context, next: () => Promise<void>) {
250
- const { roleName } = ctx.action.params.values || ctx.action.params;
251
- if (!roleName) {
252
- ctx.throw(400, 'roleName is required');
253
- }
254
-
255
- let deletedCount = 0;
256
- try {
257
- const redis = (ctx.app as any).redisConnectionManager?.getConnection();
258
- if (redis) {
259
- const pattern = `*${ACL_CACHE_PREFIX}${roleName}:*`;
260
- const rawKeys = await scanKeys(redis, pattern);
261
- if (rawKeys.length > 0) {
262
- deletedCount = await deleteKeysChunked(redis, rawKeys);
263
- }
264
- }
265
- } catch {
266
- // Redis not available
267
- }
268
-
269
- ctx.body = { success: true, deletedCount };
270
- await next();
271
- },
272
- };
1
+ import { Context } from '@nocobase/actions';
2
+ import { scanKeys, deleteKeysChunked } from '../utils/redis';
3
+
4
+ /**
5
+ * In-memory ACL stats counter.
6
+ * Tracks total checks, cache hits/misses per role:resource:action.
7
+ */
8
+ export interface AclCacheStats {
9
+ totalChecks: number;
10
+ cacheHits: number;
11
+ cacheMisses: number;
12
+ startedAt: string;
13
+ detailByRole: Record<string, { checks: number; hits: number; misses: number }>;
14
+ }
15
+
16
+ const stats: AclCacheStats = {
17
+ totalChecks: 0,
18
+ cacheHits: 0,
19
+ cacheMisses: 0,
20
+ startedAt: new Date().toISOString(),
21
+ detailByRole: {},
22
+ };
23
+
24
+ const ACL_CACHE_PREFIX = 'acl:can:';
25
+ const ACL_CACHE_TTL = 60 * 5; // 5 minutes default
26
+
27
+ function getCacheKey(role: string, resource: string, action: string): string {
28
+ return `${ACL_CACHE_PREFIX}${role}:${resource}:${action}`;
29
+ }
30
+
31
+ function recordStat(role: string, hit: boolean) {
32
+ stats.totalChecks++;
33
+ if (hit) {
34
+ stats.cacheHits++;
35
+ } else {
36
+ stats.cacheMisses++;
37
+ }
38
+ if (!stats.detailByRole[role]) {
39
+ stats.detailByRole[role] = { checks: 0, hits: 0, misses: 0 };
40
+ }
41
+ stats.detailByRole[role].checks++;
42
+ if (hit) {
43
+ stats.detailByRole[role].hits++;
44
+ } else {
45
+ stats.detailByRole[role].misses++;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Middleware that caches the permission object computed by the ACL middleware.
51
+ * Install via: app.acl.use(aclCacheMiddleware, { tag: 'aclCache', before: 'core', after: 'allow-manager' })
52
+ *
53
+ * FIX: Previously monkey-patched ctx.app.acl.can per-request, which is a race condition
54
+ * because acl is a shared singleton across concurrent requests. Now we use a post-check
55
+ * approach that reads from cache first and writes after the ACL middleware runs, without
56
+ * ever replacing the shared acl.can method.
57
+ */
58
+ export function createAclCacheMiddleware(app: any) {
59
+ return async function aclCacheMiddleware(ctx: Context, next: () => Promise<void>) {
60
+ const cache = app.cache;
61
+ if (!cache) {
62
+ return next();
63
+ }
64
+
65
+ const role = ctx.state?.currentRole;
66
+ const resourceName = ctx.permission?.resourceName || ctx.action?.resourceName;
67
+ const actionName = ctx.permission?.actionName || ctx.action?.actionName;
68
+
69
+ if (!role || !resourceName || !actionName) {
70
+ return next();
71
+ }
72
+
73
+ const cacheKey = getCacheKey(role, resourceName, actionName);
74
+
75
+ // Try reading from cache first
76
+ try {
77
+ const cached = await cache.get(cacheKey);
78
+ if (cached !== undefined && cached !== null) {
79
+ recordStat(role, true);
80
+ if (cached === '__DENIED__') {
81
+ ctx.throw(403, 'No permissions');
82
+ return;
83
+ }
84
+
85
+ // Replace the permission object computed for this request with the cached value.
86
+ ctx.permission = ctx.permission || {};
87
+ ctx.permission.can = JSON.parse(cached);
88
+ return next();
89
+ }
90
+ } catch {
91
+ // Cache read failed, proceed normally
92
+ }
93
+
94
+ recordStat(role, false);
95
+
96
+ // Let the rest of the ACL middleware run normally. Cache explicit denials too,
97
+ // but never turn a cached denial into a skipped permission check.
98
+ try {
99
+ await next();
100
+ } catch (error: any) {
101
+ if (error?.status === 403 || error?.statusCode === 403) {
102
+ cache.set(cacheKey, '__DENIED__', ACL_CACHE_TTL).catch(() => {});
103
+ }
104
+ throw error;
105
+ }
106
+
107
+ // After ACL ran, cache the permission result for future requests
108
+ // This is safe because we read ctx.permission AFTER next() completes —
109
+ // no monkey-patching of shared singletons.
110
+ try {
111
+ const result = ctx.permission?.can as any;
112
+ const valueToCache = JSON.stringify(result !== undefined && result !== null ? result : true);
113
+ cache.set(cacheKey, valueToCache, ACL_CACHE_TTL).catch(() => {});
114
+ } catch {
115
+ // Ignore cache write errors
116
+ }
117
+ };
118
+ }
119
+
120
+ export const aclCacheActions = {
121
+ /**
122
+ * GET /clusterManagerAclCache:stats
123
+ * Returns ACL cache hit/miss statistics
124
+ */
125
+ async stats(ctx: Context, next: () => Promise<void>) {
126
+ const hitRate =
127
+ stats.totalChecks > 0 ? Math.round((stats.cacheHits / stats.totalChecks) * 10000) / 100 : 0;
128
+
129
+ // Count cached keys using SCAN (production-safe)
130
+ let cachedKeys = 0;
131
+ try {
132
+ const redis = (ctx.app as any).redisConnectionManager?.getConnection();
133
+ if (redis) {
134
+ const keys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
135
+ cachedKeys = keys.length;
136
+ }
137
+ } catch {
138
+ // Redis not available
139
+ }
140
+
141
+ ctx.body = {
142
+ ...stats,
143
+ hitRate,
144
+ cachedKeys,
145
+ ttlSeconds: ACL_CACHE_TTL,
146
+ _note: 'Stats are per-node. In HA clusters each node tracks its own counters independently.',
147
+ };
148
+ await next();
149
+ },
150
+
151
+ /**
152
+ * GET /clusterManagerAclCache:listKeys
153
+ * Lists all cached ACL permission keys
154
+ */
155
+ async listKeys(ctx: Context, next: () => Promise<void>) {
156
+ const keys: { key: string; role: string; resource: string; action: string }[] = [];
157
+
158
+ try {
159
+ const redis = (ctx.app as any).redisConnectionManager?.getConnection();
160
+ if (redis) {
161
+ const rawKeys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
162
+ for (const key of rawKeys) {
163
+ const parts = key.replace(ACL_CACHE_PREFIX, '').split(':');
164
+ keys.push({
165
+ key,
166
+ role: parts[0] || '',
167
+ resource: parts[1] || '',
168
+ action: parts[2] || '',
169
+ });
170
+ }
171
+ }
172
+ } catch {
173
+ // Redis not available
174
+ }
175
+
176
+ ctx.body = { data: keys, meta: { count: keys.length } };
177
+ await next();
178
+ },
179
+
180
+ /**
181
+ * POST /clusterManagerAclCache:clear
182
+ * Clear all ACL cache entries and reset stats
183
+ */
184
+ async clear(ctx: Context, next: () => Promise<void>) {
185
+ const user = ctx.state?.currentUser?.nickname || ctx.state?.currentUser?.id || 'unknown';
186
+ ctx.app.logger.info(`[cluster-manager] Clearing all ACL cache by user ${user}`);
187
+ let deletedCount = 0;
188
+
189
+ try {
190
+ const redis = (ctx.app as any).redisConnectionManager?.getConnection();
191
+ if (redis) {
192
+ const rawKeys = await scanKeys(redis, `*${ACL_CACHE_PREFIX}*`);
193
+ if (rawKeys.length > 0) {
194
+ deletedCount = await deleteKeysChunked(redis, rawKeys);
195
+ }
196
+ }
197
+ } catch {
198
+ // Fallback: clear through app cache
199
+ try {
200
+ await ctx.app.cache.reset?.();
201
+ } catch {
202
+ // Ignore
203
+ }
204
+ }
205
+
206
+ // Also clear role caches
207
+ try {
208
+ const redis = (ctx.app as any).redisConnectionManager?.getConnection();
209
+ if (redis) {
210
+ const roleKeys = await scanKeys(redis, 'roles:*');
211
+ if (roleKeys.length > 0) {
212
+ deletedCount += await deleteKeysChunked(redis, roleKeys);
213
+ }
214
+ // Also clear system settings cache
215
+ try {
216
+ await redis.sendCommand(['DEL', 'app:systemSettings']);
217
+ deletedCount++;
218
+ } catch {
219
+ // Ignore if clear fails
220
+ }
221
+ }
222
+ } catch {
223
+ // Redis not available
224
+ }
225
+
226
+ ctx.body = { success: true, deletedCount };
227
+ await next();
228
+ },
229
+
230
+ /**
231
+ * POST /clusterManagerAclCache:resetStats
232
+ * Reset the in-memory ACL stats counters
233
+ */
234
+ async resetStats(ctx: Context, next: () => Promise<void>) {
235
+ stats.totalChecks = 0;
236
+ stats.cacheHits = 0;
237
+ stats.cacheMisses = 0;
238
+ stats.startedAt = new Date().toISOString();
239
+ stats.detailByRole = {};
240
+
241
+ ctx.body = { success: true };
242
+ await next();
243
+ },
244
+
245
+ /**
246
+ * POST /clusterManagerAclCache:clearRole
247
+ * Clear ACL cache entries for a specific role
248
+ */
249
+ async clearRole(ctx: Context, next: () => Promise<void>) {
250
+ const { roleName } = ctx.action.params.values || ctx.action.params;
251
+ if (!roleName) {
252
+ ctx.throw(400, 'roleName is required');
253
+ }
254
+
255
+ let deletedCount = 0;
256
+ try {
257
+ const redis = (ctx.app as any).redisConnectionManager?.getConnection();
258
+ if (redis) {
259
+ const pattern = `*${ACL_CACHE_PREFIX}${roleName}:*`;
260
+ const rawKeys = await scanKeys(redis, pattern);
261
+ if (rawKeys.length > 0) {
262
+ deletedCount = await deleteKeysChunked(redis, rawKeys);
263
+ }
264
+ }
265
+ } catch {
266
+ // Redis not available
267
+ }
268
+
269
+ ctx.body = { success: true, deletedCount };
270
+ await next();
271
+ },
272
+ };