'use strict'; /** * RESETEA.NET — OAuth Gmail * * Flujo: * GET /api/gmail/auth → redirige a Google para autorización * GET /api/gmail/callback → intercambia code por token de un solo uso * POST /api/gmail/send → envía la carta GDPR desde el Gmail del usuario * (el token se usa y se descarta — nunca se persiste) * * PREREQUISITO: * Crea un proyecto en Google Cloud Console: * https://console.cloud.google.com/ * → Habilita "Gmail API" * → Crea credenciales OAuth2 (tipo "Aplicación web") * → URI de redirección: https://resetea.net/api/gmail/callback * → Copia GOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET en .env */ const { google } = require('googleapis'); const { buildLetterText, PROVIDER_DATA } = require('../services/mailer'); // ── Estado temporal en memoria (un objeto por token de sesión) ── // NO se persiste en disco. Si el servidor reinicia, se pierden los // tokens pendientes (el usuario debe repetir el flujo OAuth). const pendingSends = new Map(); // sessionId → { token, letterParams } function getOAuth2Client() { return new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URI || 'https://resetea.net/api/gmail/callback' ); } // ── GET /api/gmail/auth ────────────────────────────────────────── // El frontend envía los parámetros de la carta como query params // para que los podamos recuperar en el callback. exports.authInit = (req, res) => { const { provider, name, email, nickname, phone, address, extra, requestType } = req.query; if (!provider || !email || !name) { return res.status(400).json({ error: 'Faltan parámetros obligatorios (provider, name, email).' }); } if (!PROVIDER_DATA[provider]) { return res.status(400).json({ error: 'Proveedor no soportado.' }); } // Guardamos el state en base64 (no sensible: no contiene credenciales) const state = Buffer.from(JSON.stringify({ provider, name, email, nickname, phone, address, extra, requestType })).toString('base64url'); const oauth2Client = getOAuth2Client(); const authUrl = oauth2Client.generateAuthUrl({ access_type: 'online', // no refresh token — uso puntual scope: ['https://www.googleapis.com/auth/gmail.send'], prompt: 'consent', state, }); res.redirect(authUrl); }; // ── GET /api/gmail/callback ────────────────────────────────────── exports.authCallback = async (req, res) => { const { code, state, error } = req.query; if (error) { return res.redirect('/plantillas.html?oauth=cancelled'); } if (!code || !state) { return res.status(400).send('Parámetros OAuth inválidos.'); } let params; try { params = JSON.parse(Buffer.from(state, 'base64url').toString()); } catch { return res.status(400).send('State OAuth inválido.'); } try { const oauth2Client = getOAuth2Client(); const { tokens } = await oauth2Client.getToken(code); // Enviamos el email inmediatamente (no almacenamos el token) oauth2Client.setCredentials(tokens); const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); const providerInfo = PROVIDER_DATA[params.provider]; if (!providerInfo.email) { return res.redirect(`/plantillas.html?oauth=no_email&provider=${params.provider}&formUrl=${encodeURIComponent(providerInfo.formUrl || '')}`); } const letterText = buildLetterText({ providerInfo, senderName: params.name, senderEmail: params.email, senderNick: params.nickname || '', senderPhone: params.phone || '', senderAddress: params.address || '', extra: params.extra || '', }); // Construir email RFC 2822 en base64url const subject = `Ejercicio derecho de supresión (RGPD Art. 17) — ${providerInfo.name}`; const rawEmail = [ `From: ${params.name} <${params.email}>`, `To: ${providerInfo.email}`, `Subject: ${subject}`, `MIME-Version: 1.0`, `Content-Type: text/plain; charset=UTF-8`, '', letterText, ].join('\r\n'); const encoded = Buffer.from(rawEmail).toString('base64url'); await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encoded }, }); // Token descartado aquí (fuera de scope, GC lo recogerá) res.redirect(`/plantillas.html?oauth=ok&provider=${encodeURIComponent(providerInfo.name)}`); } catch (err) { console.error('Gmail OAuth error:', err.message); res.redirect('/plantillas.html?oauth=error'); } };