Compare commits
4 Commits
v1.7.3
...
e73ae80fc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e73ae80fc5
|
||
|
|
9ccbd2ceb1
|
||
|
|
3edf5cc070
|
||
| a4d0c6aa8c |
26
README.md
26
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/backend",
|
"name": "@memegoat/backend",
|
||||||
"version": "1.7.3",
|
"version": "1.7.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe("AuthService", () => {
|
|||||||
const dto = {
|
const dto = {
|
||||||
username: "test",
|
username: "test",
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
password: "password",
|
password: "Password1!",
|
||||||
};
|
};
|
||||||
mockHashingService.hashPassword.mockResolvedValue("hashed-password");
|
mockHashingService.hashPassword.mockResolvedValue("hashed-password");
|
||||||
mockHashingService.hashEmail.mockResolvedValue("hashed-email");
|
mockHashingService.hashEmail.mockResolvedValue("hashed-email");
|
||||||
@@ -165,7 +165,7 @@ describe("AuthService", () => {
|
|||||||
|
|
||||||
describe("login", () => {
|
describe("login", () => {
|
||||||
it("should login a user", async () => {
|
it("should login a user", async () => {
|
||||||
const dto = { email: "test@example.com", password: "password" };
|
const dto = { email: "test@example.com", password: "Password1!" };
|
||||||
const user = {
|
const user = {
|
||||||
uuid: "user-id",
|
uuid: "user-id",
|
||||||
username: "test",
|
username: "test",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
IsEmail,
|
IsEmail,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsString,
|
IsString,
|
||||||
|
Matches,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
@@ -10,6 +11,10 @@ export class RegisterDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@MaxLength(32)
|
@MaxLength(32)
|
||||||
|
@Matches(/^[a-z0-9_]+$/, {
|
||||||
|
message:
|
||||||
|
"username must contain only lowercase letters, numbers, and underscores",
|
||||||
|
})
|
||||||
username!: string;
|
username!: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -21,5 +26,15 @@ export class RegisterDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
|
@Matches(/[A-Z]/, {
|
||||||
|
message: "password must contain at least one uppercase letter",
|
||||||
|
})
|
||||||
|
@Matches(/[a-z]/, {
|
||||||
|
message: "password must contain at least one lowercase letter",
|
||||||
|
})
|
||||||
|
@Matches(/[0-9]/, { message: "password must contain at least one number" })
|
||||||
|
@Matches(/[^A-Za-z0-9]/, {
|
||||||
|
message: "password must contain at least one special character",
|
||||||
|
})
|
||||||
password!: string;
|
password!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/frontend",
|
"name": "@memegoat/frontend",
|
||||||
"version": "1.7.3",
|
"version": "1.7.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const loginSchema = z.object({
|
|||||||
email: z.string().email({ message: "Email invalide" }),
|
email: z.string().email({ message: "Email invalide" }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
.min(8, { message: "Le mot de passe doit faire au moins 8 caractères" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
type LoginFormValues = z.infer<typeof loginSchema>;
|
type LoginFormValues = z.infer<typeof loginSchema>;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -29,11 +29,27 @@ import { useAuth } from "@/providers/auth-provider";
|
|||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
username: z
|
username: z
|
||||||
.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_]+$/, {
|
||||||
|
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
|
||||||
.string()
|
.string()
|
||||||
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
|
.min(8, { message: "Le mot de passe doit faire au moins 8 caractères" })
|
||||||
|
.regex(/[A-Z]/, {
|
||||||
|
message: "Le mot de passe doit contenir au moins une majuscule",
|
||||||
|
})
|
||||||
|
.regex(/[a-z]/, {
|
||||||
|
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(/[^A-Za-z0-9]/, {
|
||||||
|
message: "Le mot de passe doit contenir au moins un caractère spécial",
|
||||||
|
}),
|
||||||
displayName: z.string().optional(),
|
displayName: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,12 +100,25 @@ export default function RegisterPage() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="displayName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Le Roi des Chèvres" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Pseudo</FormLabel>
|
<FormLabel>Pseudo (minuscule)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="supergoat" {...field} />
|
<Input placeholder="supergoat" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -110,19 +139,6 @@ export default function RegisterPage() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="displayName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Nom d'affichage (Optionnel)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Le Roi des Chèvres" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@memegoat/source",
|
"name": "@memegoat/source",
|
||||||
"version": "1.7.3",
|
"version": "1.7.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"version:get": "cmake -P version.cmake GET",
|
"version:get": "cmake -P version.cmake GET",
|
||||||
|
|||||||
Reference in New Issue
Block a user