@acarmisc/backstage-plugin-litellm-backend 0.3.5 → 0.4.1
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/config.d.ts +15 -0
- package/dist/client.d.ts +13 -1
- package/dist/client.js +61 -4
- package/dist/index.cjs.js +171 -6
- package/dist/index.cjs.js.map +2 -2
- package/dist/provisioning.d.ts +5 -0
- package/dist/provisioning.js +22 -0
- package/dist/router.js +99 -1
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/package.json +2 -2
package/config.d.ts
CHANGED
|
@@ -104,6 +104,21 @@ export interface Config {
|
|
|
104
104
|
* list/mint virtual keys without holding the master key. Disabled by
|
|
105
105
|
* default; enable explicitly when the CLI is in use.
|
|
106
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* Audit log access control. When set, the /audit tab in the plugin is
|
|
109
|
+
* only visible to members of the specified Backstage group.
|
|
110
|
+
*/
|
|
111
|
+
audit?: {
|
|
112
|
+
/**
|
|
113
|
+
* Backstage group entity ref whose members can view the audit log.
|
|
114
|
+
* The plugin ships a ready-made group at catalog/litellm-admins.yaml —
|
|
115
|
+
* register the root catalog-info.yaml and set this to
|
|
116
|
+
* "group:default/litellm-admins", then add members there.
|
|
117
|
+
* When omitted the audit tab is hidden for all users.
|
|
118
|
+
*/
|
|
119
|
+
group?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
107
122
|
bridge?: {
|
|
108
123
|
/**
|
|
109
124
|
* When true, mount the /bridge/keys, /bridge/keys (POST), /bridge/models
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse } from './types';
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse, AuditLogsParams, PaginatedAuditLogs } from './types';
|
|
2
2
|
export declare class LiteLLMClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private masterKey;
|
|
@@ -53,6 +53,10 @@ export declare class LiteLLMClient {
|
|
|
53
53
|
deleteKeys(request: DeleteKeyRequest): Promise<{
|
|
54
54
|
success: boolean;
|
|
55
55
|
}>;
|
|
56
|
+
blockKey(key: string): Promise<unknown>;
|
|
57
|
+
unblockKey(key: string): Promise<unknown>;
|
|
58
|
+
resetKeySpend(key: string): Promise<unknown>;
|
|
59
|
+
getAuditLogs(params: AuditLogsParams): Promise<PaginatedAuditLogs>;
|
|
56
60
|
/**
|
|
57
61
|
* Rotates an existing key in place, returning a fresh `sk-` secret while
|
|
58
62
|
* keeping the same alias/budget/limits. `token` is the hashed token LiteLLM
|
|
@@ -71,6 +75,14 @@ export declare class LiteLLMClient {
|
|
|
71
75
|
* or `mode`.
|
|
72
76
|
*/
|
|
73
77
|
listModels(): Promise<ModelInfo[]>;
|
|
78
|
+
/**
|
|
79
|
+
* LiteLLM's `/team/info` wraps the team row inside `team_info` (alongside
|
|
80
|
+
* sibling `keys` / `team_memberships` arrays), the same shape as
|
|
81
|
+
* `/user/info` wrapping the user row inside `user_info`. Without unwrapping,
|
|
82
|
+
* `team_alias`, `members_with_roles`, `models`, budgets and limits are all
|
|
83
|
+
* undefined, so the UI fell back to displaying the raw team_id / "Untitled
|
|
84
|
+
* team".
|
|
85
|
+
*/
|
|
74
86
|
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
75
87
|
private emptyUsage;
|
|
76
88
|
/**
|
package/dist/client.js
CHANGED
|
@@ -116,9 +116,6 @@ class LiteLLMClient {
|
|
|
116
116
|
}
|
|
117
117
|
toVirtualKey(k) {
|
|
118
118
|
return {
|
|
119
|
-
// The hashed `token` never leaves LiteLLM in a usable form; the
|
|
120
|
-
// masked `key_name` ("sk-...XXXX") is what the UI displays. Fall
|
|
121
|
-
// back to `token` only when `key_name` is missing.
|
|
122
119
|
key: k.key_name ?? k.token,
|
|
123
120
|
token: k.token,
|
|
124
121
|
key_alias: k.key_alias ?? undefined,
|
|
@@ -130,6 +127,7 @@ class LiteLLMClient {
|
|
|
130
127
|
rpm_limit: k.rpm_limit ?? undefined,
|
|
131
128
|
models: k.models ?? [],
|
|
132
129
|
user_id: k.user_id ?? undefined,
|
|
130
|
+
blocked: k.blocked ?? undefined,
|
|
133
131
|
};
|
|
134
132
|
}
|
|
135
133
|
/**
|
|
@@ -166,6 +164,46 @@ class LiteLLMClient {
|
|
|
166
164
|
body: JSON.stringify(request),
|
|
167
165
|
});
|
|
168
166
|
}
|
|
167
|
+
async blockKey(key) {
|
|
168
|
+
return this.request('/key/block', {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: JSON.stringify({ key }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async unblockKey(key) {
|
|
174
|
+
return this.request('/key/unblock', {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
body: JSON.stringify({ key }),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async resetKeySpend(key) {
|
|
180
|
+
return this.request(`/key/${encodeURIComponent(key)}/reset_spend`, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: JSON.stringify({ reset_to: 0 }),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async getAuditLogs(params) {
|
|
186
|
+
const query = new URLSearchParams();
|
|
187
|
+
if (params.page !== undefined)
|
|
188
|
+
query.set('page', String(params.page));
|
|
189
|
+
if (params.page_size !== undefined)
|
|
190
|
+
query.set('page_size', String(params.page_size));
|
|
191
|
+
if (params.start_date)
|
|
192
|
+
query.set('start_date', params.start_date);
|
|
193
|
+
if (params.end_date)
|
|
194
|
+
query.set('end_date', params.end_date);
|
|
195
|
+
if (params.action)
|
|
196
|
+
query.set('action', params.action);
|
|
197
|
+
if (params.table_name)
|
|
198
|
+
query.set('table_name', params.table_name);
|
|
199
|
+
if (params.changed_by)
|
|
200
|
+
query.set('changed_by', params.changed_by);
|
|
201
|
+
if (params.sort_by)
|
|
202
|
+
query.set('sort_by', params.sort_by);
|
|
203
|
+
if (params.sort_order)
|
|
204
|
+
query.set('sort_order', params.sort_order);
|
|
205
|
+
return this.request(`/audit?${query.toString()}`);
|
|
206
|
+
}
|
|
169
207
|
/**
|
|
170
208
|
* Rotates an existing key in place, returning a fresh `sk-` secret while
|
|
171
209
|
* keeping the same alias/budget/limits. `token` is the hashed token LiteLLM
|
|
@@ -223,8 +261,27 @@ class LiteLLMClient {
|
|
|
223
261
|
}))
|
|
224
262
|
.filter((m) => m.model_name);
|
|
225
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* LiteLLM's `/team/info` wraps the team row inside `team_info` (alongside
|
|
266
|
+
* sibling `keys` / `team_memberships` arrays), the same shape as
|
|
267
|
+
* `/user/info` wrapping the user row inside `user_info`. Without unwrapping,
|
|
268
|
+
* `team_alias`, `members_with_roles`, `models`, budgets and limits are all
|
|
269
|
+
* undefined, so the UI fell back to displaying the raw team_id / "Untitled
|
|
270
|
+
* team".
|
|
271
|
+
*/
|
|
226
272
|
async getTeamInfo(teamId) {
|
|
227
|
-
|
|
273
|
+
const raw = await this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
274
|
+
const inner = raw?.team_info ?? {};
|
|
275
|
+
return {
|
|
276
|
+
team_id: raw?.team_id ?? inner.team_id ?? teamId,
|
|
277
|
+
team_alias: inner.team_alias ?? raw?.team_alias,
|
|
278
|
+
max_budget: inner.max_budget ?? raw?.max_budget,
|
|
279
|
+
spend: inner.spend ?? raw?.spend ?? 0,
|
|
280
|
+
members_with_roles: inner.members_with_roles ?? raw?.members_with_roles,
|
|
281
|
+
models: inner.models ?? raw?.models,
|
|
282
|
+
tpm_limit: inner.tpm_limit ?? raw?.tpm_limit,
|
|
283
|
+
rpm_limit: inner.rpm_limit ?? raw?.rpm_limit,
|
|
284
|
+
};
|
|
228
285
|
}
|
|
229
286
|
emptyUsage() {
|
|
230
287
|
return {
|
package/dist/index.cjs.js
CHANGED
|
@@ -172,9 +172,6 @@ var LiteLLMClient = class {
|
|
|
172
172
|
}
|
|
173
173
|
toVirtualKey(k) {
|
|
174
174
|
return {
|
|
175
|
-
// The hashed `token` never leaves LiteLLM in a usable form; the
|
|
176
|
-
// masked `key_name` ("sk-...XXXX") is what the UI displays. Fall
|
|
177
|
-
// back to `token` only when `key_name` is missing.
|
|
178
175
|
key: k.key_name ?? k.token,
|
|
179
176
|
token: k.token,
|
|
180
177
|
key_alias: k.key_alias ?? void 0,
|
|
@@ -185,7 +182,8 @@ var LiteLLMClient = class {
|
|
|
185
182
|
tpm_limit: k.tpm_limit ?? void 0,
|
|
186
183
|
rpm_limit: k.rpm_limit ?? void 0,
|
|
187
184
|
models: k.models ?? [],
|
|
188
|
-
user_id: k.user_id ?? void 0
|
|
185
|
+
user_id: k.user_id ?? void 0,
|
|
186
|
+
blocked: k.blocked ?? void 0
|
|
189
187
|
};
|
|
190
188
|
}
|
|
191
189
|
/**
|
|
@@ -222,6 +220,37 @@ var LiteLLMClient = class {
|
|
|
222
220
|
body: JSON.stringify(request)
|
|
223
221
|
});
|
|
224
222
|
}
|
|
223
|
+
async blockKey(key) {
|
|
224
|
+
return this.request("/key/block", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify({ key })
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async unblockKey(key) {
|
|
230
|
+
return this.request("/key/unblock", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: JSON.stringify({ key })
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async resetKeySpend(key) {
|
|
236
|
+
return this.request(`/key/${encodeURIComponent(key)}/reset_spend`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: JSON.stringify({ reset_to: 0 })
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async getAuditLogs(params) {
|
|
242
|
+
const query = new URLSearchParams();
|
|
243
|
+
if (params.page !== void 0) query.set("page", String(params.page));
|
|
244
|
+
if (params.page_size !== void 0) query.set("page_size", String(params.page_size));
|
|
245
|
+
if (params.start_date) query.set("start_date", params.start_date);
|
|
246
|
+
if (params.end_date) query.set("end_date", params.end_date);
|
|
247
|
+
if (params.action) query.set("action", params.action);
|
|
248
|
+
if (params.table_name) query.set("table_name", params.table_name);
|
|
249
|
+
if (params.changed_by) query.set("changed_by", params.changed_by);
|
|
250
|
+
if (params.sort_by) query.set("sort_by", params.sort_by);
|
|
251
|
+
if (params.sort_order) query.set("sort_order", params.sort_order);
|
|
252
|
+
return this.request(`/audit?${query.toString()}`);
|
|
253
|
+
}
|
|
225
254
|
/**
|
|
226
255
|
* Rotates an existing key in place, returning a fresh `sk-` secret while
|
|
227
256
|
* keeping the same alias/budget/limits. `token` is the hashed token LiteLLM
|
|
@@ -273,10 +302,29 @@ var LiteLLMClient = class {
|
|
|
273
302
|
supports_vision: m.supports_vision
|
|
274
303
|
})).filter((m) => m.model_name);
|
|
275
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* LiteLLM's `/team/info` wraps the team row inside `team_info` (alongside
|
|
307
|
+
* sibling `keys` / `team_memberships` arrays), the same shape as
|
|
308
|
+
* `/user/info` wrapping the user row inside `user_info`. Without unwrapping,
|
|
309
|
+
* `team_alias`, `members_with_roles`, `models`, budgets and limits are all
|
|
310
|
+
* undefined, so the UI fell back to displaying the raw team_id / "Untitled
|
|
311
|
+
* team".
|
|
312
|
+
*/
|
|
276
313
|
async getTeamInfo(teamId) {
|
|
277
|
-
|
|
314
|
+
const raw = await this.request(
|
|
278
315
|
`/team/info?team_id=${encodeURIComponent(teamId)}`
|
|
279
316
|
);
|
|
317
|
+
const inner = raw?.team_info ?? {};
|
|
318
|
+
return {
|
|
319
|
+
team_id: raw?.team_id ?? inner.team_id ?? teamId,
|
|
320
|
+
team_alias: inner.team_alias ?? raw?.team_alias,
|
|
321
|
+
max_budget: inner.max_budget ?? raw?.max_budget,
|
|
322
|
+
spend: inner.spend ?? raw?.spend ?? 0,
|
|
323
|
+
members_with_roles: inner.members_with_roles ?? raw?.members_with_roles,
|
|
324
|
+
models: inner.models ?? raw?.models,
|
|
325
|
+
tpm_limit: inner.tpm_limit ?? raw?.tpm_limit,
|
|
326
|
+
rpm_limit: inner.rpm_limit ?? raw?.rpm_limit
|
|
327
|
+
};
|
|
280
328
|
}
|
|
281
329
|
emptyUsage() {
|
|
282
330
|
return {
|
|
@@ -665,6 +713,20 @@ async function getOrProvisionUser(client, tokenEntityRef, userId, provisioningEn
|
|
|
665
713
|
provisioningInFlight.delete(userId);
|
|
666
714
|
}
|
|
667
715
|
}
|
|
716
|
+
async function isUserMemberOfGroup(userEntityRef, group, catalogClient, auth, logger) {
|
|
717
|
+
try {
|
|
718
|
+
const { token } = await auth.getPluginRequestToken({
|
|
719
|
+
onBehalfOf: await auth.getOwnServiceCredentials(),
|
|
720
|
+
targetPluginId: "catalog"
|
|
721
|
+
});
|
|
722
|
+
const entity = await catalogClient.getEntityByRef(userEntityRef, { token });
|
|
723
|
+
const groups = (entity?.relations ?? []).filter((r) => r.type === "memberOf").map((r) => r.targetRef);
|
|
724
|
+
return groups.includes(group);
|
|
725
|
+
} catch (err) {
|
|
726
|
+
logger.warn(`Could not check group membership for ${userEntityRef}: ${err.message}`);
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
668
730
|
async function resolveUserRole(userEntityRef, roleConfigs, catalogClient, auth, logger) {
|
|
669
731
|
if (!roleConfigs.length) return void 0;
|
|
670
732
|
try {
|
|
@@ -2183,6 +2245,7 @@ async function createRouter(options) {
|
|
|
2183
2245
|
const client = options.client ?? new LiteLLMClient({ baseUrl, masterKey });
|
|
2184
2246
|
const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
|
|
2185
2247
|
const roleConfigs = readRoleConfigs(config);
|
|
2248
|
+
const auditGroup = config.getOptionalString("litellm.audit.group");
|
|
2186
2249
|
const catalogClient = new import_catalog_client.CatalogClient({ discoveryApi: discovery });
|
|
2187
2250
|
if (provisioningEnabled) {
|
|
2188
2251
|
logger.info(
|
|
@@ -2209,7 +2272,14 @@ async function createRouter(options) {
|
|
|
2209
2272
|
auth,
|
|
2210
2273
|
logger
|
|
2211
2274
|
);
|
|
2212
|
-
|
|
2275
|
+
const canViewAudit = auditGroup && tokenEntityRef ? await isUserMemberOfGroup(
|
|
2276
|
+
tokenEntityRef,
|
|
2277
|
+
auditGroup,
|
|
2278
|
+
catalogClient,
|
|
2279
|
+
auth,
|
|
2280
|
+
logger
|
|
2281
|
+
) : false;
|
|
2282
|
+
res.json({ ...userInfo, can_view_audit: canViewAudit });
|
|
2213
2283
|
} catch (error) {
|
|
2214
2284
|
if (error instanceof ProvisioningError) {
|
|
2215
2285
|
res.status(error.status).json(error.body);
|
|
@@ -2245,6 +2315,22 @@ async function createRouter(options) {
|
|
|
2245
2315
|
res.status(500).json({ error: error.message });
|
|
2246
2316
|
}
|
|
2247
2317
|
});
|
|
2318
|
+
router.post("/keys/:keyId/regenerate", async (req, res) => {
|
|
2319
|
+
try {
|
|
2320
|
+
const { keyId } = req.params;
|
|
2321
|
+
if (!keyId) {
|
|
2322
|
+
res.status(400).json({ error: "keyId is required" });
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2326
|
+
const result = await client.regenerateKey(keyId);
|
|
2327
|
+
logger.info({ action: "key.rotate", userId: tokenEntityRef ?? "unknown", keyId });
|
|
2328
|
+
res.json(result);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
logger.error("Failed to rotate key", error);
|
|
2331
|
+
res.status(500).json({ error: error.message });
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2248
2334
|
router.post("/keys/generate", async (req, res) => {
|
|
2249
2335
|
try {
|
|
2250
2336
|
const body = req.body ?? {};
|
|
@@ -2292,6 +2378,7 @@ async function createRouter(options) {
|
|
|
2292
2378
|
...resolvedUserId && { user_id: resolvedUserId }
|
|
2293
2379
|
};
|
|
2294
2380
|
const result = await client.generateKey(request);
|
|
2381
|
+
logger.info({ action: "key.generate", userId: resolvedUserId ?? "unknown", keyAlias: body.alias });
|
|
2295
2382
|
res.json(result);
|
|
2296
2383
|
} catch (error) {
|
|
2297
2384
|
if (error instanceof ProvisioningError) {
|
|
@@ -2309,8 +2396,10 @@ async function createRouter(options) {
|
|
|
2309
2396
|
res.status(400).json({ error: "keyId is required" });
|
|
2310
2397
|
return;
|
|
2311
2398
|
}
|
|
2399
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2312
2400
|
const request = { ...req.body, key: keyId };
|
|
2313
2401
|
const result = await client.updateKey(request);
|
|
2402
|
+
logger.info({ action: "key.update", userId: tokenEntityRef ?? "unknown", keyId });
|
|
2314
2403
|
res.json(result);
|
|
2315
2404
|
} catch (error) {
|
|
2316
2405
|
logger.error("Failed to update key", error);
|
|
@@ -2324,13 +2413,89 @@ async function createRouter(options) {
|
|
|
2324
2413
|
res.status(400).json({ error: "keyId is required" });
|
|
2325
2414
|
return;
|
|
2326
2415
|
}
|
|
2416
|
+
const deleteEntityRef = await resolveUserId(req, auth);
|
|
2327
2417
|
await client.deleteKeys({ keys: [keyId] });
|
|
2418
|
+
logger.info({ action: "key.delete", userId: deleteEntityRef ?? "unknown", keyId });
|
|
2328
2419
|
res.json({ success: true });
|
|
2329
2420
|
} catch (error) {
|
|
2330
2421
|
logger.error("Failed to delete key", error);
|
|
2331
2422
|
res.status(500).json({ error: error.message });
|
|
2332
2423
|
}
|
|
2333
2424
|
});
|
|
2425
|
+
router.post("/keys/:keyId/block", async (req, res) => {
|
|
2426
|
+
try {
|
|
2427
|
+
const { keyId } = req.params;
|
|
2428
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2429
|
+
await client.blockKey(keyId);
|
|
2430
|
+
logger.info({ action: "key.block", userId: tokenEntityRef ?? "unknown", keyId });
|
|
2431
|
+
res.json({ success: true });
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
logger.error("Failed to block key", error);
|
|
2434
|
+
res.status(500).json({ error: error.message });
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2437
|
+
router.post("/keys/:keyId/unblock", async (req, res) => {
|
|
2438
|
+
try {
|
|
2439
|
+
const { keyId } = req.params;
|
|
2440
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2441
|
+
await client.unblockKey(keyId);
|
|
2442
|
+
logger.info({ action: "key.unblock", userId: tokenEntityRef ?? "unknown", keyId });
|
|
2443
|
+
res.json({ success: true });
|
|
2444
|
+
} catch (error) {
|
|
2445
|
+
logger.error("Failed to unblock key", error);
|
|
2446
|
+
res.status(500).json({ error: error.message });
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
router.post("/keys/:keyId/reset_spend", async (req, res) => {
|
|
2450
|
+
try {
|
|
2451
|
+
const { keyId } = req.params;
|
|
2452
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2453
|
+
await client.resetKeySpend(keyId);
|
|
2454
|
+
logger.info({ action: "key.reset_spend", userId: tokenEntityRef ?? "unknown", keyId });
|
|
2455
|
+
res.json({ success: true });
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
logger.error("Failed to reset key spend", error);
|
|
2458
|
+
res.status(500).json({ error: error.message });
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
router.get("/audit", async (req, res) => {
|
|
2462
|
+
if (!auditGroup) {
|
|
2463
|
+
res.status(403).json({ error: "Audit log is not configured (litellm.audit.group not set)" });
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
const tokenEntityRef = await resolveUserId(req, auth);
|
|
2467
|
+
if (!tokenEntityRef) {
|
|
2468
|
+
res.status(401).json({ error: "Authentication required" });
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const allowed = await isUserMemberOfGroup(
|
|
2472
|
+
tokenEntityRef,
|
|
2473
|
+
auditGroup,
|
|
2474
|
+
catalogClient,
|
|
2475
|
+
auth,
|
|
2476
|
+
logger
|
|
2477
|
+
);
|
|
2478
|
+
if (!allowed) {
|
|
2479
|
+
res.status(403).json({ error: "Access denied: not a member of the audit group" });
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
const { page, page_size, start_date, end_date, action, table_name, changed_by } = req.query;
|
|
2484
|
+
const result = await client.getAuditLogs({
|
|
2485
|
+
page: page ? Number(page) : void 0,
|
|
2486
|
+
page_size: page_size ? Number(page_size) : 25,
|
|
2487
|
+
start_date,
|
|
2488
|
+
end_date,
|
|
2489
|
+
action,
|
|
2490
|
+
table_name,
|
|
2491
|
+
changed_by
|
|
2492
|
+
});
|
|
2493
|
+
res.json(result);
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
logger.error("Failed to fetch audit logs", error);
|
|
2496
|
+
res.status(500).json({ error: error.message });
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2334
2499
|
router.get("/models", async (_req, res) => {
|
|
2335
2500
|
try {
|
|
2336
2501
|
const models = await client.listModels();
|