refactor: improve formatting, type safety, and component organization

- Adjusted inconsistent formatting for better readability across components and services.
- Enhanced type safety by adding placeholders for ignored error parameters and improving types across services.
- Improved component organization by reordering imports consistently and applying formatting updates in UI components.
This commit is contained in:
Mathis HERRIOT
2026-01-29 14:11:28 +01:00
parent 3edf5cc070
commit 9ccbd2ceb1
11 changed files with 133 additions and 59 deletions

View File

@@ -59,12 +59,28 @@ Pour approfondir vos connaissances techniques sur le projet :
## Comment l'utiliser ? ## Comment l'utiliser ?
### Installation locale ### Déploiement en Production
1. Clonez le dépôt. Le projet est prêt pour la production via Docker Compose.
2. Installez les dépendances avec `pnpm install`.
3. Configurez les variables d'environnement (voir `.env.example`). 1. **Prérequis** : Docker et Docker Compose installés.
4. Lancez les services via Docker ou manuellement. 2. **Variables d'environnement** : Copiez `.env.example` en `.env.prod` et ajustez les valeurs (clés secrètes, hosts, Sentry DSN, etc.).
3. **Lancement** :
```bash
docker-compose -f docker-compose.prod.yml up -d
```
4. **Services inclus** :
- **Frontend** : Next.js en mode standalone optimisé.
- **Backend** : NestJS avec clustering et monitoring Sentry.
- **Caddy** : Gestion automatique du SSL/TLS.
- **ClamAV** : Scan antivirus en temps réel des médias.
- **Redis** : Cache, sessions et limitation de débit (Throttling/Bot detection).
- **MinIO** : Stockage compatible S3.
### Sécurité et Performance
- **Transcodage Auto** : Toutes les images sont converties en WebP et les vidéos en WebM pour minimiser la bande passante.
- **Bot Detection** : Système intégré de détection et de bannissement automatique des crawlers malveillants via Redis.
- **Monitoring** : Tracking d'erreurs et profilage de performance via Sentry (Node.js et Next.js).
### Clés API ### Clés API

View File

@@ -12,7 +12,8 @@ export class RegisterDto {
@IsNotEmpty() @IsNotEmpty()
@MaxLength(32) @MaxLength(32)
@Matches(/^[a-z0-9_]+$/, { @Matches(/^[a-z0-9_]+$/, {
message: "username must contain only lowercase letters, numbers, and underscores", message:
"username must contain only lowercase letters, numbers, and underscores",
}) })
username!: string; username!: string;

View File

@@ -108,7 +108,10 @@ export default function LoginPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{show2fa ? ( {show2fa ? (
<form onSubmit={onOtpSubmit} className="space-y-6 flex flex-col items-center"> <form
onSubmit={onOtpSubmit}
className="space-y-6 flex flex-col items-center"
>
<InputOTP <InputOTP
maxLength={6} maxLength={6}
value={otpValue} value={otpValue}
@@ -126,7 +129,11 @@ export default function LoginPage() {
<InputOTPSlot index={5} /> <InputOTPSlot index={5} />
</InputOTPGroup> </InputOTPGroup>
</InputOTP> </InputOTP>
<Button type="submit" className="w-full" disabled={loading || otpValue.length !== 6}> <Button
type="submit"
className="w-full"
disabled={loading || otpValue.length !== 6}
>
{loading ? "Vérification..." : "Vérifier le code"} {loading ? "Vérification..." : "Vérifier le code"}
</Button> </Button>
<Button <Button

View File

@@ -31,7 +31,8 @@ const registerSchema = z.object({
.string() .string()
.min(3, { message: "Le pseudo doit faire au moins 3 caractères" }) .min(3, { message: "Le pseudo doit faire au moins 3 caractères" })
.regex(/^[a-z0-9_]+$/, { .regex(/^[a-z0-9_]+$/, {
message: "Le pseudo ne doit contenir que des minuscules, chiffres et underscores", message:
"Le pseudo ne doit contenir que des minuscules, chiffres et underscores",
}), }),
email: z.string().email({ message: "Email invalide" }), email: z.string().email({ message: "Email invalide" }),
password: z password: z
@@ -43,7 +44,9 @@ const registerSchema = z.object({
.regex(/[a-z]/, { .regex(/[a-z]/, {
message: "Le mot de passe doit contenir au moins une minuscule", message: "Le mot de passe doit contenir au moins une minuscule",
}) })
.regex(/[0-9]/, { message: "Le mot de passe doit contenir au moins un chiffre" }) .regex(/[0-9]/, {
message: "Le mot de passe doit contenir au moins un chiffre",
})
.regex(/[^A-Za-z0-9]/, { .regex(/[^A-Za-z0-9]/, {
message: "Le mot de passe doit contenir au moins un caractère spécial", message: "Le mot de passe doit contenir au moins un caractère spécial",
}), }),

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { import {
CheckCircle,
XCircle,
AlertCircle, AlertCircle,
MoreHorizontal,
ArrowLeft, ArrowLeft,
CheckCircle,
MoreHorizontal,
XCircle,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -34,34 +34,34 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { adminService } from "@/services/admin.service"; import { adminService } from "@/services/admin.service";
import { ReportStatus, type Report } from "@/services/report.service"; import { type Report, ReportStatus } from "@/services/report.service";
export default function AdminReportsPage() { export default function AdminReportsPage() {
const [reports, setReports] = useState<Report[]>([]); const [reports, setReports] = useState<Report[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const fetchReports = async () => { const fetchReports = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const data = await adminService.getReports(); const data = await adminService.getReports();
setReports(data); setReports(data);
} catch (error) { } catch (_error) {
toast.error("Erreur lors du chargement des signalements."); toast.error("Erreur lors du chargement des signalements.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, []);
useEffect(() => { useEffect(() => {
fetchReports(); fetchReports();
}, []); }, [fetchReports]);
const handleUpdateStatus = async (reportId: string, status: ReportStatus) => { const handleUpdateStatus = async (reportId: string, status: ReportStatus) => {
try { try {
await adminService.updateReportStatus(reportId, status); await adminService.updateReportStatus(reportId, status);
toast.success("Statut mis à jour."); toast.success("Statut mis à jour.");
fetchReports(); fetchReports();
} catch (error) { } catch (_error) {
toast.error("Erreur lors de la mise à jour du statut."); toast.error("Erreur lors de la mise à jour du statut.");
} }
}; };
@@ -128,9 +128,7 @@ export default function AdminReportsPage() {
) : ( ) : (
reports.map((report) => ( reports.map((report) => (
<TableRow key={report.uuid}> <TableRow key={report.uuid}>
<TableCell> <TableCell>{report.reporterId.substring(0, 8)}...</TableCell>
{report.reporterId.substring(0, 8)}...
</TableCell>
<TableCell> <TableCell>
{report.contentId ? ( {report.contentId ? (
<Link <Link
@@ -188,9 +186,7 @@ export default function AdminReportsPage() {
</DropdownMenuItem> </DropdownMenuItem>
{report.contentId && ( {report.contentId && (
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`/meme/${report.contentId}`}> <Link href={`/meme/${report.contentId}`}>Voir le contenu</Link>
Voir le contenu
</Link>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -3,6 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import {
AlertTriangle, AlertTriangle,
Download,
Laptop, Laptop,
Loader2, Loader2,
Moon, Moon,
@@ -12,7 +13,6 @@ import {
Sun, Sun,
Trash2, Trash2,
User as UserIcon, User as UserIcon,
Download,
} from "lucide-react"; } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@@ -20,6 +20,7 @@ import * as React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import * as z from "zod"; import * as z from "zod";
import { TwoFactorSetup } from "@/components/two-factor-setup";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -53,7 +54,6 @@ import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { TwoFactorSetup } from "@/components/two-factor-setup";
import { useAuth } from "@/providers/auth-provider"; import { useAuth } from "@/providers/auth-provider";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
@@ -326,7 +326,8 @@ export default function SettingsPage() {
<CardTitle>Portabilité des données</CardTitle> <CardTitle>Portabilité des données</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Conformément au RGPD, vous pouvez exporter l'intégralité de vos données rattachées à votre compte. Conformément au RGPD, vous pouvez exporter l'intégralité de vos données
rattachées à votre compte.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -334,7 +335,8 @@ export default function SettingsPage() {
<div className="space-y-1"> <div className="space-y-1">
<p className="font-bold">Exporter mes données</p> <p className="font-bold">Exporter mes données</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Téléchargez un fichier JSON contenant votre profil, vos mèmes et vos favoris. Téléchargez un fichier JSON contenant votre profil, vos mèmes et vos
favoris.
</p> </p>
</div> </div>
<Button <Button

View File

@@ -35,7 +35,6 @@ import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service"; import { FavoriteService } from "@/services/favorite.service";
import type { Content } from "@/types/content"; import type { Content } from "@/types/content";
import { ReportDialog } from "./report-dialog";
import { UserContentEditDialog } from "./user-content-edit-dialog"; import { UserContentEditDialog } from "./user-content-edit-dialog";
interface ContentCardProps { interface ContentCardProps {
@@ -51,7 +50,7 @@ export function ContentCard({ content, onUpdate }: ContentCardProps) {
const [isLiked, setIsLiked] = React.useState(content.isLiked || false); const [isLiked, setIsLiked] = React.useState(content.isLiked || false);
const [likesCount, setLikesCount] = React.useState(content.favoritesCount); const [likesCount, setLikesCount] = React.useState(content.favoritesCount);
const [editDialogOpen, setEditDialogOpen] = React.useState(false); const [editDialogOpen, setEditDialogOpen] = React.useState(false);
const [reportDialogOpen, setReportDialogOpen] = React.useState(false); const [_reportDialogOpen, setReportDialogOpen] = React.useState(false);
const isAuthor = user?.uuid === content.authorId; const isAuthor = user?.uuid === content.authorId;
const isVideo = !content.mimeType.startsWith("image/"); const isVideo = !content.mimeType.startsWith("image/");

View File

@@ -48,10 +48,12 @@ export function ReportDialog({
reason, reason,
description, description,
}); });
toast.success("Signalement envoyé avec succès. Merci de nous aider à maintenir la communauté sûre."); toast.success(
"Signalement envoyé avec succès. Merci de nous aider à maintenir la communauté sûre.",
);
onOpenChange(false); onOpenChange(false);
setDescription(""); setDescription("");
} catch (error) { } catch (_error) {
toast.error("Erreur lors de l'envoi du signalement."); toast.error("Erreur lors de l'envoi du signalement.");
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { Loader2, Shield, ShieldAlert, ShieldCheck } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Shield, ShieldCheck, ShieldAlert, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -18,8 +18,8 @@ import {
InputOTPSeparator, InputOTPSeparator,
InputOTPSlot, InputOTPSlot,
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import { AuthService } from "@/services/auth.service";
import { useAuth } from "@/providers/auth-provider"; import { useAuth } from "@/providers/auth-provider";
import { AuthService } from "@/services/auth.service";
export function TwoFactorSetup() { export function TwoFactorSetup() {
const { user, refreshUser } = useAuth(); const { user, refreshUser } = useAuth();
@@ -36,7 +36,7 @@ export function TwoFactorSetup() {
setQrCode(data.qrCodeUrl); setQrCode(data.qrCodeUrl);
setSecret(data.secret); setSecret(data.secret);
setStep("setup"); setStep("setup");
} catch (error) { } catch (_error) {
toast.error("Erreur lors de la configuration de la 2FA."); toast.error("Erreur lors de la configuration de la 2FA.");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -52,7 +52,7 @@ export function TwoFactorSetup() {
await refreshUser(); await refreshUser();
setStep("idle"); setStep("idle");
setOtpValue(""); setOtpValue("");
} catch (error) { } catch (_error) {
toast.error("Code invalide. Veuillez réessayer."); toast.error("Code invalide. Veuillez réessayer.");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -68,14 +68,14 @@ export function TwoFactorSetup() {
await refreshUser(); await refreshUser();
setStep("idle"); setStep("idle");
setOtpValue(""); setOtpValue("");
} catch (error) { } catch (_error) {
toast.error("Code invalide. Veuillez réessayer."); toast.error("Code invalide. Veuillez réessayer.");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
// Note: We need a way to know if 2FA is enabled. // Note: We need a way to know if 2FA is enabled.
// Assuming user object might have twoFactorEnabled property or similar. // Assuming user object might have twoFactorEnabled property or similar.
// For now, let's assume it's on the user object (we might need to add it to the type). // For now, let's assume it's on the user object (we might need to add it to the type).
const isEnabled = (user as any)?.twoFactorEnabled; const isEnabled = (user as any)?.twoFactorEnabled;
@@ -89,7 +89,8 @@ export function TwoFactorSetup() {
<CardTitle>Double Authentification (2FA)</CardTitle> <CardTitle>Double Authentification (2FA)</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Ajoutez une couche de sécurité supplémentaire à votre compte en utilisant une application d'authentification. Ajoutez une couche de sécurité supplémentaire à votre compte en utilisant
une application d'authentification.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -101,7 +102,9 @@ export function TwoFactorSetup() {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="font-bold">La 2FA est activée</p> <p className="font-bold">La 2FA est activée</p>
<p className="text-sm text-muted-foreground">Votre compte est protégé par un code temporaire.</p> <p className="text-sm text-muted-foreground">
Votre compte est protégé par un code temporaire.
</p>
</div> </div>
<Button variant="outline" size="sm" onClick={() => setStep("verify")}> <Button variant="outline" size="sm" onClick={() => setStep("verify")}>
Désactiver Désactiver
@@ -114,10 +117,21 @@ export function TwoFactorSetup() {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="font-bold">La 2FA n'est pas activée</p> <p className="font-bold">La 2FA n'est pas activée</p>
<p className="text-sm text-muted-foreground">Activez la 2FA pour mieux protéger votre compte.</p> <p className="text-sm text-muted-foreground">
Activez la 2FA pour mieux protéger votre compte.
</p>
</div> </div>
<Button variant="primary" size="sm" onClick={handleSetup} disabled={isLoading}> <Button
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Configurer"} variant="default"
size="sm"
onClick={handleSetup}
disabled={isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Configurer"
)}
</Button> </Button>
</> </>
)} )}
@@ -133,7 +147,8 @@ export function TwoFactorSetup() {
<CardHeader> <CardHeader>
<CardTitle>Configurer la 2FA</CardTitle> <CardTitle>Configurer la 2FA</CardTitle>
<CardDescription> <CardDescription>
Scannez le QR Code ci-dessous avec votre application d'authentification (Google Authenticator, Authy, etc.). Scannez le QR Code ci-dessous avec votre application d'authentification
(Google Authenticator, Authy, etc.).
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col items-center gap-6"> <CardContent className="flex flex-col items-center gap-6">
@@ -143,13 +158,17 @@ export function TwoFactorSetup() {
</div> </div>
)} )}
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<p className="text-sm font-medium text-center">Ou entrez ce code manuellement :</p> <p className="text-sm font-medium text-center">
Ou entrez ce code manuellement :
</p>
<code className="block p-2 bg-muted text-center rounded text-xs font-mono break-all"> <code className="block p-2 bg-muted text-center rounded text-xs font-mono break-all">
{secret} {secret}
</code> </code>
</div> </div>
<div className="flex flex-col items-center gap-4 w-full border-t pt-6"> <div className="flex flex-col items-center gap-4 w-full border-t pt-6">
<p className="text-sm font-medium">Entrez le code à 6 chiffres pour confirmer :</p> <p className="text-sm font-medium">
Entrez le code à 6 chiffres pour confirmer :
</p>
<InputOTP maxLength={6} value={otpValue} onChange={setOtpValue}> <InputOTP maxLength={6} value={otpValue} onChange={setOtpValue}>
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot index={0} /> <InputOTPSlot index={0} />
@@ -166,9 +185,18 @@ export function TwoFactorSetup() {
</div> </div>
</CardContent> </CardContent>
<CardFooter className="flex justify-between"> <CardFooter className="flex justify-between">
<Button variant="ghost" onClick={() => setStep("idle")}>Annuler</Button> <Button variant="ghost" onClick={() => setStep("idle")}>
<Button onClick={handleEnable} disabled={otpValue.length !== 6 || isLoading}> Annuler
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Activer la 2FA"} </Button>
<Button
onClick={handleEnable}
disabled={otpValue.length !== 6 || isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Activer la 2FA"
)}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
@@ -181,7 +209,8 @@ export function TwoFactorSetup() {
<CardHeader> <CardHeader>
<CardTitle>Désactiver la 2FA</CardTitle> <CardTitle>Désactiver la 2FA</CardTitle>
<CardDescription> <CardDescription>
Veuillez entrer le code de votre application pour désactiver la double authentification. Veuillez entrer le code de votre application pour désactiver la double
authentification.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col items-center gap-6"> <CardContent className="flex flex-col items-center gap-6">
@@ -200,9 +229,19 @@ export function TwoFactorSetup() {
</InputOTP> </InputOTP>
</CardContent> </CardContent>
<CardFooter className="flex justify-between"> <CardFooter className="flex justify-between">
<Button variant="ghost" onClick={() => setStep("idle")}>Annuler</Button> <Button variant="ghost" onClick={() => setStep("idle")}>
<Button variant="destructive" onClick={handleDisable} disabled={otpValue.length !== 6 || isLoading}> Annuler
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : "Confirmer la désactivation"} </Button>
<Button
variant="destructive"
onClick={handleDisable}
disabled={otpValue.length !== 6 || isLoading}
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"Confirmer la désactivation"
)}
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>

View File

@@ -18,7 +18,10 @@ export const adminService = {
return response.data; return response.data;
}, },
updateReportStatus: async (reportId: string, status: ReportStatus): Promise<void> => { updateReportStatus: async (
reportId: string,
status: ReportStatus,
): Promise<void> => {
await api.patch(`/reports/${reportId}/status`, { status }); await api.patch(`/reports/${reportId}/status`, { status });
}, },

View File

@@ -1,5 +1,9 @@
import api from "@/lib/api"; import api from "@/lib/api";
import type { LoginResponse, RegisterPayload, TwoFactorSetupResponse } from "@/types/auth"; import type {
LoginResponse,
RegisterPayload,
TwoFactorSetupResponse,
} from "@/types/auth";
export const AuthService = { export const AuthService = {
async login(email: string, password: string): Promise<LoginResponse> { async login(email: string, password: string): Promise<LoginResponse> {
@@ -31,7 +35,9 @@ export const AuthService = {
}, },
async setup2fa(): Promise<TwoFactorSetupResponse> { async setup2fa(): Promise<TwoFactorSetupResponse> {
const { data } = await api.post<TwoFactorSetupResponse>("/users/me/2fa/setup"); const { data } = await api.post<TwoFactorSetupResponse>(
"/users/me/2fa/setup",
);
return data; return data;
}, },