Menu
CoddyTech

Hanya Catatanmu

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

Status sudah masuk membuktikan siapa dirimu, bukan bahwa sebuah catatan adalah milikmu. GET /notes/:id mencari catatan hanya berdasarkan id-nya, jadi siapa pun yang sudah masuk dan memiliki id, dari tautan yang dibagikan atau log, dapat membaca catatan itu. Memeriksa pemilik disebut otorisasi.

Masukkan pemilik ke dalam query itu sendiri agar database tidak pernah mengembalikan dokumen milik orang lain. Berikan jawaban 404, bukan 403, jika dokumen tidak ditemukan: 403 akan memberi tahu pengunjung bahwa catatan itu ada.

const order = await orders.findOne({
    _id: new ObjectId(req.params.id),
    customer: req.user.id,
});
if (!order) {
    return res.status(404).json({ error: 'Order not found' });
}

Setiap query yang mengakses data satu pengguna memerlukan filter: untuk membaca, memperbarui, dan menghapus.

challenge icon

Tantangan

Mudah

Bab ini kembali membahas login sesi dari bab 2, dan server-nya sudah berkembang. Sekarang ada halaman di GET /, rute untuk satu catatan di GET /notes/:id dan DELETE /notes/:id, serta akun admin. Kedua rute /notes/:id memiliki celah: keduanya mencari catatan hanya berdasarkan id-nya. Pengguna mana pun yang sudah login dan mengetahui sebuah id dapat membaca atau menghapus catatan milik orang lain. Tutup celah ini.

  1. Di server.js, dalam GET /notes/:id, cari catatan berdasarkan _id dan owner sekaligus. Pemiliknya harus pengguna yang sedang login, req.user.id.
  2. Lakukan hal yang sama di DELETE /notes/:id, agar tidak ada yang bisa menghapus catatan yang bukan miliknya.
  3. Jika tidak ada catatan yang cocok, kedua rute sudah merespons dengan status 404 dan {"error": "Note not found"}. Gunakan respons itu juga untuk catatan milik orang lain. Respons 403 akan memberi tahu pengunjung bahwa catatan itu ada.

Coba: Tekan tombol Jalankan.

  • Kirim POST /signup, lalu POST /login, keduanya dengan {"username": "ada", "password": "correct horse"}.
  • Kirim POST /notes dengan {"text": "Safe code 4417"}, lalu salin _id dari respons. GET /notes/ diikuti id tersebut akan mengembalikan catatan itu.
  • Buat akun dan login dengan {"username": "bob", "password": "correct horse"}. Kirim GET /notes/ diikuti id milik ada: status 404 dan {"error": "Note not found"}. Kirim DELETE /notes/ diikuti id yang sama: hasilnya juga 404.
  • Login lagi sebagai ada, dengan body miliknya dari langkah pertama. Catatannya masih ada. Kirim DELETE sekarang: status 204.

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 dalam bentuk 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) });
    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) });
    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');
});
quiz iconUji dirimu

Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.

Semua pelajaran di Autentikasi dan Keamanan

Latihan sendiri: Playground Express