import { Router, Request, Response } from "express"; import { AppDataSource } from "../config/data-source"; import { User } from "../entities/User"; import { UserSubscription } from "../entities/UserSubscription"; import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware"; const router = Router(); /** Retourne la souscription active la plus élevée pour un userId local, ou null. */ async function getActiveSub(userId: string) { const now = new Date(); return AppDataSource.getRepository(UserSubscription) .createQueryBuilder("sub") .leftJoinAndSelect("sub.plan", "plan") .where("sub.userId = :userId", { userId }) .andWhere("sub.status IN (:...statuses)", { statuses: ["active", "trial"] }) .andWhere("(sub.endsAt IS NULL OR sub.endsAt > :now)", { now }) .orderBy("plan.level", "DESC") .addOrderBy("sub.startsAt", "DESC") .getOne(); } /** * GET /api/users/me/profile * Profil complet de l'utilisateur connecté (données locales). */ router.get("/me/profile", requireAuth, async (req: Request, res: Response): Promise => { const { user } = req as AuthenticatedRequest; const localUser = await AppDataSource.getRepository(User).findOne({ where: { superOAuthId: user.id }, relations: ["userRoles", "userRoles.role"], }); if (!localUser) { res.status(404).json({ success: false, error: "USER_NOT_FOUND" }); return; } const roles = localUser.userRoles.map((ur) => ur.role.slug); const activeSub = await getActiveSub(localUser.id); const plan = activeSub ? { slug: activeSub.plan.slug, name: activeSub.plan.name, level: activeSub.plan.level } : null; const subscription = activeSub ? { status: activeSub.status, startsAt: activeSub.startsAt.toISOString(), endsAt: activeSub.endsAt ? activeSub.endsAt.toISOString() : null, } : null; res.json({ success: true, data: { id: localUser.id, superOAuthId: localUser.superOAuthId, email: localUser.email, nickname: localUser.nickname, avatar: localUser.avatar, roles, plan, subscription, createdAt: localUser.createdAt.toISOString(), }, }); }); /** * PATCH /api/users/me * Met à jour nickname et/ou avatar de l'utilisateur connecté. * Note : le nickname local peut être écrasé au prochain login SuperOAuth. */ router.patch("/me", requireAuth, async (req: Request, res: Response): Promise => { const { user } = req as AuthenticatedRequest; const { nickname, avatar } = req.body as { nickname?: unknown; avatar?: unknown }; // Validation if (nickname !== undefined) { if (typeof nickname !== "string" || nickname.trim().length < 2 || nickname.trim().length > 100) { res.status(400).json({ success: false, error: "INVALID_NICKNAME", message: "nickname must be 2-100 characters" }); return; } } if (avatar !== undefined && avatar !== null) { if (typeof avatar !== "string") { res.status(400).json({ success: false, error: "INVALID_AVATAR" }); return; } try { const parsed = new URL(avatar); if (!["http:", "https:"].includes(parsed.protocol)) { throw new Error("invalid protocol"); } } catch { res.status(400).json({ success: false, error: "INVALID_AVATAR", message: "avatar must be a valid http/https URL or null" }); return; } } const userRepo = AppDataSource.getRepository(User); const localUser = await userRepo.findOne({ where: { superOAuthId: user.id } }); if (!localUser) { res.status(404).json({ success: false, error: "USER_NOT_FOUND" }); return; } if (nickname !== undefined) localUser.nickname = (nickname as string).trim(); if (avatar !== undefined) localUser.avatar = avatar as string | null; await userRepo.save(localUser); res.json({ success: true, data: { id: localUser.id, nickname: localUser.nickname, avatar: localUser.avatar, }, }); }); export default router;