feat: menu Notas -- apuntes rapidos personales

Nuevo menu "Notas" en el sidebar: crear, editar y eliminar apuntes rapidos, con fecha y hora de
creacion (y de ultima edicion si aplica). Siempre privadas de cada usuario -- una nota de otro
usuario responde 404, no 403, para no revelar que existe. Sin restriccion de rol.

Nuevo notes (tabla) + GET/POST/PUT/DELETE /api/notes. Probado de punta a punta en dev: crear,
editar, borrar y el formato de fecha/hora funcionan correctamente.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-17 15:54:05 -05:00
parent 9089f2ff7a
commit ceda779b75
17 changed files with 268 additions and 4 deletions
+19
View File
@@ -0,0 +1,19 @@
import { ok, asyncHandler } from '../../utils/http.js';
import * as service from './service.js';
export const getNotes = asyncHandler(async (req, res) => {
ok(res, await service.listNotes(req.currentUser));
});
export const postNote = asyncHandler(async (req, res) => {
ok(res, await service.createNote(req.body.content, req.currentUser), 201);
});
export const putNote = asyncHandler(async (req, res) => {
ok(res, await service.updateNote(Number(req.params.id), req.body.content, req.currentUser));
});
export const deleteNote = asyncHandler(async (req, res) => {
await service.removeNote(Number(req.params.id), req.currentUser);
ok(res, { deleted: true });
});
+28
View File
@@ -0,0 +1,28 @@
import { pool } from '../../config/db.js';
export async function findByUser(userId) {
const [rows] = await pool.query(
'SELECT id, user_id, content, created_at, updated_at FROM notes WHERE user_id = ? ORDER BY updated_at DESC',
[userId]
);
return rows;
}
export async function findById(id) {
const [rows] = await pool.query('SELECT id, user_id, content, created_at, updated_at FROM notes WHERE id = ?', [id]);
return rows[0] || null;
}
export async function create(userId, content) {
const [result] = await pool.query('INSERT INTO notes (user_id, content) VALUES (?, ?)', [userId, content]);
return findById(result.insertId);
}
export async function update(id, content) {
await pool.query('UPDATE notes SET content = ? WHERE id = ?', [content, id]);
return findById(id);
}
export async function remove(id) {
await pool.query('DELETE FROM notes WHERE id = ?', [id]);
}
+14
View File
@@ -0,0 +1,14 @@
import { Router } from 'express';
import { requireAuth } from '../../middleware/auth.js';
import { getNotes, postNote, putNote, deleteNote } from './controller.js';
const router = Router();
// Sin restriccion de rol: cualquier usuario autenticado tiene sus propias notas.
router.use(requireAuth);
router.get('/', getNotes);
router.post('/', postNote);
router.put('/:id', putNote);
router.delete('/:id', deleteNote);
export default router;
+30
View File
@@ -0,0 +1,30 @@
import * as repo from './repository.js';
import { HttpError } from '../../utils/http.js';
// Siempre privadas: cada quien ve y toca solo sus propias notas, sin importar el rol. Una nota
// de otro usuario responde 404 (no 403) para no revelar que existe.
async function getOwnNoteOr404(id, currentUser) {
const note = await repo.findById(id);
if (!note || note.user_id !== currentUser.id) throw new HttpError(404, 'Nota no encontrada');
return note;
}
export const listNotes = (currentUser) => repo.findByUser(currentUser.id);
export async function createNote(content, currentUser) {
const trimmed = (content || '').trim();
if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
return repo.create(currentUser.id, trimmed);
}
export async function updateNote(id, content, currentUser) {
await getOwnNoteOr404(id, currentUser);
const trimmed = (content || '').trim();
if (!trimmed) throw new HttpError(400, 'El contenido es obligatorio');
return repo.update(id, trimmed);
}
export async function removeNote(id, currentUser) {
await getOwnNoteOr404(id, currentUser);
await repo.remove(id);
}