feat: token refresh, video upload, playlist routes complets

- auth: cookie od_token 7j, refresh token od_refresh 30j, POST /api/auth/refresh, GET /api/auth/me/optional
- admin: POST /api/admin/videos/upload via multer (mp4/webm, 4Go max, UUID filename)
- playlist: PATCH /:id, DELETE /:id, POST /:id/videos, DELETE /:id/videos/:videoId
- env: UPLOADS_DIR documenté dans .env.example
This commit is contained in:
2026-03-14 14:32:18 +01:00
parent aa15dc0f54
commit c7815aac2f
6 changed files with 385 additions and 3 deletions

View File

@@ -1,4 +1,7 @@
import { Router, Request, Response } from "express";
import path from "path";
import fs from "fs";
import multer, { FileFilterCallback } from "multer";
import { AppDataSource } from "../config/data-source";
import { Video } from "../entities/Video";
import { User } from "../entities/User";
@@ -9,6 +12,26 @@ import { SubscriptionPlan } from "../entities/SubscriptionPlan";
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
import { requireAdmin } from "../middleware/admin.middleware";
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? path.join(process.cwd(), "uploads");
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
const videoStorage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, UPLOADS_DIR),
filename: (_req, _file, cb) => cb(null, `${crypto.randomUUID()}.mp4`),
});
const videoUpload = multer({
storage: videoStorage,
limits: { fileSize: 4 * 1024 * 1024 * 1024 }, // 4 Go
fileFilter: (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
if (["video/mp4", "video/webm"].includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error("INVALID_MIME_TYPE"));
}
},
});
const router = Router();
// Applique requireAuth + requireAdmin sur toutes les routes de ce routeur
@@ -19,6 +42,41 @@ router.use(requireAdmin as unknown as (req: Request, res: Response, next: () =>
// VIDEOS
// ---------------------------------------------------------------------------
/**
* POST /api/admin/videos/upload
* Upload un fichier vidéo (mp4 / webm) dans UPLOADS_DIR.
* Retourne le storageKey à passer ensuite à POST /api/admin/videos.
*/
router.post(
"/videos/upload",
(req: Request, res: Response, next) => {
videoUpload.single("file")(req, res, (err) => {
if (err) {
const message = err instanceof Error ? err.message : "UPLOAD_ERROR";
if (message === "INVALID_MIME_TYPE") {
res.status(415).json({ success: false, error: "INVALID_MIME_TYPE", message: "Only video/mp4 and video/webm are accepted" });
} else if (err instanceof multer.MulterError && err.code === "LIMIT_FILE_SIZE") {
res.status(413).json({ success: false, error: "FILE_TOO_LARGE" });
} else {
next(err);
}
return;
}
next();
});
},
(req: Request, res: Response): void => {
if (!req.file) {
res.status(400).json({ success: false, error: "NO_FILE" });
return;
}
res.status(201).json({
success: true,
data: { storageKey: req.file.filename, storageType: "local" },
});
}
);
/**
* GET /api/admin/videos
* Liste toutes les vidéos (publiées et non publiées), tous les champs.