mirror of
https://github.com/dd060606/WebAurion-API.git
synced 2026-01-18 16:47:26 +01:00
refactor: update session handling and improve view state retrieval
This commit is contained in:
@@ -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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: ");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user