Rutas protegidas por inicio de sesión
Parte de la sección Autenticación y seguridad del Journey de Node.js de Coddy. Lección 11 de 24.
Todas las rutas privadas necesitan la consulta de sesión de GET /me. Si se copia en cada ruta, tarde o temprano se olvidará en alguna y esa ruta quedará abierta para todo el mundo. Escríbela una sola vez como middleware.
El middleware se ejecuta antes que la ruta. Cuando coloca algo en req, como req.user, la ruta lo lee de ahí. El _id de un usuario es un ObjectId; user._id.toHexString() lo devuelve como texto. O bien responde y se detiene, o bien coloca lo que encontró en req y llama a next(). Pásalo a cada ruta que lo necesite, antes del controlador:
function requireAdult(req, res, next) {
if (Number(req.query.age) < 18) {
return res.status(403).json({ error: 'Adults only' });
}
req.adult = true;
next();
}
app.get('/wines', requireAdult, (req, res) => res.json(wines));El return es importante: sin él, la ruta también se ejecutaría.
Desafío
FácilLas notas siguen estando abiertas para todo el mundo. Hazlas privadas: solo un usuario que haya iniciado sesión puede leer o añadir notas, y cada usuario solo ve las suyas.
- En
server.js, escribe una función de middlewarerequireUser(req, res, next). Mueve la búsqueda de la sesión deGET /mea esta función: lee la cookiesid, busca la sesión y después busca a su usuario.- Si no existe tal usuario, responde con el estado 401 y
{"error": "Log in first"}, y detente ahí. - De lo contrario, añade el usuario a la solicitud como
req.user, un objeto con dos campos, y luego llama anext(). Los dos campos sonid, el_iddel usuario convertido en texto, yusername. El métodotoHexString()de un ObjectId devuelve su representación textual, como"66f1c2a9e4b0a1b2c3d4e5f6".
- Si no existe tal usuario, responde con el estado 401 y
- Pasa
requireUseraGET /me,GET /notesyPOST /notes, antes de la función propia de cada ruta.GET /meresponderá entonces con{"username": "ada"}, obtenido dereq.user. - En
POST /notes, guarda cada nota con un campoownerque contengareq.user.id. La respuesta es la nueva nota:{"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"} - En
GET /notes, devuelve solo las notas cuyoownersea el usuario que ha iniciado sesión.
Pruébalo: Pulsa Run. Envía GET /notes: estado 401 y {"error": "Log in first"}. Envía POST /signup y después POST /login, ambos con {"username": "ada", "password": "correct horse"}. Envía POST /notes con {"text": "Door code 1234"}: estado 201 y la nota con su owner. Envía GET /notes: una lista que contiene esa nota. Ahora regístrate e inicia sesión con {"username": "bob", "password": "correct horse"}. GET /notes devuelve [], porque bob no ha escrito nada.
Pruébalo tú mismo
import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
const sid = randomBytes(24).toString('base64url');
await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
res.cookie('sid', sid, { httpOnly: true });
res.json({ username: user.username });
});
app.get('/me', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
if (!session) {
return res.status(401).json({ error: 'Log in first' });
}
const user = await users.findOne({ _id: session.userId });
res.json({ username: user.username });
});
// Cuenta cuántas veces este navegador ha llamado a GET /visit.
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// Notas. Por ahora, cualquiera puede leerlas y escribirlas.
app.get('/notes', async (req, res) => {
res.json(await notes.find().toArray());
});
app.post('/notes', async (req, res) => {
const note = { text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Autenticación y seguridad
2Sesiones y cookies
Recordar con cookiesUna consulta de sesiónSesiones de inicio de sesiónRutas protegidas por inicio de sesiónPractica por tu cuenta: Playground de Express