- requireAdmin: charge user_roles en DB, accepte admin/super_admin - GET/POST/PATCH/DELETE /api/admin/videos (publiées + non publiées) - GET /api/admin/users avec rôles et abonnement actif - PATCH /api/admin/users/:id/roles (remplacement atomique par slugs) - GET/POST/PATCH /api/admin/plans
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { AppDataSource } from "../config/data-source";
|
|
import { Video } from "../entities/Video";
|
|
import { User } from "../entities/User";
|
|
import { UserRole } from "../entities/UserRole";
|
|
import { Role } from "../entities/Role";
|
|
import { UserSubscription } from "../entities/UserSubscription";
|
|
import { SubscriptionPlan } from "../entities/SubscriptionPlan";
|
|
import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware";
|
|
import { requireAdmin } from "../middleware/admin.middleware";
|
|
|
|
const router = Router();
|
|
|
|
// Applique requireAuth + requireAdmin sur toutes les routes de ce routeur
|
|
router.use(requireAuth as unknown as (req: Request, res: Response, next: () => void) => void);
|
|
router.use(requireAdmin as unknown as (req: Request, res: Response, next: () => void) => void);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// VIDEOS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* GET /api/admin/videos
|
|
* Liste toutes les vidéos (publiées et non publiées), tous les champs.
|
|
*/
|
|
router.get("/videos", async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const videos = await AppDataSource.getRepository(Video).find({
|
|
order: { createdAt: "DESC" },
|
|
});
|
|
res.json({ success: true, data: { videos } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/admin/videos
|
|
* Crée une nouvelle vidéo.
|
|
* Body: { title, description?, thumbnailUrl?, duration?, storageType, storageKey, requiredLevel?, isPublished? }
|
|
*/
|
|
router.post("/videos", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const {
|
|
title,
|
|
description = null,
|
|
thumbnailUrl = null,
|
|
duration = null,
|
|
storageType,
|
|
storageKey,
|
|
requiredLevel = 0,
|
|
isPublished = false,
|
|
} = req.body as {
|
|
title: string;
|
|
description?: string | null;
|
|
thumbnailUrl?: string | null;
|
|
duration?: number | null;
|
|
storageType: string;
|
|
storageKey: string;
|
|
requiredLevel?: number;
|
|
isPublished?: boolean;
|
|
};
|
|
|
|
if (!title || !storageType || !storageKey) {
|
|
res.status(400).json({ success: false, error: "MISSING_REQUIRED_FIELDS" });
|
|
return;
|
|
}
|
|
|
|
const video = AppDataSource.getRepository(Video).create({
|
|
id: crypto.randomUUID(),
|
|
title,
|
|
description,
|
|
thumbnailUrl,
|
|
duration,
|
|
storageType: storageType as Video["storageType"],
|
|
storageKey,
|
|
requiredLevel,
|
|
isPublished,
|
|
publishedAt: isPublished ? new Date() : null,
|
|
});
|
|
|
|
await AppDataSource.getRepository(Video).save(video);
|
|
res.status(201).json({ success: true, data: { video } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/admin/videos/:id
|
|
* Met à jour une vidéo (champs partiels).
|
|
*/
|
|
router.patch("/videos/:id", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const repo = AppDataSource.getRepository(Video);
|
|
const video = await repo.findOne({ where: { id: req.params.id } });
|
|
|
|
if (!video) {
|
|
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
return;
|
|
}
|
|
|
|
const allowed = [
|
|
"title", "description", "thumbnailUrl", "duration",
|
|
"storageType", "storageKey", "requiredLevel", "isPublished",
|
|
] as const;
|
|
|
|
for (const field of allowed) {
|
|
if (field in req.body) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(video as any)[field] = (req.body as Record<string, unknown>)[field];
|
|
}
|
|
}
|
|
|
|
// Mise à jour de publishedAt si on publie maintenant
|
|
if (req.body.isPublished === true && !video.publishedAt) {
|
|
video.publishedAt = new Date();
|
|
} else if (req.body.isPublished === false) {
|
|
video.publishedAt = null;
|
|
}
|
|
|
|
await repo.save(video);
|
|
res.json({ success: true, data: { video } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/admin/videos/:id
|
|
* Supprime une vidéo.
|
|
*/
|
|
router.delete("/videos/:id", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const repo = AppDataSource.getRepository(Video);
|
|
const video = await repo.findOne({ where: { id: req.params.id } });
|
|
|
|
if (!video) {
|
|
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
return;
|
|
}
|
|
|
|
await repo.remove(video);
|
|
res.json({ success: true, data: { deleted: req.params.id } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// USERS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* GET /api/admin/users
|
|
* Liste tous les utilisateurs avec leurs rôles et abonnement actif.
|
|
*/
|
|
router.get("/users", async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const users = await AppDataSource.getRepository(User).find({
|
|
relations: ["userRoles", "userRoles.role", "subscriptions", "subscriptions.plan"],
|
|
order: { createdAt: "DESC" },
|
|
});
|
|
|
|
const result = users.map((u) => ({
|
|
id: u.id,
|
|
email: u.email,
|
|
nickname: u.nickname,
|
|
isActive: u.isActive,
|
|
createdAt: u.createdAt,
|
|
roles: u.userRoles.map((ur) => ({ id: ur.role.id, slug: ur.role.slug, name: ur.role.name })),
|
|
activeSubscription: u.subscriptions.find((s) => s.status === "active")
|
|
? {
|
|
id: u.subscriptions.find((s) => s.status === "active")!.id,
|
|
status: u.subscriptions.find((s) => s.status === "active")!.status,
|
|
startsAt: u.subscriptions.find((s) => s.status === "active")!.startsAt,
|
|
endsAt: u.subscriptions.find((s) => s.status === "active")!.endsAt,
|
|
plan: u.subscriptions.find((s) => s.status === "active")!.plan,
|
|
}
|
|
: null,
|
|
}));
|
|
|
|
res.json({ success: true, data: { users: result } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/admin/users/:id/roles
|
|
* Assigne des rôles à un utilisateur (remplace les rôles existants).
|
|
* Body: { roles: string[] } — slugs de rôles
|
|
*/
|
|
router.patch("/users/:id/roles", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const { roles } = req.body as { roles: string[] };
|
|
|
|
if (!Array.isArray(roles)) {
|
|
res.status(400).json({ success: false, error: "INVALID_BODY" });
|
|
return;
|
|
}
|
|
|
|
const userRepo = AppDataSource.getRepository(User);
|
|
const roleRepo = AppDataSource.getRepository(Role);
|
|
const userRoleRepo = AppDataSource.getRepository(UserRole);
|
|
|
|
const user = await userRepo.findOne({ where: { id: req.params.id } });
|
|
if (!user) {
|
|
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
return;
|
|
}
|
|
|
|
// Résoudre les slugs en entités Role
|
|
const roleEntities = await roleRepo
|
|
.createQueryBuilder("role")
|
|
.where("role.slug IN (:...slugs)", { slugs: roles.length > 0 ? roles : ["__none__"] })
|
|
.getMany();
|
|
|
|
if (roleEntities.length !== roles.length) {
|
|
res.status(400).json({ success: false, error: "INVALID_ROLE_SLUGS" });
|
|
return;
|
|
}
|
|
|
|
// Supprimer tous les rôles existants pour cet user
|
|
await userRoleRepo.delete({ userId: req.params.id });
|
|
|
|
// Insérer les nouveaux rôles
|
|
const newUserRoles = roleEntities.map((role) => {
|
|
const ur = new UserRole();
|
|
ur.userId = req.params.id;
|
|
ur.roleId = role.id;
|
|
return ur;
|
|
});
|
|
|
|
if (newUserRoles.length > 0) {
|
|
await userRoleRepo.save(newUserRoles);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
userId: req.params.id,
|
|
roles: roleEntities.map((r) => ({ id: r.id, slug: r.slug, name: r.name })),
|
|
},
|
|
});
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SUBSCRIPTION PLANS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* GET /api/admin/plans
|
|
* Liste tous les plans d'abonnement.
|
|
*/
|
|
router.get("/plans", async (_req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const plans = await AppDataSource.getRepository(SubscriptionPlan).find({
|
|
order: { level: "ASC" },
|
|
});
|
|
res.json({ success: true, data: { plans } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/admin/plans
|
|
* Crée un plan d'abonnement.
|
|
* Body: { slug, name, level, priceInCents, features?, isActive? }
|
|
*/
|
|
router.post("/plans", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const {
|
|
slug,
|
|
name,
|
|
level,
|
|
priceInCents,
|
|
features = null,
|
|
isActive = true,
|
|
} = req.body as {
|
|
slug: string;
|
|
name: string;
|
|
level: number;
|
|
priceInCents: number;
|
|
features?: Record<string, unknown> | null;
|
|
isActive?: boolean;
|
|
};
|
|
|
|
if (!slug || !name || level === undefined || priceInCents === undefined) {
|
|
res.status(400).json({ success: false, error: "MISSING_REQUIRED_FIELDS" });
|
|
return;
|
|
}
|
|
|
|
const plan = AppDataSource.getRepository(SubscriptionPlan).create({
|
|
id: crypto.randomUUID(),
|
|
slug: slug as SubscriptionPlan["slug"],
|
|
name,
|
|
level,
|
|
priceInCents,
|
|
features,
|
|
isActive,
|
|
});
|
|
|
|
await AppDataSource.getRepository(SubscriptionPlan).save(plan);
|
|
res.status(201).json({ success: true, data: { plan } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /api/admin/plans/:id
|
|
* Met à jour un plan (isActive, priceInCents, features).
|
|
*/
|
|
router.patch("/plans/:id", async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const repo = AppDataSource.getRepository(SubscriptionPlan);
|
|
const plan = await repo.findOne({ where: { id: req.params.id } });
|
|
|
|
if (!plan) {
|
|
res.status(404).json({ success: false, error: "NOT_FOUND" });
|
|
return;
|
|
}
|
|
|
|
const allowed = ["isActive", "priceInCents", "features"] as const;
|
|
|
|
for (const field of allowed) {
|
|
if (field in req.body) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(plan as any)[field] = (req.body as Record<string, unknown>)[field];
|
|
}
|
|
}
|
|
|
|
await repo.save(plan);
|
|
res.json({ success: true, data: { plan } });
|
|
} catch {
|
|
res.status(500).json({ success: false, error: "INTERNAL_ERROR" });
|
|
}
|
|
});
|
|
|
|
export default router;
|