feat(backend): mount API routes + cookie-parser + CORS with credentials

- index.ts: mount /api/auth, /api/videos, /api/playlists; add cookie-parser; CORS with credentials + FRONTEND_URL env
- auth.middleware: read token from Bearer header OR od_token httpOnly cookie
- routes: auth (session/logout/me), videos (level-gated), playlists (CRUD + share management)
- deps: cookie-parser + @types/cookie-parser
This commit is contained in:
2026-03-14 07:10:47 +01:00
parent 71d90eb133
commit f3e392ff1b
7 changed files with 401 additions and 8 deletions

View File

@@ -0,0 +1,83 @@
import { Router, Request, Response } from "express";
import { AppDataSource } from "../config/data-source";
import { Video } from "../entities/Video";
import { requireAuth, AuthenticatedRequest, AuthenticatedUser } from "../middleware/auth.middleware";
import { UserSubscription } from "../entities/UserSubscription";
const router = Router();
/** Récupère le niveau de plan actif d'un user (0 = free si aucun abonnement actif) */
async function getUserPlanLevel(userId: string): Promise<number> {
const sub = await AppDataSource.getRepository(UserSubscription).findOne({
where: { userId, 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 {
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 {
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
export default router;