UNPKG

free-mail-sender

Version:

This Strapi plugin is designed to streamline the process of sending emails through the REST API. It enables you to send emails using different providers and customize the email content to suit your specific needs.

540 lines (486 loc) 16.6 kB
import React, { useState } from "react"; import { Button, Box, Typography, Textarea } from "@strapi/design-system"; import { ModalLayout, ModalBody, ModalHeader, ModalFooter, } from "@strapi/design-system/ModalLayout"; import { Stack } from "@strapi/design-system/Stack"; const HomePage = () => { const [publicKey, setPublicKey] = useState(""); const [privateKey, setPrivateKey] = useState(""); const [isModalVisible, setIsModalVisible] = useState(false); // Función auxiliar para convertir ArrayBuffer a base64 const arrayBufferToBase64 = (buffer) => { let binary = ""; const bytes = new Uint8Array(buffer); const length = bytes.byteLength; for (let i = 0; i < length; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }; // Convertir base64 a PEM format const formatToPEM = (base64Key, type) => { const header = type === "public" ? "-----BEGIN PUBLIC KEY-----" : "-----BEGIN PRIVATE KEY-----"; const footer = type === "public" ? "-----END PUBLIC KEY-----" : "-----END PRIVATE KEY-----"; // Dividir en líneas de 64 caracteres const lines = base64Key.match(/.{1,64}/g) || []; return [header, ...lines, footer].join("\n"); }; // Generar las claves públicas y privadas RSA compatibles con el sistema híbrido const handleGenerateKeys = async () => { try { console.log("🔑 Generating RSA key pair..."); const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, // Tamaño de la clave en bits publicExponent: new Uint8Array([1, 0, 1]), // Exponente público hash: "SHA-256", // Hash utilizado (compatible con el backend) }, true, ["encrypt", "decrypt"], // Operaciones disponibles ); // Exportar claves en formato correcto const publicKeyArrayBuffer = await crypto.subtle.exportKey( "spki", // Formato SPKI para la clave pública keyPair.publicKey, ); const privateKeyArrayBuffer = await crypto.subtle.exportKey( "pkcs8", // Formato PKCS#8 para la clave privada keyPair.privateKey, ); // Convertir las claves a base64 const publicKeyBase64 = arrayBufferToBase64(publicKeyArrayBuffer); const privateKeyBase64 = arrayBufferToBase64(privateKeyArrayBuffer); console.log("✅ Keys generated successfully"); console.log("📏 Public key length:", publicKeyBase64.length); console.log("📏 Private key length:", privateKeyBase64.length); // Establecer las claves en el estado setPublicKey(publicKeyBase64); setPrivateKey(privateKeyBase64); } catch (error) { console.error("❌ Error generating keys:", error); alert("Error generando las llaves: " + error.message); } }; // Función para mostrar el modal const handleShowModal = () => { setIsModalVisible(true); }; // Función para cerrar el modal const handleCloseModal = () => { setIsModalVisible(false); }; // Función de prueba para verificar que las claves funcionan const testKeys = async () => { if (!publicKey || !privateKey) { alert("Primero genera las claves"); return; } try { console.log("🧪 Testing key compatibility..."); // Simular encriptación híbrida (como en el frontend) const testData = "Test message for key compatibility"; // Generar clave AES const aesKey = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"], ); // Exportar clave AES const aesKeyRaw = await crypto.subtle.exportKey("raw", aesKey); // Importar clave pública RSA const publicKeyBuffer = Uint8Array.from(atob(publicKey), (c) => c.charCodeAt(0), ); const importedPublicKey = await crypto.subtle.importKey( "spki", publicKeyBuffer.buffer, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"], ); // Encriptar clave AES con RSA const encryptedAESKey = await crypto.subtle.encrypt( { name: "RSA-OAEP" }, importedPublicKey, aesKeyRaw, ); console.log("✅ Key compatibility test passed!"); alert( "✅ Claves generadas correctamente y son compatibles con el sistema híbrido", ); } catch (error) { console.error("❌ Key compatibility test failed:", error); alert("❌ Error en las claves: " + error.message); } }; // Copiar al portapapeles const copyToClipboard = (text, type) => { navigator.clipboard .writeText(text) .then(() => { alert(`${type} copiada al portapapeles`); }) .catch((err) => { console.error("Error copying to clipboard:", err); }); }; return ( <Box padding={6}> <Typography variant="alpha" style={{ marginBottom: "10px" }} lineHeight="lg" > 🔐 Generador de Claves </Typography> <Typography variant="beta" style={{ marginBottom: "20px", color: "#d32f2f" }} lineHeight="lg" > ⚠️ IMPORTANTE: Una vez generadas, guarda ambos tokens. No se pueden recuperar. </Typography> <Stack horizontal spacing={4} style={{ marginBottom: "20px" }}> {/* Botón para generar las claves */} <Button variant="primary" onClick={handleGenerateKeys}> 🔑 Generar Claves RSA </Button> {/* Botón de prueba */} {publicKey && privateKey && ( <Button variant="secondary" onClick={testKeys}> 🧪 Probar Compatibilidad </Button> )} </Stack> {/* Mostrar las claves generadas */} {publicKey && ( <Box marginTop={6}> <Box marginBottom={4}> <Stack horizontal spacing={2} style={{ alignItems: "center", marginBottom: "10px" }} > <Typography variant="delta" fontWeight="bold"> 🌐 Cliente Token (Clave Pública): </Typography> <Button variant="tertiary" size="S" onClick={() => copyToClipboard(publicKey, "Clave pública")} > 📋 Copiar </Button> </Stack> <Textarea value={publicKey} readOnly rows={3} style={{ marginBottom: "10px", fontFamily: "monospace", fontSize: "12px", }} /> <Typography variant="omega" style={{ color: "#666" }}> 💡 Usa esta clave en tu aplicación React/frontend para encriptar emails </Typography> </Box> <Box marginBottom={4}> <Stack horizontal spacing={2} style={{ alignItems: "center", marginBottom: "10px" }} > <Typography variant="delta" fontWeight="bold"> 🔒 Server Token (Clave Privada): </Typography> <Button variant="tertiary" size="S" onClick={() => copyToClipboard(privateKey, "Clave privada")} > 📋 Copiar </Button> </Stack> <Textarea value={privateKey} readOnly rows={4} style={{ marginBottom: "10px", fontFamily: "monospace", fontSize: "12px", }} /> <Typography variant="omega" style={{ color: "#666" }}> 💡 Configura esta clave en las variables de entorno de Strapi (FREE_MAIL_SENDER_TOKEN) </Typography> </Box> <Box background="primary100" padding={3} style={{ borderRadius: "4px", marginTop: "20px" }} > <Typography variant="omega" fontWeight="semiBold" style={{ color: "#0066cc" }} > ✅ Sistema Híbrido RSA + AES: </Typography> <ul style={{ marginTop: "10px", paddingLeft: "20px" }}> <li>RSA encripta claves AES de 256 bits</li> <li>AES-GCM encripta los datos del email</li> <li>Sin límite de tamaño para emails</li> <li>Compatible con Web Crypto API</li> </ul> </Box> </Box> )} {/* Botón para ver el tutorial */} <Button variant="secondary" onClick={handleShowModal} style={{ marginTop: "20px" }} > 📚 Ver Tutorial </Button> {/* Modal para el tutorial */} {isModalVisible && ( <ModalLayout onClose={handleCloseModal} labelledBy="tutorial-modal-title" > <ModalHeader> <Typography fontWeight="bold" textColor="neutral800" as="h2" id="tutorial-modal-title" > 🔐 Tutorial: Sistema de Email </Typography> </ModalHeader> <ModalBody> <Stack spacing={4}> <Box style={{ padding: "16px", backgroundColor: "#32324d", color: "#fff", borderRadius: "4px", }} > <Typography variant="sigma" fontWeight="bold" color="white"> 📋 Guía de Implementación Paso a Paso </Typography> </Box> {/* Paso 1 */} <Box> <Typography variant="beta" fontWeight="bold" marginBottom={2}> 1. 🔧 Configurar Variables de Entorno: </Typography> <Typography marginBottom={2}> Configura las claves en tus aplicaciones: </Typography> <Typography variant="delta" fontWeight="bold" marginBottom={1}> Frontend (.env): </Typography> <Box background="neutral100" padding={3} marginBottom={2} style={{ borderRadius: "4px" }} > <Typography variant="code"> {`REACT_APP_PUBLIC_KEY=${publicKey ? publicKey.substring(0, 50) + "..." : "TU_CLAVE_PUBLICA_AQUI"}`} </Typography> </Box> <Typography variant="delta" fontWeight="bold" marginBottom={1}> Backend (Strapi .env): </Typography> <Box background="neutral100" padding={3} marginBottom={2} style={{ borderRadius: "4px" }} > <Typography variant="code"> {`FREE_MAIL_SENDER_TOKEN=${privateKey ? privateKey.substring(0, 50) + "..." : "TU_CLAVE_PRIVADA_AQUI"}`} </Typography> </Box> </Box> {/* Paso 2 - Funciones auxiliares completas */} <Box> <Typography variant="beta" fontWeight="bold" marginBottom={2}> 2. 🔐 Código Frontend (React): </Typography> <Box background="neutral100" padding={3} marginTop={2} style={{ borderRadius: "4px" }} > <Typography variant="code"> <pre style={{ fontSize: "11px", overflow: "auto" }}> {`// Funciones auxiliares de conversión const base64ToArrayBuffer = (base64) => { const binaryString = window.atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; }; const arrayBufferToBase64 = (buffer) => { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); }; // Importar clave pública RSA const importPublicKey = async (publicKeyBase64) => { const keyData = base64ToArrayBuffer(publicKeyBase64); return await window.crypto.subtle.importKey( 'spki', keyData, { name: 'RSA-OAEP', hash: 'SHA-256', }, false, ['encrypt'] ); }; // Encriptar datos con AES-GCM const encryptWithAES = async (data, aesKey) => { const encoder = new TextEncoder(); const dataBuffer = encoder.encode(data); // Generar IV aleatorio (12 bytes para GCM) const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encryptedBuffer = await window.crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv, }, aesKey, dataBuffer ); return { encryptedData: arrayBufferToBase64(encryptedBuffer), iv: arrayBufferToBase64(iv.buffer) }; }; // Encriptar clave AES con RSA const encryptAESKeyWithRSA = async (aesKeyRaw, publicKeyBase64) => { const publicKey = await importPublicKey(publicKeyBase64); const encryptedKeyBuffer = await window.crypto.subtle.encrypt( { name: 'RSA-OAEP' }, publicKey, aesKeyRaw ); return arrayBufferToBase64(encryptedKeyBuffer); }; // Encriptación Híbrida RSA+AES const encryptData = async (data, publicKeyBase64) => { // Generar clave AES const aesKey = await window.crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); // Encriptar datos con AES const { encryptedData, iv } = await encryptWithAES(data, aesKey); // Encriptar clave AES con RSA const aesKeyRaw = await window.crypto.subtle.exportKey('raw', aesKey); const encryptedAESKey = await encryptAESKeyWithRSA(aesKeyRaw, publicKeyBase64); // Empaquetar todo return window.btoa(JSON.stringify({ encryptedKey: encryptedAESKey, encryptedData: encryptedData, iv: iv })); };`} </pre> </Typography> </Box> </Box> {/* Paso 3 */} <Box> <Typography variant="beta" fontWeight="bold" marginBottom={2}> 3. 📧 Enviar Email: </Typography> <Box background="neutral100" padding={3} marginTop={2} style={{ borderRadius: "4px" }} > <Typography variant="code"> <pre style={{ fontSize: "11px" }}> {`const sendEmail = async () => { const mail = JSON.stringify({ toEmail: ["usuario@ejemplo.com"], subject: "Mi asunto", mailText: "Contenido del email..." }); const encryptedMail = await encryptData(mail, PUBLIC_KEY); const response = await fetch("/api/free-mail-sender/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mail: encryptedMail }) }); };`} </pre> </Typography> </Box> </Box> {/* Nota Final */} <Box background="danger100" padding={3} style={{ borderRadius: "4px" }} > <Typography variant="omega" fontWeight="semiBold"> ⚠️ Seguridad Importante: </Typography> <ul style={{ marginTop: "10px", paddingLeft: "20px" }}> <li>Nunca compartas tu clave privada</li> <li>Usa HTTPS en producción</li> <li>Rota las claves periódicamente</li> <li>Valida todos los inputs</li> </ul> </Box> </Stack> </ModalBody> <ModalFooter startActions={ <Button onClick={handleCloseModal} variant="tertiary"> Cerrar </Button> } /> </ModalLayout> )} </Box> ); }; export default HomePage;