Skip to content

Commit

Permalink
Merge pull request #17 from PapillonApp/dev
Browse files Browse the repository at this point in the history
Passage vers `main`
  • Loading branch information
ecnivtwelve authored Apr 14, 2024
2 parents ff7a917 + 9e66a09 commit 40bdcdc
Show file tree
Hide file tree
Showing 31 changed files with 429 additions and 161 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
- **0.2.0**: Réécriture en **typescript**
- **1.0.0**: Quand le module sera stable

## 0.2.8
- Prise en charge de la **double authentification**
- Avec ajout des types
- Réécriture de `Request.ts` pour une meilleure gestion et flexibilité des requêtes vers Ecoledirecte
- Amélioration de l'exemple `login.ts`: 2FA et interface agréable
- Ajout de `ora` et `enquirer` aux dépendances de développement
- Ajout de `getProfilePictureBase64` dans `getDownloads.ts` pour télécharger la photo de profil en base64
- Mise à jour de la documentation en conséquences

## 0.2.7
- Conversion des dates de la vie-scolaire renvoyé par ED (Merci Rémy)
- Fix de la réponse de l'EDT
Expand Down
179 changes: 126 additions & 53 deletions DOCUMENTATION.md

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions examples/homeworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import { login, ED } from "./login";

login().then(() => {
ED.homeworks.fetch().then(homeworks => {
console.log("Devoirs:");
Object.keys(homeworks).forEach(key => {
console.log(`\tPour le ${key}:`);
console.log(`[${key}]`);
const work = homeworks[key];
work.forEach(subject => {
console.log(`\t\tDevoirs en ${subject.matiere} (${subject.codeMatiere}), donné le ${subject.donneLe}. ${subject.effectue ? "Effectué": "Non effectué"}, ${subject.interrogation ? "interrogation prévue": "pas d'interrogation"} et ${subject.rendreEnLigne ? "documents à rendre en ligne": "rien à rendre en ligne"}.`);
console.log(`\tDevoirs en ${subject.matiere} (${subject.codeMatiere}), donné le ${subject.donneLe}. ${subject.effectue ? "Effectué": "Non effectué"}, ${subject.interrogation ? "interrogation prévue": "pas d'interrogation"} et ${subject.rendreEnLigne ? "documents à rendre en ligne": "rien à rendre en ligne"}.`);
});
});
});
Expand Down
46 changes: 40 additions & 6 deletions examples/login.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
import {EDCore} from "../index";
import {studentAccount} from "~/types";
import { v4 as uuidv4 } from "uuid";
// @ts-ignore
import { Select } from "enquirer";
import ora, {Ora} from "ora";
import {AccountInfo} from "../src/utils/types/accounts";

export const ED = new EDCore();

const username = "";
const password = "";
const uid = uuidv4();

async function handle2FA() {
const token = await ED.auth.get2FAToken(username, password);
const QCM = await ED.auth.get2FA(token);
const chooseAnswer = new Select({
name: 'answer',
message: QCM.question,
choices: QCM.propositions
});
const answer = await chooseAnswer.run()
const loader = ora('Envoie de la réponse...').start();
const authFactors = await ED.auth.resolve2FA(answer);
loader.succeed('Envoie de la réponse')
loader.start('Connexion...')
await ED.auth.login(username, password, uid, authFactors)
loggedInHook(loader)
}

function loggedInHook(loader: Ora) {
const account = ED.student as AccountInfo;
loader.succeed(`Connecté en tant que ${account.prenom} ${account.nom}`);
}

export async function login() {
await ED.auth.login("jean", "jean%", uuidv4()).then(() => {
const account = ED.student as studentAccount;
console.log(`Logged in as ${account.particule} ${account.prenom} ${account.nom}`);
}).catch(err => {
console.error(`Failed to login: Error ${err.code}: ${err.message}`);
const loader = ora('Authentification...').start();
await ED.auth.login(username, password, uid).then(() => {
loggedInHook(loader);
}).catch(async err => {
if (err.code == 12) {
loader.fail('La double authentification est activée, répondez à la question pour vous connecter');
await handle2FA();
return;
}
loader.fail(`Failed to login: Error ${err.code}: ${err.message}`);
process.exit();
});
}
4 changes: 2 additions & 2 deletions examples/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { login, ED } from "./login";

login().then(() => {
ED.timeline.fetch().then(timeline => {
console.log("Timeline personnelle:");
console.log("[Timeline personnelle]");
timeline.forEach(event => {
console.log(`\t${event.titre}, ${event.date} (${event.soustitre}, ${event.contenu}).`);
});
});
ED.timeline.fetchCommonTimeline().then(data => {
console.log("Timeline commune:");
console.log("[Timeline commune]");
data.postits.forEach(postit => {
console.log(`\t[POSTIT] ${postit.contenu} par ${postit.auteur.particule} ${postit.auteur.nom}`);
});
Expand Down
8 changes: 4 additions & 4 deletions examples/timetable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { login, ED } from "./login";
login().then(() => {
// La date doit être donnée en YYYY-MM-DD
const today = new Date();
const todayDate = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
const month = today.getMonth() + 1 >= 10 ? today.getMonth(): `0${today.getMonth() + 2}`
const todayDate = `${today.getFullYear()}-${month}-${today.getDate()}`;
ED.timetable.fetchByDay(todayDate).then(timetable => {
console.log("Emploi du temps d'aujourd'hui:");
Object.keys(timetable).forEach(key => {
const matiere = timetable[key];
console.log("\nEmploi du temps d'aujourd'hui:");
timetable.forEach(matiere => {
console.log(`\t${matiere.text ? matiere.text: matiere.matiere} (${matiere.codeMatiere}) avec ${matiere.prof ? matiere.prof: "pas de prof"}, de ${matiere.start_date} à ${matiere.end_date} en salle ${matiere.salle ? matiere.salle: "pas de salle"}.`);
});
});
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@papillonapp/ed-core",
"version": "0.2.7",
"version": "0.2.8",
"description": "API EcoleDirecte pour PapillonApp (c)",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -37,13 +37,15 @@
"@stylistic/eslint-plugin": "^1.5.3",
"@types/jest": "^29.5.12",
"@typescript-eslint/parser": "^6.18.0",
"enquirer": "^2.4.1",
"eslint": "^8.56.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.7.0",
"ora": "^5.0.0",
"remove": "^0.1.5",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
Expand Down
99 changes: 84 additions & 15 deletions src/Request.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { API } from "./constants";
import {API, VERSION} from "./constants";
import {Session} from "./session";
import {
A2F_ERROR,
INVALID_API_URL,
INVALID_BODY,
INVALID_VERSION,
OBJECT_NOT_FOUND,
SESSION_EXPIRED,
TOKEN_INVALID,
UNAUTHORIZED,
WRONG_CREDENTIALS,
INVALID_API_URL,
OBJECT_NOT_FOUND,
INVALID_BODY,
A2F_ERROR
WRONG_CREDENTIALS
} from "~/errors";
import {RequestOptions} from "~/utils/types/requests";
import {response} from "~/types/v3/responses/default/responses";
Expand All @@ -29,28 +30,93 @@ class Request {
};
}

async blob(url: string, body: string) {
/**
*
* @param url Path to fetch or Url to fetch
* @param body request payload
* @param completeUrl set to true, `url` will be used as a full url, not a route of "api.ecoledirecte.com"
* @param method GET request or POST request
*/
async blob(url: string, body: string, completeUrl: boolean = false, method: "POST" | "GET" = "POST") {
if(this.session.isLoggedIn) this.requestOptions.headers["X-token"] = this.session._token;
const finalUrl = API + url;
const finalUrl = completeUrl ? url: API + url;
if (method == "GET") {
return await fetch(finalUrl, {
method: method,
headers: this.requestOptions.headers
}).then(response => response.blob());
}
return await fetch(finalUrl, {
method: "POST",
method: method,
headers: this.requestOptions.headers,
body: body
}).then(response => response.blob());
}

async post(url: string, body: string) {
if(this.session.isLoggedIn) this.requestOptions.headers["X-token"] = this.session._token;
const finalUrl = API + url;
return await fetch(finalUrl, {
/**
*
* @param url The path to fetch
* @param body The string formatted body data
* @param params A string containing extra parameters (e.g "foo=bar&mode=auto")
* @param ignoreErrors Disable error handling, will return a response, even if it's an error response
*/
async post(url: string, body: string, params?: string, ignoreErrors: boolean = false) {
const paramsString = params ? "&" + params: "";
const finalUrl = `${API}${url}${url.includes("?") ? `&verbe=post&v=${VERSION}${paramsString}` : `?verbe=post&v=${VERSION}${paramsString}`}`;
return await this.request(finalUrl, body, ignoreErrors);
}

/**
*
* @param url The path to fetch
* @param body The string formatted body data
* @param params A string containing extra parameters (e.g "foo=bar&mode=auto")
* @param ignoreErrors Disable error handling, will return a response, even if it's an error response
*/
async get(url: string, body: string, params?: string, ignoreErrors: boolean = false) {
const paramsString = params ? "&" + params: "";
const finalUrl = `${API}${url}${url.includes("?") ? `&verbe=get&v=${VERSION}${paramsString}` : `?verbe=get&v=${VERSION}${paramsString}`}`;
return await this.request(finalUrl, body, ignoreErrors);
}

/**
*
* @param url The path to fetch
* @param body The string formatted body data
* @param params A string containing extra parameters (e.g "foo=bar&mode=auto")
* @param ignoreErrors Disable error handling, will return a response, even if it's an error response
*/
async delete(url: string, body: string, params?: string, ignoreErrors: boolean = false) {
const paramsString = params ? "&" + params: "";
const finalUrl = `${API}${url}${url.includes("?") ? `&verbe=delete&v=${VERSION}${paramsString}` : `?verbe=delete&v=${VERSION}${paramsString}`}`;
return await this.request(finalUrl, body, ignoreErrors);
}

/**
*
* @param url The path to fetch
* @param body The string formatted body data
* @param params A string containing extra parameters (e.g "foo=bar&mode=auto")
* @param ignoreErrors Disable error handling, will return a response, even if it's an error response
*/
async put(url: string, body: string, params?: string, ignoreErrors: boolean = false) {
const paramsString = params ? "&" + params: "";
const finalUrl = `${API}${url}${url.includes("?") ? `&verbe=put&v=${VERSION}${paramsString}` : `?verbe=put&v=${VERSION}${paramsString}`}`;
return await this.request(finalUrl, body, ignoreErrors);
}

async request(url: string, body: string, ignoreErrors: boolean = false) {
if(this.session._token) this.requestOptions.headers["X-token"] = this.session._token;
return await fetch(url, {
method: "POST",
headers: this.requestOptions.headers,
body: body
})
.then(res => res.text())
.then(res => {
const response = res.startsWith("{") ? JSON.parse(res) : res;
if(typeof response != "object" && response.includes("<title>Loading...</title>")) throw INVALID_API_URL.drop();
const response = res.startsWith("{") ? JSON.parse(res): res;
if (ignoreErrors) return response;
if (typeof response != "object" && response.includes("<title>Loading...</title>")) throw INVALID_API_URL.drop();
if (response.code == 525) {
throw SESSION_EXPIRED.drop();
}
Expand All @@ -75,6 +141,9 @@ class Request {
if(response.code == 250) {
throw A2F_ERROR.drop();
}
if(response.code == 517) {
throw INVALID_VERSION.drop();
}
return response;
}) as Promise<response>;
}
Expand Down
59 changes: 55 additions & 4 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import {account, loginRes, loginResData} from "~/types/v3";
import {
account,
doubleauthResData,
doubleauthResSuccess,
doubleauthValidationResData,
doubleauthValidationResSuccess,
loginRes,
loginResData
} from "~/types/v3";
import bodyToString from "./utils/body";
import {Session} from "./session";
import {EstablishmentInfo} from "~/utils/types/establishments";
import {AccountInfo, Profile} from "~/utils/types/accounts";
import {authRequestData} from "~/types/v3/requests/student";
import {authRequestData, loginQCMValidationRequestData} from "~/types/v3/requests/student";
import { body } from "~/types/v3/requests/default/body";
import {decodeString, encodeString} from "~/utils/base64";

class Auth {

Expand Down Expand Up @@ -39,7 +49,7 @@ class Auth {
sexe: profile.sexe ?? "",
classe: profile.classe,
photo: profile.photo ?? ""
};
} as AccountInfo;
}

#parseLoginResponse(response: loginRes) {
Expand All @@ -59,7 +69,7 @@ class Auth {
}
}

async login(username: string, password: string, uuid: string) {
async login(username: string, password: string, uuid: string, fa?: { cv: string, cn: string }) {
const url = "/login.awp";
const body = {
identifiant: username,
Expand All @@ -68,11 +78,52 @@ class Auth {
sesouvenirdemoi: true,
uuid: uuid
} as authRequestData;
if (fa?.cv && fa?.cn) {
body.fa = [{ cv: fa.cv, cn: fa.cn }];
}
return await this.session.request.post(url, bodyToString(body)).then((response: loginRes) => {
this.#parseLoginResponse(response);
});
}

async get2FAToken(username: string, password: string): Promise<string> {
const url = "/login.awp";
const body = {
identifiant: username,
motdepasse: encodeURIComponent(password)
} as authRequestData;
return await this.session.request.post(url, bodyToString(body), "", true).then((response: loginRes) => response.token);
}

async get2FA(token: string): Promise<doubleauthResData> {
const url = "/connexion/doubleauth.awp";
const body = {} as body;

this.session._token = token;
return await this.session.request.get(url, bodyToString(body)).then((response: doubleauthResSuccess) => {
const parsedData = response.data;
const choices = [];

parsedData.question = decodeString(parsedData.question, false);
for (const choice of parsedData.propositions) {
choices.push(decodeString(choice, false));
}
parsedData.propositions = choices;

return parsedData;
});
}

async resolve2FA(anwser: string): Promise<doubleauthValidationResData> {
const url = "/connexion/doubleauth.awp";
const body = {
choix: encodeString(anwser)
} as loginQCMValidationRequestData;
return await this.session.request.post(url, bodyToString(body)).then((response: doubleauthValidationResSuccess) => {
return response.data;
});
}

async renewToken(username: string, uuid: string, accessToken: string) {
const url = "/login.awp";
const body = {
Expand Down
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const API = "https://api.ecoledirecte.com/v3";
export const VERSION = "6.15.1";

export default {
API
API,
VERSION
};
2 changes: 2 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const OBJECT_NOT_FOUND = error(10, "The object you were trying to retrieve was n
const INVALID_BODY = error(11, "Values provided in body are wrong and the request errored with code 512.");
const A2F_ERROR = error(12, "Dual authentication required");
const ACCOUNT_DISABLED = error(13, "Disabled Account");
const INVALID_VERSION = error(14, "Please update the application to the latest version");

function error(code: number, message: ErrorMessage){
return {
Expand All @@ -40,4 +41,5 @@ export {
INVALID_BODY,
A2F_ERROR,
ACCOUNT_DISABLED,
INVALID_VERSION
};
Loading

0 comments on commit 40bdcdc

Please sign in to comment.