goooood
This commit is contained in:
@@ -8,37 +8,29 @@ export async function POST(req: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const formData = await req.formData();
|
const formData = await req.formData();
|
||||||
const file = formData.get("file") as File | null;
|
const file = formData.get("file") as File | null;
|
||||||
console.log("📁 Fichier reçu:", file?.name, file?.type);
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
console.log("❌ Aucun fichier reçu");
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Aucun fichier reçu." },
|
{ error: "Aucun fichier reçu." },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
console.log("📁 Fichier reçu:", file.name, "| Type:", file.type);
|
||||||
|
|
||||||
let fileContent = "";
|
let fileContent = "";
|
||||||
const fileType = file.type;
|
const fileType = file.type;
|
||||||
console.log("🔍 Type de fichier:", fileType);
|
|
||||||
|
|
||||||
|
// --- LOGIQUE D'EXTRACTION DE TEXTE (INCHANGÉE) ---
|
||||||
if (fileType === "application/pdf") {
|
if (fileType === "application/pdf") {
|
||||||
console.log("📄 Traitement PDF en cours...");
|
console.log("📄 Traitement PDF en cours...");
|
||||||
try {
|
try {
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
console.log("📊 Taille du buffer:", buffer.length);
|
|
||||||
|
|
||||||
const data = await pdf(buffer);
|
const data = await pdf(buffer);
|
||||||
fileContent = data.text;
|
fileContent = data.text;
|
||||||
console.log(
|
console.log("✅ Extraction PDF réussie, longueur:", fileContent.length);
|
||||||
"✅ Extraction PDF réussie, longueur du texte:",
|
|
||||||
fileContent.length
|
|
||||||
);
|
|
||||||
} catch (pdfError) {
|
} catch (pdfError) {
|
||||||
console.error("❌ Erreur PDF:", pdfError);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: `Erreur lors du traitement du PDF: ${
|
error: `Erreur traitement PDF: ${
|
||||||
pdfError instanceof Error ? pdfError.message : "Erreur inconnue"
|
pdfError instanceof Error ? pdfError.message : "Erreur inconnue"
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
@@ -55,14 +47,13 @@ export async function POST(req: NextRequest) {
|
|||||||
const result = await mammoth.extractRawText({ arrayBuffer });
|
const result = await mammoth.extractRawText({ arrayBuffer });
|
||||||
fileContent = result.value;
|
fileContent = result.value;
|
||||||
console.log(
|
console.log(
|
||||||
"✅ Extraction Word réussie, longueur du texte:",
|
"✅ Extraction Word réussie, longueur:",
|
||||||
fileContent.length
|
fileContent.length
|
||||||
);
|
);
|
||||||
} catch (wordError) {
|
} catch (wordError) {
|
||||||
console.error("❌ Erreur Word:", wordError);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: `Erreur lors du traitement du document Word: ${
|
error: `Erreur traitement Word: ${
|
||||||
wordError instanceof Error ? wordError.message : "Erreur inconnue"
|
wordError instanceof Error ? wordError.message : "Erreur inconnue"
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
@@ -78,10 +69,9 @@ export async function POST(req: NextRequest) {
|
|||||||
fileContent.length
|
fileContent.length
|
||||||
);
|
);
|
||||||
} catch (textError) {
|
} catch (textError) {
|
||||||
console.error("❌ Erreur texte:", textError);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: `Erreur lors de la lecture du fichier texte: ${
|
error: `Erreur lecture texte: ${
|
||||||
textError instanceof Error ? textError.message : "Erreur inconnue"
|
textError instanceof Error ? textError.message : "Erreur inconnue"
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
@@ -90,7 +80,6 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérification du contenu extrait
|
|
||||||
if (!fileContent || fileContent.trim().length === 0) {
|
if (!fileContent || fileContent.trim().length === 0) {
|
||||||
console.log("⚠️ Contenu vide détecté");
|
console.log("⚠️ Contenu vide détecté");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -99,83 +88,20 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔍 Contenu extrait, longueur:", fileContent.length);
|
// =========================================================================
|
||||||
|
// CONFIGURATION PRESIDIO ANALYZER (SIMPLIFIÉE)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Toute la configuration (recognizers, allow_list, etc.) est maintenant dans le default.yaml du service.
|
||||||
|
// L'API a juste besoin d'envoyer le texte et la langue.
|
||||||
const analyzerConfig = {
|
const analyzerConfig = {
|
||||||
text: fileContent,
|
text: fileContent,
|
||||||
language: "fr",
|
language: "fr",
|
||||||
ad_hoc_recognizers: [
|
// Plus de ad_hoc_recognizers ici !
|
||||||
{
|
|
||||||
name: "BelgianNRNRecognizer",
|
|
||||||
supported_entity: "BE_NATIONAL_REGISTER_NUMBER",
|
|
||||||
supported_language: "fr",
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
name: "NRN_Pattern",
|
|
||||||
regex:
|
|
||||||
"\\b(?:[0-9]{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12][0-9]|3[01]))-?\\d{3}\\.?\\d{2}\\b",
|
|
||||||
score: 1.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: ["registre national", "nrn", "niss"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "BelgianEnterpriseRecognizer",
|
|
||||||
supported_entity: "BE_ENTERPRISE_NUMBER",
|
|
||||||
supported_language: "fr",
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
name: "BTW_Pattern",
|
|
||||||
regex: "\\bBE\\s?0\\d{3}\\.\\d{3}\\.\\d{3}\\b",
|
|
||||||
score: 0.95,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: ["entreprise", "btw", "tva"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IBANRecognizer",
|
|
||||||
supported_entity: "IBAN",
|
|
||||||
supported_language: "fr",
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
name: "IBAN_Pattern",
|
|
||||||
regex: "\\b[A-Z]{2}\\d{2}\\s?(?:\\d{4}\\s?){4,7}\\d{1,4}\\b",
|
|
||||||
score: 0.95,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: ["iban", "compte", "bancaire"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PhoneRecognizer",
|
|
||||||
supported_entity: "PHONE_NUMBER",
|
|
||||||
supported_language: "fr",
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
name: "Phone_Pattern",
|
|
||||||
regex:
|
|
||||||
"\\b(?:(?:\\+|00)(?:32|33|352)|0)\\s?[1-9](?:[\\s.-]?\\d{2}){3,4}\\b",
|
|
||||||
score: 0.8,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: ["téléphone", "tel", "mobile", "gsm"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "EmailRecognizer",
|
|
||||||
supported_entity: "EMAIL_ADDRESS",
|
|
||||||
supported_language: "fr",
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
name: "Email_Pattern",
|
|
||||||
regex: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b",
|
|
||||||
score: 1.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
context: ["email", "courriel", "adresse électronique"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 Appel à Presidio Analyzer...");
|
console.log("🔍 Appel à Presidio Analyzer...");
|
||||||
|
// Mettez votre URL externe ici, ou utilisez le nom de service Docker si approprié
|
||||||
const presidioAnalyzerUrl =
|
const presidioAnalyzerUrl =
|
||||||
"http://ocs00s000ssow8kssossocco.51.68.233.212.sslip.io/analyze";
|
"http://ocs00s000ssow8kssossocco.51.68.233.212.sslip.io/analyze";
|
||||||
|
|
||||||
@@ -189,29 +115,28 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log("📊 Statut Analyzer:", analyzeResponse.status);
|
console.log("📊 Statut Analyzer:", analyzeResponse.status);
|
||||||
|
|
||||||
if (!analyzeResponse.ok) {
|
if (!analyzeResponse.ok) {
|
||||||
const errorBody = await analyzeResponse.text();
|
const errorBody = await analyzeResponse.text();
|
||||||
console.error("❌ Erreur Analyzer:", errorBody);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: `Erreur Analyzer: ${errorBody}` },
|
||||||
error: `Erreur de l'analyseur Presidio (${analyzeResponse.status}): ${errorBody}`,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let analyzerResults;
|
const analyzerResults = await analyzeResponse.json();
|
||||||
try {
|
console.log("✅ Analyzer a trouvé", analyzerResults.length, "entités.");
|
||||||
analyzerResults = await analyzeResponse.json();
|
|
||||||
console.log("✅ Analyzer réussi, résultats:", analyzerResults.length);
|
// =========================================================================
|
||||||
} catch (jsonError) {
|
// CONFIGURATION PRESIDIO ANONYMIZER
|
||||||
console.error("❌ Erreur parsing JSON Analyzer:", jsonError);
|
// =========================================================================
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Erreur lors du parsing de la réponse de l'analyseur" },
|
// L'Anonymizer lira la config d'anonymisation du default.yaml de l'Analyzer
|
||||||
{ status: 500 }
|
// ou vous pouvez définir des transformations spécifiques ici si besoin.
|
||||||
);
|
// Pour commencer, on envoie juste les résultats de l'analyse.
|
||||||
}
|
const anonymizerConfig = {
|
||||||
|
text: fileContent,
|
||||||
|
analyzer_results: analyzerResults,
|
||||||
|
};
|
||||||
|
|
||||||
console.log("🔍 Appel à Presidio Anonymizer...");
|
console.log("🔍 Appel à Presidio Anonymizer...");
|
||||||
const presidioAnonymizerUrl =
|
const presidioAnonymizerUrl =
|
||||||
@@ -223,64 +148,34 @@ export async function POST(req: NextRequest) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(anonymizerConfig),
|
||||||
text: fileContent,
|
|
||||||
analyzer_results: analyzerResults,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📊 Statut Anonymizer:", anonymizeResponse.status);
|
console.log("📊 Statut Anonymizer:", anonymizeResponse.status);
|
||||||
|
|
||||||
if (!anonymizeResponse.ok) {
|
if (!anonymizeResponse.ok) {
|
||||||
const errorBody = await anonymizeResponse.text();
|
const errorBody = await anonymizeResponse.text();
|
||||||
console.error("❌ Erreur Anonymizer:", errorBody);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: `Erreur Anonymizer: ${errorBody}` },
|
||||||
error: `Erreur de l'anonymiseur Presidio (${anonymizeResponse.status}): ${errorBody}`,
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let anonymizerResult;
|
const anonymizerResult = await anonymizeResponse.json();
|
||||||
try {
|
console.log("✅ Anonymisation réussie.");
|
||||||
anonymizerResult = await anonymizeResponse.json();
|
|
||||||
console.log("✅ Anonymizer réussi");
|
|
||||||
} catch (jsonError) {
|
|
||||||
console.error("❌ Erreur parsing JSON Anonymizer:", jsonError);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Erreur lors du parsing de la réponse de l'anonymiseur" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
anonymizedText: anonymizerResult.text,
|
anonymizedText: anonymizerResult.text,
|
||||||
piiCount: analyzerResults.length,
|
piiCount: analyzerResults.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("✅ Traitement terminé avec succès");
|
return NextResponse.json(result, { status: 200 });
|
||||||
return NextResponse.json(result, {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error("❌ Erreur générale:", err);
|
console.error("❌ Erreur générale:", err);
|
||||||
const errorMessage =
|
|
||||||
err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: "Une erreur inconnue est survenue sur le serveur.";
|
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: errorMessage },
|
|
||||||
{
|
{
|
||||||
status: 500,
|
error: err instanceof Error ? err.message : "Erreur serveur inconnue.",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
{ status: 500 }
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
app/page.tsx
101
app/page.tsx
@@ -18,18 +18,8 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
Shield,
|
Shield,
|
||||||
Clock,
|
Clock,
|
||||||
User,
|
|
||||||
AtSign,
|
|
||||||
MapPin,
|
|
||||||
Cake,
|
|
||||||
Home as HomeIcon,
|
|
||||||
Venus,
|
|
||||||
Phone,
|
|
||||||
Building,
|
|
||||||
Fingerprint,
|
|
||||||
CreditCard,
|
|
||||||
Check,
|
|
||||||
Eye,
|
Eye,
|
||||||
|
TestTube,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import PresidioModal from "./components/PresidioModal";
|
import PresidioModal from "./components/PresidioModal";
|
||||||
|
|
||||||
@@ -51,23 +41,6 @@ interface ProcessedFile {
|
|||||||
anonymizedText?: string;
|
anonymizedText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnonymizationOptions {
|
|
||||||
[key: string]: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const piiOptions = [
|
|
||||||
{ id: "name", label: "Nom & Prénom", icon: User },
|
|
||||||
{ id: "email", label: "Email", icon: AtSign },
|
|
||||||
{ id: "location", label: "Lieu", icon: MapPin },
|
|
||||||
{ id: "birthdate", label: "Date de naissance", icon: Cake },
|
|
||||||
{ id: "address", label: "Adresse", icon: HomeIcon },
|
|
||||||
{ id: "gender", label: "Genre / Sexe", icon: Venus },
|
|
||||||
{ id: "phone", label: "Téléphone", icon: Phone },
|
|
||||||
{ id: "organization", label: "Entreprise", icon: Building },
|
|
||||||
{ id: "idNumber", label: "N° Identification", icon: Fingerprint },
|
|
||||||
{ id: "financial", label: "Données financières", icon: CreditCard },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
@@ -75,19 +48,12 @@ export default function Home() {
|
|||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const [history, setHistory] = useState<ProcessedFile[]>([]);
|
const [history, setHistory] = useState<ProcessedFile[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [anonymizationOptions, setAnonymizationOptions] =
|
|
||||||
useState<AnonymizationOptions>(
|
|
||||||
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
|
|
||||||
);
|
|
||||||
const [showPresidioModal, setShowPresidioModal] = useState(false);
|
const [showPresidioModal, setShowPresidioModal] = useState(false);
|
||||||
const [anonymizedResult, setAnonymizedResult] = useState<{
|
const [anonymizedResult, setAnonymizedResult] = useState<{
|
||||||
text: string;
|
text: string;
|
||||||
piiCount: number;
|
piiCount: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const handleOptionChange = (id: string) =>
|
|
||||||
setAnonymizationOptions((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files?.length) {
|
if (e.target.files?.length) {
|
||||||
setFile(e.target.files[0]);
|
setFile(e.target.files[0]);
|
||||||
@@ -180,6 +146,29 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadTestDocument = () => {
|
||||||
|
try {
|
||||||
|
// Créer un document de test avec des données PII fictives
|
||||||
|
const testContent = `Rapport pour la société belge "Solution Globale SPRL" (BCE : BE 0987.654.321).
|
||||||
|
Contact principal : M. Luc Dubois, né le 15/03/1975.
|
||||||
|
Son numéro de registre national est le 75.03.15-123.45.
|
||||||
|
Adresse : Avenue des Arts 56, 1000 Bruxelles.
|
||||||
|
Téléphone : +32 2 555 12 34. Email : luc.dubois@solutionglobale.be.
|
||||||
|
Le paiement de la facture a été effectué par carte VISA 4979 1234 5678 9012.
|
||||||
|
Le remboursement sera versé sur le compte IBAN BE12 3456 7890 1234, code SWIFT : GEBABEBB.`;
|
||||||
|
|
||||||
|
const testBlob = new Blob([testContent], { type: "text/plain" });
|
||||||
|
const testFile = new File([testBlob], "document-test.txt", {
|
||||||
|
type: "text/plain",
|
||||||
|
});
|
||||||
|
|
||||||
|
setFile(testFile);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("Erreur lors du chargement du document de test");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const processFile = async () => {
|
const processFile = async () => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
@@ -198,6 +187,7 @@ export default function Home() {
|
|||||||
},
|
},
|
||||||
...prev,
|
...prev,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -444,36 +434,19 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
|
||||||
<h3 className="text-base font-black text-white uppercase tracking-wide mb-3">
|
{/* Bouton pour charger un document de test */}
|
||||||
Options d'Anonymisation
|
<div className="flex-grow flex items-center justify-center">
|
||||||
</h3>
|
<Button
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-2 gap-x-4 gap-y-2">
|
onClick={loadTestDocument}
|
||||||
{piiOptions.map((option) => (
|
disabled={isProcessing}
|
||||||
<label
|
className="bg-[#061717] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1 h-12 px-6 text-base font-black uppercase tracking-wide disabled:opacity-50"
|
||||||
key={option.id}
|
>
|
||||||
htmlFor={option.id}
|
<TestTube className="h-5 w-5 mr-2" />
|
||||||
className="flex items-center gap-2 cursor-pointer group"
|
Charger Document de Test
|
||||||
>
|
</Button>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={option.id}
|
|
||||||
className="sr-only peer"
|
|
||||||
checked={anonymizationOptions[option.id]}
|
|
||||||
onChange={() => handleOptionChange(option.id)}
|
|
||||||
/>
|
|
||||||
<div className="w-5 h-5 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center transition-all peer-checked:bg-[#F7AB6E] peer-checked:border-[#F7AB6E]">
|
|
||||||
{anonymizationOptions[option.id] && (
|
|
||||||
<Check className="h-4 w-4 text-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="font-bold text-xs text-white uppercase">
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user