Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async function startServer() {
app.use('/api/discord', authenticateUser, discordRoutes);
app.use('/api/tent', authenticateUser, tentRoutes);
app.use('/api/bus', authenticateUser, busRoutes);
app.use("/api/uploads/imgnews", express.static(path.join(__dirname, "/uploads/imgnews")));
app.use("/api/uploads/news", express.static(path.join(__dirname, "/uploads/news")));
app.use("/api/uploads/foodmenu", express.static(path.join(__dirname, "/uploads/foodmenu")));
app.use("/api/uploads/plannings", express.static(path.join(__dirname, "/uploads/plannings")));
app.use("/api/exports/bus", express.static(path.join(__dirname, "/exports/bus")));
Expand Down
68 changes: 56 additions & 12 deletions backend/src/controllers/im_export.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import * as user_service from '../services/user.service';
import { Error, Ok } from "../utils/responses";
import { spreadsheet_id } from "../utils/secret";
import { getLatestUploadedDocument, isSafeUploadSegment, removeUploadedDocuments, toUploadedDocumentStatus } from "../utils/uploadDocuments";

export const exportAllDataToSheets = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -108,6 +109,7 @@
const file = req.file;

try {

// Supprimer l'ancien Menu si un nouveau est uploadé
if (file) {
const targetDir = path.join(__dirname, "../../foodmenu");
Expand All @@ -116,6 +118,7 @@
fs.rmSync(targetDir, { recursive: true, force: true });
fs.mkdirSync(targetDir);
}

}

Ok(res, { msg: "Menu mis à jour avec succès" });
Expand All @@ -127,19 +130,7 @@
};

export const updatePlannings = async (req: Request, res: Response) => {
const file = req.file;

try {
// Supprimer l'ancien Planning si un nouveau est uploadé
if (file) {
const targetDir = path.join(__dirname, "../../plannings");

if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
fs.mkdirSync(targetDir);
}
}

Ok(res, { msg: "Planning mis à jour avec succès" });
return;
} catch (err) {
Expand All @@ -157,3 +148,56 @@
Error(res, { msg: "Erreur lors de l'export CSV" });
}
};

export const getUploadedDocumentStatus = async (req: Request, res: Response) => {
const { category, item } = req.params;

if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) {
Error(res, { msg: "Paramètres invalides" });
return;
}

try {
const latestDocument = await getLatestUploadedDocument(category, item);

Ok(res, {
data: toUploadedDocumentStatus(category, latestDocument),
});
} catch (err: any) {

Check warning on line 166 in backend/src/controllers/im_export.controller.ts

View workflow job for this annotation

GitHub Actions / lint-api

Unexpected any. Specify a different type
if (err?.code === "ENOENT") {
Ok(res, {
data: toUploadedDocumentStatus(category, null),
});
return;
}

console.error(err);
Error(res, { msg: "Erreur lors de la vérification du document" });
}
};

export const deleteDocument = async (req: Request, res: Response) => {
const { category, item } = req.params;

if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) {
Error(res, { msg: "Paramètres invalides" });
return;
}

try {
const deletedCount = await removeUploadedDocuments(category, item);

if (deletedCount === 0) {
return Ok(res, { msg: "Aucun document à supprimer" });
}

Ok(res, {});
} catch (err: any) {

Check warning on line 195 in backend/src/controllers/im_export.controller.ts

View workflow job for this annotation

GitHub Actions / lint-api

Unexpected any. Specify a different type
if (err?.code === "ENOENT") {
Ok(res, {});
return;
}

Error(res, { msg: "Erreur lors de la vérification du document" });
}
};
105 changes: 85 additions & 20 deletions backend/src/controllers/news.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,72 @@ import * as user_service from '../services/user.service';
import * as template from "../utils/emailtemplates";
import { Error, Ok } from "../utils/responses";

const toStoredUploadPath = (imageUrl: string) => {
if (!imageUrl) {
return null;
}

let normalized = imageUrl.trim();
if (!normalized) {
return null;
}

// Accept absolute URLs and keep only the pathname part.
if (normalized.startsWith("http://") || normalized.startsWith("https://")) {
try {
normalized = new URL(normalized).pathname;
} catch {
return null;
}
}

if (normalized.startsWith("/api/")) {
normalized = normalized.slice(4);
}

if (!normalized.startsWith("/uploads/")) {
return null;
}

return normalized;
};

const resolveStoredImagePath = (imageUrl: string) => {
const storedPath = toStoredUploadPath(imageUrl);
if (!storedPath) {
return null;
}

return path.resolve(process.cwd(), storedPath.replace(/^\//, ""));
};

const deleteImageIfExists = (imageUrl: string) => {
const imagePath = resolveStoredImagePath(imageUrl);
if (!imagePath) {
return;
}

if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
};

export const createNews = async (req: Request, res: Response) => {
const { title, description, type, published, target } = req.body;
const { title, description, type, published, target, image_url } = req.body;
const file = req.file;

try {
const image_url = file ? `/api/uploads/imgnews/${file.filename}` : undefined;
const resolvedImageUrl = file
? `/uploads/news/${file.filename}`
: image_url;

const news = await news_service.createNews(
title,
description,
type,
published,
published === true || published === "true",
target,
image_url);
resolvedImageUrl);
Ok(res, { msg: "Actu créée avec succès", data: news });
} catch (err) {
console.error(err);
Expand Down Expand Up @@ -103,41 +156,53 @@ export const deleteNews = async (req: Request, res: Response) => {
try {
const existing = await news_service.getNewsById(Number(newsId));
if (existing?.image_url) {
const imagePath = path.join(__dirname, "../../", existing.image_url);
if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
deleteImageIfExists(existing.image_url);
}

await news_service.deleteNews(Number(newsId));
Ok(res, { msg: "Actus supprimée avec succès !" });
;

} catch {
} catch (err) {
console.error(err);
Error(res, { msg: "Erreur lors de la suppression de l'actus" });
}
};

export const updateNews = async (req: Request, res: Response) => {
const { id, title, description, type, target } = req.body;
const { id, title, description, type, target, image_url } = req.body;
const file = req.file;
const image_url = file ? `/api/uploads/imgnews/${file.filename}` : undefined;
const hasImageUrlField = Object.prototype.hasOwnProperty.call(req.body, "image_url");
const resolvedImageUrl = file
? `/uploads/news/${file.filename}`
: hasImageUrlField
? (image_url ?? null)
: undefined;

try {
const existing = await news_service.getNewsById(Number(id));
if (!existing) {
Error(res, { msg: "Actu introuvable" });
return;
}

// Supprimer l'ancienne image si une nouvelle est uploadée
if (file && existing.image_url) {
const oldPath = path.join(__dirname, "../../", existing.image_url);
if (fs.existsSync(oldPath)) {
fs.unlinkSync(oldPath);
}
const shouldReplaceImage = typeof resolvedImageUrl === "string";
const shouldRemoveImage = resolvedImageUrl === null;

// Supprimer l'ancienne image si elle est remplacée ou explicitement supprimée.
if (existing.image_url && ((shouldReplaceImage && existing.image_url !== resolvedImageUrl) || shouldRemoveImage)) {
deleteImageIfExists(existing.image_url);
}

const updates: any = { title, description, type, target };
if (image_url) updates.image_url = image_url;
const updates: {
title: string;
description: string;
type: string;
target: string;
image_url?: string | null;
} = { title, description, type, target };
if (resolvedImageUrl !== undefined) {
updates.image_url = resolvedImageUrl;
}

const updated = await news_service.updateNews(Number(id), updates);

Expand Down
85 changes: 64 additions & 21 deletions backend/src/middlewares/multer.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ import fs from "fs/promises";
import multer from "multer";
import path from "path";
import { Error } from "../utils/responses";
import { isSafeUploadSegment, removeUploadedDocuments } from "../utils/uploadDocuments";

export const createUploadMiddleware = (
relativeUploadDir: string,
modifiedName: boolean = true
) => {
const uploadPath = path.resolve(process.cwd(), relativeUploadDir);
export enum MIMEType {
PDF = "application/pdf",
PNG = "image/png",
JPEG = "image/jpeg",
}

const acceptedMIMETypesByItem: Record<string, Record<string, MIMEType[]>> = {
foodmenu: {
menu: [MIMEType.PDF],
},
news: {},
plannings: {
tc: [MIMEType.PDF],
bacheloria: [MIMEType.PDF],
fise: [MIMEType.PDF],
fisea: [MIMEType.PDF],
master: [MIMEType.PDF],
},
};

export const createUploadMiddleware = () => {
// On stocke d'abord en mémoire pour vérifier le type réel
const storage = multer.memoryStorage();

Expand All @@ -24,46 +40,73 @@ export const createUploadMiddleware = (
const verifyAndSave = async (
req: Request,
res: Response,
next: NextFunction
next: NextFunction,
) => {
try {
if (!req.file) return Error(res, { msg: "Aucun fichier reçu" });


const category = req.params.category;
const item = req.params.item;

if (!category || !item) {
return Error(res, { msg: "Catégorie ou item manquant" });
}

if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) {
return Error(res, { msg: "Paramètres invalides" });
}

const { originalname, buffer } = req.file;

// Vérif du vrai type
const { fileTypeFromBuffer } = await import("file-type");
const detected = await fileTypeFromBuffer(buffer);
console.log(
`[UPLOAD] Type détecté: ${detected?.mime || "inconnu"}`
);

const isImage = detected?.mime?.startsWith("image/");
const isPDF = detected?.mime === "application/pdf";
const detectedMime = detected?.mime as MIMEType | undefined;

const acceptedTypes = category === "news" ? [MIMEType.PNG, MIMEType.JPEG] : acceptedMIMETypesByItem[category]?.[item];


if (!isImage && !isPDF) {
Error(res, { msg: "Seules les images et les PDF sont autorisés" });

if (!acceptedTypes) {
return Error(res, { msg: "Catégorie ou item inconnu" });
}

const isAccepted = !!detectedMime && acceptedTypes.includes(detectedMime);

if (!isAccepted) {
Error(res, { msg: "Type de fichier non autorisé" });
return;
}

const uploadPath = path.resolve(process.cwd(), "uploads", category);

// Création dossier si nécessaire
await fs.mkdir(uploadPath, { recursive: true });

if (category === "news") {
if (!/^\d+$/.test(item)) {
return Error(res, { msg: "Le nom du fichier doit être composé uniquement de chiffres." });
}

const currentEntries = await fs.readdir(uploadPath);
if (currentEntries.length >= 100) {
return Error(res, { msg: "Le nombre maximal d'images d'actualités a été atteind." });
}
}

// Supprime tout document existant avec le même basename, quelle que soit l'extension.
await removeUploadedDocuments(category, item);

const ext = path.extname(originalname);
const baseName = path.basename(originalname, ext);
const timestamp = Date.now();
const finalName = modifiedName
? `${baseName}-${timestamp}${ext}`
: originalname;
const finalName = `${item}${ext}`;

const finalPath = path.join(uploadPath, finalName);

// Sauvegarde du fichier sur disque
await fs.writeFile(finalPath, buffer);

// On rajoute le chemin pour les middlewares suivants
(req as any).savedFilePath = finalPath;

next();
} catch (err) {
next(err);
Expand Down
Loading
Loading