Funções de administrador
Parte da seção Autenticação e segurança do Journey de Node.js da Coddy. Lição 19 de 24.
GET /admin apenas verifica se alguém está conectado, então todos os membros podem acessá-lo. Uma função armazenada no usuário, como 'member' ou 'admin', define o que ele pode fazer. Uma fábrica de middleware, uma função que retorna um middleware, verifica uma função por rota:
function requirePlan(plan) {
return (req, res, next) => {
if (req.user.plan !== plan) {
return res.status(402).json({ error: `Needs the ${plan} plan` });
}
next();
};
}
app.get('/exports', requireUser, requirePlan('pro'), sendExports);Uma função é sempre definida pelo servidor. O cadastro copia apenas os campos esperados de req.body, então um visitante que envia "role": "admin" não consegue nada com isso.
Desafio
FácilGET /admin mostra quantas contas existem e, no momento, todos os membros conectados podem ver isso. Apenas administradores devem poder ver. Cada usuário recebe uma função, definida pelo servidor, e a rota a verifica.
- Em
server.js, emPOST /signup, salve cada novo usuário com"role": "member". Nunca aceite a função do corpo da solicitação: um visitante que enviar"role": "admin"ainda se torna um membro. - Escreva uma função
requireRole(role)que retorne um middleware. Esse middleware verificareq.user.role:- Quando não for
role, responda com o status 403 e{"error": "Not allowed"}. 403 Forbidden significa que o servidor sabe quem você é, mas você não pode fazer isso. 401 significa que ele não sabe quem você é. - Quando for
role, chamenext().
- Quando não for
- Adicione
requireRole('admin')aGET /admin, depois derequireUser. Ele lêreq.user, que só é definido porrequireUser.
O servidor já cria uma conta de administrador quando é iniciado. O nome de usuário é admin, e a senha é admin-pass-for-tests, que vem da variável de ambiente ADMIN_PASSWORD.
Experimente: Pressione Executar. Envie GET /admin: status 401 e {"error": "Log in first"}. Agora, uma usuária chamada mallory tenta se tornar administradora. Envie POST /signup com {"username": "mallory", "password": "correct horse", "role": "admin"} e, em seguida, POST /login com {"username": "mallory", "password": "correct horse"}. Envie GET /admin: status 403 e {"error": "Not allowed"}, porque o campo extra não a tornou administradora. Envie POST /login com {"username": "admin", "password": "admin-pass-for-tests"} e, em seguida, GET /admin: {"users": 2}.
Experimente você mesmo
import express from 'express';
import { ObjectId } from 'mongodb';
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());
// A única conta de administrador. Sua senha vem do ambiente.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });
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,
sameSite: 'lax',
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ username: user.username });
});
// Encontra o usuário pelo cookie de sessão ou retorna 401.
async function requireUser(req, res, next) {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
const user = session && (await users.findOne({ _id: session.userId }));
if (!user) {
return res.status(401).json({ error: 'Log in first' });
}
req.user = { id: user._id.toHexString(), username: user.username, role: user.role };
next();
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
app.post('/logout', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
if (sid) {
await sessions.deleteOne({ _id: sid });
}
res.clearCookie('sid');
res.status(204).end();
});
// Suas notas em uma página.
app.get('/', requireUser, async (req, res) => {
const list = await notes.find({ owner: req.user.id }).toArray();
const items = list.map((note) => `<li>${note.text}</li>`).join('');
res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});
// Notas: cada usuário lê e escreve apenas as suas próprias.
app.get('/notes', requireUser, async (req, res) => {
res.json(await notes.find({ owner: req.user.id }).toArray());
});
app.post('/notes', requireUser, async (req, res) => {
const note = { owner: req.user.id, text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.get('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const note = await notes.findOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
app.delete('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// Para administradores: quantas contas existem.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Todas as lições de Autenticação e segurança
1Senhas
Bytes e texto hexadecimalDerivando uma chave de senhaUm novo saltNunca armazene senhasComparando bytes da chaveVerificando uma senhaRegras de cadastro4Proprietários e Segurança
Somente suas anotaçõesFunções de administradorPratique por conta própria: Playground de Express