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) => {
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<string>(
"faces/LearnerNotationListPage.xhtml",
);

View File

@@ -17,8 +17,7 @@ class PlanningApi {
return new Promise<PlanningEvent[]>(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",

View File

@@ -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<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
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<string> {
public getViewState(subMenuName: string): Promise<string> {
return new Promise<string>(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<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;

View File

@@ -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 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,6 +1,8 @@
import { login } from "../src/api/Session";
import Session from "../src/api/Session";
describe("CacheTests", () => {
it("should test for function performance", async () => {
it(
"should test for function performance",
async () => {
const username = process.env.TEST_USERNAME;
const password = process.env.TEST_PASSWORD;
if (!username || !password) {
@@ -8,22 +10,26 @@ describe("CacheTests", () => {
"TEST_USERNAME or TEST_PASSWORD is not set in environment variables.",
);
}
const session = await login(username, password);
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`);
console.log(
`fetchPlanning (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`);
console.log(
`fetchPlanning (avec cache): ${duration.toFixed(2)} ms`,
);
//Première fois sans le cache on récupère les notes
start = performance.now();
@@ -43,5 +49,7 @@ describe("CacheTests", () => {
// console.log("Les notes ", JSON.stringify(notes));
expect(notes).toBeInstanceOf(Array);
expect(planning).toBeInstanceOf(Array);
});
},
15 * 1000,
);
});

View File

@@ -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: ");

View File

@@ -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);

View File

@@ -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();
});
});