From 1618cbfd4095e8d7812d91bb9f7a4adf49a934ed Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 18 Aug 2026 16:16:22 +0200 Subject: [PATCH] Add user stats endpoint with JWT authentication and data size calculation --- src/controllers/userController.js | 74 +++++++++++++++++++++++++++++++ src/routes/userRoutes.js | 8 ++++ 2 files changed, 82 insertions(+) create mode 100644 src/controllers/userController.js create mode 100644 src/routes/userRoutes.js diff --git a/src/controllers/userController.js b/src/controllers/userController.js new file mode 100644 index 0000000..61f0e3d --- /dev/null +++ b/src/controllers/userController.js @@ -0,0 +1,74 @@ +const jwt = require('jsonwebtoken'); +const Note = require('../models/Note'); +const Category = require('../models/Category'); + +const JWT_SECRET = process.env.JWT_SECRET; + +const getStats = async (req, res) => { + try { + // 1. Verificamos el token (igual que en syncController) + const authorizationHeader = req.headers.authorization || ''; + if (!authorizationHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Authorization header missing' }); + } + + const token = authorizationHeader.slice(7).trim(); + const payload = jwt.verify(token, JWT_SECRET); + const userId = payload && payload.id; + if (!userId) return res.status(401).json({ error: 'Usuario inválido' }); + + // 2. Traemos solo los campos pesados para no saturar la RAM del servidor + const notes = await Note.findAll({ + where: { userId }, + attributes: ['title', 'body', 'permanentDelete'] + }); + + const categories = await Category.findAll({ + where: { userId }, + attributes: ['encrypted_name'] + }); + + // 3. Calculamos el tamaño exacto en Bytes usando Buffer + let totalBytes = 0; + + notes.forEach(note => { + // Si está borrada permanentemente no ocupa texto real + if (note.permanentDelete) return; + + if (note.title) totalBytes += Buffer.byteLength(note.title, 'utf8'); + if (note.body) totalBytes += Buffer.byteLength(note.body, 'utf8'); + // Sumamos ~150 bytes por nota para representar el peso de los UUIDs (id), fechas y booleanos + totalBytes += 150; + }); + + categories.forEach(cat => { + if (cat.encrypted_name) totalBytes += Buffer.byteLength(cat.encrypted_name, 'utf8'); + totalBytes += 100; // Margen para UUIDs, colores e iconos + }); + + // 4. Formateamos el resultado + let formattedSize = '0 KB'; + if (totalBytes > 0) { + if (totalBytes < 1024 * 1024) { + formattedSize = `${(totalBytes / 1024).toFixed(2)} KB`; + } else { + formattedSize = `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`; + } + } + + res.json({ + totalNotes: notes.length, + totalCategories: categories.length, + sizeBytes: totalBytes, + formattedSize: formattedSize + }); + + } catch (error) { + if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Token inválido o expirado' }); + } + res.status(500).json({ error: error.message }); + } +}; + +module.exports = { getStats }; \ No newline at end of file diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js new file mode 100644 index 0000000..d09f45c --- /dev/null +++ b/src/routes/userRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const userController = require('../controllers/userController'); + +// Ruta GET protegida por token +router.get('/user/stats', userController.getStats); + +module.exports = router; \ No newline at end of file