n8n-nodes-amis-v1 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MisaAmis = void 0;
|
|
40
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
41
|
+
const axios_1 = __importDefault(require("axios"));
|
|
42
|
+
const axios_cookiejar_support_1 = require("axios-cookiejar-support");
|
|
43
|
+
const tough_cookie_1 = require("tough-cookie");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
class MisaAmis {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.description = {
|
|
50
|
+
displayName: 'MISA AMIS',
|
|
51
|
+
name: 'misaAmis',
|
|
52
|
+
icon: 'fa:file-invoice-dollar',
|
|
53
|
+
group: ['transform'],
|
|
54
|
+
version: 1,
|
|
55
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
56
|
+
description: 'Interact with MISA AMIS API',
|
|
57
|
+
defaults: {
|
|
58
|
+
name: 'MISA AMIS',
|
|
59
|
+
},
|
|
60
|
+
inputs: ['main'],
|
|
61
|
+
outputs: ['main'],
|
|
62
|
+
credentials: [
|
|
63
|
+
{
|
|
64
|
+
name: 'misaAmisUser',
|
|
65
|
+
required: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
properties: [
|
|
69
|
+
{
|
|
70
|
+
displayName: 'Resource',
|
|
71
|
+
name: 'resource',
|
|
72
|
+
type: 'options',
|
|
73
|
+
noDataExpression: true,
|
|
74
|
+
options: [
|
|
75
|
+
{
|
|
76
|
+
name: 'Order',
|
|
77
|
+
value: 'order',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
default: 'order',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
displayName: 'Operation',
|
|
84
|
+
name: 'operation',
|
|
85
|
+
type: 'options',
|
|
86
|
+
noDataExpression: true,
|
|
87
|
+
displayOptions: {
|
|
88
|
+
show: {
|
|
89
|
+
resource: ['order'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
options: [
|
|
93
|
+
{
|
|
94
|
+
name: 'Get Detail',
|
|
95
|
+
value: 'getDetail',
|
|
96
|
+
description: 'Get full detail of a sales order',
|
|
97
|
+
action: 'Get order detail',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
default: 'getDetail',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Order ID (Key)',
|
|
104
|
+
name: 'orderId',
|
|
105
|
+
type: 'string',
|
|
106
|
+
default: '',
|
|
107
|
+
required: true,
|
|
108
|
+
displayOptions: {
|
|
109
|
+
show: {
|
|
110
|
+
resource: ['order'],
|
|
111
|
+
operation: ['getDetail'],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
description: 'The UUID Key of the order.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
displayName: 'MISA Context (JSON)',
|
|
118
|
+
name: 'misaContext',
|
|
119
|
+
type: 'string',
|
|
120
|
+
default: '',
|
|
121
|
+
description: 'Paste the X-MISA-Context header value from your browser dev tools.',
|
|
122
|
+
required: true, // For now, we force user to provide it logic
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async execute() {
|
|
128
|
+
const items = this.getInputData();
|
|
129
|
+
const returnData = [];
|
|
130
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
131
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
132
|
+
// 1. Load Session
|
|
133
|
+
const credentials = await this.getCredentials('misaAmisUser');
|
|
134
|
+
const userIdentity = credentials.userIdentity;
|
|
135
|
+
const n8nDir = process.env.N8N_USER_FOLDER || path.join(os.homedir(), '.n8n');
|
|
136
|
+
const sessionFilePath = path.join(n8nDir, 'misa_sessions', `${userIdentity}.json`);
|
|
137
|
+
if (!fs.existsSync(sessionFilePath)) {
|
|
138
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Session file not found for user: ${userIdentity}. Please run Login Node first.`);
|
|
139
|
+
}
|
|
140
|
+
const jarJson = JSON.parse(fs.readFileSync(sessionFilePath, 'utf8'));
|
|
141
|
+
const jar = tough_cookie_1.CookieJar.deserializeSync(jarJson);
|
|
142
|
+
const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ jar }));
|
|
143
|
+
for (let i = 0; i < items.length; i++) {
|
|
144
|
+
try {
|
|
145
|
+
if (resource === 'order' && operation === 'getDetail') {
|
|
146
|
+
const orderId = this.getNodeParameter('orderId', i);
|
|
147
|
+
const misaContext = this.getNodeParameter('misaContext', i);
|
|
148
|
+
// Construct req param
|
|
149
|
+
const reqObj = [
|
|
150
|
+
{
|
|
151
|
+
"Type": "sa_order",
|
|
152
|
+
"Key": orderId,
|
|
153
|
+
"Retype": 3520, // Default based on sample
|
|
154
|
+
"RefTypeCategory": 352,
|
|
155
|
+
"View": "view_sa_order",
|
|
156
|
+
"Details": [
|
|
157
|
+
{ "Type": "sa_order_detail", "Alias": "detail", "View": "view_sa_order_detail" },
|
|
158
|
+
{ "Type": "wesign_document", "Alias": "wesign_document", "ForeignKey": "refid", "Mode": "View" }
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
const reqBase64 = Buffer.from(JSON.stringify(reqObj)).toString('base64');
|
|
163
|
+
const url = `https://actapp.misa.vn/g1/api/sa/v1/sa_order/detail_full?req=${reqBase64}`;
|
|
164
|
+
const headers = {
|
|
165
|
+
'Accept': 'application/json, text/plain, */*',
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
'X-MISA-Context': misaContext,
|
|
168
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
|
169
|
+
// Authorization: 'Bearer ...' // We hope cookies are enough, or allow user to pass it?
|
|
170
|
+
// Usually MISA Web Session uses cookies. The Bearer token in cURL might be redundant or derived.
|
|
171
|
+
// Let's try without Bearer first. If fail, we ask user.
|
|
172
|
+
};
|
|
173
|
+
const response = await client.get(url, { headers });
|
|
174
|
+
returnData.push({
|
|
175
|
+
json: response.data
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (this.continueOnFail()) {
|
|
181
|
+
returnData.push({ json: { error: error.message } });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [returnData];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.MisaAmis = MisaAmis;
|
|
@@ -66,14 +66,51 @@ class MisaAmisLogin {
|
|
|
66
66
|
},
|
|
67
67
|
],
|
|
68
68
|
properties: [
|
|
69
|
+
{
|
|
70
|
+
displayName: 'Operation',
|
|
71
|
+
name: 'operation',
|
|
72
|
+
type: 'options',
|
|
73
|
+
noDataExpression: true,
|
|
74
|
+
options: [
|
|
75
|
+
{
|
|
76
|
+
name: 'Generate QR',
|
|
77
|
+
value: 'generateQr',
|
|
78
|
+
description: 'Generate a QR code for login',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Check Login',
|
|
82
|
+
value: 'checkLogin',
|
|
83
|
+
description: 'Check status of QR scan',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
default: 'generateQr',
|
|
87
|
+
},
|
|
69
88
|
{
|
|
70
89
|
displayName: 'User Identity (Email/ID)',
|
|
71
90
|
name: 'userIdentity',
|
|
72
91
|
type: 'string',
|
|
73
92
|
default: '',
|
|
74
93
|
placeholder: 'hiennv',
|
|
75
|
-
description: 'Unique identifier for
|
|
94
|
+
description: 'Unique identifier for sessions.',
|
|
95
|
+
required: true,
|
|
96
|
+
displayOptions: {
|
|
97
|
+
show: {
|
|
98
|
+
operation: ['checkLogin', 'generateQr'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Request ID',
|
|
104
|
+
name: 'requestId',
|
|
105
|
+
type: 'string',
|
|
106
|
+
default: '',
|
|
76
107
|
required: true,
|
|
108
|
+
displayOptions: {
|
|
109
|
+
show: {
|
|
110
|
+
operation: ['checkLogin'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
description: 'The CDRequestId returned from "Generate QR" step',
|
|
77
114
|
},
|
|
78
115
|
],
|
|
79
116
|
};
|
|
@@ -81,100 +118,99 @@ class MisaAmisLogin {
|
|
|
81
118
|
async execute() {
|
|
82
119
|
const items = this.getInputData();
|
|
83
120
|
const returnData = [];
|
|
121
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
84
122
|
const userIdentity = this.getNodeParameter('userIdentity', 0);
|
|
85
|
-
const credentials = await this.getCredentials('misaAmisApp');
|
|
86
123
|
const n8nDir = process.env.N8N_USER_FOLDER || path.join(os.homedir(), '.n8n');
|
|
87
124
|
const storagePath = path.join(n8nDir, 'misa_sessions');
|
|
88
125
|
if (!fs.existsSync(storagePath)) {
|
|
89
126
|
fs.mkdirSync(storagePath, { recursive: true });
|
|
90
127
|
}
|
|
91
|
-
const sessionFilePath = path.join(storagePath, `${userIdentity}.json`);
|
|
92
128
|
const clientId = '6bcbc4d1-5426-42f7-bc61-69cac2e229f4';
|
|
93
|
-
const jar = new tough_cookie_1.CookieJar();
|
|
94
|
-
const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ jar }));
|
|
95
129
|
const headers = {
|
|
96
130
|
'Content-Type': 'application/json',
|
|
97
131
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
|
98
132
|
'Origin': 'https://amisapp.misa.vn',
|
|
99
133
|
'Referer': 'https://amisapp.misa.vn/',
|
|
100
134
|
};
|
|
135
|
+
const jar = new tough_cookie_1.CookieJar();
|
|
136
|
+
const client = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({ jar }));
|
|
101
137
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
138
|
+
if (operation === 'generateQr') {
|
|
139
|
+
// --- Operation: Generate QR ---
|
|
140
|
+
const genQrPayload = {
|
|
141
|
+
clientId: clientId,
|
|
142
|
+
deviceId: clientId,
|
|
143
|
+
userAgent: headers['User-Agent'],
|
|
144
|
+
deviceName: "Desktop",
|
|
145
|
+
deviceType: "Desktop",
|
|
146
|
+
osName: "Windows",
|
|
147
|
+
osVersion: "10",
|
|
148
|
+
browser: "Google Chrome",
|
|
149
|
+
browserVersion: "143.0"
|
|
150
|
+
};
|
|
151
|
+
const genQrRes = await client.post('https://id.misa.vn/api/login-cross-device/gen-qrcode', genQrPayload, { headers });
|
|
152
|
+
if (!genQrRes.data || !genQrRes.data.CDRequestId) {
|
|
153
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to generate QR Code Request ID');
|
|
154
|
+
}
|
|
155
|
+
const cdRequestId = genQrRes.data.CDRequestId;
|
|
156
|
+
const qrContent = `https://amisapp.misa.vn/shared?app=qrlogin&qrid=${cdRequestId}&domain=amisapp.misa.vn`;
|
|
157
|
+
const qrBuffer = await qrcode_1.default.toBuffer(qrContent);
|
|
158
|
+
returnData.push({
|
|
159
|
+
json: {
|
|
160
|
+
message: "QR Generated. Scan it and then use 'Check Login' step.",
|
|
161
|
+
requestId: cdRequestId,
|
|
162
|
+
clientId: clientId,
|
|
163
|
+
qrContent: qrContent
|
|
164
|
+
},
|
|
165
|
+
binary: {
|
|
166
|
+
qr: {
|
|
167
|
+
data: qrBuffer.toString('base64'),
|
|
168
|
+
mimeType: 'image/png',
|
|
169
|
+
fileName: 'qr.png',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
116
173
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Unless we save the Jar *Pending* state?
|
|
141
|
-
// Complexity++
|
|
142
|
-
// I will keep the polling.
|
|
143
|
-
// I suspect the user might be using a sophisticated viewer or just wants the property there.
|
|
144
|
-
while (!pollingSuccess && attempts < maxAttempts) {
|
|
145
|
-
attempts++;
|
|
146
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
147
|
-
try {
|
|
148
|
-
const pollUrl = `https://id.misa.vn/api/login-cross-device/v2/polling?cdRequestId=${cdRequestId}&clientId=${clientId}&deviceId=${clientId}`;
|
|
149
|
-
const pollRes = await client.get(pollUrl, { headers });
|
|
150
|
-
lastPollData = pollRes.data;
|
|
151
|
-
if (JSON.stringify(pollRes.data).includes("Success") || JSON.stringify(pollRes.data).includes("v1/auth/token")) {
|
|
152
|
-
pollingSuccess = true;
|
|
174
|
+
else if (operation === 'checkLogin') {
|
|
175
|
+
// --- Operation: Check Login ---
|
|
176
|
+
const cdRequestId = this.getNodeParameter('requestId', 0);
|
|
177
|
+
const sessionFilePath = path.join(storagePath, `${userIdentity}.json`);
|
|
178
|
+
let pollingSuccess = false;
|
|
179
|
+
let attempts = 0;
|
|
180
|
+
const maxAttempts = 60; // 2 minutes (2s interval)
|
|
181
|
+
while (!pollingSuccess && attempts < maxAttempts) {
|
|
182
|
+
attempts++;
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
184
|
+
try {
|
|
185
|
+
const pollUrl = `https://id.misa.vn/api/login-cross-device/v2/polling?cdRequestId=${cdRequestId}&clientId=${clientId}&deviceId=${clientId}`;
|
|
186
|
+
const pollRes = await client.get(pollUrl, { headers });
|
|
187
|
+
if (JSON.stringify(pollRes.data).includes("Success") || JSON.stringify(pollRes.data).includes("v1/auth/token")) {
|
|
188
|
+
pollingSuccess = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error.response && error.response.status >= 400 && error.response.status < 500) {
|
|
193
|
+
// Client Error (400/404) -> Request ID invalid or expired
|
|
194
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Polling Error ${error.response.status}: ${JSON.stringify(error.response.data)}. Please Generate QR again.`);
|
|
195
|
+
}
|
|
196
|
+
// Ignore server errors or network glitches (retry)
|
|
153
197
|
}
|
|
154
198
|
}
|
|
155
|
-
|
|
156
|
-
|
|
199
|
+
if (pollingSuccess) {
|
|
200
|
+
const serializedJar = await jar.serialize();
|
|
201
|
+
fs.writeFileSync(sessionFilePath, JSON.stringify(serializedJar, null, 2));
|
|
202
|
+
returnData.push({
|
|
203
|
+
json: {
|
|
204
|
+
message: "Login Successful",
|
|
205
|
+
userIdentity: userIdentity,
|
|
206
|
+
sessionFile: sessionFilePath
|
|
207
|
+
}
|
|
208
|
+
});
|
|
157
209
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const serializedJar = await jar.serialize();
|
|
161
|
-
fs.writeFileSync(sessionFilePath, JSON.stringify(serializedJar, null, 2));
|
|
162
|
-
}
|
|
163
|
-
returnData.push({
|
|
164
|
-
json: {
|
|
165
|
-
message: pollingSuccess ? "Login Successful" : "Timed out",
|
|
166
|
-
userIdentity: userIdentity,
|
|
167
|
-
sessionFile: sessionFilePath,
|
|
168
|
-
qrContent: qrContent
|
|
169
|
-
},
|
|
170
|
-
binary: {
|
|
171
|
-
qr: {
|
|
172
|
-
data: qrBuffer.toString('base64'),
|
|
173
|
-
mimeType: 'image/png',
|
|
174
|
-
fileName: 'qr.png',
|
|
175
|
-
}
|
|
210
|
+
else {
|
|
211
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Login timeout or failed. Please scan QR Code again.');
|
|
176
212
|
}
|
|
177
|
-
}
|
|
213
|
+
}
|
|
178
214
|
}
|
|
179
215
|
catch (error) {
|
|
180
216
|
if (this.continueOnFail()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-amis-v1",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "n8n node for AMIS v1",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"n8n": {
|
|
11
11
|
"nodes": [
|
|
12
|
-
"dist/nodes/MisaAmisLogin/MisaAmisLogin.node.js"
|
|
12
|
+
"dist/nodes/MisaAmisLogin/MisaAmisLogin.node.js",
|
|
13
|
+
"dist/nodes/MisaAmis/MisaAmis.node.js"
|
|
13
14
|
],
|
|
14
15
|
"credentials": [
|
|
15
16
|
"dist/credentials/MisaAmisApp.credentials.js",
|