- w1/w3-body: disable calls now send username + assert code===200
(v3.63 validator rejects {id,disabled} with 400 -> silent no-disable)
- regenerated W1/W3 json, deployed live via REST API, activeVersionId re-aligned
- NocoDB hooks repointed to http://n8n:5678 (old auto.hoelee.com chain loses
events on VPS stalls; W2 hook path was also 404 since creation)
- deploy_w1_w3.mjs: reusable PUT deploy via n8n API key from SECRETS.md
- PROJECT_STATE: e2e verification results + updated next steps
82 lines
3.6 KiB
JavaScript
82 lines
3.6 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) {
|
|
// AList v3.63 /user/update REQUIRES username — {id,disabled} alone = 400 silent no-op (2026-08-25).
|
|
if (!DRY_RUN && cust.alist_user_id) {
|
|
const disRes = await alistReq(ctx, 'POST', '/api/admin/user/update', { id: cust.alist_user_id, username: cust.username, disabled: true }, token);
|
|
if (!disRes || disRes.code !== 200) throw new Error('AList disable failed: ' + JSON.stringify(disRes));
|
|
}
|
|
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,
|
|
} };
|