Files
alist-serve-resource/n8n/w3-body.js
T
hoelee 3ecee3099e feat(W1): status lifecycle (suspend/delete/resume) + shared effective-grants lib
- Extract calculateEffectiveGrants into n8n/shared/effective-grants.js
  (single source of truth), inlined at build time into W1/W2/W3 via
  gen_w*.js + inline_shared.js — removes the duplicated algorithm.
- W1 now provisions with a status branch:
  suspended/deleted -> disable AList user + clear role scopes (soft delete)
  active -> create/update user+role, RECOMPUTE desired scopes (never restore
  stale), re-enable
- Loop guard corrected: check AList user LIVE disabled state via
  GET /api/admin/user/get (sync_status alone breaks suspend->resume).
- W2/W3 rebuilt on shared lib (identical behaviour).
- Tests: test_effective_grants.js (15), test_w3_refactored.js (13),
  test_w1_lifecycle.js (23) — all green.
- Drop superseded w2-compute.js / w3-compute.js / test_w3.js.
2026-08-23 04:59:43 +08:00

79 lines
3.3 KiB
JavaScript

// ============================================================================
// W3 runtime body — 5-min expiry sweep / full reconciliation.
// Rebuilt by `node n8n/gen_w3.js` (single source of truth = shared/effective-grants.js).
// dryRun: W3_DRY_RUN=true -> only print the plan (no writes).
// autoDisable: ALIST_AUTO_DISABLE_ON_EXPIRE=true -> disable AList user when a
// previously-scoped customer's desired set becomes empty. Default off.
// ============================================================================
// __SHARED_INLINE__ (replaced by shared/effective-grants.js source at build)
// ============================================================================
const ctx = { $env, helpers: this.helpers };
const DRY_RUN = ($env.W3_DRY_RUN || '').toLowerCase() === 'true';
const AUTO_DISABLE = ($env.ALIST_AUTO_DISABLE_ON_EXPIRE || '').toLowerCase() === 'true';
const T_CUST = $env.NOCODB_TABLE_CUSTOMERS;
// admin login once for the whole sweep
const login = await alistReq(ctx, 'POST', '/api/auth/login', {
username: $env.ALIST_ADMIN_USERNAME,
password: $env.ALIST_ADMIN_PASSWORD,
}, null);
const token = login?.data?.token;
if (!token) throw new Error('AList login failed');
const customers = (await ncGet(ctx, `/api/v2/tables/${T_CUST}/records?limit=10000`)).list || [];
const changed = [], errors = [];
for (const cust of customers) {
try {
if (!cust.alist_role_id) continue; // not provisioned yet
const cid = cust.Id;
// desired scopes via shared algorithm
const g = await calculateEffectiveGrants(ctx, cid);
const desiredPaths = g.desiredPaths;
// current scopes from AList
const role = await alistReq(ctx, 'GET', `/api/admin/role/get?id=${cust.alist_role_id}`, null, token);
const currentPaths = (role?.data?.permission_scopes || []).map(s => s.path);
const toAdd = desiredPaths.filter(p => !currentPaths.includes(p));
const toRemove = currentPaths.filter(p => !desiredPaths.includes(p));
const scopesChanged = toAdd.length > 0 || toRemove.length > 0;
const nowEmpty = desiredPaths.length === 0;
// only disable when a customer that HAD scopes now has none (guard
// currentPaths>0 avoids killing a fresh customer with an empty role).
const shouldDisable = AUTO_DISABLE && nowEmpty && cust.status === 'active' && currentPaths.length > 0;
const applied = [];
if (scopesChanged) {
if (!DRY_RUN) await alistReq(ctx, 'POST', '/api/admin/role/update', {
id: cust.alist_role_id,
name: role?.data?.name,
description: role?.data?.description,
permission_scopes: g.newScopes,
}, token);
applied.push('role_scopes');
}
if (shouldDisable) {
if (!DRY_RUN && cust.alist_user_id)
await alistReq(ctx, 'POST', '/api/admin/user/update', { id: cust.alist_user_id, disabled: true }, token);
applied.push('disable');
}
if (applied.length) {
changed.push({
customerId: cid, username: cust.username, roleId: cust.alist_role_id,
toAdd, toRemove, desiredPaths, applied,
});
}
} catch (e) {
errors.push({ customerId: cust.Id, username: cust.username, error: e.message });
}
}
return { json: {
dryRun: DRY_RUN, autoDisable: AUTO_DISABLE, at: new Date().toISOString(),
customersScanned: customers.length, customersChanged: changed.length,
changed, errors,
} };