feat: observability — Winston logging, pagination admin, N+1 playlists
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s

This commit is contained in:
2026-03-14 23:21:42 +01:00
parent 31edea9dd9
commit 494206b5b3
6 changed files with 131 additions and 49 deletions

View File

@@ -11,6 +11,7 @@ import { UserSubscription } from "../entities/UserSubscription";
import { SubscriptionPlan } from "../entities/SubscriptionPlan";
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
import { requireAdmin } from "../middleware/admin.middleware";
import logger from "../utils/logger";
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? path.join(process.cwd(), "uploads");
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
@@ -80,14 +81,27 @@ router.post(
/**
* GET /api/admin/videos
* Liste toutes les vidéos (publiées et non publiées), tous les champs.
* Query: ?page=1&limit=20
*/
router.get("/videos", async (_req: Request, res: Response): Promise<void> => {
router.get("/videos", async (req: Request, res: Response): Promise<void> => {
const rawPage = Number(req.query.page ?? 1);
const rawLimit = Number(req.query.limit ?? 20);
if (!Number.isInteger(rawPage) || rawPage < 1 ||
!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > 100) {
res.status(400).json({ success: false, error: "INVALID_PAGINATION" });
return;
}
try {
const videos = await AppDataSource.getRepository(Video).find({
const [videos, total] = await AppDataSource.getRepository(Video).findAndCount({
order: { createdAt: "DESC" },
skip: (rawPage - 1) * rawLimit,
take: rawLimit,
});
res.json({ success: true, data: { videos } });
} catch {
res.json({ success: true, data: videos, total, page: rawPage, limit: rawLimit });
} catch (err) {
logger.error("GET /admin/videos — failed to list videos", { err });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -139,7 +153,8 @@ router.post("/videos", async (req: Request, res: Response): Promise<void> => {
await AppDataSource.getRepository(Video).save(video);
res.status(201).json({ success: true, data: { video } });
} catch {
} catch (err) {
logger.error("POST /admin/videos — failed to create video", { err });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -179,7 +194,8 @@ router.patch("/videos/:id", async (req: Request, res: Response): Promise<void> =
await repo.save(video);
res.json({ success: true, data: { video } });
} catch {
} catch (err) {
logger.error("PATCH /admin/videos/:id — failed to update video", { err, id: req.params.id });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -200,7 +216,8 @@ router.delete("/videos/:id", async (req: Request, res: Response): Promise<void>
await repo.remove(video);
res.json({ success: true, data: { deleted: req.params.id } });
} catch {
} catch (err) {
logger.error("DELETE /admin/videos/:id — failed to delete video", { err, id: req.params.id });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -212,15 +229,27 @@ router.delete("/videos/:id", async (req: Request, res: Response): Promise<void>
/**
* GET /api/admin/users
* Liste tous les utilisateurs avec leurs rôles et abonnement actif.
* Query: ?page=1&limit=20
*/
router.get("/users", async (_req: Request, res: Response): Promise<void> => {
router.get("/users", async (req: Request, res: Response): Promise<void> => {
const rawPage = Number(req.query.page ?? 1);
const rawLimit = Number(req.query.limit ?? 20);
if (!Number.isInteger(rawPage) || rawPage < 1 ||
!Number.isInteger(rawLimit) || rawLimit < 1 || rawLimit > 100) {
res.status(400).json({ success: false, error: "INVALID_PAGINATION" });
return;
}
try {
const users = await AppDataSource.getRepository(User).find({
const [users, total] = await AppDataSource.getRepository(User).findAndCount({
relations: ["userRoles", "userRoles.role", "subscriptions", "subscriptions.plan"],
order: { createdAt: "DESC" },
skip: (rawPage - 1) * rawLimit,
take: rawLimit,
});
const result = users.map((u) => ({
const data = users.map((u) => ({
id: u.id,
email: u.email,
nickname: u.nickname,
@@ -228,13 +257,14 @@ router.get("/users", async (_req: Request, res: Response): Promise<void> => {
createdAt: u.createdAt,
roles: u.userRoles.map((ur) => ({ id: ur.role.id, slug: ur.role.slug, name: ur.role.name })),
activeSubscription: (() => {
const sub = u.subscriptions.find((s) => s.status === "active");
return sub ? { id: sub.id, status: sub.status, startsAt: sub.startsAt, endsAt: sub.endsAt, plan: sub.plan } : null;
})(),
const sub = u.subscriptions.find((s) => s.status === "active");
return sub ? { id: sub.id, status: sub.status, startsAt: sub.startsAt, endsAt: sub.endsAt, plan: sub.plan } : null;
})(),
}));
res.json({ success: true, data: { users: result } });
} catch {
res.json({ success: true, data, total, page: rawPage, limit: rawLimit });
} catch (err) {
logger.error("GET /admin/users — failed to list users", { err });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -296,7 +326,8 @@ router.patch("/users/:id/roles", async (req: Request, res: Response): Promise<vo
roles: roleEntities.map((r) => ({ id: r.id, slug: r.slug, name: r.name })),
},
});
} catch {
} catch (err) {
logger.error("PATCH /admin/users/:id/roles — failed to assign roles", { err, id: req.params.id });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -315,7 +346,8 @@ router.get("/plans", async (_req: Request, res: Response): Promise<void> => {
order: { level: "ASC" },
});
res.json({ success: true, data: { plans } });
} catch {
} catch (err) {
logger.error("GET /admin/plans — failed to list plans", { err });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -360,7 +392,8 @@ router.post("/plans", async (req: Request, res: Response): Promise<void> => {
await AppDataSource.getRepository(SubscriptionPlan).save(plan);
res.status(201).json({ success: true, data: { plan } });
} catch {
} catch (err) {
logger.error("POST /admin/plans — failed to create plan", { err });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -390,7 +423,8 @@ router.patch("/plans/:id", async (req: Request, res: Response): Promise<void> =>
await repo.save(plan);
res.json({ success: true, data: { plan } });
} catch {
} catch (err) {
logger.error("PATCH /admin/plans/:id — failed to update plan", { err, id: req.params.id });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});
@@ -445,7 +479,8 @@ router.post("/users/:id/subscriptions", async (req: Request, res: Response): Pro
await subRepo.save(sub);
res.status(201).json({ success: true, data: { subscription: { ...sub, plan } } });
} catch {
} catch (err) {
logger.error("POST /admin/users/:id/subscriptions — failed to assign subscription", { err, id: req.params.id });
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
}
});