feat(n8n): W5 返回每产品 paths[] (Products→Resources alist_path) 支持 footer modal path 展示
- w5-body.js: 每 product 走 Products→Resources mm link 取 alist_path, 复用 effective-grants 的
'先 fetch 全 Resources 索引 Id→alist_path' 模式 (link API 只返 {Id,name} 不返 alist_path)
- 新增 paths 字段于 purchases[]; 无关联资源返 []
- deploy_w5.mjs: PUT /api/v1/workflows/MNC0BZc1FqCKFwIV 部署脚本
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,23 @@
|
||||
// Deploy regenerated W5 (v30, adds paths) to live n8n via public API.
|
||||
// Usage: N8N_API_KEY=... node deploy_w5.mjs
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const N8N = process.env.N8N_BASE || "http://192.168.137.2:5678";
|
||||
const KEY = process.env.N8N_API_KEY;
|
||||
if (!KEY) { console.error("N8N_API_KEY env required"); process.exit(1); }
|
||||
const H = { "X-N8N-API-KEY": KEY, "Content-Type": "application/json" };
|
||||
|
||||
const wf = JSON.parse(await readFile("W5-purchase-check.json", "utf-8"));
|
||||
delete wf.pinData;
|
||||
delete wf.active;
|
||||
delete wf.id;
|
||||
|
||||
// W5 workflow id (live) = MNC0BZc1FqCKFwIV
|
||||
const ID = "MNC0BZc1FqCKFwIV";
|
||||
const r = await fetch(`${N8N}/api/v1/workflows/${ID}`, {
|
||||
method: "PUT", headers: H, body: JSON.stringify(wf),
|
||||
});
|
||||
const txt = await r.text();
|
||||
console.log("W5 PUT", r.status, txt.slice(0, 300));
|
||||
if (r.status !== 200) process.exit(1);
|
||||
console.log("DEPLOY OK");
|
||||
+40
-4
@@ -1,14 +1,20 @@
|
||||
// ============================================================================
|
||||
// W5 runtime body — footer purchase/expiry lookup (PUBLIC, READ-ONLY).
|
||||
// Trigger: GET webhook /dk-purchase-check?u=<username>
|
||||
// Returns: { username, known, purchases: [{product, expires_at, display_state}] }
|
||||
// Returns: { username, known, purchases: [{product, expires_at, display_state, paths:[...]}] }
|
||||
//
|
||||
// v30 (2026-08-28): adds `paths` per purchase — each product's resources' alist_path
|
||||
// (via Products→Resources mm link NOCODB_LINK_PROD_RESOURCES), so the footer modal
|
||||
// can show a collapsible resource-path list. Products with no linked resources
|
||||
// return paths: [].
|
||||
//
|
||||
// Security model (no HMAC — key would ship to the client, so it is theatre):
|
||||
// - CORS: response header Access-Control-Allow-Origin: https://drive.digikedai.com
|
||||
// → only the AList portal origin may READ the response from a browser.
|
||||
// - Minimal data: product name + expiry + display_state only (no internal Ids).
|
||||
// - Minimal data: product name + expiry + display_state + public folder paths only
|
||||
// (no internal NocoDB Ids, no customer PII).
|
||||
// - Server-side enumeration is mitigated by a Cloudflare WAF rate-limit +
|
||||
// Origin/Referer rule on auto.hoelee.com (see delivery guide).
|
||||
// Origin/Referer rule (see delivery guide).
|
||||
// - No AList auth requests of any kind (username comes from the caller's own
|
||||
// JWT, decoded client-side in the footer) → no empty-client device drift.
|
||||
// ============================================================================
|
||||
@@ -44,6 +50,9 @@ if (!username) return { json: empty() };
|
||||
try {
|
||||
const T_CUST = ctx.$env.NOCODB_TABLE_CUSTOMERS;
|
||||
const T_CP = ctx.$env.NOCODB_TABLE_CUSTOMERPRODUCTS;
|
||||
const T_PROD = ctx.$env.NOCODB_TABLE_PRODUCTS;
|
||||
const T_RES = ctx.$env.NOCODB_TABLE_RESOURCES;
|
||||
const LNK_PROD_RES = ctx.$env.NOCODB_LINK_PROD_RESOURCES; // c4ko4x8kwi16swg
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// 1. resolve username -> customer (username is unique in Customers)
|
||||
@@ -58,14 +67,41 @@ try {
|
||||
`/api/v2/tables/${T_CP}/records?where=(customer.Id,eq,${customer.Id})&limit=500`
|
||||
)).list || [];
|
||||
|
||||
// 3. resolve paths per product (Products→Resources mm link → alist_path).
|
||||
// Fetch all Resources once, index Id → alist_path (link API only returns
|
||||
// {Id, name}, not alist_path — see effective-grants.js pattern).
|
||||
const allRes = (await ncGet(
|
||||
`/api/v2/tables/${T_RES}/records?limit=10000&fields=Id,alist_path`
|
||||
)).list || [];
|
||||
const resPath = {};
|
||||
allRes.forEach((r) => { resPath[r.Id] = r.alist_path; });
|
||||
|
||||
const prodIds = [...new Set(cps.map((c) => linkId(c.product)).filter(Boolean))];
|
||||
|
||||
const pathsByProduct = {}; // pid -> [alist_path, ...]
|
||||
for (const pid of prodIds) {
|
||||
let paths = [];
|
||||
try {
|
||||
const res = (await ncGet(
|
||||
`/api/v2/tables/${T_PROD}/links/${LNK_PROD_RES}/records/${pid}`
|
||||
)).list || [];
|
||||
paths = res.map((r) => resPath[r.Id]).filter((p) => p != null && String(p).length > 0);
|
||||
} catch (e) {
|
||||
paths = []; // graceful: no paths rather than failing the whole lookup
|
||||
}
|
||||
pathsByProduct[pid] = paths.sort();
|
||||
}
|
||||
|
||||
const purchases = cps
|
||||
.map((c) => {
|
||||
const productObj = (typeof c.product === 'object' && c.product) ? c.product : null;
|
||||
const name = productObj && productObj.name ? String(productObj.name) : 'Product';
|
||||
const pid = linkId(c.product);
|
||||
const expires = c.expires_at ? String(c.expires_at).slice(0, 10) : null;
|
||||
let state = (c.status && c.status !== 'active') ? c.status : 'active';
|
||||
if (state === 'active' && expires && expires <= today) state = 'expired';
|
||||
return { product: name, expires_at: expires, display_state: state };
|
||||
const paths = pid ? (pathsByProduct[pid] || []) : [];
|
||||
return { product: name, expires_at: expires, display_state: state, paths };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const rank = { active: 0, suspended: 1, revoked: 2, expired: 3 };
|
||||
|
||||
Reference in New Issue
Block a user