Setiap layanan autentikasi berdiri di atas satu pertanyaan sederhana: di mana daftar pengguna disimpan, dan apa isi setiap barisnya. Pada bagian ini kita membangun fondasi itu dengan node:sqlite — satu tabel pengguna yang bisa hidup di memori untuk demo dan test, atau di berkas untuk pemakaian nyata. Desainnya mengikuti pola repository dari Section Architecture dan Design: handler HTTP tidak pernah menulis SQL, hanya memanggil method penyimpanan. Satu keputusan paling penting sudah diambil sejak awal: kolom sandi tidak menyimpan sandi asli, melainkan rekaman hash yang cara pembuatannya dibahas tuntas di Bagian 2.

Tabel Pengguna dan Skemanya #

Empat kolom cukup untuk seluruh proyek ini. Berikut peta lengkapnya:

KolomIsiAlasan
idangka bertambah otomatispengenal stabil yang tidak berubah walau nama diganti
namateks unik 3-30 karakterkunci login sekaligus pengenal yang terlihat pengguna
hash_sandistring salt:hashbukti sandi; sandi asli tidak pernah disimpan
peranpengguna atau admindasar keputusan izin pada Bagian 3

Skema ini dibuat oleh function buatDb yang selalu dipanggil sebelum penyimpanan dipakai:

js src/db.js
export function buatDb(lokasi = ':memory:') {
  const db = new DatabaseSync(lokasi);
  db.exec(`
    CREATE TABLE IF NOT EXISTS pengguna (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      nama TEXT NOT NULL UNIQUE,
      hash_sandi TEXT NOT NULL,
      peran TEXT NOT NULL DEFAULT 'pengguna'
    )
  `);
  return db;
}

Mari kita bedah baris-per-barisnya:

  1. Baris 5 (lokasi = ':memory:'): string khusus ':memory:' membuat database hidup di memori — tiap panggilan menghasilkan database baru yang hilang saat proses berakhir, tepat untuk test yang butuh kondisi awal bersih. src/cli.js nanti mengirim path berkas data/auth.db agar data bertahan.
  2. Baris 6 (new DatabaseSync(lokasi)): class DatabaseSync membuka koneksi sinkron; operasi SQLite lokal selesai dalam mikrodetik sehingga pola sinkron ini justru menyederhanakan kode handler.
  3. Baris 7-14 (CREATE TABLE IF NOT EXISTS): penanda IF NOT EXISTS membuat function ini aman dipanggil berulang — pada berkas yang sudah berisi tabel, perintah ini tidak melempar error. UNIQUE pada kolom nama menempatkan janji “nama tidak boleh ganda” di database itu sendiri.
  4. Baris 12 (DEFAULT 'pengguna'): kolom peran punya nilai bawaan, sehingga penyimpanan tetap sehat walau ada baris yang ditulis tanpa peran eksplisit.

Query Parameterized dalam Praktik #

Semua akses data terkumpul dalam buatPenyimpananPengguna — function yang menerima koneksi db dan mengembalikan tiga method. Chapter Query Parameterized menjelaskan mengapa nilai tidak pernah dirangkai ke string SQL; di sini pola itu menjadi satu-satunya jalan karena seluruh statement memakai placeholder ?:

js src/db.js
export function buatPenyimpananPengguna(db) {
  const tambahPengguna = db.prepare(
    'INSERT INTO pengguna (nama, hash_sandi, peran) VALUES (?, ?, ?)');
  const cariDenganNama = db.prepare(
    'SELECT id, nama, hash_sandi, peran FROM pengguna WHERE nama = ?');
  const daftarSemua = db.prepare(
    'SELECT id, nama, peran FROM pengguna ORDER BY id');

  return {
    tambah(nama, hashSandi, peran) {
      const hasil = tambahPengguna.run(nama, hashSandi, peran);
      return { id: Number(hasil.lastInsertRowid), nama, peran };
    },
    cariNama(nama) {
      return cariDenganNama.get(nama) ?? null;
    },
    daftar() {
      return daftarSemua.all().map((baris) => ({
        id: Number(baris.id),
        nama: baris.nama,
        peran: baris.peran,
      }));
    },
  };
}

Beberapa keputusan penting pada potongan di atas:

  1. Baris 19-24 (tiga db.prepare): statement dikompilasi sekali saat penyimpanan dibuat, lalu dijalankan berkali-kali dengan nilai berbeda — pola prepare-then-run yang sudah kita kenal dari Chapter SQLite dengan node:sqlite.
  2. Baris 27-30 (tambah): run() mengikat tiga nilai ke tiga placeholder secara berurutan. hasil.lastInsertRowid berisi id yang baru dibuat; Number() menjaga nilainya tetap number untuk id berukuran wajar.
  3. Baris 31-33 (cariNama): method .get() mengembalikan baris pertama atau undefined; operator ?? mengubahnya menjadi null agar handler cukup memeriksa satu nilai kosong. Baris yang dikembalikan memuat hash_sandi karena verifikasi login membutuhkannya — data ini tidak pernah keluar dari sisi server.
  4. Baris 34-40 (daftar): statement SQL-nya memilih id, nama, peran saja — kolom hash_sandi tidak ikut terseleksi, sehingga endpoint /admin di Bagian 3 tidak mungkin membocorkan hash sekalipun lupa memfilternya. Pemetaan map juga menyalin baris ke object biasa dengan urutan field yang stabil.

Menolak Nama Ganda lewat Constraint #

Ketika insert melanggar UNIQUE, node:sqlite melempar error bertanda khusus. Function kecil ini mengenali tanda tersebut:

js src/db.js
// Deteksi pelanggaran constraint UNIQUE pada kolom nama.
export function galatNamaTerpakai(err) {
  return err.code === 'ERR_SQLITE_ERROR'
    && err.message.includes('UNIQUE constraint failed: pengguna.nama');
}

Dua pemeriksaan berlapis pada baris 46-47: kode ERR_SQLITE_ERROR memastikan error berasal dari SQLite, dan isi pesan memastikan pelanggarannya memang kolom nama — bukan constraint lain yang mungkin ditambahkan kelak. Handler pendaftaran di Bagian 2 menangkap error ini dan memetakkannya ke status 409; error lain diteruskan sehingga tetap menjadi urusan penangan error terpusat.

Menguji Penyimpanan #

Empat test unit mengunci kontrak penyimpanan: roundtrip tambah-cari, nama asing menghasilkan null, nama ganda melempar error yang dikenali galatNamaTerpakai, dan daftar() tidak memuat hash_sandi:

bash
node --test tests/penyimpanan.test.js

Output yang Dihasilkan:

text
▶ penyimpanan pengguna
  ✔ tambah lalu cariNama mengembalikan pengguna lengkap
  ✔ cariNama untuk nama asing mengembalikan null
  ✔ nama sama ditolak constraint UNIQUE
  ✔ daftar menampilkan nama dan peran tanpa hash_sandi
✔ penyimpanan pengguna
ℹ tests 4
ℹ suites 1
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Penyimpanan kini siap dipakai: tabel berdiri, query aman, dan nama ganda ditolak di lapis paling bawah. Yang belum ada adalah isi kolom hash_sandi — bagaimana sandi asli diubah menjadi rekaman yang aman disimpan, dan bagaimana pendaftaran serta login memakainya, itu pekerjaan Bagian 2.