Lewati ke konten

Praktik 9: CRUD API Users

InternalSebagian terujiArti badge ini

Materi Praktik 9 membangun API CRUD untuk tabel users: register, login, list, create, update, dan delete, dengan token JWT dan pemeriksaan role.

Berkas yang diperiksa, semuanya di Praktik 9/Membuat CRUD API users/:

jwt.js, roles.js, verifyBearerToken.js, verifyCredentials.js, serta create/route.js, delete/route.js, list/route.js, login/route.js, register/route.js, dan update/route.js.

Semua berkas memakai akhiran baris CRLF.

P9-1. Token diterima lewat query string (Tinggi)

Section titled “P9-1. Token diterima lewat query string (Tinggi)”

verifyBearerToken.js menyediakan dua jalur masuk token. Selain header Authorization, ada juga:

export function getTokenParams(request) {
const { searchParams } = new URL(request.url);
return searchParams.get("access_token");
}

Lalu keduanya diterima:

const token = getBearerToken(request) || getTokenParams(request);

Token di URL ikut tercatat di access log Nginx, log Cloud Build, riwayat browser, dan header Referer saat halaman memuat sumber daya lain.

P9-2. Respons create mengembalikan hash password (Tinggi)

Section titled “P9-2. Respons create mengembalikan hash password (Tinggi)”

create/route.js mengembalikan objek hasil Prisma apa adanya:

return NextResponse.json(
{ message: "Berhasil membuat user baru", data: newUser },
{ status: 201 }
);

Prisma create mengembalikan seluruh kolom, termasuk password yang berisi hash bcrypt.

Perlu dicatat: list/route.js dan update/route.js justru sudah memakai select yang benar. Hanya create yang terlewat, jadi ini kelalaian pada satu berkas, bukan pola menyeluruh.

P9-3. update selalu menulis is_active meski tidak dikirim (Sedang)

Section titled “P9-3. update selalu menulis is_active meski tidak dikirim (Sedang)”
data: {
role: data.role,
is_active: data.is_active,
}

Permintaan yang hanya ingin mengubah role tetap mengirim is_active: undefined. Prisma memperlakukan undefined sebagai “jangan ubah”, sehingga saat ini tidak merusak. Tetapi perilaku itu bergantung pada detail implementasi, bukan pada niat penulis kode. Begitu nilainya berubah menjadi null, kolomnya benar-benar dikosongkan.

P9-4. Dua daftar role yang saling bertentangan (Sedang)

Section titled “P9-4. Dua daftar role yang saling bertentangan (Sedang)”
Berkas Daftar role yang diizinkan
create/route.js ["editor", "admin"]
update/route.js ["viewer", "admin"]

roles.js mendefinisikan tiga role: viewer, admin, dan super_admin. Namun register memakai editor sebagai role bawaan, dan create mengizinkan editor.

Akibat praktisnya: editor boleh dibuat tetapi tidak bisa diubah, dan admin boleh dibuat tetapi create menolak viewer yang sebenarnya boleh.

P9-5. payload tidak pernah dipakai (Sedang)

Section titled “P9-5. payload tidak pernah dipakai (Sedang)”

Empat route menulis const { payload, error, status } = requireAuth(...) tetapi payload tidak pernah dipakai. Ini menandakan pemeriksaan identitas pemanggil memang direncanakan tetapi tidak pernah diselesaikan, dan itulah yang membuat P9-6 mungkin terjadi.

P9-6. Super admin bisa menghapus akunnya sendiri (Sedang)

Section titled “P9-6. Super admin bisa menghapus akunnya sendiri (Sedang)”

delete/route.js hanya memeriksa bahwa pemanggil adalah super_admin. Tidak ada penjagaan terhadap data.user_id === payload.user_id. Bila akun super admin terakhir dihapus, tidak ada lagi yang bisa mengelola user.

P9-7. crypto.randomUUID() tanpa impor di register (Rendah)

Section titled “P9-7. crypto.randomUUID() tanpa impor di register (Rendah)”

create/route.js memuat import crypto from "crypto". register/route.js memanggil crypto.randomUUID() tanpa impor apa pun, dan mengandalkan global crypto Node 18 ke atas. Keduanya berjalan, tetapi tidak konsisten.

P9-8. Pesan galat database diteruskan mentah (Sedang)

Section titled “P9-8. Pesan galat database diteruskan mentah (Sedang)”

Enam berkas mengembalikan { message: err.message }. Pesan Prisma memuat nama tabel, nama kolom, dan kadang potongan query.

P9-9. bcrypt.hashSync di dalam request handler (Sedang)

Section titled “P9-9. bcrypt.hashSync di dalam request handler (Sedang)”

create/route.js baris 20 memanggil bcrypt.hashSync(data.password, 10) pada jalur permintaan. Versi sinkron memblokir event loop Node selama proses hashing. Materi Praktik 8 dan 9 sendiri memakai hashSync di REPL, dan di sana itu wajar karena di luar jalur permintaan.

P9-10. Kunci nama ditulis dua kali (Sedang)

Section titled “P9-10. Kunci nama ditulis dua kali (Sedang)”

Pada create/route.js, baris 34 dan 36:

data: {
user_id: crypto.randomUUID(),
nama: data.nama,
email: data.email,
nama: data.nama,
password: password,

Tidak berbahaya karena nilainya sama, tetapi menandakan berkas ini disunting setengah jalan.

P9-11. Materi menyuruh npm install crypto (Rendah)

Section titled “P9-11. Materi menyuruh npm install crypto (Rendah)”
npm install jsonwebtoken
npm install bcryptjs
npm install crypto

Paket crypto di npm berstatus deprecated. Halaman paketnya menyatakan paket itu sudah tidak didukung dan namanya dipertahankan hanya untuk mencegah penyalahgunaan. Isinya hanya meneruskan ke modul bawaan Node, dan kode di praktik ini memang tidak pernah memakainya.

P9-12. Contoh kode memakai tanda kutip melengkung (Rendah)

Section titled “P9-12. Contoh kode memakai tanda kutip melengkung (Rendah)”

Materi memuat:

const hash = bcrypt.hashSync('PasswordRahasia123', 10);

Tanda kutipnya adalah U+2018 dan U+2019, bukan '. Kode itu tidak bisa dijalankan bila disalin apa adanya dari dokumen.

P9-13. Penghapusan data memakai POST (Rendah)

Section titled “P9-13. Penghapusan data memakai POST (Rendah)”

delete/route.js mengekspor POST, bukan DELETE. Bukan bug, tetapi melanggar konvensi REST dan menyulitkan pembatasan akses di tingkat proxy.

Hapus getTokenParams() dan jalur query string, sisakan hanya header:

export function getBearerToken(request) {
const authHeader = request.headers.get("authorization");
if (!authHeader) return null;
const [skema, nilai] = authHeader.split(" ");
if (!skema || skema.toLowerCase() !== "bearer") return null;
if (!nilai || !nilai.trim()) return null;
return nilai.trim();
}

Sekaligus seragamkan pesan galat menjadi satu 401 Unauthorized untuk semua kegagalan autentikasi, supaya penyerang tidak bisa membedakan token kosong, kedaluwarsa, dan salah tanda tangan.

Berkas verifyBearerToken.js memuat versi lengkapnya.

Tambahkan select pada operasi create:

const penggunaBaru = await db.users.create({
data: { user_id: randomUUID(), nama, email, password: passwordHash, role, is_active: isActive },
select: { user_id: true, nama: true, email: true, role: true, is_active: true, created_at: true },
});

Aturan yang sebaiknya dipegang: setiap operasi Prisma yang hasilnya menyentuh respons harus memakai select eksplisit. Tanpa itu, kolom baru yang ditambahkan nanti otomatis ikut terkirim.

Untuk P9-3, update menulis kolom yang tidak dikirim

Section titled “Untuk P9-3, update menulis kolom yang tidak dikirim”

Susun objek perubahan dari field yang benar-benar ada:

const perubahan = {};
if (data.role !== undefined) perubahan.role = data.role;
if (data.is_active !== undefined) perubahan.is_active = data.is_active;
if (Object.keys(perubahan).length === 0) {
return NextResponse.json({ message: "Tidak ada field yang diubah" }, { status: 400 });
}
const pengguna = await db.users.update({ where: { user_id: userId }, data: perubahan, select: {...} });

Letakkan daftarnya di satu tempat, roles.js:

export const ROLE_LEVELS = Object.freeze({
viewer: 1, editor: 2, admin: 3, super_admin: 4,
});
export const ROLE_DAPAT_DIBUAT = Object.freeze(["viewer", "editor", "admin"]);
export const ROLE_DAPAT_DIUBAH = Object.freeze(["viewer", "editor", "admin"]);

Lalu setiap route mengimpor dari sana. Dengan begitu daftar tidak bisa menyimpang antar berkas.

hasRequiredRole() juga dibuat tahan nilai tak dikenal, sehingga salah ketik role ditolak dengan jelas alih-alih menghasilkan undefined >= 1 yang bernilai salah.

Untuk P9-6, super admin bisa menghapus akunnya sendiri

Section titled “Untuk P9-6, super admin bisa menghapus akunnya sendiri”

Pakai payload yang sudah tersedia, dan tambahkan penjagaan:

const { payload, error, status } = requireAuth(request, "super_admin");
if (error) return NextResponse.json({ message: error }, { status });
if (data.user_id === payload.user_id) {
return NextResponse.json(
{ message: "Tidak bisa menghapus akun sendiri" },
{ status: 400 },
);
}

Penjagaan yang sama berlaku untuk update, agar super admin tidak bisa menurunkan atau menonaktifkan akunnya sendiri.

Pakai modul bawaan Node di semua berkas:

import { randomUUID } from "node:crypto";

Hapus dependensi crypto dari package.json, karena paket npm itu berstatus deprecated dan hanya meneruskan ke modul bawaan.

Catat di server, kirim pesan umum ke klien:

} catch (err) {
console.error("[users/create]", err);
return NextResponse.json({ message: "Terjadi kesalahan pada server" }, { status: 500 });
}
const passwordHash = await bcrypt.hash(password, 12);

Versi asinkron tidak memblokir event loop. Cost dinaikkan ke 12, karena 10 sudah mulai murah pada perangkat keras sekarang.

Hapus salah satu baris. Ini juga tertangkap otomatis oleh aturan ESLint no-dupe-keys bila linting diaktifkan.

Hapus baris npm install crypto dari modul. Paket itu berstatus deprecated dan hanya meneruskan ke modul bawaan Node, sementara kodenya memakai node:crypto.

Ganti U+2018 dan U+2019 pada contoh kode menjadi tanda kutip lurus, sehingga blok kodenya bisa disalin dan langsung dijalankan. Cara memeriksa seluruh paket materi:

Terminal window
grep -rn $'[\u2018\u2019]' --include='*.docx' . 2>/dev/null || true

Karena DOCX berbentuk arsip, pemeriksaan yang lebih andal dilakukan setelah teksnya diekstrak.

Ganti nama ekspornya:

export async function DELETE(request) { ... }

Sesuaikan pemanggilnya. Selain mengikuti konvensi REST, ini memudahkan pembatasan akses di tingkat proxy, karena metode bisa dibedakan tanpa membaca badan permintaan.

Berkas Isi
Praktik 9/perbaikan/verifyBearerToken.js Token hanya dari header, pesan galat seragam
Praktik 9/perbaikan/roles.js Satu sumber daftar role
Praktik 9/perbaikan/create-route.js Validasi, hash async, select tanpa password
Praktik 9/perbaikan/update-route.js Hanya ubah field yang dikirim
Praktik 9/tools/audit-api.mjs Pemeriksa statis, 12 jenis pemeriksaan
Praktik 9/tools/uji-api.mjs Uji API pada aplikasi yang berjalan
Praktik 9/tools/hash-password.mjs Hash password lewat prompt tersembunyi
Butir Tingkat Inti
P9-1 Tinggi Token diterima lewat query string
P9-2 Tinggi Respons create mengembalikan hash password
P9-3 Sedang update menulis kolom yang tidak dikirim
P9-4 Sedang Dua daftar role yang berbeda untuk aturan yang sama
P9-5 Sedang payload tidak pernah dipakai
P9-6 Sedang Super admin bisa menghapus akunnya sendiri
P9-7 Rendah Impor crypto tidak konsisten antar berkas
P9-8 Sedang Pesan galat database diteruskan mentah
P9-9 Sedang hashSync di jalur permintaan
P9-10 Sedang Kunci nama ditulis dua kali
P9-11 Rendah Paket crypto dari npm tidak diperlukan
P9-12 Rendah Tanda kutip melengkung pada contoh kode
P9-13 Rendah Penghapusan memakai POST