Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passage vers main #17

Merged
merged 44 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
774f904
feat(types): ajout des types 2FA
LeMaitre4523 Apr 11, 2024
6a0d9ba
feat(auth): ajout fonctions de login 2FA
LeMaitre4523 Apr 11, 2024
3058714
feat(error): ajout erreur INVALID_VERSION
LeMaitre4523 Apr 11, 2024
17983a7
feat(constants): ajout de la constantes VERSION
LeMaitre4523 Apr 11, 2024
f58c01d
feat(request): ajout d'une erreur, mini refonte en cours
LeMaitre4523 Apr 11, 2024
427b5fd
Merge branch 'PapillonApp:dev' into dev
LeMaitre4523 Apr 12, 2024
1bf9005
fix(requests): nouvelles fonctions plus concises
camarm-dev Apr 12, 2024
040bc15
feat(requests): ajout de `put` et des arguments dans l'url
camarm-dev Apr 12, 2024
fd75fc7
feat(docs): documentation JSDoc dans `Request.ts`
camarm-dev Apr 12, 2024
755aed2
feat(requests): utilisations des nouvelles fonctions
camarm-dev Apr 12, 2024
042bc5b
feat(types): updated git submodule
camarm-dev Apr 12, 2024
4dc7561
feat(doubleauth): implémentation des types de l'authentification QCM
camarm-dev Apr 12, 2024
e09930f
fix(documents): utilisation de doublequote
camarm-dev Apr 12, 2024
8117cda
feat(doubleauth): meilleur typing; moins confus
camarm-dev Apr 12, 2024
a4b7e95
feat(types): updated git submodule
camarm-dev Apr 12, 2024
964e693
feat(base64): fonctions liées à la base64 compatibles avec react native
camarm-dev Apr 13, 2024
f4064ae
feat(doc): améliration de la documentation
camarm-dev Apr 13, 2024
4391040
feat(types): updated git submodule
camarm-dev Apr 13, 2024
b3495a6
fix(lint): code property
camarm-dev Apr 13, 2024
8765e56
feat(request): X-token est rempli si le token est défini + ajout d'un…
camarm-dev Apr 13, 2024
dfaaa42
feat(doc): documentation de doubleauth
camarm-dev Apr 13, 2024
61bd0f1
feat(base64): possibilité de ne pas décoder l'élément comme URI
camarm-dev Apr 13, 2024
76e90df
feat(doc): MÀJ de la documentation
camarm-dev Apr 13, 2024
7c224ce
fix(doc): problème de lien
camarm-dev Apr 13, 2024
63d07e1
feat(doubleauth): gestion de la double authentification
camarm-dev Apr 13, 2024
1fb847d
feat(examples): ajout de `enquier` pour une meilleure expérience à l'…
camarm-dev Apr 13, 2024
43e3e24
feat(examples): ajout de `ora` pour une meilleure expérience à l'exéc…
camarm-dev Apr 13, 2024
d43b92e
feat(examples): downgraded ora for compatibility
camarm-dev Apr 13, 2024
b36214d
feat(examples): implémentation de la double authentification dans `ex…
camarm-dev Apr 13, 2024
b22ad8c
feat(version): changement de version + changelog
camarm-dev Apr 13, 2024
d67abad
feat(doc): MÀJ de le documentation pour `Request.ts`
camarm-dev Apr 13, 2024
87a1ef6
feat(example): meilleur output
camarm-dev Apr 13, 2024
4b1fba2
fix(types): mauvais type
camarm-dev Apr 13, 2024
ec19066
fix(types): typing de la réponse avec `AccountInfo`
camarm-dev Apr 13, 2024
af55b1f
feat(request): amélioration de la fonction `blob`
camarm-dev Apr 13, 2024
bf6948b
feat: possibilité de choisir l'encodage de la source pour encoder une…
camarm-dev Apr 13, 2024
d709cf4
feat(downloads): téléchargement de la photo de profil
camarm-dev Apr 13, 2024
110748f
feat(typing): bon type pour session.student
camarm-dev Apr 13, 2024
a24f28e
feat(doc): MÀJ de la documentation
camarm-dev Apr 13, 2024
df2479d
feat(version): MÀJ du changelog
camarm-dev Apr 13, 2024
ebe96ad
fix: import inutile
camarm-dev Apr 13, 2024
940dafb
fix(exemple): le mois doit contenir 2 chiffres
camarm-dev Apr 14, 2024
b04290d
fix(type): la réponse est une liste, pas un objet
camarm-dev Apr 14, 2024
9e66a09
Merge pull request #16 from camarm-dev/dev
ecnivtwelve Apr 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading