refactor: update session handling and improve view state retrieval

This commit is contained in:
dd060606
2025-10-23 11:53:28 +02:00
parent bb3369a047
commit 2efa0f3c73
8 changed files with 209 additions and 108 deletions

View File

@@ -14,8 +14,7 @@ class NotesApi {
return new Promise<NotesList[]>(async (resolve, reject) => { return new Promise<NotesList[]>(async (resolve, reject) => {
try { try {
// On initialise le ViewState (obligatoire pour avoir une réponse correcteur du backend) // 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("Mes notes");
await this.session.getViewState("1_1");
const response = await this.session.sendGET<string>( const response = await this.session.sendGET<string>(
"faces/LearnerNotationListPage.xhtml", "faces/LearnerNotationListPage.xhtml",
); );

View File

@@ -17,8 +17,7 @@ class PlanningApi {
return new Promise<PlanningEvent[]>(async (resolve, reject) => { return new Promise<PlanningEvent[]>(async (resolve, reject) => {
try { try {
// On récupère le ViewState pour effectuer la requête // 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("Mon planning");
let viewState = await this.session.getViewState("1_4");
// On envoie enfin la requête pour obtenir l'emploi du temps // On envoie enfin la requête pour obtenir l'emploi du temps
const params = getJSFFormParams( const params = getJSFFormParams(
"j_idt118", "j_idt118",

View File

@@ -1,26 +1,89 @@
import axios, { AxiosInstance } from "axios";
import PlanningApi from "./PlanningApi"; import PlanningApi from "./PlanningApi";
import { getJSFFormParams, getViewState } from "../utils/AurionUtils"; import {
getJSFFormParams,
getName,
getSidebarMenuId,
getViewState,
} from "../utils/AurionUtils";
import NotesApi from "./NotesApi"; import NotesApi from "./NotesApi";
import axios, { AxiosInstance } from "axios";
export class Session { export class Session {
private client: AxiosInstance; 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) //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) //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 viewStateCache: string = "";
private subMenuIdCache: string = ""; private subMenuIdCache: string = "";
constructor(baseURL: string, token: string) { // Nom et prénom de l'utilisateur
private username: string = "";
constructor() {
this.client = axios.create({ this.client = axios.create({
baseURL, baseURL: this.baseURL,
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "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<void>} Une promesse qui se résout si l'authentification réussit.
* @throws {Error} Si l'authentification échoue.
*/
public login(username: string, password: string): Promise<void> {
return new Promise<void>((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 // API pour le calendrier
public getPlanningApi(): PlanningApi { public getPlanningApi(): PlanningApi {
return new PlanningApi(this); return new PlanningApi(this);
@@ -91,10 +154,10 @@ export class Session {
} }
// Récupération du ViewState pour effectuer les différentes requêtes // Récupération du ViewState pour effectuer les différentes requêtes
public getViewState(subMenuId: string): Promise<string> { public getViewState(subMenuName: string): Promise<string> {
return new Promise<string>(async (resolve, reject) => { return new Promise<string>(async (resolve, reject) => {
//On optimise l'accès au ViewState //On optimise l'accès au ViewState
if (this.viewStateCache && this.subMenuIdCache === subMenuId) { if (this.viewStateCache && this.subMenuIdCache === subMenuName) {
return resolve(this.viewStateCache); return resolve(this.viewStateCache);
} }
try { try {
@@ -105,8 +168,25 @@ export class Session {
if (viewState) { if (viewState) {
// Ici 291906 correspond au menu 'Scolarité' dans la sidebar // Ici 291906 correspond au menu 'Scolarité' dans la sidebar
// Requête utile pour intialiser le ViewState (obligatoire pour effectuer une requête) // 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 // On récupère le ViewState pour effectuer la prochaine requête
viewState = await this.sendSidebarSubmenuRequest( viewState = await this.sendSidebarSubmenuRequest(
subMenuId, subMenuId,
@@ -114,13 +194,21 @@ export class Session {
); );
if (viewState) { if (viewState) {
this.viewStateCache = viewState; this.viewStateCache = viewState;
this.subMenuIdCache = subMenuId; this.subMenuIdCache = subMenuName;
return resolve(viewState); return resolve(viewState);
} }
} }
return reject(new Error("Viewstate not found")); return reject(
new Error(
"Viewstate not found, subMenuName: " + subMenuName,
),
);
} catch (error) { } 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<Session>} 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<Session> {
const baseURL = "https://web.isen-ouest.fr/webAurion";
return new Promise<Session>((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; export default Session;

View File

@@ -32,3 +32,50 @@ export function getJSFFormParams(
params.append("javax.faces.ViewState", viewState); params.append("javax.faces.ViewState", viewState);
return params; 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 lattribut 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;
}

View File

@@ -1,47 +1,55 @@
import { login } from "../src/api/Session"; import Session from "../src/api/Session";
describe("CacheTests", () => { describe("CacheTests", () => {
it("should test for function performance", async () => { it(
const username = process.env.TEST_USERNAME; "should test for function performance",
const password = process.env.TEST_PASSWORD; async () => {
if (!username || !password) { const username = process.env.TEST_USERNAME;
throw new Error( const password = process.env.TEST_PASSWORD;
"TEST_USERNAME or TEST_PASSWORD is not set in environment variables.", 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 //Première fois sans le cache on récupère les notes
let start = performance.now(); start = performance.now();
let planning = await session.getPlanningApi().fetchPlanning(); let notes = await session.getNotesApi().fetchNotes();
let end = performance.now(); end = performance.now();
let duration = end - start; duration = end - start;
console.log(`fetchPlanning (sans cache): ${duration.toFixed(2)} ms`); console.log(`fetchNotes (sans cache): ${duration.toFixed(2)} ms`);
//Deuxième fois avec le cache on récupère l'emploi du temps //Deuxième fois avec le cache on récupère les notes
start = performance.now(); start = performance.now();
planning = await session.getPlanningApi().fetchPlanning(); notes = await session.getNotesApi().fetchNotes();
end = performance.now(); end = performance.now();
duration = end - start; duration = end - start;
console.log(`fetchPlanning (avec cache): ${duration.toFixed(2)} ms`); console.log(`fetchNotes (avec cache): ${duration.toFixed(2)} ms`);
//Première fois sans le cache on récupère les notes // console.log("Le planning", JSON.stringify(planning));
start = performance.now(); // console.log("Les notes ", JSON.stringify(notes));
let notes = await session.getNotesApi().fetchNotes(); expect(notes).toBeInstanceOf(Array);
end = performance.now(); expect(planning).toBeInstanceOf(Array);
duration = end - start; },
console.log(`fetchNotes (sans cache): ${duration.toFixed(2)} ms`); 15 * 1000,
);
//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);
});
}); });

View File

@@ -1,4 +1,4 @@
import { login } from "../src/api/Session"; import Session from "../src/api/Session";
import { noteAverage } from "../src/utils/NotesUtils"; import { noteAverage } from "../src/utils/NotesUtils";
describe("NotesApi", () => { describe("NotesApi", () => {
it("should receive notes", async () => { 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(); const notes = await session.getNotesApi().fetchNotes();
console.log(JSON.stringify(notes, null, 2)); console.log(JSON.stringify(notes, null, 2));
console.log("Les moyennes: "); console.log("Les moyennes: ");

View File

@@ -1,4 +1,4 @@
import { login } from "../src/api/Session"; import Session from "../src/api/Session";
import { getScheduleDates } from "../src/utils/PlanningUtils"; import { getScheduleDates } from "../src/utils/PlanningUtils";
describe("PlanningApi", () => { describe("PlanningApi", () => {
it("should receive a planning", async () => { 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(); const planning = await session.getPlanningApi().fetchPlanning();
console.log(planning); console.log(planning);
expect(planning).toBeInstanceOf(Array); expect(planning).toBeInstanceOf(Array);

View File

@@ -1,4 +1,4 @@
import { login } from "../src/api/Session"; import Session from "../src/api/Session";
describe("AuthApi", () => { describe("AuthApi", () => {
it("should log in a user and receive a session", async () => { it("should log in a user and receive a session", async () => {
const username = process.env.TEST_USERNAME; const username = process.env.TEST_USERNAME;
@@ -9,7 +9,9 @@ describe("AuthApi", () => {
); );
} }
const result = await login(username, password); const session = new Session();
expect(result).toBeDefined(); await session.login(username, password);
expect(session).toBeDefined();
}); });
}); });