Menu
CoddyTech

Peran Admin

Termasuk bagian Autentikasi dan Keamanan dalam perjalanan Node.js di Coddy. Pelajaran 19 dari 24.

GET /admin hanya memeriksa apakah seseorang sudah masuk, jadi setiap anggota bisa melihatnya. Peran yang disimpan pada pengguna, seperti 'member' atau 'admin', menentukan apa yang boleh mereka lakukan. Factory middleware, yaitu fungsi yang mengembalikan middleware, memeriksa satu peran untuk setiap rute:

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

Peran selalu ditetapkan oleh server. Pendaftaran hanya menyalin kolom yang diharapkan dari req.body, jadi pengunjung yang mengirim "role": "admin" tidak akan mendapatkan apa pun dari tindakan itu.

challenge icon

Tantangan

Mudah

GET /admin menampilkan jumlah akun yang ada, dan saat ini setiap anggota yang sudah login bisa melihatnya. Hanya admin yang boleh melihatnya. Setiap pengguna mendapat peran yang ditetapkan oleh server, lalu rute memeriksanya.

  1. Di server.js, dalam POST /signup, simpan setiap pengguna baru dengan "role": "member". Jangan pernah mengambil peran dari isi badan permintaan: pengunjung yang mengirim "role": "admin" tetap menjadi anggota.
  2. Tulis fungsi requireRole(role) yang mengembalikan middleware. Middleware itu memeriksa req.user.role:
    • Jika nilainya bukan role, berikan respons dengan status 403 dan {"error": "Not allowed"}. 403 Forbidden berarti server tahu siapa kamu, tetapi kamu tidak boleh melakukan ini. 401 berarti server tidak tahu siapa kamu.
    • Jika nilainya adalah role, panggil next().
  3. Pasang requireRole('admin') pada GET /admin, setelah requireUser. Fungsi ini membaca req.user, yang hanya diatur oleh requireUser.

Server sudah membuat satu akun admin saat dijalankan. Nama penggunanya adalah admin, dan kata sandinya adalah admin-pass-for-tests, yang berasal dari variabel lingkungan ADMIN_PASSWORD.

Coba: Tekan Jalankan. Kirim GET /admin: status 401 dan {"error": "Log in first"}. Sekarang pengguna bernama mallory mencoba menjadikan dirinya admin. Kirim POST /signup dengan {"username": "mallory", "password": "correct horse", "role": "admin"}, lalu POST /login dengan {"username": "mallory", "password": "correct horse"}. Kirim GET /admin: status 403 dan {"error": "Not allowed"}, karena kolom tambahan itu tidak menjadikannya admin. Kirim POST /login dengan {"username": "admin", "password": "admin-pass-for-tests"}, lalu GET /admin: {"users": 2}.

Coba sendiri

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

// Satu-satunya akun admin. Kata sandinya berasal dari environment.
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 });
});

// Mencari pengguna dari cookie sesi, atau mengembalikan 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();
});

// Catatanmu sebagai halaman.
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>`);
});

// Catatan: setiap pengguna hanya membaca dan menulis catatannya sendiri.
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();
});

// Untuk admin: jumlah akun yang ada.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

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

Semua pelajaran di Autentikasi dan Keamanan

Latihan sendiri: Playground Express