Version sin Vite.

This commit is contained in:
2026-02-18 16:50:48 +01:00
commit cb312f680a
21 changed files with 2583 additions and 0 deletions

0
backend/.dockerignore Normal file
View File

19
backend/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:18-alpine
# Directorio de trabajo
WORKDIR /app
# Copiamos dependencias
COPY package*.json ./
# Instalamos
RUN npm install
# Copiamos el resto del código
COPY . .
# Exponemos el puerto 9002 (que configuramos en index.js)
EXPOSE 9002
# Arrancamos
CMD ["npm", "run", "dev"]

View File

@@ -0,0 +1,13 @@
module.exports = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) return res.status(403).json({ error: 'No se permite el acceso sin token' });
const token = authHeader.split(' ')[1];
// Aquí la lógica de validación de tu token
if (token) {
next(); // Si el token es válido, dejamos pasar
} else {
res.status(401).json({ error: 'Token inválido o expirado' });
}
};

1532
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
backend/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "blog-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"jsonwebtoken": "^9.0.2",
"pg": "^8.11.3"
},
"devDependencies": {
"nodemon": "^3.0.3"
}
}

19
backend/src/config/db.js Normal file
View File

@@ -0,0 +1,19 @@
const { Pool } = require('pg');
/**
* Configuración de la conexión a PostgreSQL
* Host 'db' para conectar dentro de la red de Docker
*/
const pool = new Pool({
user: 'josemi',
host: 'db',
database: 'blog_db',
password: 'josemivi',
port: 5432,
});
pool.on('connect', () => {
console.log('✅ Conexión establecida con PostgreSQL');
});
module.exports = pool;

View File

@@ -0,0 +1,32 @@
const pool = require('../config/db');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const login = async (req, res) => {
const { username, password } = req.body;
try {
const result = await pool.query('SELECT * FROM users WHERE username = $1', [username]);
if (result.rows.length === 0) return res.status(400).json({ error: 'Usuario no encontrado' });
const user = result.rows[0];
// NOTA: Si el hash del SQL falla, puedes usar /register para crear uno nuevo
// Aquí comparamos contraseña plana vs hash
const validPass = await bcrypt.compare(password, user.password);
if (!validPass) return res.status(400).json({ error: 'Contraseña incorrecta' });
const token = jwt.sign({ id: user.id, username: user.username }, process.env.JWT_SECRET || 'secreto', { expiresIn: '24h' });
res.json({ token, username: user.username });
} catch (err) { res.status(500).json({ error: err.message }); }
};
const register = async (req, res) => {
const { username, password } = req.body;
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
try {
await pool.query('INSERT INTO users (username, password) VALUES ($1, $2)', [username, hash]);
res.json({ message: 'Usuario creado exitosamente' });
} catch (err) { res.status(500).json({ error: err.message }); }
};
module.exports = { login, register };

View File

@@ -0,0 +1,71 @@
const pool = require('../config/db');
// Obtener todos los posts
exports.getPosts = async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: 'Error obteniendo posts' });
}
};
// Login (Asegúrate de que esta función esté aquí, que es la que suele fallar)
exports.login = async (req, res) => {
const { username, password } = req.body;
// Por ahora validación simple, luego puedes meter ciberseguridad real
if (username === 'josemi' && password === 'josemivi') {
res.json({ token: 'token-seguro-josemi-' + Date.now() });
} else {
res.status(401).json({ error: 'Credenciales incorrectas' });
}
};
// Contador de visitas
exports.addView = async (req, res) => {
try {
await pool.query('UPDATE posts SET views = views + 1 WHERE id = $1', [req.params.id]);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Error view' });
}
};
// Crear post
exports.createPost = async (req, res) => {
const { title, tags, content } = req.body;
const slug = title.toLowerCase().replace(/ /g, '-');
try {
const result = await pool.query(
'INSERT INTO posts (title, slug, tags, content) VALUES ($1, $2, $3, $4) RETURNING *',
[title, slug, tags, content]
);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: 'Error creando post' });
}
};
// Eliminar post
exports.deletePost = async (req, res) => {
try {
await pool.query('DELETE FROM posts WHERE id = $1', [req.params.id]);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Error eliminando' });
}
};
// Estadísticas
exports.getStats = async (req, res) => {
try {
const total = await pool.query('SELECT SUM(views) as total FROM posts');
const top = await pool.query('SELECT title, views FROM posts ORDER BY views DESC LIMIT 5');
res.json({
total: total.rows[0].total || 0,
top: top.rows
});
} catch (err) {
res.status(500).json({ error: 'Error stats' });
}
};

48
backend/src/index.js Normal file
View File

@@ -0,0 +1,48 @@
const express = require('express');
const cors = require('cors');
const postRoutes = require('./routes/postRoutes');
const pool = require('./config/db');
const app = express();
app.use(cors());
app.use(express.json());
// --- INICIALIZACIÓN DE BASE DE DATOS ---
(async () => {
const client = await pool.connect();
try {
// 1. Crear tablas si no existen
await client.query(`
CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, username TEXT UNIQUE, password TEXT);
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT,
tags TEXT,
url TEXT,
image_url TEXT,
description TEXT,
type TEXT CHECK(type IN ('POST', 'LINK')) DEFAULT 'POST',
views INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// 2. Migración: Asegurar que la columna 'views' existe (por si la tabla ya estaba creada)
await client.query(`ALTER TABLE posts ADD COLUMN IF NOT EXISTS views INTEGER DEFAULT 0;`);
console.log('✅ Base de datos PostgreSQL sincronizada');
} catch (err) {
console.error('❌ Error DB:', err);
} finally {
client.release();
}
})();
app.use('/posts', postRoutes);
const PORT = 9002;
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 Servidor backend corriendo en puerto ${PORT}`);
});

View File

@@ -0,0 +1,16 @@
const jwt = require('jsonwebtoken');
const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) return res.status(403).json({ error: 'Token requerido' });
try {
const cleanToken = token.replace('Bearer ', '');
const decoded = jwt.verify(cleanToken, process.env.JWT_SECRET || 'secreto');
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Token inválido' });
}
};
module.exports = verifyToken;

View File

@@ -0,0 +1,22 @@
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');
// Middleware de verificación simple
const verifyToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
if (!authHeader) return res.status(403).json({ error: 'Acceso denegado' });
next();
};
// RUTAS PÚBLICAS
router.get('/', postController.getPosts);
router.post('/login', postController.login); // <--- Aquí estaba el error si no existía en el controller
router.post('/view/:id', postController.addView);
// RUTAS PROTEGIDAS
router.get('/stats', verifyToken, postController.getStats);
router.post('/', verifyToken, postController.createPost);
router.delete('/:id', verifyToken, postController.deletePost);
module.exports = router;