From 13ccdbc2ab555fb83b3614354972669b254c033c Mon Sep 17 00:00:00 2001 From: Mathis HERRIOT <197931332+0x485254@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:48:59 +0100 Subject: [PATCH] feat: introduce reporting system and two-factor authentication (2FA) - Added `ReportDialog` component for user-generated content reporting. - Integrated `ReportService` with create, update, and fetch report functionalities. - Enhanced `AuthService` with 2FA setup, enable, disable, and verification methods. - Updated types to include 2FA responses and reporting-related data. - Enhanced `ContentCard` UI to support reporting functionality. - Improved admin services to manage user reports and statuses. --- frontend/src/components/content-card.tsx | 9 ++ frontend/src/components/report-dialog.tsx | 117 ++++++++++++++++++++++ frontend/src/services/admin.service.ts | 18 ++++ frontend/src/services/auth.service.ts | 23 ++++- frontend/src/services/report.service.ts | 40 ++++++++ frontend/src/types/auth.ts | 10 +- 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/report-dialog.tsx create mode 100644 frontend/src/services/report.service.ts diff --git a/frontend/src/components/content-card.tsx b/frontend/src/components/content-card.tsx index c250cff..778a2ad 100644 --- a/frontend/src/components/content-card.tsx +++ b/frontend/src/components/content-card.tsx @@ -3,6 +3,7 @@ import { Edit, Eye, + Flag, Heart, MoreHorizontal, Share2, @@ -34,6 +35,7 @@ import { useAuth } from "@/providers/auth-provider"; import { ContentService } from "@/services/content.service"; import { FavoriteService } from "@/services/favorite.service"; import type { Content } from "@/types/content"; +import { ReportDialog } from "./report-dialog"; import { UserContentEditDialog } from "./user-content-edit-dialog"; interface ContentCardProps { @@ -49,6 +51,7 @@ export function ContentCard({ content, onUpdate }: ContentCardProps) { const [isLiked, setIsLiked] = React.useState(content.isLiked || false); const [likesCount, setLikesCount] = React.useState(content.favoritesCount); const [editDialogOpen, setEditDialogOpen] = React.useState(false); + const [reportDialogOpen, setReportDialogOpen] = React.useState(false); const isAuthor = user?.uuid === content.authorId; const isVideo = !content.mimeType.startsWith("image/"); @@ -188,6 +191,12 @@ export function ContentCard({ content, onUpdate }: ContentCardProps) { Partager + {!isAuthor && ( + setReportDialogOpen(true)}> + + Signaler + + )} diff --git a/frontend/src/components/report-dialog.tsx b/frontend/src/components/report-dialog.tsx new file mode 100644 index 0000000..666ccb8 --- /dev/null +++ b/frontend/src/components/report-dialog.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { ReportReason, ReportService } from "@/services/report.service"; + +interface ReportDialogProps { + contentId?: string; + tagId?: string; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function ReportDialog({ + contentId, + tagId, + open, + onOpenChange, +}: ReportDialogProps) { + const [reason, setReason] = useState(ReportReason.INAPPROPRIATE); + const [description, setDescription] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async () => { + setIsSubmitting(true); + try { + await ReportService.create({ + contentId, + tagId, + reason, + description, + }); + toast.success("Signalement envoyé avec succès. Merci de nous aider à maintenir la communauté sûre."); + onOpenChange(false); + setDescription(""); + } catch (error) { + toast.error("Erreur lors de l'envoi du signalement."); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + Signaler le contenu + + Pourquoi signalez-vous ce contenu ? Un modérateur examinera votre demande. + + + + + Raison + setReason(value as ReportReason)} + > + + + + + Inapproprié + Spam + Droit d'auteur + Autre + + + + + Description (optionnelle) + setDescription(e.target.value)} + /> + + + + onOpenChange(false)} + disabled={isSubmitting} + > + Annuler + + + {isSubmitting ? "Envoi..." : "Signaler"} + + + + + ); +} diff --git a/frontend/src/services/admin.service.ts b/frontend/src/services/admin.service.ts index 2546254..d53dc3c 100644 --- a/frontend/src/services/admin.service.ts +++ b/frontend/src/services/admin.service.ts @@ -1,4 +1,5 @@ import api from "@/lib/api"; +import type { Report, ReportStatus } from "./report.service"; export interface AdminStats { users: number; @@ -11,4 +12,21 @@ export const adminService = { const response = await api.get("/admin/stats"); return response.data; }, + + getReports: async (limit = 10, offset = 0): Promise => { + const response = await api.get("/reports", { params: { limit, offset } }); + return response.data; + }, + + updateReportStatus: async (reportId: string, status: ReportStatus): Promise => { + await api.patch(`/reports/${reportId}/status`, { status }); + }, + + deleteUser: async (userId: string): Promise => { + await api.delete(`/users/${userId}`); + }, + + updateUser: async (userId: string, data: any): Promise => { + await api.patch(`/users/admin/${userId}`, data); + }, }; diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts index 32b10fb..3d1a3e8 100644 --- a/frontend/src/services/auth.service.ts +++ b/frontend/src/services/auth.service.ts @@ -1,5 +1,5 @@ import api from "@/lib/api"; -import type { LoginResponse, RegisterPayload } from "@/types/auth"; +import type { LoginResponse, RegisterPayload, TwoFactorSetupResponse } from "@/types/auth"; export const AuthService = { async login(email: string, password: string): Promise { @@ -10,6 +10,14 @@ export const AuthService = { return data; }, + async verify2fa(userId: string, token: string): Promise { + const { data } = await api.post("/auth/verify-2fa", { + userId, + token, + }); + return data; + }, + async register(payload: RegisterPayload): Promise { await api.post("/auth/register", payload); }, @@ -21,4 +29,17 @@ export const AuthService = { async refresh(): Promise { await api.post("/auth/refresh"); }, + + async setup2fa(): Promise { + const { data } = await api.post("/users/me/2fa/setup"); + return data; + }, + + async enable2fa(token: string): Promise { + await api.post("/users/me/2fa/enable", { token }); + }, + + async disable2fa(token: string): Promise { + await api.post("/users/me/2fa/disable", { token }); + }, }; diff --git a/frontend/src/services/report.service.ts b/frontend/src/services/report.service.ts new file mode 100644 index 0000000..cfcfb81 --- /dev/null +++ b/frontend/src/services/report.service.ts @@ -0,0 +1,40 @@ +import api from "@/lib/api"; + +export enum ReportReason { + INAPPROPRIATE = "inappropriate", + SPAM = "spam", + COPYRIGHT = "copyright", + OTHER = "other", +} + +export enum ReportStatus { + PENDING = "pending", + REVIEWED = "reviewed", + RESOLVED = "resolved", + DISMISSED = "dismissed", +} + +export interface CreateReportPayload { + contentId?: string; + tagId?: string; + reason: ReportReason; + description?: string; +} + +export interface Report { + uuid: string; + reporterId: string; + contentId?: string; + tagId?: string; + reason: ReportReason; + description?: string; + status: ReportStatus; + createdAt: string; + updatedAt: string; +} + +export const ReportService = { + async create(payload: CreateReportPayload): Promise { + await api.post("/reports", payload); + }, +}; diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts index 0056339..7ccdadd 100644 --- a/frontend/src/types/auth.ts +++ b/frontend/src/types/auth.ts @@ -1,6 +1,8 @@ export interface LoginResponse { message: string; - userId: string; + userId?: string; + access_token?: string; + refresh_token?: string; } export interface RegisterPayload { @@ -17,6 +19,12 @@ export interface AuthStatus { username: string; displayName?: string; avatarUrl?: string; + role?: string; }; isLoading: boolean; } + +export interface TwoFactorSetupResponse { + qrCodeUrl: string; + secret: string; +}