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.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// ============================================================================
|
||||
// W2 runtime body — enforced CustomerProduct -> AList role scopes sync.
|
||||
// Rebuilt by `node n8n/gen_w2.js` (single source of truth = shared/effective-grants.js).
|
||||
// ============================================================================
|
||||
// __SHARED_INLINE__ (replaced by shared/effective-grants.js source at build)
|
||||
// ============================================================================
|
||||
|
||||
// n8n Code node context: ctx = { $env, helpers } — but helpers is `this.helpers`
|
||||
// inside the node. Build ctx to hand to the shared library.
|
||||
const ctx = {
|
||||
$env,
|
||||
helpers: this.helpers,
|
||||
};
|
||||
|
||||
const custProdId = $json.customerProductId;
|
||||
|
||||
// 1. re-fetch this CP record to find its customer link (may be null mid-draft)
|
||||
const CP = await ncGet(ctx, `/api/v2/tables/${$env.NOCODB_TABLE_CUSTOMERPRODUCTS}/records/${custProdId}`);
|
||||
const customerId = linkId(CP.customer);
|
||||
if (!customerId) {
|
||||
// Customer link not yet filled (record being drafted). Skip gracefully; the
|
||||
// webhook re-fires when customer is set later.
|
||||
return { json: { skipWriteBack: true, customerProductId: custProdId, reason: 'no customer link yet' } };
|
||||
}
|
||||
|
||||
// 2. compute what the customer SHOULD have right now (shared algorithm)
|
||||
const grants = await calculateEffectiveGrants(ctx, customerId);
|
||||
|
||||
// 3. fetch customer admin facts (role id, alist user id)
|
||||
const cust = await ncGet(ctx, `/api/v2/tables/${$env.NOCODB_TABLE_CUSTOMERS}/records/${customerId}`);
|
||||
const roleId = cust.alist_role_id;
|
||||
const userId = cust.alist_user_id;
|
||||
|
||||
if (!roleId) {
|
||||
return { json: { skipWriteBack: true, customerId, reason: 'customer not provisioned (no role id)' } };
|
||||
}
|
||||
|
||||
// 4. admin login
|
||||
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');
|
||||
|
||||
// 5. current scopes vs desired scopes -> diff
|
||||
const role = await alistReq(ctx, 'GET', `/api/admin/role/get?id=${roleId}`, null, token);
|
||||
const currentScopes = role?.data?.permission_scopes ?? [];
|
||||
const currentPaths = currentScopes.map(s => s.path);
|
||||
const toAdd = grants.desiredPaths.filter(p => !currentPaths.includes(p));
|
||||
const toRemove = currentPaths.filter(p => !grants.desiredPaths.includes(p));
|
||||
const changed = toAdd.length > 0 || toRemove.length > 0;
|
||||
|
||||
// 6. apply if changed
|
||||
if (changed) {
|
||||
await alistReq(ctx, 'POST', '/api/admin/role/update', {
|
||||
id: roleId,
|
||||
name: role?.data?.name,
|
||||
description: role?.data?.description,
|
||||
permission_scopes: grants.newScopes,
|
||||
}, token);
|
||||
}
|
||||
|
||||
return {
|
||||
json: {
|
||||
customerId,
|
||||
customerProductId: custProdId,
|
||||
roleId,
|
||||
roleName: role?.data?.name,
|
||||
effective: grants.desired,
|
||||
desiredPaths: grants.desiredPaths,
|
||||
currentScopes,
|
||||
diff: { toAdd, toRemove },
|
||||
newScopes: grants.newScopes,
|
||||
skipWriteBack: changed ? false : true,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user