diff --git a/src/api/NotesApi.ts b/src/api/NotesApi.ts index bda08aa..f38fd28 100644 --- a/src/api/NotesApi.ts +++ b/src/api/NotesApi.ts @@ -14,8 +14,7 @@ class NotesApi { return new Promise(async (resolve, reject) => { try { // On initialise le ViewState (obligatoire pour avoir une réponse correcteur du backend) - // Ici 1_1 correspond au sous-menu 'Mes notes' dans la sidebar - await this.session.getViewState("1_1"); + await this.session.getViewState("Mes notes"); const response = await this.session.sendGET( "faces/LearnerNotationListPage.xhtml", ); diff --git a/src/api/PlanningApi.ts b/src/api/PlanningApi.ts index b2c66be..7e08c3f 100644 --- a/src/api/PlanningApi.ts +++ b/src/api/PlanningApi.ts @@ -17,8 +17,7 @@ class PlanningApi { return new Promise(async (resolve, reject) => { try { // On récupère le ViewState pour effectuer la requête - // Ici 1_4 correspond au sous-menu 'Emploi du temps' dans la sidebar - let viewState = await this.session.getViewState("1_4"); + let viewState = await this.session.getViewState("Mon planning"); // On envoie enfin la requête pour obtenir l'emploi du temps const params = getJSFFormParams( "j_idt118", diff --git a/src/api/Session.ts b/src/api/Session.ts index c583083..4473bfc 100644 --- a/src/api/Session.ts +++ b/src/api/Session.ts @@ -1,26 +1,89 @@ -import axios, { AxiosInstance } from "axios"; import PlanningApi from "./PlanningApi"; -import { getJSFFormParams, getViewState } from "../utils/AurionUtils"; +import { + getJSFFormParams, + getName, + getSidebarMenuId, + getViewState, +} from "../utils/AurionUtils"; import NotesApi from "./NotesApi"; +import axios, { AxiosInstance } from "axios"; export class Session { private client: AxiosInstance; + private baseURL: string = "https://web.isen-ouest.fr/webAurion"; //Permet de sauvegarder le ViewState et le subMenuId pour les réutiliser dans les prochaines requêtes (optimisation) //Cela a pour but d'éviter d'effectuer 3 requêtes lorsque l'on refait la même demande (emploi du temps de la semaine suivante par exemple) private viewStateCache: string = ""; private subMenuIdCache: string = ""; - constructor(baseURL: string, token: string) { + // Nom et prénom de l'utilisateur + private username: string = ""; + + constructor() { this.client = axios.create({ - baseURL, + baseURL: this.baseURL, headers: { "Content-Type": "application/x-www-form-urlencoded", - Cookie: `JSESSIONID=${token}`, + "Accept-Language": "fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3", }, }); } + /** + * Authentifie un utilisateur avec un nom d'utilisateur et un mot de passe. + * + * @param {string} username - Le nom d'utilisateur de l'utilisateur. + * @param {string} password - Le mot de passe de l'utilisateur. + * @returns {Promise} Une promesse qui se résout si l'authentification réussit. + * @throws {Error} Si l'authentification échoue. + */ + public login(username: string, password: string): Promise { + return new Promise((resolve, reject) => { + const params = new URLSearchParams(); + params.append("username", username); + params.append("password", password); + this.client + .post("/login", params, { + maxRedirects: 0, + }) + .then((response) => { + //Si la réponse est 302 et que le cookie est défini alors on retourne une nouvelle session + if ( + response.status === 302 && + response.headers["set-cookie"] + ) { + const token = response.headers["set-cookie"][0] + .split(";")[0] + .split("=")[1]; + //On ajoute le cookie JSESSIONID aux en-têtes par défaut pour les prochaines requêtes + this.client.defaults.headers.Cookie = `JSESSIONID=${token}`; + resolve(); + } else { + reject(response); + } + }) + .catch((err) => { + //Si la réponse est 302 et que le cookie est défini alors on retourne une nouvelle session + if (err.response && err.response.status === 302) { + const token = err.response.headers["set-cookie"][0] + .split(";")[0] + .split("=")[1]; + //On ajoute le cookie JSESSIONID aux en-têtes par défaut pour les prochaines requêtes + this.client.defaults.headers.Cookie = `JSESSIONID=${token}`; + resolve(); + } else { + reject(err); + } + }); + }); + } + + //On retourne le nom de l'utilisateur + public getUsername(): string { + return this.username; + } + // API pour le calendrier public getPlanningApi(): PlanningApi { return new PlanningApi(this); @@ -91,10 +154,10 @@ export class Session { } // Récupération du ViewState pour effectuer les différentes requêtes - public getViewState(subMenuId: string): Promise { + public getViewState(subMenuName: string): Promise { return new Promise(async (resolve, reject) => { //On optimise l'accès au ViewState - if (this.viewStateCache && this.subMenuIdCache === subMenuId) { + if (this.viewStateCache && this.subMenuIdCache === subMenuName) { return resolve(this.viewStateCache); } try { @@ -105,8 +168,25 @@ export class Session { if (viewState) { // Ici 291906 correspond au menu 'Scolarité' dans la sidebar // Requête utile pour intialiser le ViewState (obligatoire pour effectuer une requête) - await this.sendSidebarRequest("291906", viewState); + const sidebarResponse = await this.sendSidebarRequest( + "291906", + viewState, + ); + // On récupère le sidebar_menuid correspondant au sous-menu demandé + const subMenuId = getSidebarMenuId( + sidebarResponse, + subMenuName, + ); + // Vérification de l'existence du subMenuId + if (!subMenuId) { + return reject( + new Error( + "Sidebar menu ID not found, subMenuName: " + + subMenuName, + ), + ); + } // On récupère le ViewState pour effectuer la prochaine requête viewState = await this.sendSidebarSubmenuRequest( subMenuId, @@ -114,13 +194,21 @@ export class Session { ); if (viewState) { this.viewStateCache = viewState; - this.subMenuIdCache = subMenuId; + this.subMenuIdCache = subMenuName; return resolve(viewState); } } - return reject(new Error("Viewstate not found")); + return reject( + new Error( + "Viewstate not found, subMenuName: " + subMenuName, + ), + ); } catch (error) { - reject(new Error("Viewstate not found")); + reject( + new Error( + "Viewstate not found, subMenuName: " + subMenuName, + ), + ); } }); } @@ -140,50 +228,4 @@ export class Session { } } -/** - * Authentifie un utilisateur avec un nom d'utilisateur et un mot de passe. - * - * @param {string} username - Le nom d'utilisateur de l'utilisateur. - * @param {string} password - Le mot de passe de l'utilisateur. - * @returns {Promise} Une promesse qui se résout avec une instance de Session si l'authentification réussit. - * @throws {Error} Si l'authentification échoue. - */ -export function login(username: string, password: string): Promise { - const baseURL = "https://web.isen-ouest.fr/webAurion"; - return new Promise((resolve, reject) => { - const params = new URLSearchParams(); - params.append("username", username); - params.append("password", password); - axios - .post(`${baseURL}/login`, params, { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - maxRedirects: 0, - }) - .then((response) => { - //Si la réponse est 302 et que le cookie est défini alors on retourne une nouvelle session - if (response.status === 302 && response.headers["set-cookie"]) { - const token = response.headers["set-cookie"][0] - .split(";")[0] - .split("=")[1]; - resolve(new Session(baseURL, token)); - } else { - reject(response); - } - }) - .catch((err) => { - //Si la réponse est 302 et que le cookie est défini alors on retourne une nouvelle session - if (err.response && err.response.status === 302) { - const token = err.response.headers["set-cookie"][0] - .split(";")[0] - .split("=")[1]; - resolve(new Session(baseURL, token)); - } else { - reject(err); - } - }); - }); -} - export default Session; diff --git a/src/utils/AurionUtils.ts b/src/utils/AurionUtils.ts index 496e2fa..442258a 100644 --- a/src/utils/AurionUtils.ts +++ b/src/utils/AurionUtils.ts @@ -32,3 +32,50 @@ export function getJSFFormParams( params.append("javax.faces.ViewState", viewState); return params; } + +// Récupération du prénom / nom de l'utilisateur lors de la connexion +export function getName(html: string): string { + const parser = load(html); + //On recherche de l'élément qui contient le prénom et le nom + const usernameElement = parser("li.ui-widget-header > h3"); + if (usernameElement.length > 0) { + //On récupère le texte contenu dans l'élément + const username = usernameElement.text(); + return username; + } + return ""; +} + +/** + * Extrait le sidebar_menuid correspondant à un texte donné dans le menu. + * @param {string} html - Le contenu HTML à parser + * @param {string} label - Le texte du menu à rechercher (ex: "Mon planning" ou "Mes notes") + * @returns {string|null} Le sidebar_menuid correspondant, ou null si non trouvé + */ +export function getSidebarMenuId(html: string, label: string): string | null { + const parser = load(html); + + // Cherche le span dont le texte commence par `label` + const span = parser("span.ui-menuitem-text") + .filter((_, el) => { + const text = parser(el).text().trim(); + return text.startsWith(label); + }) + .first(); + + if (!span.length) { + // Non trouvé + return null; + } + + // Trouve l’attribut onclick du lien parent + const onclick = span.closest("a").attr("onclick"); + if (!onclick) { + // Non trouvé + return null; + } + + // Extrait la valeur form:sidebar_menuid:'X_Y' + const match = onclick.match(/'form:sidebar_menuid':'([^']+)'/); + return match ? match[1] : null; +} diff --git a/tests/Cache.test.ts b/tests/Cache.test.ts index 0472b62..bfa9b68 100644 --- a/tests/Cache.test.ts +++ b/tests/Cache.test.ts @@ -1,47 +1,55 @@ -import { login } from "../src/api/Session"; +import Session from "../src/api/Session"; describe("CacheTests", () => { - it("should test for function performance", async () => { - const username = process.env.TEST_USERNAME; - const password = process.env.TEST_PASSWORD; - if (!username || !password) { - throw new Error( - "TEST_USERNAME or TEST_PASSWORD is not set in environment variables.", + it( + "should test for function performance", + async () => { + const username = process.env.TEST_USERNAME; + const password = process.env.TEST_PASSWORD; + if (!username || !password) { + throw new Error( + "TEST_USERNAME or TEST_PASSWORD is not set in environment variables.", + ); + } + const session = new Session(); + await session.login(username, password); + + //Première fois sans le cache on récupère l'emploi du temps + let start = performance.now(); + let planning = await session.getPlanningApi().fetchPlanning(); + let end = performance.now(); + let duration = end - start; + console.log( + `fetchPlanning (sans cache): ${duration.toFixed(2)} ms`, ); - } - const session = await login(username, password); + //Deuxième fois avec le cache on récupère l'emploi du temps + start = performance.now(); + planning = await session.getPlanningApi().fetchPlanning(); + end = performance.now(); + duration = end - start; + console.log( + `fetchPlanning (avec cache): ${duration.toFixed(2)} ms`, + ); - //Première fois sans le cache on récupère l'emploi du temps - let start = performance.now(); - let planning = await session.getPlanningApi().fetchPlanning(); - let end = performance.now(); - let duration = end - start; - console.log(`fetchPlanning (sans cache): ${duration.toFixed(2)} ms`); + //Première fois sans le cache on récupère les notes + start = performance.now(); + let notes = await session.getNotesApi().fetchNotes(); + end = performance.now(); + duration = end - start; + console.log(`fetchNotes (sans cache): ${duration.toFixed(2)} ms`); - //Deuxième fois avec le cache on récupère l'emploi du temps - start = performance.now(); - planning = await session.getPlanningApi().fetchPlanning(); - end = performance.now(); - duration = end - start; - console.log(`fetchPlanning (avec cache): ${duration.toFixed(2)} ms`); + //Deuxième fois avec le cache on récupère les notes + start = performance.now(); + notes = await session.getNotesApi().fetchNotes(); + end = performance.now(); + duration = end - start; + console.log(`fetchNotes (avec cache): ${duration.toFixed(2)} ms`); - //Première fois sans le cache on récupère les notes - start = performance.now(); - let notes = await session.getNotesApi().fetchNotes(); - end = performance.now(); - duration = end - start; - console.log(`fetchNotes (sans cache): ${duration.toFixed(2)} ms`); - - //Deuxième fois avec le cache on récupère les notes - start = performance.now(); - notes = await session.getNotesApi().fetchNotes(); - end = performance.now(); - duration = end - start; - console.log(`fetchNotes (avec cache): ${duration.toFixed(2)} ms`); - - // console.log("Le planning", JSON.stringify(planning)); - // console.log("Les notes ", JSON.stringify(notes)); - expect(notes).toBeInstanceOf(Array); - expect(planning).toBeInstanceOf(Array); - }); + // console.log("Le planning", JSON.stringify(planning)); + // console.log("Les notes ", JSON.stringify(notes)); + expect(notes).toBeInstanceOf(Array); + expect(planning).toBeInstanceOf(Array); + }, + 15 * 1000, + ); }); diff --git a/tests/NotesApi.test.ts b/tests/NotesApi.test.ts index 6824a05..1faff8d 100644 --- a/tests/NotesApi.test.ts +++ b/tests/NotesApi.test.ts @@ -1,4 +1,4 @@ -import { login } from "../src/api/Session"; +import Session from "../src/api/Session"; import { noteAverage } from "../src/utils/NotesUtils"; describe("NotesApi", () => { it("should receive notes", async () => { @@ -10,7 +10,9 @@ describe("NotesApi", () => { ); } - const session = await login(username, password); + const session = new Session(); + await session.login(username, password); + const notes = await session.getNotesApi().fetchNotes(); console.log(JSON.stringify(notes, null, 2)); console.log("Les moyennes: "); diff --git a/tests/PlanningApi.test.ts b/tests/PlanningApi.test.ts index cf697a3..9a128c3 100644 --- a/tests/PlanningApi.test.ts +++ b/tests/PlanningApi.test.ts @@ -1,4 +1,4 @@ -import { login } from "../src/api/Session"; +import Session from "../src/api/Session"; import { getScheduleDates } from "../src/utils/PlanningUtils"; describe("PlanningApi", () => { it("should receive a planning", async () => { @@ -10,7 +10,9 @@ describe("PlanningApi", () => { ); } - const session = await login(username, password); + const session = new Session(); + await session.login(username, password); + const planning = await session.getPlanningApi().fetchPlanning(); console.log(planning); expect(planning).toBeInstanceOf(Array); diff --git a/tests/Session.test.ts b/tests/Session.test.ts index c0c6587..757a837 100644 --- a/tests/Session.test.ts +++ b/tests/Session.test.ts @@ -1,4 +1,4 @@ -import { login } from "../src/api/Session"; +import Session from "../src/api/Session"; describe("AuthApi", () => { it("should log in a user and receive a session", async () => { const username = process.env.TEST_USERNAME; @@ -9,7 +9,9 @@ describe("AuthApi", () => { ); } - const result = await login(username, password); - expect(result).toBeDefined(); + const session = new Session(); + await session.login(username, password); + + expect(session).toBeDefined(); }); });