feat: observability — Winston logging, pagination admin, N+1 playlists
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 27s
This commit is contained in:
@@ -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" });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user