@studentsphere/linkgor 0.0.1-alpha
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.
- package/LICENSE +674 -0
- package/README.md +191 -0
- package/dist/index.d.mts +111 -0
- package/dist/index.d.ts +111 -0
- package/dist/index.js +369 -0
- package/dist/index.mjs +336 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CD_INSTANCES: () => CD_INSTANCES,
|
|
24
|
+
IGENSIA_INSTANCES: () => IGENSIA_INSTANCES,
|
|
25
|
+
INSTANCES: () => INSTANCES,
|
|
26
|
+
getCASURL: () => getCASURL,
|
|
27
|
+
getPlanning: () => getPlanning,
|
|
28
|
+
getProfile: () => getProfile,
|
|
29
|
+
loginWithCredentials: () => loginWithCredentials
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(index_exports);
|
|
32
|
+
|
|
33
|
+
// src/constants.ts
|
|
34
|
+
var LOGIN_SERVER_ENDPOINT = "https://cas-p.wigorservices.net/cas/login";
|
|
35
|
+
var CD_SCHOOLS_TIMETABLE_ENDPOINT = "https://ws-edt-cd.wigorservices.net/Home/Get";
|
|
36
|
+
var IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT = "https://ws-edt-igs.wigorservices.net/Home/Get";
|
|
37
|
+
var CD_INSTANCES = [
|
|
38
|
+
{ id: "3a", name: "3A" },
|
|
39
|
+
{ id: "epsi", name: "EPSI" },
|
|
40
|
+
{ id: "esail", name: "ESAIL" },
|
|
41
|
+
{ id: "icl", name: "ICL" },
|
|
42
|
+
{ id: "idrac-business-school", name: "IDRAC Business School" },
|
|
43
|
+
{ id: "ieft", name: "IEFT" },
|
|
44
|
+
{ id: "iet", name: "IET" },
|
|
45
|
+
{ id: "ifag", name: "IFAG" },
|
|
46
|
+
{ id: "igefi", name: "IGEFI" },
|
|
47
|
+
{ id: "ihedrea", name: "IHEDREA" },
|
|
48
|
+
{ id: "ileri", name: "ILERI" },
|
|
49
|
+
{ id: "sup-de-com", name: "SUP DE COM" },
|
|
50
|
+
{ id: "viva-mundi", name: "VIVA MUNDI" },
|
|
51
|
+
{ id: "wis", name: "WIS" }
|
|
52
|
+
];
|
|
53
|
+
var IGENSIA_INSTANCES = [
|
|
54
|
+
{ id: "american-business-college", name: "American Business College" },
|
|
55
|
+
{ id: "business-science-institute", name: "Business Science Institute" },
|
|
56
|
+
{ id: "cnva", name: "CNVA" },
|
|
57
|
+
{ id: "ecm", name: "ECM" },
|
|
58
|
+
{ id: "emi", name: "EMI" },
|
|
59
|
+
{ id: "esa", name: "ESA" },
|
|
60
|
+
{ id: "esam", name: "ESAM" },
|
|
61
|
+
{ id: "icd-business-school", name: "ICD Business School" },
|
|
62
|
+
{ id: "igensia-rh", name: "IGENSIA RH" },
|
|
63
|
+
{ id: "imis", name: "IMIS" },
|
|
64
|
+
{ id: "imsi", name: "IMSI" },
|
|
65
|
+
{ id: "ipi", name: "IPI" },
|
|
66
|
+
{ id: "iscpa", name: "ISCPA" },
|
|
67
|
+
{ id: "ismm", name: "ISMM" }
|
|
68
|
+
];
|
|
69
|
+
var INSTANCES = [
|
|
70
|
+
...CD_INSTANCES,
|
|
71
|
+
...IGENSIA_INSTANCES
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// src/utils/cas.ts
|
|
75
|
+
var getCASURL = (_instanceId) => {
|
|
76
|
+
return LOGIN_SERVER_ENDPOINT;
|
|
77
|
+
};
|
|
78
|
+
function parseCasAttributes(html) {
|
|
79
|
+
const attributes = {};
|
|
80
|
+
const regex = /<tr[^>]*>\s*<td[^>]*>\s*<code>\s*<kbd>\s*([^<]+?)\s*<\/kbd>\s*<\/code>\s*<\/td>\s*<td[^>]*>\s*<code>\s*<kbd>\s*([\s\S]*?)\s*<\/kbd>\s*<\/code>\s*<\/td>\s*<\/tr>/gi;
|
|
81
|
+
for (const match of html.matchAll(regex)) {
|
|
82
|
+
const key = match[1].trim();
|
|
83
|
+
let val = match[2].trim();
|
|
84
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
85
|
+
val = val.slice(1, -1);
|
|
86
|
+
}
|
|
87
|
+
attributes[key] = val;
|
|
88
|
+
}
|
|
89
|
+
return attributes;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/utils/cookie.ts
|
|
93
|
+
function updateCookies(jar, url, setCookieHeaders) {
|
|
94
|
+
const parsedUrl = new URL(url);
|
|
95
|
+
const hostname = parsedUrl.hostname;
|
|
96
|
+
for (const header of setCookieHeaders) {
|
|
97
|
+
const parts = header.split(";");
|
|
98
|
+
const firstPart = parts[0]?.split("=");
|
|
99
|
+
if (firstPart && firstPart.length === 2 && firstPart[0]) {
|
|
100
|
+
const name = firstPart[0].trim();
|
|
101
|
+
const value = firstPart[1].trim();
|
|
102
|
+
let targetDomain = hostname;
|
|
103
|
+
let path = "/";
|
|
104
|
+
let isDeletion = value === "";
|
|
105
|
+
let expiresAt;
|
|
106
|
+
for (const part of parts.slice(1)) {
|
|
107
|
+
const p = part.trim().toLowerCase();
|
|
108
|
+
if (p.startsWith("domain=")) {
|
|
109
|
+
const d = p.split("=")[1]?.trim();
|
|
110
|
+
if (d) targetDomain = d.startsWith(".") ? d : `.${d}`;
|
|
111
|
+
} else if (p.startsWith("path=")) {
|
|
112
|
+
path = p.split("=")[1]?.trim() || "/";
|
|
113
|
+
} else if (p.startsWith("expires=")) {
|
|
114
|
+
const exp = p.split("=")[1]?.trim();
|
|
115
|
+
if (exp) {
|
|
116
|
+
const date = new Date(exp);
|
|
117
|
+
if (date.getTime() < Date.now()) isDeletion = true;
|
|
118
|
+
expiresAt = date.getTime();
|
|
119
|
+
}
|
|
120
|
+
} else if (p.startsWith("max-age=")) {
|
|
121
|
+
const maxAge = parseInt(p.split("=")[1]?.trim() || "0", 10);
|
|
122
|
+
if (maxAge <= 0) isDeletion = true;
|
|
123
|
+
else expiresAt = Date.now() + maxAge * 1e3;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let domainCookies = jar.get(targetDomain);
|
|
127
|
+
if (!domainCookies) {
|
|
128
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
129
|
+
jar.set(targetDomain, domainCookies);
|
|
130
|
+
}
|
|
131
|
+
if (isDeletion) {
|
|
132
|
+
domainCookies.delete(name);
|
|
133
|
+
} else {
|
|
134
|
+
domainCookies.set(name, { value, path, expires: expiresAt });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function serializeCookies(jar, url) {
|
|
140
|
+
const parsedUrl = new URL(url);
|
|
141
|
+
const hostname = parsedUrl.hostname;
|
|
142
|
+
const path = parsedUrl.pathname;
|
|
143
|
+
const cookiesToSet = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const [domain, domainCookies] of jar.entries()) {
|
|
145
|
+
if (hostname === domain || domain.startsWith(".") && hostname.endsWith(domain)) {
|
|
146
|
+
for (const [name, cookie] of domainCookies.entries()) {
|
|
147
|
+
if (cookie.expires && cookie.expires < Date.now()) {
|
|
148
|
+
domainCookies.delete(name);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (path.startsWith(cookie.path)) {
|
|
152
|
+
cookiesToSet.set(name, cookie.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return Array.from(cookiesToSet.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/utils/planning.ts
|
|
161
|
+
var getPlanningServer = (instanceId) => {
|
|
162
|
+
if (!instanceId) {
|
|
163
|
+
throw new Error("Instance ID is required");
|
|
164
|
+
}
|
|
165
|
+
const instance = INSTANCES.find((inst) => inst.id === instanceId);
|
|
166
|
+
if (!instance) {
|
|
167
|
+
throw new Error(`Unknown instance: ${instanceId}`);
|
|
168
|
+
}
|
|
169
|
+
const isCD = CD_INSTANCES.some((inst) => inst.id === instanceId);
|
|
170
|
+
if (isCD) {
|
|
171
|
+
return CD_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
172
|
+
}
|
|
173
|
+
return IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/services/profile.ts
|
|
177
|
+
async function getProfile(instanceId, token) {
|
|
178
|
+
const jar = /* @__PURE__ */ new Map();
|
|
179
|
+
try {
|
|
180
|
+
const jarArray = JSON.parse(token);
|
|
181
|
+
for (const [domain, entries] of jarArray) {
|
|
182
|
+
jar.set(domain, new Map(entries));
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new Error(`Failed to parse cookie jar token: ${e}`);
|
|
186
|
+
}
|
|
187
|
+
const casUrl = getCASURL(instanceId);
|
|
188
|
+
const response = await fetch(casUrl, {
|
|
189
|
+
headers: {
|
|
190
|
+
"User-Agent": "linkgor",
|
|
191
|
+
Cookie: serializeCookies(jar, casUrl)
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to fetch profile: ${response.status} ${response.statusText}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const html = await response.text();
|
|
200
|
+
const attributes = parseCasAttributes(html);
|
|
201
|
+
if (!attributes.sAMAccountName && !attributes.mail && !attributes.cn) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Failed to parse profile attributes. CAS session might be expired."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
firstname: attributes.givenName || "",
|
|
208
|
+
lastname: attributes.sn || "",
|
|
209
|
+
email: attributes.mail || "",
|
|
210
|
+
username: attributes.sAMAccountName || "",
|
|
211
|
+
cn: attributes.cn || "",
|
|
212
|
+
city: attributes.l || "",
|
|
213
|
+
country: attributes.co || ""
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/services/planning.ts
|
|
218
|
+
async function loginWithCredentials(instanceId, username, password) {
|
|
219
|
+
const jar = /* @__PURE__ */ new Map();
|
|
220
|
+
const loginServer = LOGIN_SERVER_ENDPOINT;
|
|
221
|
+
const scheduleServer = getPlanningServer(instanceId);
|
|
222
|
+
const now = /* @__PURE__ */ new Date();
|
|
223
|
+
const from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
224
|
+
const to = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
225
|
+
const params = new URLSearchParams({
|
|
226
|
+
sort: "",
|
|
227
|
+
group: "",
|
|
228
|
+
filter: "",
|
|
229
|
+
dateDebut: from.toISOString(),
|
|
230
|
+
dateFin: to.toISOString()
|
|
231
|
+
});
|
|
232
|
+
const serviceUrl = `${scheduleServer}?${params.toString()}`;
|
|
233
|
+
const currentUrl = `${loginServer}?service=${encodeURIComponent(serviceUrl)}`;
|
|
234
|
+
const getRes = await fetch(currentUrl, {
|
|
235
|
+
headers: { "User-Agent": "wigor-papillon" }
|
|
236
|
+
});
|
|
237
|
+
updateCookies(jar, currentUrl, getRes.headers.getSetCookie());
|
|
238
|
+
const html = await getRes.text();
|
|
239
|
+
const extractHiddenFields = (htmlText) => {
|
|
240
|
+
const fields = {};
|
|
241
|
+
const regex = /<input[^>]+type="hidden"[^>]+name="([^"]+)"[^>]+value="([^"]*)"/gi;
|
|
242
|
+
for (const match of htmlText.matchAll(regex)) {
|
|
243
|
+
if (match[1]) fields[match[1]] = match[2] || "";
|
|
244
|
+
}
|
|
245
|
+
return fields;
|
|
246
|
+
};
|
|
247
|
+
const hidden = extractHiddenFields(html);
|
|
248
|
+
const form = new URLSearchParams();
|
|
249
|
+
form.append("username", username);
|
|
250
|
+
form.append("password", password);
|
|
251
|
+
for (const [k, v] of Object.entries(hidden)) {
|
|
252
|
+
if (k !== "username" && k !== "password") form.append(k, v);
|
|
253
|
+
}
|
|
254
|
+
if (!form.has("_eventId")) form.append("_eventId", "submit");
|
|
255
|
+
let response = await fetch(currentUrl, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
259
|
+
"User-Agent": "wigor-papillon",
|
|
260
|
+
Cookie: serializeCookies(jar, currentUrl)
|
|
261
|
+
},
|
|
262
|
+
body: form.toString(),
|
|
263
|
+
redirect: "manual"
|
|
264
|
+
});
|
|
265
|
+
let redirectCount = 0;
|
|
266
|
+
const maxRedirects = 15;
|
|
267
|
+
const seenTickets = /* @__PURE__ */ new Set();
|
|
268
|
+
let followUrl = currentUrl;
|
|
269
|
+
while (true) {
|
|
270
|
+
updateCookies(jar, followUrl, response.headers.getSetCookie());
|
|
271
|
+
if (response.status >= 300 && response.status < 400) {
|
|
272
|
+
let location = response.headers.get("location");
|
|
273
|
+
if (!location) break;
|
|
274
|
+
const urlObj = new URL(location, followUrl);
|
|
275
|
+
const ticket = urlObj.searchParams.get("ticket");
|
|
276
|
+
if (ticket) {
|
|
277
|
+
if (seenTickets.has(ticket)) {
|
|
278
|
+
urlObj.searchParams.delete("ticket");
|
|
279
|
+
location = urlObj.toString();
|
|
280
|
+
} else {
|
|
281
|
+
seenTickets.add(ticket);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
followUrl = new URL(location, followUrl).toString();
|
|
285
|
+
const cookieHeader = serializeCookies(jar, followUrl);
|
|
286
|
+
response = await fetch(followUrl, {
|
|
287
|
+
headers: {
|
|
288
|
+
"User-Agent": "wigor-papillon",
|
|
289
|
+
Cookie: cookieHeader
|
|
290
|
+
},
|
|
291
|
+
redirect: "manual"
|
|
292
|
+
});
|
|
293
|
+
redirectCount++;
|
|
294
|
+
if (redirectCount > maxRedirects) break;
|
|
295
|
+
} else {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (response.status >= 400) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Authentication failed with status ${response.status} at ${followUrl}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const jarArray = Array.from(jar.entries()).map(([domain, map]) => {
|
|
305
|
+
return [domain, Array.from(map.entries())];
|
|
306
|
+
});
|
|
307
|
+
const token = JSON.stringify(jarArray);
|
|
308
|
+
const profile = await getProfile(instanceId, token);
|
|
309
|
+
return {
|
|
310
|
+
firstname: profile.firstname,
|
|
311
|
+
lastname: profile.lastname,
|
|
312
|
+
token
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function getPlanning(instanceId, token) {
|
|
316
|
+
const jar = /* @__PURE__ */ new Map();
|
|
317
|
+
try {
|
|
318
|
+
const jarArray = JSON.parse(token);
|
|
319
|
+
for (const [domain, entries] of jarArray) {
|
|
320
|
+
jar.set(domain, new Map(entries));
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
throw new Error(`Failed to parse cookie jar token: ${e}`);
|
|
324
|
+
}
|
|
325
|
+
const scheduleServer = getPlanningServer(instanceId);
|
|
326
|
+
const now = /* @__PURE__ */ new Date();
|
|
327
|
+
const currentYear = now.getFullYear();
|
|
328
|
+
const currentMonth = now.getMonth();
|
|
329
|
+
let fromYear = currentYear;
|
|
330
|
+
let toYear = currentYear + 1;
|
|
331
|
+
if (currentMonth < 7) {
|
|
332
|
+
fromYear = currentYear - 1;
|
|
333
|
+
toYear = currentYear;
|
|
334
|
+
}
|
|
335
|
+
const from = /* @__PURE__ */ new Date(`${fromYear}-09-01T00:00:00Z`);
|
|
336
|
+
const to = /* @__PURE__ */ new Date(`${toYear}-08-31T23:59:59Z`);
|
|
337
|
+
const params = new URLSearchParams({
|
|
338
|
+
dateDebut: from.toISOString(),
|
|
339
|
+
dateFin: to.toISOString()
|
|
340
|
+
});
|
|
341
|
+
const fullScheduleUrl = `${scheduleServer}?${params.toString()}`;
|
|
342
|
+
const response = await fetch(fullScheduleUrl, {
|
|
343
|
+
headers: {
|
|
344
|
+
"User-Agent": "linkgor",
|
|
345
|
+
Cookie: serializeCookies(jar, scheduleServer)
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
if (!response.ok || response.status === 302) {
|
|
349
|
+
const text = await response.text();
|
|
350
|
+
if (text.includes("cas/login") || response.status === 302) {
|
|
351
|
+
throw new Error("Session expired or redirected to CAS login.");
|
|
352
|
+
}
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Failed to fetch schedule: ${response.status} ${response.statusText}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const json = await response.json();
|
|
358
|
+
return json.Data || [];
|
|
359
|
+
}
|
|
360
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
361
|
+
0 && (module.exports = {
|
|
362
|
+
CD_INSTANCES,
|
|
363
|
+
IGENSIA_INSTANCES,
|
|
364
|
+
INSTANCES,
|
|
365
|
+
getCASURL,
|
|
366
|
+
getPlanning,
|
|
367
|
+
getProfile,
|
|
368
|
+
loginWithCredentials
|
|
369
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var LOGIN_SERVER_ENDPOINT = "https://cas-p.wigorservices.net/cas/login";
|
|
3
|
+
var CD_SCHOOLS_TIMETABLE_ENDPOINT = "https://ws-edt-cd.wigorservices.net/Home/Get";
|
|
4
|
+
var IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT = "https://ws-edt-igs.wigorservices.net/Home/Get";
|
|
5
|
+
var CD_INSTANCES = [
|
|
6
|
+
{ id: "3a", name: "3A" },
|
|
7
|
+
{ id: "epsi", name: "EPSI" },
|
|
8
|
+
{ id: "esail", name: "ESAIL" },
|
|
9
|
+
{ id: "icl", name: "ICL" },
|
|
10
|
+
{ id: "idrac-business-school", name: "IDRAC Business School" },
|
|
11
|
+
{ id: "ieft", name: "IEFT" },
|
|
12
|
+
{ id: "iet", name: "IET" },
|
|
13
|
+
{ id: "ifag", name: "IFAG" },
|
|
14
|
+
{ id: "igefi", name: "IGEFI" },
|
|
15
|
+
{ id: "ihedrea", name: "IHEDREA" },
|
|
16
|
+
{ id: "ileri", name: "ILERI" },
|
|
17
|
+
{ id: "sup-de-com", name: "SUP DE COM" },
|
|
18
|
+
{ id: "viva-mundi", name: "VIVA MUNDI" },
|
|
19
|
+
{ id: "wis", name: "WIS" }
|
|
20
|
+
];
|
|
21
|
+
var IGENSIA_INSTANCES = [
|
|
22
|
+
{ id: "american-business-college", name: "American Business College" },
|
|
23
|
+
{ id: "business-science-institute", name: "Business Science Institute" },
|
|
24
|
+
{ id: "cnva", name: "CNVA" },
|
|
25
|
+
{ id: "ecm", name: "ECM" },
|
|
26
|
+
{ id: "emi", name: "EMI" },
|
|
27
|
+
{ id: "esa", name: "ESA" },
|
|
28
|
+
{ id: "esam", name: "ESAM" },
|
|
29
|
+
{ id: "icd-business-school", name: "ICD Business School" },
|
|
30
|
+
{ id: "igensia-rh", name: "IGENSIA RH" },
|
|
31
|
+
{ id: "imis", name: "IMIS" },
|
|
32
|
+
{ id: "imsi", name: "IMSI" },
|
|
33
|
+
{ id: "ipi", name: "IPI" },
|
|
34
|
+
{ id: "iscpa", name: "ISCPA" },
|
|
35
|
+
{ id: "ismm", name: "ISMM" }
|
|
36
|
+
];
|
|
37
|
+
var INSTANCES = [
|
|
38
|
+
...CD_INSTANCES,
|
|
39
|
+
...IGENSIA_INSTANCES
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// src/utils/cas.ts
|
|
43
|
+
var getCASURL = (_instanceId) => {
|
|
44
|
+
return LOGIN_SERVER_ENDPOINT;
|
|
45
|
+
};
|
|
46
|
+
function parseCasAttributes(html) {
|
|
47
|
+
const attributes = {};
|
|
48
|
+
const regex = /<tr[^>]*>\s*<td[^>]*>\s*<code>\s*<kbd>\s*([^<]+?)\s*<\/kbd>\s*<\/code>\s*<\/td>\s*<td[^>]*>\s*<code>\s*<kbd>\s*([\s\S]*?)\s*<\/kbd>\s*<\/code>\s*<\/td>\s*<\/tr>/gi;
|
|
49
|
+
for (const match of html.matchAll(regex)) {
|
|
50
|
+
const key = match[1].trim();
|
|
51
|
+
let val = match[2].trim();
|
|
52
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
53
|
+
val = val.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
attributes[key] = val;
|
|
56
|
+
}
|
|
57
|
+
return attributes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/utils/cookie.ts
|
|
61
|
+
function updateCookies(jar, url, setCookieHeaders) {
|
|
62
|
+
const parsedUrl = new URL(url);
|
|
63
|
+
const hostname = parsedUrl.hostname;
|
|
64
|
+
for (const header of setCookieHeaders) {
|
|
65
|
+
const parts = header.split(";");
|
|
66
|
+
const firstPart = parts[0]?.split("=");
|
|
67
|
+
if (firstPart && firstPart.length === 2 && firstPart[0]) {
|
|
68
|
+
const name = firstPart[0].trim();
|
|
69
|
+
const value = firstPart[1].trim();
|
|
70
|
+
let targetDomain = hostname;
|
|
71
|
+
let path = "/";
|
|
72
|
+
let isDeletion = value === "";
|
|
73
|
+
let expiresAt;
|
|
74
|
+
for (const part of parts.slice(1)) {
|
|
75
|
+
const p = part.trim().toLowerCase();
|
|
76
|
+
if (p.startsWith("domain=")) {
|
|
77
|
+
const d = p.split("=")[1]?.trim();
|
|
78
|
+
if (d) targetDomain = d.startsWith(".") ? d : `.${d}`;
|
|
79
|
+
} else if (p.startsWith("path=")) {
|
|
80
|
+
path = p.split("=")[1]?.trim() || "/";
|
|
81
|
+
} else if (p.startsWith("expires=")) {
|
|
82
|
+
const exp = p.split("=")[1]?.trim();
|
|
83
|
+
if (exp) {
|
|
84
|
+
const date = new Date(exp);
|
|
85
|
+
if (date.getTime() < Date.now()) isDeletion = true;
|
|
86
|
+
expiresAt = date.getTime();
|
|
87
|
+
}
|
|
88
|
+
} else if (p.startsWith("max-age=")) {
|
|
89
|
+
const maxAge = parseInt(p.split("=")[1]?.trim() || "0", 10);
|
|
90
|
+
if (maxAge <= 0) isDeletion = true;
|
|
91
|
+
else expiresAt = Date.now() + maxAge * 1e3;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let domainCookies = jar.get(targetDomain);
|
|
95
|
+
if (!domainCookies) {
|
|
96
|
+
domainCookies = /* @__PURE__ */ new Map();
|
|
97
|
+
jar.set(targetDomain, domainCookies);
|
|
98
|
+
}
|
|
99
|
+
if (isDeletion) {
|
|
100
|
+
domainCookies.delete(name);
|
|
101
|
+
} else {
|
|
102
|
+
domainCookies.set(name, { value, path, expires: expiresAt });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function serializeCookies(jar, url) {
|
|
108
|
+
const parsedUrl = new URL(url);
|
|
109
|
+
const hostname = parsedUrl.hostname;
|
|
110
|
+
const path = parsedUrl.pathname;
|
|
111
|
+
const cookiesToSet = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const [domain, domainCookies] of jar.entries()) {
|
|
113
|
+
if (hostname === domain || domain.startsWith(".") && hostname.endsWith(domain)) {
|
|
114
|
+
for (const [name, cookie] of domainCookies.entries()) {
|
|
115
|
+
if (cookie.expires && cookie.expires < Date.now()) {
|
|
116
|
+
domainCookies.delete(name);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (path.startsWith(cookie.path)) {
|
|
120
|
+
cookiesToSet.set(name, cookie.value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return Array.from(cookiesToSet.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/utils/planning.ts
|
|
129
|
+
var getPlanningServer = (instanceId) => {
|
|
130
|
+
if (!instanceId) {
|
|
131
|
+
throw new Error("Instance ID is required");
|
|
132
|
+
}
|
|
133
|
+
const instance = INSTANCES.find((inst) => inst.id === instanceId);
|
|
134
|
+
if (!instance) {
|
|
135
|
+
throw new Error(`Unknown instance: ${instanceId}`);
|
|
136
|
+
}
|
|
137
|
+
const isCD = CD_INSTANCES.some((inst) => inst.id === instanceId);
|
|
138
|
+
if (isCD) {
|
|
139
|
+
return CD_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
140
|
+
}
|
|
141
|
+
return IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/services/profile.ts
|
|
145
|
+
async function getProfile(instanceId, token) {
|
|
146
|
+
const jar = /* @__PURE__ */ new Map();
|
|
147
|
+
try {
|
|
148
|
+
const jarArray = JSON.parse(token);
|
|
149
|
+
for (const [domain, entries] of jarArray) {
|
|
150
|
+
jar.set(domain, new Map(entries));
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
throw new Error(`Failed to parse cookie jar token: ${e}`);
|
|
154
|
+
}
|
|
155
|
+
const casUrl = getCASURL(instanceId);
|
|
156
|
+
const response = await fetch(casUrl, {
|
|
157
|
+
headers: {
|
|
158
|
+
"User-Agent": "linkgor",
|
|
159
|
+
Cookie: serializeCookies(jar, casUrl)
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Failed to fetch profile: ${response.status} ${response.statusText}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const html = await response.text();
|
|
168
|
+
const attributes = parseCasAttributes(html);
|
|
169
|
+
if (!attributes.sAMAccountName && !attributes.mail && !attributes.cn) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Failed to parse profile attributes. CAS session might be expired."
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
firstname: attributes.givenName || "",
|
|
176
|
+
lastname: attributes.sn || "",
|
|
177
|
+
email: attributes.mail || "",
|
|
178
|
+
username: attributes.sAMAccountName || "",
|
|
179
|
+
cn: attributes.cn || "",
|
|
180
|
+
city: attributes.l || "",
|
|
181
|
+
country: attributes.co || ""
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/services/planning.ts
|
|
186
|
+
async function loginWithCredentials(instanceId, username, password) {
|
|
187
|
+
const jar = /* @__PURE__ */ new Map();
|
|
188
|
+
const loginServer = LOGIN_SERVER_ENDPOINT;
|
|
189
|
+
const scheduleServer = getPlanningServer(instanceId);
|
|
190
|
+
const now = /* @__PURE__ */ new Date();
|
|
191
|
+
const from = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
192
|
+
const to = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
193
|
+
const params = new URLSearchParams({
|
|
194
|
+
sort: "",
|
|
195
|
+
group: "",
|
|
196
|
+
filter: "",
|
|
197
|
+
dateDebut: from.toISOString(),
|
|
198
|
+
dateFin: to.toISOString()
|
|
199
|
+
});
|
|
200
|
+
const serviceUrl = `${scheduleServer}?${params.toString()}`;
|
|
201
|
+
const currentUrl = `${loginServer}?service=${encodeURIComponent(serviceUrl)}`;
|
|
202
|
+
const getRes = await fetch(currentUrl, {
|
|
203
|
+
headers: { "User-Agent": "wigor-papillon" }
|
|
204
|
+
});
|
|
205
|
+
updateCookies(jar, currentUrl, getRes.headers.getSetCookie());
|
|
206
|
+
const html = await getRes.text();
|
|
207
|
+
const extractHiddenFields = (htmlText) => {
|
|
208
|
+
const fields = {};
|
|
209
|
+
const regex = /<input[^>]+type="hidden"[^>]+name="([^"]+)"[^>]+value="([^"]*)"/gi;
|
|
210
|
+
for (const match of htmlText.matchAll(regex)) {
|
|
211
|
+
if (match[1]) fields[match[1]] = match[2] || "";
|
|
212
|
+
}
|
|
213
|
+
return fields;
|
|
214
|
+
};
|
|
215
|
+
const hidden = extractHiddenFields(html);
|
|
216
|
+
const form = new URLSearchParams();
|
|
217
|
+
form.append("username", username);
|
|
218
|
+
form.append("password", password);
|
|
219
|
+
for (const [k, v] of Object.entries(hidden)) {
|
|
220
|
+
if (k !== "username" && k !== "password") form.append(k, v);
|
|
221
|
+
}
|
|
222
|
+
if (!form.has("_eventId")) form.append("_eventId", "submit");
|
|
223
|
+
let response = await fetch(currentUrl, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
227
|
+
"User-Agent": "wigor-papillon",
|
|
228
|
+
Cookie: serializeCookies(jar, currentUrl)
|
|
229
|
+
},
|
|
230
|
+
body: form.toString(),
|
|
231
|
+
redirect: "manual"
|
|
232
|
+
});
|
|
233
|
+
let redirectCount = 0;
|
|
234
|
+
const maxRedirects = 15;
|
|
235
|
+
const seenTickets = /* @__PURE__ */ new Set();
|
|
236
|
+
let followUrl = currentUrl;
|
|
237
|
+
while (true) {
|
|
238
|
+
updateCookies(jar, followUrl, response.headers.getSetCookie());
|
|
239
|
+
if (response.status >= 300 && response.status < 400) {
|
|
240
|
+
let location = response.headers.get("location");
|
|
241
|
+
if (!location) break;
|
|
242
|
+
const urlObj = new URL(location, followUrl);
|
|
243
|
+
const ticket = urlObj.searchParams.get("ticket");
|
|
244
|
+
if (ticket) {
|
|
245
|
+
if (seenTickets.has(ticket)) {
|
|
246
|
+
urlObj.searchParams.delete("ticket");
|
|
247
|
+
location = urlObj.toString();
|
|
248
|
+
} else {
|
|
249
|
+
seenTickets.add(ticket);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
followUrl = new URL(location, followUrl).toString();
|
|
253
|
+
const cookieHeader = serializeCookies(jar, followUrl);
|
|
254
|
+
response = await fetch(followUrl, {
|
|
255
|
+
headers: {
|
|
256
|
+
"User-Agent": "wigor-papillon",
|
|
257
|
+
Cookie: cookieHeader
|
|
258
|
+
},
|
|
259
|
+
redirect: "manual"
|
|
260
|
+
});
|
|
261
|
+
redirectCount++;
|
|
262
|
+
if (redirectCount > maxRedirects) break;
|
|
263
|
+
} else {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (response.status >= 400) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Authentication failed with status ${response.status} at ${followUrl}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const jarArray = Array.from(jar.entries()).map(([domain, map]) => {
|
|
273
|
+
return [domain, Array.from(map.entries())];
|
|
274
|
+
});
|
|
275
|
+
const token = JSON.stringify(jarArray);
|
|
276
|
+
const profile = await getProfile(instanceId, token);
|
|
277
|
+
return {
|
|
278
|
+
firstname: profile.firstname,
|
|
279
|
+
lastname: profile.lastname,
|
|
280
|
+
token
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function getPlanning(instanceId, token) {
|
|
284
|
+
const jar = /* @__PURE__ */ new Map();
|
|
285
|
+
try {
|
|
286
|
+
const jarArray = JSON.parse(token);
|
|
287
|
+
for (const [domain, entries] of jarArray) {
|
|
288
|
+
jar.set(domain, new Map(entries));
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
throw new Error(`Failed to parse cookie jar token: ${e}`);
|
|
292
|
+
}
|
|
293
|
+
const scheduleServer = getPlanningServer(instanceId);
|
|
294
|
+
const now = /* @__PURE__ */ new Date();
|
|
295
|
+
const currentYear = now.getFullYear();
|
|
296
|
+
const currentMonth = now.getMonth();
|
|
297
|
+
let fromYear = currentYear;
|
|
298
|
+
let toYear = currentYear + 1;
|
|
299
|
+
if (currentMonth < 7) {
|
|
300
|
+
fromYear = currentYear - 1;
|
|
301
|
+
toYear = currentYear;
|
|
302
|
+
}
|
|
303
|
+
const from = /* @__PURE__ */ new Date(`${fromYear}-09-01T00:00:00Z`);
|
|
304
|
+
const to = /* @__PURE__ */ new Date(`${toYear}-08-31T23:59:59Z`);
|
|
305
|
+
const params = new URLSearchParams({
|
|
306
|
+
dateDebut: from.toISOString(),
|
|
307
|
+
dateFin: to.toISOString()
|
|
308
|
+
});
|
|
309
|
+
const fullScheduleUrl = `${scheduleServer}?${params.toString()}`;
|
|
310
|
+
const response = await fetch(fullScheduleUrl, {
|
|
311
|
+
headers: {
|
|
312
|
+
"User-Agent": "linkgor",
|
|
313
|
+
Cookie: serializeCookies(jar, scheduleServer)
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (!response.ok || response.status === 302) {
|
|
317
|
+
const text = await response.text();
|
|
318
|
+
if (text.includes("cas/login") || response.status === 302) {
|
|
319
|
+
throw new Error("Session expired or redirected to CAS login.");
|
|
320
|
+
}
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Failed to fetch schedule: ${response.status} ${response.statusText}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const json = await response.json();
|
|
326
|
+
return json.Data || [];
|
|
327
|
+
}
|
|
328
|
+
export {
|
|
329
|
+
CD_INSTANCES,
|
|
330
|
+
IGENSIA_INSTANCES,
|
|
331
|
+
INSTANCES,
|
|
332
|
+
getCASURL,
|
|
333
|
+
getPlanning,
|
|
334
|
+
getProfile,
|
|
335
|
+
loginWithCredentials
|
|
336
|
+
};
|