Menu
CoddyTech

Rute yang Dilindungi Login

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

Setiap route privat memerlukan pencarian sesi dari GET /me. Jika disalin ke setiap route, pencarian itu bisa terlupakan, sehingga route tersebut terbuka untuk semua orang. Tulis sekali saja sebagai middleware.

Middleware berjalan sebelum route. Saat middleware menyimpan sesuatu pada req, seperti req.user, route membacanya dari sana. _id pengguna adalah ObjectId; user._id.toHexString() menghasilkan nilainya dalam bentuk teks. Middleware akan memberikan respons lalu berhenti, atau menyimpan hasil temuannya pada req dan memanggil next(). Teruskan middleware itu ke setiap route yang memerlukannya, sebelum handler:

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

Kata kunci return itu penting: tanpanya, route juga akan dijalankan.

challenge icon

Tantangan

Mudah

Catatan masih bisa diakses semua orang. Jadikan catatan pribadi: hanya pengguna yang sudah masuk yang bisa membaca atau menambahkan catatan, dan setiap pengguna hanya melihat catatannya sendiri.

  1. Di server.js, tulis fungsi middleware requireUser(req, res, next). Pindahkan pencarian sesi dari GET /me ke fungsi ini: baca cookie sid, cari sesi, lalu cari penggunanya.
    • Jika tidak ada pengguna seperti itu, kirim respons dengan status 401 dan {"error": "Log in first"}, lalu berhenti.
    • Jika ada, tempatkan pengguna pada permintaan sebagai req.user, sebuah object dengan dua field, lalu panggil next(). Kedua field tersebut adalah id, yaitu _id pengguna dalam bentuk teks, dan username. Method toHexString() milik ObjectId menghasilkan teksnya, seperti "66f1c2a9e4b0a1b2c3d4e5f6".
  2. Berikan requireUser ke GET /me, GET /notes, dan POST /notes, sebelum fungsi masing-masing rute. Setelah itu, GET /me mengirim respons {"username": "ada"}, yang dibaca dari req.user.
  3. Di POST /notes, simpan setiap catatan dengan field owner yang berisi req.user.id. Responsnya adalah catatan baru: {"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"}
  4. Di GET /notes, kembalikan hanya catatan yang owner-nya adalah pengguna yang sudah masuk.

Coba: Tekan Jalankan. Kirim GET /notes: status 401 dan {"error": "Log in first"}. Kirim POST /signup, lalu POST /login, keduanya dengan {"username": "ada", "password": "correct horse"}. Kirim POST /notes dengan {"text": "Door code 1234"}: status 201 dan catatan dengan owner-nya. Kirim GET /notes: list berisi satu catatan itu. Sekarang daftar dan masuk dengan {"username": "bob", "password": "correct horse"}. GET /notes mengembalikan [], karena bob belum menulis apa pun.

Coba sendiri

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

// Menghitung berapa kali browser ini memanggil 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 });
});

// Catatan. Untuk saat ini, siapa saja bisa membaca dan menulisnya.
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');
});
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