import { Router, Request, Response } from "express"; import { AppDataSource } from "../config/data-source"; import logger from "../utils/logger"; import { Video } from "../entities/Video"; import { User } from "../entities/User"; import { requireAuth, AuthenticatedRequest, AuthenticatedUser } from "../middleware/auth.middleware"; import { UserSubscription } from "../entities/UserSubscription"; const router = Router(); /** Résout le superOAuthId vers l'UUID DB, retourne null si user inconnu */ async function resolveDbUserId(superOAuthId: string): Promise { const user = await AppDataSource.getRepository(User).findOne({ where: { superOAuthId } }); return user?.id ?? null; } /** Récupère le niveau de plan actif d'un user (0 = free si aucun abonnement actif) */ async function getUserPlanLevel(superOAuthId: string): Promise { const dbUserId = await resolveDbUserId(superOAuthId); if (!dbUserId) return 0; const sub = await AppDataSource.getRepository(UserSubscription).findOne({ where: { userId: dbUserId, status: "active" }, relations: ["plan"], order: { startsAt: "DESC" }, }); return sub?.plan.level ?? 0; } /** * GET /api/videos * Liste les vidéos publiées. Filtre selon le niveau de plan de l'utilisateur. * Sans auth → niveau 0 (free uniquement). */ router.get("/", async (req: Request, res: Response): Promise => { try { const user = (req as AuthenticatedRequest).user as AuthenticatedUser | undefined; const userLevel = user ? await getUserPlanLevel(user.id) : 0; const videos = await AppDataSource.getRepository(Video).find({ where: { isPublished: true }, order: { publishedAt: "DESC" }, select: ["id", "title", "description", "thumbnailUrl", "duration", "storageType", "storageKey", "requiredLevel", "publishedAt"], }); // Injequer un flag `locked` côté client pour les vidéos hors niveau const result = videos.map((v) => ({ ...v, locked: v.requiredLevel > userLevel, // Ne pas exposer storageKey si la vidéo est verrouillée storageKey: v.requiredLevel > userLevel ? null : v.storageKey, })); res.json({ success: true, data: { videos: result } }); } catch (err) { logger.error("GET /videos — failed to list videos", { err }); res.status(500).json({ success: false, error: "INTERNAL_ERROR" }); } }); /** * GET /api/videos/:id * Détail d'une vidéo. Retourne 403 si requiredLevel > userLevel. */ router.get("/:id", async (req: Request, res: Response): Promise => { try { const user = (req as AuthenticatedRequest).user as AuthenticatedUser | undefined; const userLevel = user ? await getUserPlanLevel(user.id) : 0; const video = await AppDataSource.getRepository(Video).findOne({ where: { id: req.params.id, isPublished: true }, }); if (!video) { res.status(404).json({ success: false, error: "NOT_FOUND" }); return; } if (video.requiredLevel > userLevel) { res.status(403).json({ success: false, error: "INSUFFICIENT_PLAN", data: { requiredLevel: video.requiredLevel, userLevel }, }); return; } res.json({ success: true, data: { video } }); } catch (err) { logger.error("GET /videos/:id — failed to fetch video", { err, id: req.params.id }); res.status(500).json({ success: false, error: "INTERNAL_ERROR" }); } }); export default router;