All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
96 lines
3.4 KiB
TypeScript
96 lines
3.4 KiB
TypeScript
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<string | null> {
|
|
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<number> {
|
|
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<void> => {
|
|
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<void> => {
|
|
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;
|