- Move repo to project root to include both public/ and api/ - Add .gitignore excluding node_modules and .env - Include API routes (erase, gmail_oauth), services (mailer), and config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
4.7 KiB
JavaScript
131 lines
4.7 KiB
JavaScript
'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');
|
|
}
|
|
};
|