Lewati ke konten

Hash password

Prompt tersembunyi, cost 12, ada mode pemeriksaan.

Berkas: alat/hari-3/Praktik 9/tools/hash-password.mjs

Praktik 9/tools/hash-password.mjs
#!/usr/bin/env node
// =====================================================================
// hash-password.mjs
//
// Menggantikan langkah manual di Praktik 8 dan Praktik 9:
//
// node # masuk REPL
// const bcrypt = require('bcryptjs');
// const hash = bcrypt.hashSync('PasswordRahasia123', 10);
// console.log(hash);
//
// kemudian salin hasilnya dan paste ke kolom password di DBeaver.
//
// Cara itu punya tiga masalah:
// - hasilnya harus disalin manual, gampang salah ketik
// - password asli tertinggal di history terminal dan di scrollback
// - tidak bisa diulang oleh orang lain dengan hasil yang sama
//
// Skrip ini membaca password dari argumen, variabel lingkungan, atau
// prompt tersembunyi (tidak muncul di layar dan tidak masuk history).
//
// Pakai:
// node hash-password.mjs # prompt tersembunyi
// node hash-password.mjs --cek '$2b$10$...' # periksa apakah sebuah nilai hash bcrypt
// PASSWORD=rahasia node hash-password.mjs # lewat variabel lingkungan
// node hash-password.mjs --cost 12 # tentukan cost
//
// Hasil bisa langsung dipakai:
// psql "$DATABASE_URL" -v hash="$(node hash-password.mjs)" \
// -v email=admin@example.com -f 02_seed_super_admin.sql
// =====================================================================
import { createInterface } from 'node:readline';
const argv = process.argv.slice(2);
const idxCek = argv.indexOf('--cek');
const idxCost = argv.indexOf('--cost');
// ---------------------------------------------------------------------
// Mode --cek: memeriksa apakah sebuah nilai sudah berupa hash bcrypt.
// Dipakai sebelum menulis ke database, supaya password polos tidak
// pernah tersimpan.
// ---------------------------------------------------------------------
if (idxCek > -1) {
const nilai = argv[idxCek + 1] ?? '';
const cocok = /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(nilai);
if (cocok) {
const cost = Number(nilai.slice(4, 6));
console.log('Hash bcrypt yang sah.');
console.log(` cost : ${cost}${cost < 10 ? ' (terlalu rendah, minimal 10)' : ''}`);
console.log(` awalan : ${nilai.slice(0, 7)}`);
process.exit(cost < 10 ? 1 : 0);
}
console.error('BUKAN hash bcrypt.');
console.error(' Hash bcrypt berbentuk $2a$10$... atau $2b$12$... dengan panjang 60 karakter.');
if (nilai && !nilai.startsWith('$2')) {
console.error(' Nilai yang diberikan terlihat seperti password polos. Jangan simpan ke database.');
}
process.exit(1);
}
// ---------------------------------------------------------------------
// Tentukan cost. Bawaan 12, bukan 10: 10 masih diterima, tetapi makin
// murah perangkat keras, makin murah pula menebak. Praktik memakai 10.
// ---------------------------------------------------------------------
const cost = idxCost > -1 ? Number(argv[idxCost + 1]) : 12;
if (!Number.isInteger(cost) || cost < 10 || cost > 15) {
console.error('Cost harus bilangan bulat antara 10 dan 15.');
process.exit(2);
}
// ---------------------------------------------------------------------
// Ambil password.
// ---------------------------------------------------------------------
function tanyaTersembunyi(pertanyaan) {
return new Promise((selesai) => {
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
const asli = rl._writeToOutput?.bind(rl);
// Jangan tampilkan ketikan.
rl._writeToOutput = function (teks) {
if (teks.includes(pertanyaan)) asli?.(teks);
};
rl.question(pertanyaan, (jawaban) => {
rl._writeToOutput = asli;
rl.close();
process.stdout.write('\n');
selesai(jawaban);
});
});
}
let password = process.env.PASSWORD ?? null;
if (!password) {
const posisi = argv.find((a) => !a.startsWith('--') && a !== String(cost));
if (posisi) {
password = posisi;
console.error('Catatan: password diberikan sebagai argumen, sehingga tercatat di history shell.');
console.error('Lebih aman: jalankan tanpa argumen untuk memasukkan lewat prompt tersembunyi.');
}
}
if (!password) {
if (!process.stdin.isTTY) {
console.error('Tidak ada password. Pakai PASSWORD=... atau jalankan di terminal interaktif.');
process.exit(2);
}
password = await tanyaTersembunyi('Password: ');
const ulang = await tanyaTersembunyi('Ulangi : ');
if (password !== ulang) {
console.error('Password tidak sama.');
process.exit(1);
}
}
if (password.length < 8) {
console.error('Password minimal 8 karakter.');
process.exit(1);
}
// ---------------------------------------------------------------------
// Hitung hash. bcryptjs dicari dari beberapa lokasi supaya skrip ini bisa
// dipakai baik dari dalam proyek Next.js maupun dari folder review.
// ---------------------------------------------------------------------
async function muatBcrypt() {
const kandidat = ['bcryptjs', 'bcrypt'];
for (const nama of kandidat) {
try {
return { nama, mod: await import(nama) };
} catch { /* coba berikutnya */ }
}
return null;
}
const bcrypt = await muatBcrypt();
if (!bcrypt) {
console.error('Paket bcryptjs tidak ditemukan.');
console.error('Pasang lebih dahulu: npm install bcryptjs');
console.error('Atau hitung di dalam folder proyek Next.js, lalu jalankan skrip ini dari sana.');
process.exit(3);
}
const mod = bcrypt.mod.default ?? bcrypt.mod;
const mulai = Date.now();
const hash = await mod.hash(password, cost);
const durasi = Date.now() - mulai;
// Keluarkan HANYA hash ke stdout, supaya bisa dipakai dengan $( ).
console.log(hash);
console.error('');
console.error(`Pustaka : ${bcrypt.nama}, cost ${cost}, ${durasi} ms`);
console.error('');
console.error('Pakai hash ini untuk mengisi kolom password, misalnya:');
console.error(' psql "$DATABASE_URL" -v email=admin@example.com \\');
console.error(` -v hash='${hash.slice(0, 10)}...' -f "Praktik 6/sql/02_seed_super_admin.sql"`);
console.error('');
console.error('Jangan pernah menyimpan password polos ke kolom password.');