API Migrada

This commit is contained in:
2025-10-25 04:34:00 -06:00
commit 71d4a56a88
16 changed files with 2309 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.env
dist/
npm-debug.log*
pnpm-debug.log*
.env.production
node_modules/
uploads/

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# UNOCIAL API DOCUMENTATION
**Unocial** is a work in progress social media project made by students from the Autonomous University of Chiapas. This is the official API for that project.
This API was made with Express.js and using PostgreSQL as for the database. This project is currently has demonstrative purposes.

46
app.js Normal file
View File

@@ -0,0 +1,46 @@
import express from 'express';
import cors from 'cors';
import indexRoutes from './routes/index.js';
import apiRoutes from './routes/api.js';
import userRoutes from './routes/user.js';
import postRoutes from './routes/posts.js';
import commentRoutes from './routes/comments.js';
const app = express();
const port = 3000;
app.use(express.json());
app.use(cors());
let corsOptions = {
origin: '*', // Reemplaza con el origen permitido
optionsSuccessStatus: 200 // Algunos navegadores (como IE11) requieren este estado
};
//app.get('/api/post/:postId', (req, res) => {
//
//})
/*
En este punto añadimos el sistema de usuarios
ENDPOINT HTTP REQ ACCIÓN RESTRICCIÓN
/api/signup - POST (Registrar nuevo usuario) Todo el mundo
/api/login - POST (Iniciar sesión) Todo el mundo
/api/user - GET (Ver perfil propio de usuario) Usuario
/api/user/:userId - PATCH (Modificar perfil) Usuario
/api/user/all - GET (Ver todos los usuarios) Todos los usuarios
/api/user/:userID - DELETE (Eliminar usuario) Administradores o Usuario.
*/
app.use('/', indexRoutes); // Rutas base
app.use('/api', apiRoutes); // Rutas de API
app.use('/api/user', userRoutes); // Rutas de usuarios
app.use('/api/post', postRoutes); // Rutas de posts
app.use('/api/comments', commentRoutes); // Rutas de comentarios
app.use('/uploads', express.static('uploads'));
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
console.log(`URL at: http://localhost:${port}`)
})

5
dotenv_temp.txt Normal file
View File

@@ -0,0 +1,5 @@
DB_USER=""
DB_PASS=""
DB_HOST=""
DB_PORT=""
DB_NAME=""

View File

@@ -0,0 +1,117 @@
import {pool} from '../lib/database.js';
export const createNewComment = async (req, res) => {
try {
const { contenido, id_usuario, id_post } = req.body;
const insertQuery = await pool.query(
'INSERT INTO comentarios (contenido, id_usuario, id_post, fecha_comentario) VALUES ($1, $2, $3, NOW()) RETURNING *',
[contenido, id_usuario, id_post]
);
// Fetch the inserted comment joined with the user info to return a consistent shape
const insertedId = insertQuery.rows[0].id_comentario;
const result = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, comentarios.*
FROM comentarios
JOIN usuarios ON comentarios.id_usuario = usuarios.id_usuario
WHERE comentarios.id_comentario = $1`,
[insertedId]
);
res.json(result.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const getCommentsByPostId = async (req, res) => {
try {
const { id_post } = req.params;
const query = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, comentarios.*
FROM comentarios
JOIN usuarios ON comentarios.id_usuario = usuarios.id_usuario
WHERE id_post = $1
ORDER BY comentarios.fecha_comentario DESC`,
[id_post]
);
res.json(query.rows);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const deleteComment = async (req, res) => {
try {
const { id_comentario } = req.params;
const query = await pool.query(
'DELETE FROM comentarios WHERE id_comentario = $1 RETURNING *',
[id_comentario]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const getAllComments = async (req, res) => {
try {
const query = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, comentarios.*
FROM comentarios
JOIN usuarios ON comentarios.id_usuario = usuarios.id_usuario
ORDER BY fecha_comentario DESC`
);
res.json(query.rows);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const getCommentById = async (req, res) => {
try {
const { id_comentario } = req.params;
const query = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, comentarios.*
FROM comentarios
JOIN usuarios ON comentarios.id_usuario = usuarios.id_usuario
WHERE id_comentario = $1`,
[id_comentario]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const editComment = async (req, res) => {
try {
const { id_comentario } = req.params;
const { contenido } = req.body;
const query = await pool.query(
'UPDATE comentarios SET contenido = $1 WHERE id_comentario = $2 RETURNING *',
[contenido, id_comentario]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}

149
functions/postFunctions.js Normal file
View File

@@ -0,0 +1,149 @@
import {pool} from '../lib/database.js';
export const createNewPost = async (req, res) => {
try {
const { contenido, id_usuario } = req.body;
if (!contenido || !id_usuario) {
return res.status(400).json({
error: 'Contenido y ID de usuario son requeridos'
});
}
// Get the file path if an image was uploaded
let imagePath = null;
if (req.file) {
imagePath = `/uploads/${req.processedFilename}`;
}
// Create the post in the database
const query = await pool.query(
`INSERT INTO posts (contenido, imagen, id_usuario, fecha_publicacion)
VALUES ($1, $2, $3, NOW())
RETURNING *`,
[contenido, imagePath, id_usuario]
);
// Get the created post with user information
const postQuery = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, posts.*
FROM posts
JOIN usuarios ON posts.id_usuario = usuarios.id_usuario
WHERE posts.id_post = $1`,
[query.rows[0].id_post]
);
res.status(201).json(postQuery.rows[0]);
} catch (err) {
console.error('Error creating post:', err);
res.status(500).json({
error: 'Error al crear la publicación: ' + err.message
});
}
}
export const getPostById = async (req, res) => {
try {
const { id_post } = req.params;
const query = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, posts.*
FROM posts
JOIN usuarios ON posts.id_usuario = usuarios.id_usuario
WHERE id_post = $1`,
[id_post]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const deletePost = async (req, res) => {
try {
const { id_post } = req.params;
const query = await pool.query(
'DELETE FROM posts WHERE id_post = $1 RETURNING *',
[id_post]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const getAllPosts = async (req, res) => {
try {
const query = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, posts.*
FROM posts
JOIN usuarios ON posts.id_usuario = usuarios.id_usuario
ORDER BY fecha_publicacion DESC`
);
res.json(query.rows);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const getPostsByUserId = async (req, res) => {
try {
const result = await pool.query(
`SELECT usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma, posts.*
FROM posts
JOIN usuarios ON posts.id_usuario = usuarios.id_usuario
WHERE usuarios.id_usuario = $1
ORDER BY fecha_publicacion DESC`,
[req.params.userId]
);
res.json(result.rows);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const editPost = async (req, res) => {
try {
const { id_post } = req.params;
const { contenido, imagen } = req.body;
const query = await pool.query(
'UPDATE posts SET contenido = $1, imagen = $2 WHERE id_post = $3 RETURNING *',
[contenido, imagen, id_post]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
/*export const getFeedPosts = async (req, res) => {
try {
const query = await pool.query(
`SELECT posts.*, usuarios.nombre, usuarios.apellido_pa, usuarios.apellido_ma
FROM posts
JOIN usuarios ON posts.id_usuario = usuarios.id_usuario
ORDER BY posts.fecha_publicacion DESC`
);
res.json(query.rows);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}*/

120
functions/userFunctions.js Normal file
View File

@@ -0,0 +1,120 @@
import pg from 'pg'
import bcrypt from "bcrypt";
import { pool } from '../lib/database.js';
export const getAllUsers = async (req, res) => {
try {
const result = await pool.query("SELECT nombre, apellido_pa, apellido_ma, fecha_registro, nombre_carrera, nombre_facultad FROM usuarios JOIN carreras ON usuarios.id_carrera = carreras.id_carrera JOIN facultades ON carreras.id_facultad = facultades.id_facultad;");
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
export const getSpecificUser = async (req, res) => {
try {
const result = await pool.query("SELECT nombre, apellido_pa, apellido_ma, fecha_registro, nombre_carrera, nombre_facultad FROM usuarios JOIN carreras ON usuarios.id_carrera = carreras.id_carrera JOIN facultades ON carreras.id_facultad = facultades.id_facultad WHERE id_usuario = $1;",
[req.params.userId]
);
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
export const validateForUser = async (req, res) => {
try {
const { correo_unach, clave } = req.body;
const query = await pool.query(`
SELECT usuarios.*, carreras.nombre_carrera, facultades.nombre_facultad
FROM usuarios
JOIN carreras ON usuarios.id_carrera = carreras.id_carrera
JOIN facultades ON carreras.id_facultad = facultades.id_facultad
WHERE correo_unach = $1
`, [correo_unach]);
const user = query.rows[0];
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const match = await bcrypt.compare(clave, user.clave_hasheada);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Clean sensitive data and return user info
const userResponse = {
id_usuario: user.id_usuario,
nombre: user.nombre,
apellido_pa: user.apellido_pa,
apellido_ma: user.apellido_ma,
correo_unach: user.correo_unach,
carrera: user.nombre_carrera,
facultad: user.nombre_facultad
};
res.json(userResponse);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
}
export const editUser = async (req, res) => {
try {
const { userId } = req.params;
const { nombre, apellido_pa, apellido_ma, id_carrera } = req.body;
const query = await pool.query(
'UPDATE usuarios SET nombre = $1, apellido_pa = $2, apellido_ma = $3, id_carrera = $5 WHERE id_usuario = $4 RETURNING *',
[nombre, apellido_pa, apellido_ma, userId, id_carrera]
);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const deleteUser = async (req, res) => {
try {
const { userId } = req.params;
const query = await pool.query(
'DELETE FROM usuarios WHERE id_usuario = $1 RETURNING *',
[userId]
);
res.json({ message: `User ${userId} deleted`, user: query.rows[0] });
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
export const createNewUser = async (req,res) => {
try {
const { correo_unach, clave, matricula, nombre, apellido_pa, apellido_ma, id_carrera } = req.body;
const query = await pool.query(
'INSERT INTO public.Usuarios (correo_unach, clave_hasheada, matricula, nombre, apellido_ma, apellido_pa, id_carrera) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *',
[correo_unach, hashPassword(clave), matricula, nombre, apellido_ma, apellido_pa, id_carrera]
);
console.log(`Creado el usuario: ${JSON.stringify(query.rows[0])}`);
res.json(query.rows[0]);
}
catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
throw err;
}
}
function hashPassword(password) {
const saltRounds = 15;
const salt = bcrypt.genSaltSync(saltRounds);
return bcrypt.hashSync(password, salt);
}

12
lib/database.js Normal file
View File

@@ -0,0 +1,12 @@
import dotenv from 'dotenv';
import { Pool } from 'pg';
dotenv.config();
export const pool = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
});

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "sm",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.15.0",
"dependencies": {
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"file-type": "^21.0.0",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"pg": "^8.16.3",
"sharp": "^0.34.4",
"socket.io": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

1651
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- bcrypt

19
routes/api.js Normal file
View File

@@ -0,0 +1,19 @@
import { Router } from 'express';
import { createNewUser, validateForUser } from '../functions/userFunctions.js';
const router = Router();
router.get('/', async (req, res) => {
res.send({ Status: `Running` });
console.log(`${req.ip} - ${req.baseUrl}${req.url}: ${res.statusCode}, ${res.statusMessage}`);
});
router.post('/signup', createNewUser);
router.post('/login', validateForUser);
router.post('/logout', (req,res) => {
res.send({ message: "logout endpoint" });
console.log(`${req.ip} - ${req.baseUrl}${req.url}: ${res.statusCode}, ${res.statusMessage}`);
});
export default router;

22
routes/comments.js Normal file
View File

@@ -0,0 +1,22 @@
import { Router } from 'express';
import { createNewComment, getCommentsByPostId, deleteComment, getAllComments, getCommentById, editComment } from '../functions/commentFunctions.js';
const router = Router();
// Validate create comment payload
const validateCommentPayload = (req, res, next) => {
// ...simple validation...
const { contenido, id_usuario, id_post } = req.body;
if (!contenido || !id_usuario || !id_post) {
return res.status(400).json({ error: 'contenido, id_usuario and id_post are required' });
}
next();
};
router.post('/', validateCommentPayload, createNewComment);
router.get('/', getAllComments);
router.get('/post/:id_post', getCommentsByPostId);
router.get('/:id_comentario', getCommentById);
router.put('/:id_comentario', editComment);
router.delete('/:id_comentario', deleteComment);
export default router;

10
routes/index.js Normal file
View File

@@ -0,0 +1,10 @@
import { Router } from 'express';
const router = Router();
router.get('/', (req, res) => {
res.send({ RESPONSE: "This is the API's base route. This is a testing thing." });
console.log(`HTTP RESPONSE: ${res.statusCode}, ${res.statusMessage}`);
});
export default router;

98
routes/posts.js Normal file
View File

@@ -0,0 +1,98 @@
import { Router } from 'express';
import { createNewPost, getPostById, deletePost, getAllPosts, getPostsByUserId, editPost } from '../functions/postFunctions.js';
const router = Router();
import path from 'path';
import multer from 'multer';
import fs from 'fs/promises';
import sharp from 'sharp';
import crypto from 'crypto';
import { fileTypeFromBuffer } from 'file-type';
/*const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});*/
const storage = multer.memoryStorage();
const fileFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Solo se permiten imágenes'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB limit
}
});
async function processImage(file) {
const imagePath = path.join(process.cwd(), 'uploads');
const buffer = file.buffer;
const hash = crypto.createHash('sha256').update(buffer).digest('hex');
const mime = await fileTypeFromBuffer(buffer).then(type => type?.mime);
// Validate MIME type
if (!mime || !['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(mime)) {
throw new Error('Unsupported image format');
}
// File save name and path if not GIF
let filename;
if (mime === 'image/gif') {
filename = `${hash}.gif`;
await fs.writeFile(path.join(imagePath, filename), buffer);
} else {
filename = `${hash}.webp`;
await sharp(buffer)
.resize({ width: 800, height: 800, fit: 'inside', withoutEnlargement: true })
.rotate()
.normalise()
.withMetadata()
.toFormat('webp', { quality: 80 })
.toFile(path.join(imagePath, filename));
}
return filename;
}
const processUploadedImage = async (req, res, next) => {
try {
if (req.file) {
const filename = await processImage(req.file);
req.processedFilename = filename;
}
next();
} catch (error) {
console.error('Error processing image:', error);
res.status(400).json({ error: 'Error al procesar la imagen: ' + error.message });
}
};
// Validate post creation payload
const validatePostPayload = (req, res, next) => {
const { contenido, id_usuario } = req.body;
if (!contenido || !id_usuario) {
return res.status(400).json({ error: 'contenido and id_usuario are required' });
}
next();
};
router.post('/', upload.single('imagen'), validatePostPayload, processUploadedImage, createNewPost);
router.get('/:id_post', getPostById);
router.delete('/:id_post', deletePost);
router.get('/', getAllPosts);
router.get('/user/:userId', getPostsByUserId);
router.put('/:id_post', editPost);
export default router;

15
routes/user.js Normal file
View File

@@ -0,0 +1,15 @@
import { Router } from 'express';
import { editUser, getAllUsers, getSpecificUser, deleteUser } from '../functions/userFunctions.js';
const router = Router();
router.get('/:userId', getSpecificUser);
router.patch('/:userId', editUser);
router.get('/', getAllUsers);
router.delete('/:userId', deleteUser);
export default router;