frontend/app/lib/api.ts
Nathan Lamy 135058e2e1
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 2m8s
feat: improve auth flow
2025-08-21 18:46:36 +02:00

451 lines
9.6 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { DateTime, Duration } from "luxon";
const BASE_URL = import.meta.env.VITE_API_URL;
const makePostRequest = async (
url: string,
body: object,
error = "Une erreur est survenue",
method = "POST"
) => {
const response = await fetch(BASE_URL + url, {
method,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
credentials: "include", // Include cookies for authentication
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || error);
return data?.data || data;
};
const makeRequest = async (url: string, error = "Une erreur est survenue") => {
const response = await fetch(BASE_URL + url, { credentials: "include" });
const data = await response.json();
if (!response.ok) throw new Error(data.error || error);
return data?.data || data;
};
/**
* === AUTH API ===
*/
export const requestLogin = async (email: string, token: string) => {
return makePostRequest(
"/auth/request",
{ email, token },
"Échec de la demande de connexion"
);
};
export const verifyOtp = async ({
otpCode,
email,
}: {
otpCode: string;
email: string;
}) => {
return makePostRequest(
"/auth/verify",
{ email, code: otpCode },
"Code de vérification invalide"
);
};
export const getClasses = async () => {
try {
const res = await fetch("/classes.json");
return res.json();
} catch (error) {
console.error("Error fetching classes:", error);
return [];
}
};
export const registerUser = async (
userId: number,
className: string,
token: string
) => {
return makePostRequest(
"/auth/register",
{ userId, className, token },
"Échec de l'inscription"
);
};
export const getNames = async (className: string) => {
return makeRequest(
`/auth/autocomplete?className=${encodeURIComponent(className)}`,
"Échec de la récupération des noms"
);
};
/**
* === USER API ===
*/
const fetchUser = async () => {
return makeRequest(
"/users/@me",
"Échec de la récupération des informations utilisateur"
);
};
const defaultUser = {
id: 0,
firstName: "",
lastName: "",
fullName: "",
email: "",
className: "",
preferences: [] as UserPreferences,
};
export type User = typeof defaultUser;
export const AUTH_ERROR = "Unauthorized access";
export const useUser = () => {
const { data, ...props } = useQuery({
queryKey: ["user"],
queryFn: fetchUser,
staleTime: Duration.fromObject({
minutes: 5, // 5 minutes
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
return {
user: (data ? Object.assign(defaultUser, data) : defaultUser) as User,
...props,
};
};
export const logout = async () => {
return makePostRequest("/auth/logout", {}, "Échec de la déconnexion", "POST");
};
export const updateUserPreferences = async (
preferences: { name: string; emoji: string; color: string }[]
) => {
return makePostRequest(
"/users/@me",
{ preferences },
"Échec de la mise à jour des préférences utilisateur",
"POST"
);
};
interface UserPreference {
name: string;
emoji: string;
color: string;
}
export type UserPreferences = UserPreference[];
/**
* === COLLES API ===
*/
export const getColles = async (startDate: DateTime) => {
return makeRequest(
`/colles?startDate=${startDate.toISODate()}`,
"Échec de la récupération des colles"
);
};
export interface Colle {
relatives: Colle[];
id: number;
date: string; // ISO date string
subject: {
id: number;
name: string;
};
examiner: {
id: number;
name: string;
};
room: {
id: number;
name: string;
};
student: User;
grade?: number; // Nullable grade
bjsecret?: string; // Optional field
bjid?: string; // Optional field
content?: string; // Optional field
comment?: string; // Optional field
attachments?: Attachment[]; // Optional field, array of attachment URLs
}
interface CollePayload {
classColles: Colle[];
studentColles: Colle[];
favoriteColles: Colle[];
healthyUntil: Date;
lastSync: Date;
}
export const useColles = (startDate: DateTime) => {
// Check if the start date is the current week
// This is used to determine if the data is "actual" or not
const isActual = DateTime.now()
.startOf("week")
.equals(startDate.startOf("week"));
const options = isActual
? {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
staleTime: 0,
gcTime: 0,
}
: {
staleTime: Duration.fromObject({
hours: 1, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
};
const { data, ...props } = useQuery({
queryKey: ["colles", startDate.toISODate()],
queryFn: () => getColles(startDate),
...options,
});
const mergedData = Object.assign(
{
classColles: [],
studentColles: [],
favoriteColles: [],
healthyUntil: new Date(0),
lastSync: new Date(0),
},
data || {}
) as CollePayload;
return {
...mergedData,
...props,
};
};
const fetchColle = async (id: number) => {
return makeRequest(`/colles/${id}`, "Échec de la récupération de la colle");
};
const defaultColle = {
id: 0,
date: "",
subject: {
id: 0,
name: "",
},
examiner: {
id: 0,
name: "",
},
room: {
id: 0,
name: "",
},
student: defaultUser,
attachments: [],
};
export const useColle = (id: number) => {
const { data, ...props } = useQuery({
queryKey: ["colle", id],
queryFn: () => fetchColle(id),
staleTime: Duration.fromObject({
seconds: 30, // 30 seconds
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
return {
colle: (data || defaultColle) as Colle,
...props,
};
};
export const refreshColle = async (id: number) => {
return makePostRequest(
`/colles/${id}/refresh`,
{},
"Échec de la demande de rafraîchissement de la colle",
"POST"
);
};
export interface Attachment {
path: string;
name: string;
}
/**
* === SUBJECTS API ===
*/
export const getSubjects = async () => {
return makeRequest("/subjects", "Échec de la récupération des matières");
};
export const useSubjects = () => {
const { data, ...props } = useQuery({
queryKey: ["subjects"],
queryFn: getSubjects,
staleTime: Duration.fromObject({
hours: 1, // 1 day
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
return {
subjects: (data as string[]) || [],
...props,
};
};
/**
* === NOTIFICATIONS API ===
*/
export const subscribe = async (data: any) => {
return makePostRequest(
"/notifications",
data,
"Échec de l'abonnement aux notifications"
);
};
export const unsubscribe = async (id: string) => {
return makePostRequest(
`/notifications/${id}/unsubscribe`,
{},
"Échec de la désinscription des notifications"
);
};
export const updateSubscription = async (id: string, events: string[]) => {
return makePostRequest(
`/notifications/${id}`,
{ events },
"Échec de la mise à jour de l'abonnement aux notifications"
);
};
export const getNotifications = async () => {
return makeRequest(
"/notifications",
"Échec de la récupération des notifications"
);
};
export interface Event {
id: string;
enabled: boolean;
}
interface Subscription {
id: string;
device: string;
events: Event[];
enabled: boolean;
}
export const useNotifications = () => {
const { data, ...props } = useQuery({
queryKey: ["notifications"],
queryFn: getNotifications,
staleTime: Duration.fromObject({
hours: 0, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
return {
notifications: (data as Subscription[]) || [],
...props,
};
};
export const testNotification = async (id: string) => {
return makePostRequest(
`/notifications/${id}/test`,
{},
"Échec de l'envoi de la notification de test"
);
};
/**
* === GRADES API ===
*/
export const getGrades = async (period: string) => {
return makeRequest(
`/grades?period=${encodeURIComponent(period)}`,
"Échec de la récupération des notes"
);
};
export const useGrades = (period: string) => {
const { data, ...props } = useQuery({
queryKey: ["grades", period],
queryFn: () => getGrades(period),
staleTime: Duration.fromObject({
hours: 0, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
const grades = (data?.grades as PeriodResult[]) || [];
const subjects = (data?.subjects as string[]) || [];
return {
grades,
subjects,
...props,
};
};
interface SubjectPerformance {
subject: string;
average: number;
}
interface PeriodResult {
period: string;
average: number;
}
export const getAverages = async (period: string) => {
return makeRequest(
`/averages?period=${encodeURIComponent(period)}`,
"Échec de la récupération des moyennes"
);
};
export const useAverages = (period: string) => {
const { data, ...props } = useQuery({
queryKey: ["averages", period],
queryFn: () => getAverages(period),
staleTime: Duration.fromObject({
hours: 0, // 1 hour
}).toMillis(),
gcTime: Duration.fromObject({
days: 3, // 3 days
}).toMillis(),
});
const subjectAverages = (data?.subjectAverages as SubjectPerformance[]) || [];
const globalAverage = data?.globalAverage || 0;
return {
subjectAverages,
globalAverage,
...props,
};
};