Menu
CoddyTech

Role administratora

Część sekcji Uwierzytelnianie i bezpieczeństwo ścieżki Node.js w Coddy. Lekcja 19 z 24.

Żądanie GET /admin sprawdza tylko, czy ktoś jest zalogowany, więc widzi je każdy użytkownik. Rola przypisana użytkownikowi, taka jak 'member' lub 'admin', określa, co może on robić. Fabryka middleware, czyli funkcja zwracająca middleware, sprawdza jedną rolę dla każdej trasy:

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);

Rolę zawsze ustawia serwer. Podczas rejestracji kopiowane są tylko oczekiwane pola z req.body, więc osoba odwiedzająca, która wyśle "role": "admin", nic na tym nie zyska.

challenge icon

Wyzwanie

Łatwy

GET /admin pokazuje, ile istnieje kont, a obecnie każdy zalogowany członek może to zobaczyć. Tylko administratorzy powinni mieć do tego dostęp. Każdy użytkownik otrzymuje rolę ustawianą przez serwer, a trasa ją sprawdza.

  1. W pliku server.js, w POST /signup, zapisuj każdego nowego użytkownika z rolą "role": "member". Nigdy nie pobieraj roli z treści żądania: odwiedzający, który wyśle "role": "admin", i tak zostanie członkiem.
  2. Napisz funkcję requireRole(role), która zwraca oprogramowanie pośredniczące. To oprogramowanie sprawdza req.user.role:
    • Jeśli wartość nie jest równa role, zwróć odpowiedź ze statusem 403 i {"error": "Not allowed"}. 403 Forbidden oznacza, że serwer wie, kim jesteś, ale nie możesz tego zrobić. 401 oznacza, że serwer nie wie, kim jesteś.
    • Jeśli wartość jest równa role, wywołaj next().
  3. Dodaj requireRole('admin') do GET /admin, za requireUser. Ta funkcja odczytuje req.user, które ustawia wyłącznie requireUser.

Serwer już podczas uruchamiania tworzy jedno konto administratora. Jego nazwa użytkownika to admin, a hasło to admin-pass-for-tests, które pochodzi ze zmiennej środowiskowej ADMIN_PASSWORD.

Wypróbuj: Naciśnij Uruchom. Wyślij GET /admin: status 401 i {"error": "Log in first"}. Teraz użytkowniczka o nazwie mallory próbuje nadać sobie uprawnienia administratora. Wyślij POST /signup z {"username": "mallory", "password": "correct horse", "role": "admin"}, a następnie POST /login z {"username": "mallory", "password": "correct horse"}. Wyślij GET /admin: status 403 i {"error": "Not allowed"}, ponieważ dodatkowe pole nie nadało jej uprawnień administratora. Wyślij POST /login z {"username": "admin", "password": "admin-pass-for-tests"}, a następnie GET /admin: {"users": 2}.

Spróbuj swoich sił

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());

// Jedyne konto administratora. Jego hasło pochodzi ze zmiennej środowiskowej.
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 });
});

// Znajduje użytkownika na podstawie ciasteczka sesyjnego albo zwraca 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();
});

// Twoje notatki wyświetlane jako strona.
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>`);
});

// Notatki: każdy użytkownik odczytuje i zapisuje tylko własne.
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();
});

// Dla administratorów: liczba kont.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

Wszystkie lekcje w sekcji Uwierzytelnianie i bezpieczeństwo

Poćwicz samodzielnie: Edytor online Express