Skip to content

Commit

Permalink
chore: merge pull request #273 from tom-theret/service/uphf
Browse files Browse the repository at this point in the history
feat(service): Ajout de l'Université Polytechnique Haut-de-France
  • Loading branch information
Vexcited authored Oct 9, 2024
2 parents 3e58d9d + 71e5301 commit 25b4333
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 3 deletions.
Binary file added assets/images/service_uphf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"scolengo-api": "^3.0.5",
"text-encoding": "^0.7.0",
"turbawself": "^1.1.1",
"uphf-api": "^2.1.3",
"zustand": "^4.5.2"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/router/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type RouteParameters = {
UnivRennes2_Login: undefined;
UnivLimoges_Login: undefined;
UnivSorbonneParisNord_login: undefined;
UnivUphf_Login: undefined;

// login.skolengo
SkolengoAuthenticationSelector: undefined;
Expand Down
7 changes: 6 additions & 1 deletion src/router/screens/login/identityProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import UnivRennes1_Login from "@/views/login/IdentityProvider/providers/UnivRenn
import UnivLimoges_Login from "@/views/login/IdentityProvider/providers/UnivLimoges";
import UnivRennes2_Login from "@/views/login/IdentityProvider/providers/UnivRennes2";
import UnivSorbonneParisNord_login from "@/views/login/IdentityProvider/providers/UnivSorbonneParisNord";
import UnivUphf_Login from "@/views/login/IdentityProvider/providers/UnivUphf";

export default [
createScreen("IdentityProviderSelector", IdentityProviderSelector, {
headerTitle: "Fournisseur d'identité",
headerTitle: "Universités et autres",
headerBackVisible: true
}),

Expand All @@ -32,5 +33,9 @@ export default [
headerTitle: "Université Sorbonne Paris Nord",
}),

createScreen("UnivUphf_Login", UnivUphf_Login, {
headerBackVisible: true,
headerTitle: "Université Polytechnique Hauts-de-France",
}),

] as const;
10 changes: 10 additions & 0 deletions src/services/news.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Information } from "./shared/Information";
import { checkIfSkoSupported } from "./skolengo/default-personalization";
import { error } from "@/utils/logger/logger";
import { newsRead } from "pawnote";
import { ca } from "date-fns/locale";

/**
* Updates the state and cache for the news.
Expand All @@ -30,6 +31,12 @@ export async function updateNewsInCache <T extends Account> (account: T): Promis
useNewsStore.getState().updateInformations(informations);
break;
}
case AccountService.UPHF: {
const { getNews } = await import("./uphf/data/news");
const informations = await getNews(account);
useNewsStore.getState().updateInformations(informations);
break;
}
default: {
throw new Error("Service not implemented.");
}
Expand All @@ -48,6 +55,9 @@ export async function setNewsRead <T extends Account> (account: T, message: Info
case AccountService.Local: {
break;
}
case AccountService.UPHF: {
break;
}
default: {
throw new Error("Service not implemented.");
}
Expand Down
4 changes: 4 additions & 0 deletions src/services/reload-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export async function reload <T extends Account> (account: T): Promise<Reconnect
const res = await reload(account);
return { instance: res.instance, authentication: res.authentication };
}
case AccountService.UPHF: {
const { reloadInstance } = await import("./uphf/reload-uphf");
return await reloadInstance(account.authentication) as Reconnected<T>;
}
default: {
throw new Error("Service not implemented.");
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/shared/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class ErrorServiceUnauthenticated extends Error {
constructor (service: "pronote"|"ARD"|"turboself"|"skolengo") {
constructor (service: "pronote"|"ARD"|"turboself"|"skolengo"|"UPHF") {
super(`${service}: "account.instance" is not defined, you need to authenticate first.`);
this.name = "ErrorServiceUnauthenticated";
}
Expand Down
6 changes: 6 additions & 0 deletions src/services/timetable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export async function updateTimetableForWeekInCache <T extends Account> (account
useTimetableStore.getState().updateClasses(epochWeekNumber, timetable);
break;
}
case AccountService.UPHF: {
const { getTimetableForWeek } = await import("./uphf/data/timetable");
const timetable = await getTimetableForWeek(account, epochWeekNumber);
useTimetableStore.getState().updateClasses(epochWeekNumber, timetable);
break;
}
default: {
throw new Error("Service not implemented.");
}
Expand Down
22 changes: 22 additions & 0 deletions src/services/uphf/data/news.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { UphfAccount } from "@/stores/account/types";
import { Information } from "../../shared/Information";
import { type ActualitiesResponse, UPHF } from "uphf-api";
import { AttachmentType } from "@/services/shared/Attachment";

const parseInformation = (i: ActualitiesResponse): Information => ({
id: i.pubDate,
title: i.title,
date: new Date(i.pubDate),
acknowledged: false,
attachments: [{"name": i.title,"type":"link" as AttachmentType, "url": i.link}],
content: i.content,
author: "UPHF Actualités",
category: "Actualités",
read: false,
ref: i,
});

export const getNews = async (account: UphfAccount): Promise<Information[]> => {
const news = await UPHF.getActualities();
return news.map(parseInformation);
};
38 changes: 38 additions & 0 deletions src/services/uphf/data/timetable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { UphfAccount } from "@/stores/account/types";
import type { Timetable, TimetableClass} from "../../shared/Timetable";
import { weekNumberToDateRange } from "@/utils/epochWeekNumber";
import type { EventResponse } from "uphf-api";
import { ErrorServiceUnauthenticated } from "@/services/shared/errors";

const decodeTimetableClass = (c: EventResponse): TimetableClass => ({
subject: c.course.label,
id: c.id,
type: "lesson",
title: c.course.label,
startTimestamp: new Date(c.startDateTime).getTime(),
endTimestamp: new Date(c.endDateTime).getTime(),
room: c.course.online ? "En ligne" : c.rooms.map((room: any) => room.label).join(", "),
teacher: c.teachers.map((teacher: any) => teacher.displayname).join(", "),
backgroundColor: c.course.color,
source: "UPHF",
});

export const getTimetableForWeek = async (account: UphfAccount, weekNumber: number): Promise<Timetable> => {
if (!account.instance)
throw new ErrorServiceUnauthenticated("UPHF");

const timetable = await account.instance.getSchedule({startDate: weekNumberToDateRange(weekNumber).start.toISOString().split("T")[0], endDate:weekNumberToDateRange(weekNumber).end.toISOString().split("T")[0]});
const eventsList = timetable.plannings.flatMap((planning) =>
planning.events.map((event: EventResponse) => ({
id: event.id,
startDateTime: event.startDateTime,
endDateTime: event.endDateTime,
course: event.course,
rooms: event.rooms.length >= 1 ? event.rooms : [{id: null, label: null, type: null}],
teachers: event.teachers.length >= 1 ? event.teachers : [{id: null, displayname: null, email: null}],
group: event.groups.length >= 1 ? event.groups : [{id: null, label: null}],
}))
);

return await eventsList.map(decodeTimetableClass); // TODO for Papillon team: add the group to the timetable
};
24 changes: 24 additions & 0 deletions src/services/uphf/default-personalization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Personalization } from "@/stores/account/types";
import { defaultTabs } from "@/consts/DefaultTabs";
import type pronote from "pawnote";

import colors from "@/utils/data/colors.json";

const defaultUphfTabs = [
"Home",
"Lessons",
"News",
] as typeof defaultTabs[number]["tab"][];

const defaultPersonalization = async (): Promise<Partial<Personalization>> => {
return {
color: colors[0],
magicEnabled: true,
tabs: defaultTabs.filter(current => defaultUphfTabs.includes(current.tab)).map((tab, index) => ({
name: tab.tab,
enabled: index <= 4
}))
};
};

export default defaultPersonalization;
14 changes: 14 additions & 0 deletions src/services/uphf/reload-uphf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { UphfAccount } from "@/stores/account/types";
import type { Reconnected } from "../reload-account";
import { authWithRefreshToken } from "uphf-api";

export const reloadInstance = async (authentication: UphfAccount["authentication"]): Promise<Reconnected<UphfAccount>> => {
const session = await authWithRefreshToken({ refreshAuthToken: authentication.refreshAuthToken });

return {
instance: session,
authentication: {
refreshAuthToken: session.userData.refreshAuthToken
}
};
};
13 changes: 12 additions & 1 deletion src/stores/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Account as PawdirecteAccount, Session as PawdirecteSession } from
import type { Session as TSSession, Authentication as TSAuthentication } from "turbawself";
import type { Client as ARDClient } from "pawrd";
import type ScolengoAPI from "scolengo-api";
import type UphfAPI from "uphf-api";
import { SkolengoAuthConfig } from "@/services/skolengo/skolengo-types";
import { User as ScolengoAPIUser } from "scolengo-api/types/models/Common";

Expand Down Expand Up @@ -70,7 +71,8 @@ export enum AccountService {
Turboself,
ARD,
Parcoursup,
Onisep
Onisep,
UPHF
}

/**
Expand Down Expand Up @@ -125,6 +127,14 @@ export interface SkolengoAccount extends BaseAccount {
userInfo: ScolengoAPIUser
}

export interface UphfAccount extends BaseAccount {
service: AccountService.UPHF
instance?: UphfAPI.UPHF
authentication: {
refreshAuthToken: string
}
}

export interface LocalAccount extends BaseAccount {
service: AccountService.Local

Expand Down Expand Up @@ -163,6 +173,7 @@ export type PrimaryAccount = (
| PronoteAccount
| EcoleDirecteAccount
| SkolengoAccount
| UphfAccount
| LocalAccount
);
export type ExternalAccount = (
Expand Down
89 changes: 89 additions & 0 deletions src/views/login/IdentityProvider/providers/UnivUphf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from "react";
import type { Screen } from "@/router/helpers/types";

import { authWithCredentials } from "uphf-api";
import uuid from "@/utils/uuid-v4";

import { useAccounts, useCurrentAccount } from "@/stores/account";
import { AccountService, type UphfAccount } from "@/stores/account/types";
import defaultPersonalization from "@/services/uphf/default-personalization";
import LoginView from "@/components/Templates/LoginView";

const UnivUphf_Login: Screen<"UnivUphf_Login"> = ({ navigation }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const createStoredAccount = useAccounts(store => store.create);
const switchTo = useCurrentAccount(store => store.switchTo);

const handleLogin = async (username: string, password: string) => {
try {
setLoading(true);
setError(null);

const account = await authWithCredentials({ username, password });

const local_account: UphfAccount = {
instance: undefined,

localID: uuid(),
service: AccountService.UPHF,

isExternal: false,
linkedExternalLocalIDs: [],

name: account.userData.name + " " + account.userData.firstname,
studentName: {
last: account.userData.name,
first: account.userData.firstname
},
className: "", // TODO ?
schoolName: "Université Polytechnique Hauts-de-France",

authentication: {
refreshAuthToken: account.userData.refreshAuthToken,
},
personalization: await defaultPersonalization()
};

createStoredAccount(local_account);
setLoading(false);
switchTo(local_account);

// We need to wait a tick to make sure the account is set before navigating.
queueMicrotask(() => {
// Reset the navigation stack to the "Home" screen.
// Prevents the user from going back to the login screen.
navigation.reset({
index: 0,
routes: [{ name: "AccountCreated" }],
});
});
}
catch (error) {
if (error instanceof Error) {
setError(error.message);
}
else {
setError("Erreur inconnue");
}

setLoading(false);
console.error(error);
}
};

return (
<>
<LoginView
serviceIcon={require("@/../assets/images/service_uphf.png")}
serviceName="Université Polytechnique Hauts-de-France"
loading={loading}
error={error}
onLogin={(username, password) => handleLogin(username, password)}
/>
</>
);
};

export default UnivUphf_Login;

0 comments on commit 25b4333

Please sign in to comment.