Praktik 9: CRUD API Users
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 jsonwebtokennpm install bcryptjsnpm install cryptoPaket 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.
Solusi
Section titled “Solusi”Untuk P9-1, token lewat query string
Section titled “Untuk P9-1, token lewat query string”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.
Untuk P9-2, hash password di respons
Section titled “Untuk P9-2, hash password di respons”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: {...} });Untuk P9-4, dua daftar role
Section titled “Untuk P9-4, dua daftar role”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-5, payload tidak dipakai
Section titled “Untuk P9-5, payload tidak dipakai”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.
Untuk P9-7, impor crypto tidak konsisten
Section titled “Untuk P9-7, impor crypto tidak konsisten”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.
Untuk P9-8, pesan galat database
Section titled “Untuk P9-8, pesan galat database”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 });}Untuk P9-9, hashSync di jalur permintaan
Section titled “Untuk P9-9, hashSync di jalur permintaan”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.
Untuk P9-10, kunci nama ganda
Section titled “Untuk P9-10, kunci nama ganda”Hapus salah satu baris. Ini juga tertangkap otomatis oleh aturan ESLint no-dupe-keys bila
linting diaktifkan.
Untuk P9-11, npm install crypto
Section titled “Untuk P9-11, npm install crypto”Hapus baris npm install crypto dari modul. Paket itu berstatus deprecated dan hanya meneruskan
ke modul bawaan Node, sementara kodenya memakai node:crypto.
Untuk P9-12, tanda kutip melengkung
Section titled “Untuk P9-12, tanda kutip melengkung”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:
grep -rn $'[\u2018\u2019]' --include='*.docx' . 2>/dev/null || trueKarena DOCX berbentuk arsip, pemeriksaan yang lebih andal dilakukan setelah teksnya diekstrak.
Untuk P9-13, penghapusan memakai POST
Section titled “Untuk P9-13, penghapusan memakai POST”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 siap pakai
Section titled “Berkas siap pakai”| 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 |
Ringkasan
Section titled “Ringkasan”| 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 |