aifastdb 2.2.2 → 2.2.6
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/aifastdb.win32-x64-msvc.node +0 -0
- package/dist/dev-plan-store.d.ts +104 -0
- package/dist/dev-plan-store.d.ts.map +1 -1
- package/dist/dev-plan-store.js +331 -34
- package/dist/dev-plan-store.js.map +1 -1
- package/dist/federation/FederatedDb.d.ts +138 -1
- package/dist/federation/FederatedDb.d.ts.map +1 -1
- package/dist/federation/FederatedDb.js +158 -0
- package/dist/federation/FederatedDb.js.map +1 -1
- package/dist/federation/index.d.ts +1 -1
- package/dist/federation/index.d.ts.map +1 -1
- package/dist/federation/index.js.map +1 -1
- package/dist/federation/types.d.ts +136 -4
- package/dist/federation/types.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.d.ts +5 -16
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +6 -689
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/security/server/routes.d.ts +46 -0
- package/dist/security/server/routes.d.ts.map +1 -1
- package/dist/security/server/routes.js +568 -1
- package/dist/security/server/routes.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 提供 RESTful API 端点
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.hasAdminAccess = exports.isAdminRequest = exports.matchAdminRoute = exports.adminRoutes = exports.routes = exports.deleteDatabaseHandler = exports.createDatabaseHandler = exports.getDatabaseInfoHandler = exports.listDatabasesHandler = exports.detailedHealthHandler = exports.healthCheckHandler = exports.readinessHandler = exports.livenessHandler = exports.revokeApiKeyHandler = exports.createApiKeyHandler = exports.queryAuditLogsHandler = exports.listUsersHandler = exports.createUserHandler = exports.collectionStatsHandler = exports.listCollectionsHandler = exports.getHandler = exports.forgetHandler = exports.recallHandler = exports.rememberHandler = exports.refreshTokenHandler = exports.logoutHandler = exports.loginHandler = void 0;
|
|
7
|
+
exports.hasAdminAccess = exports.isAdminRequest = exports.matchAdminRoute = exports.adminRoutes = exports.routes = exports.getRecordActionsHandler = exports.updateProtectionHandler = exports.updateCollaboratorHandler = exports.removeCollaboratorHandler = exports.addCollaboratorHandler = exports.updateVisibilityHandler = exports.federationQueryWithAuthHandler = exports.federationWriteWithAuthHandler = exports.deleteDatabaseHandler = exports.createDatabaseHandler = exports.getDatabaseInfoHandler = exports.listDatabasesHandler = exports.detailedHealthHandler = exports.healthCheckHandler = exports.readinessHandler = exports.livenessHandler = exports.revokeApiKeyHandler = exports.createApiKeyHandler = exports.queryAuditLogsHandler = exports.listUsersHandler = exports.createUserHandler = exports.collectionStatsHandler = exports.listCollectionsHandler = exports.getHandler = exports.forgetHandler = exports.recallHandler = exports.rememberHandler = exports.refreshTokenHandler = exports.logoutHandler = exports.loginHandler = void 0;
|
|
8
8
|
exports.matchRoute = matchRoute;
|
|
9
9
|
const intranet_1 = require("../modes/intranet");
|
|
10
10
|
const production_1 = require("../modes/production");
|
|
@@ -1062,6 +1062,554 @@ const deleteDatabaseHandler = async (ctx, security, db, root) => {
|
|
|
1062
1062
|
};
|
|
1063
1063
|
exports.deleteDatabaseHandler = deleteDatabaseHandler;
|
|
1064
1064
|
// ============================================================================
|
|
1065
|
+
// 联邦存储 — 记录级权限管理路由 (T15.10)
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
/**
|
|
1068
|
+
* 联邦存储:带鉴权写入
|
|
1069
|
+
* POST /api/v2/federation/stores/:alias/write
|
|
1070
|
+
*/
|
|
1071
|
+
const federationWriteWithAuthHandler = async (ctx, security, db, root) => {
|
|
1072
|
+
const federation = root?.federation;
|
|
1073
|
+
if (!federation) {
|
|
1074
|
+
return {
|
|
1075
|
+
status: 500,
|
|
1076
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1077
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
const pathParts = ctx.path.split('/');
|
|
1081
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1082
|
+
const alias = pathParts[storesIdx + 1];
|
|
1083
|
+
const { op, accessContext } = ctx.body || {};
|
|
1084
|
+
if (!alias || !op) {
|
|
1085
|
+
return {
|
|
1086
|
+
status: 400,
|
|
1087
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1088
|
+
body: { error: 'Bad Request', message: 'Store alias and write operation are required' },
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
// Build access context from request body or authenticated user
|
|
1093
|
+
const authCtx = accessContext || {
|
|
1094
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1095
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1096
|
+
storeAlias: alias,
|
|
1097
|
+
systemOverride: false,
|
|
1098
|
+
};
|
|
1099
|
+
const result = federation.writeWithAuth(alias, op, authCtx);
|
|
1100
|
+
return {
|
|
1101
|
+
status: 201,
|
|
1102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1103
|
+
body: { success: true, data: result },
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
const message = error?.message || String(error);
|
|
1108
|
+
const status = message.includes('Permission denied') || message.includes('permission') ? 403 : 400;
|
|
1109
|
+
return {
|
|
1110
|
+
status,
|
|
1111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1112
|
+
body: { error: status === 403 ? 'Forbidden' : 'Bad Request', message },
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
exports.federationWriteWithAuthHandler = federationWriteWithAuthHandler;
|
|
1117
|
+
/**
|
|
1118
|
+
* 联邦存储:带鉴权查询
|
|
1119
|
+
* POST /api/v2/federation/stores/:alias/query
|
|
1120
|
+
*/
|
|
1121
|
+
const federationQueryWithAuthHandler = async (ctx, security, db, root) => {
|
|
1122
|
+
const federation = root?.federation;
|
|
1123
|
+
if (!federation) {
|
|
1124
|
+
return {
|
|
1125
|
+
status: 500,
|
|
1126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1127
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
const pathParts = ctx.path.split('/');
|
|
1131
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1132
|
+
const alias = pathParts[storesIdx + 1];
|
|
1133
|
+
const { target, filters, limit, offset, accessContext } = ctx.body || {};
|
|
1134
|
+
if (!alias || !target) {
|
|
1135
|
+
return {
|
|
1136
|
+
status: 400,
|
|
1137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1138
|
+
body: { error: 'Bad Request', message: 'Store alias and target type are required' },
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
const authCtx = accessContext || {
|
|
1143
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1144
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1145
|
+
storeAlias: alias,
|
|
1146
|
+
systemOverride: false,
|
|
1147
|
+
};
|
|
1148
|
+
const result = federation.queryWithAuth(alias, target, authCtx, filters, limit, offset);
|
|
1149
|
+
return {
|
|
1150
|
+
status: 200,
|
|
1151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1152
|
+
body: { success: true, data: result },
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
catch (error) {
|
|
1156
|
+
const message = error?.message || String(error);
|
|
1157
|
+
const status = message.includes('Permission denied') ? 403 : 500;
|
|
1158
|
+
return {
|
|
1159
|
+
status,
|
|
1160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1161
|
+
body: { error: status === 403 ? 'Forbidden' : 'Internal Server Error', message },
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
exports.federationQueryWithAuthHandler = federationQueryWithAuthHandler;
|
|
1166
|
+
/**
|
|
1167
|
+
* 修改记录可见性
|
|
1168
|
+
* PUT /api/v2/federation/stores/:alias/records/:id/visibility
|
|
1169
|
+
*/
|
|
1170
|
+
const updateVisibilityHandler = async (ctx, security, db, root) => {
|
|
1171
|
+
const federation = root?.federation;
|
|
1172
|
+
if (!federation) {
|
|
1173
|
+
return {
|
|
1174
|
+
status: 500,
|
|
1175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1176
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const pathParts = ctx.path.split('/');
|
|
1180
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1181
|
+
const alias = pathParts[storesIdx + 1];
|
|
1182
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1183
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1184
|
+
const { visibility, userIds, roles } = ctx.body || {};
|
|
1185
|
+
if (!alias || !recordId || !visibility) {
|
|
1186
|
+
return {
|
|
1187
|
+
status: 400,
|
|
1188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1189
|
+
body: { error: 'Bad Request', message: 'Store alias, record ID, and visibility are required' },
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
// Get current record to check ownership
|
|
1194
|
+
const authCtx = {
|
|
1195
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1196
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1197
|
+
storeAlias: alias,
|
|
1198
|
+
systemOverride: false,
|
|
1199
|
+
};
|
|
1200
|
+
const actions = federation.getRecordActions(alias, recordId, authCtx);
|
|
1201
|
+
if (actions && !actions.canShare) {
|
|
1202
|
+
return {
|
|
1203
|
+
status: 403,
|
|
1204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1205
|
+
body: { error: 'Forbidden', message: 'Only the owner or admin can modify visibility' },
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
// Persist visibility change via NAPI
|
|
1209
|
+
federation.updateVisibility(alias, recordId, {
|
|
1210
|
+
visibilityType: visibility,
|
|
1211
|
+
userIds: userIds || undefined,
|
|
1212
|
+
roles: roles || undefined,
|
|
1213
|
+
}, authCtx);
|
|
1214
|
+
return {
|
|
1215
|
+
status: 200,
|
|
1216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1217
|
+
body: {
|
|
1218
|
+
success: true,
|
|
1219
|
+
data: {
|
|
1220
|
+
id: recordId,
|
|
1221
|
+
visibility,
|
|
1222
|
+
userIds: userIds || [],
|
|
1223
|
+
roles: roles || [],
|
|
1224
|
+
updatedBy: authCtx.caller,
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
return {
|
|
1231
|
+
status: 500,
|
|
1232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1233
|
+
body: { error: 'Internal Server Error', message: error?.message || String(error) },
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
exports.updateVisibilityHandler = updateVisibilityHandler;
|
|
1238
|
+
/**
|
|
1239
|
+
* 添加协作者
|
|
1240
|
+
* POST /api/v2/federation/stores/:alias/records/:id/collaborators
|
|
1241
|
+
*/
|
|
1242
|
+
const addCollaboratorHandler = async (ctx, security, db, root) => {
|
|
1243
|
+
const federation = root?.federation;
|
|
1244
|
+
if (!federation) {
|
|
1245
|
+
return {
|
|
1246
|
+
status: 500,
|
|
1247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1248
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const pathParts = ctx.path.split('/');
|
|
1252
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1253
|
+
const alias = pathParts[storesIdx + 1];
|
|
1254
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1255
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1256
|
+
const { userId, permissions } = ctx.body || {};
|
|
1257
|
+
if (!alias || !recordId || !userId || !permissions) {
|
|
1258
|
+
return {
|
|
1259
|
+
status: 400,
|
|
1260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1261
|
+
body: {
|
|
1262
|
+
error: 'Bad Request',
|
|
1263
|
+
message: 'Store alias, record ID, userId, and permissions are required',
|
|
1264
|
+
},
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
const authCtx = {
|
|
1269
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1270
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1271
|
+
storeAlias: alias,
|
|
1272
|
+
systemOverride: false,
|
|
1273
|
+
};
|
|
1274
|
+
const actions = federation.getRecordActions(alias, recordId, authCtx);
|
|
1275
|
+
if (actions && !actions.canShare) {
|
|
1276
|
+
return {
|
|
1277
|
+
status: 403,
|
|
1278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1279
|
+
body: { error: 'Forbidden', message: 'Only the owner or admin can manage collaborators' },
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
// Persist collaborator addition via NAPI
|
|
1283
|
+
const grantedAt = Date.now();
|
|
1284
|
+
federation.addCollaborator(alias, recordId, {
|
|
1285
|
+
userId,
|
|
1286
|
+
permissions,
|
|
1287
|
+
grantedAt,
|
|
1288
|
+
grantedBy: authCtx.caller,
|
|
1289
|
+
}, authCtx);
|
|
1290
|
+
return {
|
|
1291
|
+
status: 201,
|
|
1292
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1293
|
+
body: {
|
|
1294
|
+
success: true,
|
|
1295
|
+
data: {
|
|
1296
|
+
id: recordId,
|
|
1297
|
+
collaborator: {
|
|
1298
|
+
userId,
|
|
1299
|
+
permissions,
|
|
1300
|
+
grantedAt,
|
|
1301
|
+
grantedBy: authCtx.caller,
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
return {
|
|
1309
|
+
status: 500,
|
|
1310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1311
|
+
body: { error: 'Internal Server Error', message: error?.message || String(error) },
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
exports.addCollaboratorHandler = addCollaboratorHandler;
|
|
1316
|
+
/**
|
|
1317
|
+
* 移除协作者
|
|
1318
|
+
* DELETE /api/v2/federation/stores/:alias/records/:id/collaborators/:userId
|
|
1319
|
+
*/
|
|
1320
|
+
const removeCollaboratorHandler = async (ctx, security, db, root) => {
|
|
1321
|
+
const federation = root?.federation;
|
|
1322
|
+
if (!federation) {
|
|
1323
|
+
return {
|
|
1324
|
+
status: 500,
|
|
1325
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1326
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
const pathParts = ctx.path.split('/');
|
|
1330
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1331
|
+
const alias = pathParts[storesIdx + 1];
|
|
1332
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1333
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1334
|
+
const collaboratorsIdx = pathParts.indexOf('collaborators');
|
|
1335
|
+
const targetUserId = pathParts[collaboratorsIdx + 1];
|
|
1336
|
+
if (!alias || !recordId || !targetUserId) {
|
|
1337
|
+
return {
|
|
1338
|
+
status: 400,
|
|
1339
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1340
|
+
body: { error: 'Bad Request', message: 'Store alias, record ID, and user ID are required' },
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
try {
|
|
1344
|
+
const authCtx = {
|
|
1345
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1346
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1347
|
+
storeAlias: alias,
|
|
1348
|
+
systemOverride: false,
|
|
1349
|
+
};
|
|
1350
|
+
const actions = federation.getRecordActions(alias, recordId, authCtx);
|
|
1351
|
+
if (actions && !actions.canShare) {
|
|
1352
|
+
return {
|
|
1353
|
+
status: 403,
|
|
1354
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1355
|
+
body: { error: 'Forbidden', message: 'Only the owner or admin can manage collaborators' },
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
// Persist collaborator removal via NAPI
|
|
1359
|
+
federation.removeCollaborator(alias, recordId, targetUserId, authCtx);
|
|
1360
|
+
return {
|
|
1361
|
+
status: 200,
|
|
1362
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1363
|
+
body: {
|
|
1364
|
+
success: true,
|
|
1365
|
+
data: {
|
|
1366
|
+
id: recordId,
|
|
1367
|
+
removedUserId: targetUserId,
|
|
1368
|
+
removedBy: authCtx.caller,
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
return {
|
|
1375
|
+
status: 500,
|
|
1376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1377
|
+
body: { error: 'Internal Server Error', message: error?.message || String(error) },
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
exports.removeCollaboratorHandler = removeCollaboratorHandler;
|
|
1382
|
+
/**
|
|
1383
|
+
* 修改协作者权限
|
|
1384
|
+
* PUT /api/v2/federation/stores/:alias/records/:id/collaborators/:userId
|
|
1385
|
+
*
|
|
1386
|
+
* 更新指定协作者的权限列表。仅记录所有者或管理员可操作。
|
|
1387
|
+
* 如果 permissions 为空数组,等同于移除该协作者。
|
|
1388
|
+
*
|
|
1389
|
+
* Request body: { permissions: string[] }
|
|
1390
|
+
* permissions: ['read', 'write', 'delete', 'share']
|
|
1391
|
+
*/
|
|
1392
|
+
const updateCollaboratorHandler = async (ctx, security, db, root) => {
|
|
1393
|
+
const federation = root?.federation;
|
|
1394
|
+
if (!federation) {
|
|
1395
|
+
return {
|
|
1396
|
+
status: 500,
|
|
1397
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1398
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const pathParts = ctx.path.split('/');
|
|
1402
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1403
|
+
const alias = pathParts[storesIdx + 1];
|
|
1404
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1405
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1406
|
+
const collaboratorsIdx = pathParts.indexOf('collaborators');
|
|
1407
|
+
const targetUserId = pathParts[collaboratorsIdx + 1];
|
|
1408
|
+
const { permissions } = ctx.body || {};
|
|
1409
|
+
if (!alias || !recordId || !targetUserId || !Array.isArray(permissions)) {
|
|
1410
|
+
return {
|
|
1411
|
+
status: 400,
|
|
1412
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1413
|
+
body: {
|
|
1414
|
+
error: 'Bad Request',
|
|
1415
|
+
message: 'Store alias, record ID, user ID, and permissions array are required',
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
// Validate permission values
|
|
1420
|
+
const validPermissions = ['read', 'write', 'delete', 'share'];
|
|
1421
|
+
const invalidPerms = permissions.filter((p) => !validPermissions.includes(p));
|
|
1422
|
+
if (invalidPerms.length > 0) {
|
|
1423
|
+
return {
|
|
1424
|
+
status: 400,
|
|
1425
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1426
|
+
body: {
|
|
1427
|
+
error: 'Bad Request',
|
|
1428
|
+
message: `Invalid permissions: ${invalidPerms.join(', ')}. Valid values: ${validPermissions.join(', ')}`,
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
const authCtx = {
|
|
1434
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1435
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1436
|
+
storeAlias: alias,
|
|
1437
|
+
systemOverride: false,
|
|
1438
|
+
};
|
|
1439
|
+
// Use the NAPI updateCollaborator method which checks share permission internally
|
|
1440
|
+
federation.updateCollaborator(alias, recordId, {
|
|
1441
|
+
userId: targetUserId,
|
|
1442
|
+
permissions,
|
|
1443
|
+
grantedBy: authCtx.caller,
|
|
1444
|
+
}, authCtx);
|
|
1445
|
+
return {
|
|
1446
|
+
status: 200,
|
|
1447
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1448
|
+
body: {
|
|
1449
|
+
success: true,
|
|
1450
|
+
data: {
|
|
1451
|
+
id: recordId,
|
|
1452
|
+
userId: targetUserId,
|
|
1453
|
+
permissions,
|
|
1454
|
+
updatedBy: authCtx.caller,
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
// Distinguish permission errors from other errors
|
|
1461
|
+
if (error?.message?.includes('Permission denied')) {
|
|
1462
|
+
return {
|
|
1463
|
+
status: 403,
|
|
1464
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1465
|
+
body: { error: 'Forbidden', message: error.message },
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
if (error?.message?.includes('not found')) {
|
|
1469
|
+
return {
|
|
1470
|
+
status: 404,
|
|
1471
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1472
|
+
body: { error: 'Not Found', message: error.message },
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
status: 500,
|
|
1477
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1478
|
+
body: { error: 'Internal Server Error', message: error?.message || String(error) },
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
exports.updateCollaboratorHandler = updateCollaboratorHandler;
|
|
1483
|
+
/**
|
|
1484
|
+
* 修改记录保护级别 (仅 Admin)
|
|
1485
|
+
* PUT /api/v2/federation/stores/:alias/records/:id/protection
|
|
1486
|
+
*/
|
|
1487
|
+
const updateProtectionHandler = async (ctx, security, db, root) => {
|
|
1488
|
+
const federation = root?.federation;
|
|
1489
|
+
if (!federation) {
|
|
1490
|
+
return {
|
|
1491
|
+
status: 500,
|
|
1492
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1493
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
const pathParts = ctx.path.split('/');
|
|
1497
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1498
|
+
const alias = pathParts[storesIdx + 1];
|
|
1499
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1500
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1501
|
+
const { protection } = ctx.body || {};
|
|
1502
|
+
if (!alias || !recordId || !protection) {
|
|
1503
|
+
return {
|
|
1504
|
+
status: 400,
|
|
1505
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1506
|
+
body: { error: 'Bad Request', message: 'Store alias, record ID, and protection level are required' },
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
// Reject setting protection to "system" via API
|
|
1510
|
+
if (protection === 'system') {
|
|
1511
|
+
return {
|
|
1512
|
+
status: 400,
|
|
1513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1514
|
+
body: {
|
|
1515
|
+
error: 'PROTECTION_ERROR',
|
|
1516
|
+
message: 'Cannot set protection level to "system" via API. Use system_override internally.',
|
|
1517
|
+
},
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
const authCtx = {
|
|
1522
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1523
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1524
|
+
storeAlias: alias,
|
|
1525
|
+
systemOverride: false,
|
|
1526
|
+
};
|
|
1527
|
+
federation.downgradeProtection(alias, recordId, protection, authCtx);
|
|
1528
|
+
return {
|
|
1529
|
+
status: 200,
|
|
1530
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1531
|
+
body: {
|
|
1532
|
+
success: true,
|
|
1533
|
+
data: {
|
|
1534
|
+
id: recordId,
|
|
1535
|
+
protection,
|
|
1536
|
+
updatedBy: authCtx.caller,
|
|
1537
|
+
},
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
const message = error?.message || String(error);
|
|
1543
|
+
const isProtectionError = message.includes('protection') || message.includes('Protection')
|
|
1544
|
+
|| message.includes('admin') || message.includes('Admin');
|
|
1545
|
+
return {
|
|
1546
|
+
status: isProtectionError ? 403 : 500,
|
|
1547
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1548
|
+
body: {
|
|
1549
|
+
error: isProtectionError ? 'PROTECTION_ERROR' : 'Internal Server Error',
|
|
1550
|
+
message,
|
|
1551
|
+
},
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
exports.updateProtectionHandler = updateProtectionHandler;
|
|
1556
|
+
/**
|
|
1557
|
+
* 获取记录可用操作
|
|
1558
|
+
* GET /api/v2/federation/stores/:alias/records/:id/actions
|
|
1559
|
+
*/
|
|
1560
|
+
const getRecordActionsHandler = async (ctx, security, db, root) => {
|
|
1561
|
+
const federation = root?.federation;
|
|
1562
|
+
if (!federation) {
|
|
1563
|
+
return {
|
|
1564
|
+
status: 500,
|
|
1565
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1566
|
+
body: { error: 'Internal Server Error', message: 'Federation not available' },
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const pathParts = ctx.path.split('/');
|
|
1570
|
+
const storesIdx = pathParts.indexOf('stores');
|
|
1571
|
+
const alias = pathParts[storesIdx + 1];
|
|
1572
|
+
const recordsIdx = pathParts.indexOf('records');
|
|
1573
|
+
const recordId = pathParts[recordsIdx + 1];
|
|
1574
|
+
if (!alias || !recordId) {
|
|
1575
|
+
return {
|
|
1576
|
+
status: 400,
|
|
1577
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1578
|
+
body: { error: 'Bad Request', message: 'Store alias and record ID are required' },
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
const authCtx = {
|
|
1583
|
+
caller: ctx.security?.user?.id || 'anonymous',
|
|
1584
|
+
roles: ctx.security?.user?.role ? [ctx.security.user.role] : [],
|
|
1585
|
+
storeAlias: alias,
|
|
1586
|
+
systemOverride: false,
|
|
1587
|
+
};
|
|
1588
|
+
const actions = federation.getRecordActions(alias, recordId, authCtx);
|
|
1589
|
+
return {
|
|
1590
|
+
status: 200,
|
|
1591
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1592
|
+
body: { success: true, data: { actions } },
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
catch (error) {
|
|
1596
|
+
const message = error?.message || String(error);
|
|
1597
|
+
if (message.includes('not found')) {
|
|
1598
|
+
return {
|
|
1599
|
+
status: 404,
|
|
1600
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1601
|
+
body: { error: 'Not Found', message },
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
status: 500,
|
|
1606
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1607
|
+
body: { error: 'Internal Server Error', message },
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
exports.getRecordActionsHandler = getRecordActionsHandler;
|
|
1612
|
+
// ============================================================================
|
|
1065
1613
|
// 路由表
|
|
1066
1614
|
// ============================================================================
|
|
1067
1615
|
/**
|
|
@@ -1098,6 +1646,25 @@ exports.routes = [
|
|
|
1098
1646
|
{ method: 'GET', path: '/api/v1/databases/:name', handler: exports.getDatabaseInfoHandler, requireAuth: true, permissions: ['read'] },
|
|
1099
1647
|
{ method: 'POST', path: '/api/v1/databases', handler: exports.createDatabaseHandler, requireAuth: true, permissions: ['write'] },
|
|
1100
1648
|
{ method: 'DELETE', path: '/api/v1/databases/:name', handler: exports.deleteDatabaseHandler, requireAuth: true, permissions: ['delete'] },
|
|
1649
|
+
// ================================================================
|
|
1650
|
+
// 联邦存储 v2 — 记录级权限管理路由 (T15.10)
|
|
1651
|
+
// ================================================================
|
|
1652
|
+
// 联邦带鉴权写入
|
|
1653
|
+
{ method: 'POST', path: '/api/v2/federation/stores/:alias/write', handler: exports.federationWriteWithAuthHandler, requireAuth: true, permissions: ['write'] },
|
|
1654
|
+
// 联邦带鉴权查询
|
|
1655
|
+
{ method: 'POST', path: '/api/v2/federation/stores/:alias/query', handler: exports.federationQueryWithAuthHandler, requireAuth: true, permissions: ['read'] },
|
|
1656
|
+
// 修改记录可见性
|
|
1657
|
+
{ method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/visibility', handler: exports.updateVisibilityHandler, requireAuth: true, permissions: ['write'] },
|
|
1658
|
+
// 添加协作者
|
|
1659
|
+
{ method: 'POST', path: '/api/v2/federation/stores/:alias/records/:id/collaborators', handler: exports.addCollaboratorHandler, requireAuth: true, permissions: ['write'] },
|
|
1660
|
+
// 移除协作者
|
|
1661
|
+
{ method: 'DELETE', path: '/api/v2/federation/stores/:alias/records/:id/collaborators/:userId', handler: exports.removeCollaboratorHandler, requireAuth: true, permissions: ['write'] },
|
|
1662
|
+
// 修改协作者权限
|
|
1663
|
+
{ method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/collaborators/:userId', handler: exports.updateCollaboratorHandler, requireAuth: true, permissions: ['write'] },
|
|
1664
|
+
// 修改记录保护级别
|
|
1665
|
+
{ method: 'PUT', path: '/api/v2/federation/stores/:alias/records/:id/protection', handler: exports.updateProtectionHandler, requireAuth: true, permissions: ['write'] },
|
|
1666
|
+
// 获取记录可用操作
|
|
1667
|
+
{ method: 'GET', path: '/api/v2/federation/stores/:alias/records/:id/actions', handler: exports.getRecordActionsHandler, requireAuth: true, permissions: ['read'] },
|
|
1101
1668
|
];
|
|
1102
1669
|
/**
|
|
1103
1670
|
* 路由匹配器
|