Phase 1-3: AList stock deployment, NocoDB 7-table schema, W1/W2 n8n workflows + docs

This commit is contained in:
2026-08-22 19:26:06 +08:00
commit 71acb88078
21 changed files with 2127 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
// W2 core computation: effective desired grants + diff vs AList.
// Uses this.helpers.httpRequest (n8n Code node) for all HTTP.
// Config read via process.env (n8n $env proxy is unreliable for custom vars).
const NC = process.env.NOCODB_URL;
const NC_TOKEN = process.env.NOCODB_TOKEN;
const ALIST = process.env.ALIST_URL;
const T_CUSTPROD = process.env.NOCODB_TABLE_CUSTOMERPRODUCTS;
const T_PROD = process.env.NOCODB_TABLE_PRODUCTS;
const T_CUST = process.env.NOCODB_TABLE_CUSTOMERS;
const LNK_PROD_RES = process.env.NOCODB_LINK_PROD_RESOURCES; // c4ko4x8kwi16swg
async function ncGet(path) {
return await this.helpers.httpRequest({
method: 'GET',
url: NC + path,
headers: { 'xc-token': NC_TOKEN },
json: true,
});
}
async function alistReq(method, path, body, token) {
return await this.helpers.httpRequest({
method,
url: ALIST + path,
headers: token ? { Authorization: token } : {},
body,
json: true,
});
}
function linkId(v) {
if (v == null) return null;
if (typeof v === 'object' && !Array.isArray(v)) return v.Id ?? v.id ?? null;
if (Array.isArray(v)) return v[0]?.Id ?? v[0]?.id ?? null;
return v;
}
const custProdId = $json.customerProductId;
// 1. re-fetch the CP record
const cp = await ncGet(`/api/v2/tables/${T_CUSTPROD}/records/${custProdId}`);
const customerId = linkId(cp.customer);
if (!customerId) throw new Error('CP has no customer link');
// 2. load all active CPs for this customer
const cps = await ncGet(`/api/v2/tables/${T_CUSTPROD}/records?where=(customer,eq,${customerId})&limit=1000`);
const active = (cps.list || []).filter(r => {
if (r.status && r.status !== 'active') return false;
return true;
});
// 3. build resource path -> max expires_at
const effective = {};
for (const c of active) {
const pid = linkId(c.product);
if (!pid) continue;
const resList = await ncGet(`/api/v2/tables/${T_PROD}/links/${LNK_PROD_RES}/records/${pid}`);
const exp = c.expires_at; // 'YYYY-MM-DD'
for (const res of (resList.list || [])) {
const path = res.alist_path;
if (!path) continue;
const cur = effective[path];
if (!cur || !cur.expires || (exp && exp > cur.expires)) {
effective[path] = { expires: exp || null, permission: 0 };
}
}
}
// 4. admin login
const login = await alistReq('POST', '/api/auth/login', {
username: process.env.ALIST_ADMIN_USERNAME,
password: process.env.ALIST_ADMIN_PASSWORD,
}, null);
const token = login?.data?.token;
if (!token) throw new Error('AList login failed');
// customer's role id
const cust = await ncGet(`/api/v2/tables/${T_CUST}/records/${customerId}`);
const roleId = cust.alist_role_id;
let currentScopes = [];
if (roleId) {
const role = await alistReq('GET', `/api/admin/role/get?id=${roleId}`, null, token);
currentScopes = role?.data?.permission_scopes ?? [];
}
const desiredPaths = Object.keys(effective);
const currentPaths = currentScopes.map(s => s.path);
const toAdd = desiredPaths.filter(p => !currentPaths.includes(p));
const toRemove = currentPaths.filter(p => !desiredPaths.includes(p));
let roleName = null, roleDesc = null;
if (roleId) {
const role = await alistReq('GET', `/api/admin/role/get?id=${roleId}`, null, token);
roleName = role?.data?.name;
roleDesc = role?.data?.description;
}
return {
json: {
customerId,
roleId,
roleName,
roleDesc,
token,
effective,
desiredPaths,
currentScopes,
diff: { toAdd, toRemove },
newScopes: desiredPaths.map(p => ({ path: p, permission: effective[p].permission })),
},
};