n8n-nodes-amis-v1 0.1.2 → 0.1.4
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.
|
@@ -7,14 +7,6 @@ class MisaAmisApp {
|
|
|
7
7
|
this.displayName = 'MISA AMIS App Config';
|
|
8
8
|
this.documentationUrl = 'https://amisapp.misa.vn/';
|
|
9
9
|
this.properties = [
|
|
10
|
-
{
|
|
11
|
-
displayName: 'Storage Path',
|
|
12
|
-
name: 'storagePath',
|
|
13
|
-
type: 'string',
|
|
14
|
-
default: 'C:/Users/Admin/.n8n/misa_sessions',
|
|
15
|
-
description: 'Local directory to store user sessions (cookies). Ensure n8n has write access.',
|
|
16
|
-
required: true,
|
|
17
|
-
},
|
|
18
10
|
{
|
|
19
11
|
displayName: 'API URL (Optional)',
|
|
20
12
|
name: 'apiUrl',
|
|
@@ -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,95 @@ 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
|
-
// Impossible if node is synchronous.
|
|
137
|
-
// Maybe they mean: The node returns the QR image. THEN I scan it. THEN I run another node?
|
|
138
|
-
// But the node is "MISA AMIS Login". It implies it does the logging in.
|
|
139
|
-
// If it outputs QR and finishes, it loses context (CookieJar).
|
|
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
|
+
// Ignore poling errors
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
|
-
|
|
156
|
-
|
|
195
|
+
if (pollingSuccess) {
|
|
196
|
+
const serializedJar = await jar.serialize();
|
|
197
|
+
fs.writeFileSync(sessionFilePath, JSON.stringify(serializedJar, null, 2));
|
|
198
|
+
returnData.push({
|
|
199
|
+
json: {
|
|
200
|
+
message: "Login Successful",
|
|
201
|
+
userIdentity: userIdentity,
|
|
202
|
+
sessionFile: sessionFilePath
|
|
203
|
+
}
|
|
204
|
+
});
|
|
157
205
|
}
|
|
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
|
-
}
|
|
206
|
+
else {
|
|
207
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Login timeout or failed. Please scan QR Code again.');
|
|
176
208
|
}
|
|
177
|
-
}
|
|
209
|
+
}
|
|
178
210
|
}
|
|
179
211
|
catch (error) {
|
|
180
212
|
if (this.continueOnFail()) {
|