Alur Reset Password yang Aman: Kirim Link Reset via Email dengan MailAnvil + Express.js
Setiap aplikasi dengan login wajib punya alur reset password — dan ini sering diremehkan. Padahal, email reset password adalah email paling sensitif di seluruh aplikasi: kalau nyangkut di spam, user terkunci; kalau link-nya bocor, akun orang lain bisa diambil alih. Bagi fintech kecil seperti e-wallet di Indonesia, ini bukan cuma soal UX — ini soal kepercayaan.
Tutorial ini membangun alur lengkap: user lupa password → menerima link via email → reset password baru. Stack: Express.js + MailAnvil email API.
Kenapa alur ini sering gagal
Tiga kegagalan paling umum yang saya lihat di project-pinjaman developer Indonesia:
- Link expired tapi tidak ada pesan yang jelas — user klik link tua, dapat error kosong, hapus aplikasi.
- Enumerasi email — endpoint lupa-password menjawab beda untuk "email terdaftar" vs "email tidak ada". Penyerang pakai ini untuk mengintai akun mana yang ada.
- Token tidak sekali pakai — link reset yang sudah dipakai masih bisa dipakai lagi. Fatal untuk akun dengan saldo, seperti e-wallet atau platform investasi retail.
Mari kita perbaiki ketiganya.
1. Generate token yang aman
Jangan pakai UUID biasa atau timestamp. Pakai token kriptografis acak:
import crypto from 'node:crypto';
function buatTokenReset() {
const token = crypto.randomBytes(32).toString('hex'); // 256-bit
const hash = crypto.createHash('sha256').update(token).digest('hex');
const expiry = Date.now() + 60 * 60 * 1000; // 1 jam
return { token, hash, expiry };
}
Simpan hash-nya saja di database, bukan token mentah. Ini pola yang sama dengan penyimpanan API key — kalau database bocor, token tidak bisa dipakai ulang.
await db.execute(
`INSERT INTO password_resets (email, token_hash, expires_at, used, created_at)
VALUES (?, ?, ?, 0, datetime('now'))`,
[email, hash, new Date(expiry).toISOString()]
);
2. Kirim email reset via MailAnvil
const res = await fetch('https://api.mailanvil.com/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MAILANVIL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `reset-${hash}`, // klik ganda "kirim ulang" tidak dobel kirim
},
body: JSON.stringify({
from: '[email protected]',
to: [email],
subject: 'Reset password akun kamu',
html: `
<p>Ada permintaan reset password untuk akun kamu.</p>
<p><a href="https://app.domainkamu.com/reset?token=${token}"
style="background:#ff801f;color:#000;padding:12px 24px;border-radius:9999px;">
Reset password</a></p>
<p style="color:#666;font-size:12px;">Link berlaku 1 jam.
Kalau kamu tidak meminta ini, abaikan email ini — password kamu tidak berubah.</p>
`,
}),
});
Tiga detail penting:
Idempotency-Key— user sering klik "kirim ulang" dua-tiga kali karena panik. Dengan idempotency key, MailAnvil tidak mengirim duplikat.- Jangan sebut username atau info akun di email — email bisa diteruskan. Cukup "akun kamu".
- Sender dari domain terverifikasi — verifikasi DKIM dulu di dashboard MailAnvil, kalau tidak email masuk spam atau ditolak.
3. Anti-enumerasi di endpoint lupa-password
Responnya harus selalu sama, terlepas dari email ada atau tidak:
app.post('/api/forgot-password', async (req, res) => {
const { email } = req.body;
const user = await cariUserByEmail(email);
if (user) {
const { token, hash, expiry } = buatTokenReset();
await simpanToken(user.email, hash, expiry);
await kirimEmailReset(email, token);
}
// email tidak terdaftar → tidak kirim apa-apa, tapi respon sama:
res.json({ message: 'Kalau email kamu terdaftar, link reset sudah dikirim.' });
});
User yang sah tidak kehilangan apa-apa; penyerang tidak mendapat informasi.
4. Verifikasi + sekali pakai
app.post('/api/reset-password', async (req, res) => {
const { token, passwordBaru } = req.body;
const hash = crypto.createHash('sha256').update(token).digest('hex');
const row = await db.query(
`SELECT email, expires_at, used FROM password_resets
WHERE token_hash = ? AND used = 0 AND expires_at > datetime('now')`,
[hash]
);
if (!row || !row.length) {
return res.status(400).json({
error: { code: 'invalid_token', message: 'Link sudah kedaluwarsa. Minta link baru.' }
});
}
await updatePassword(row[0].email, await hashPassword(passwordBaru));
await db.execute(`UPDATE password_resets SET used = 1 WHERE token_hash = ?`, [hash]);
// bonus: invalidate semua sesi aktif akun ini
res.json({ message: 'Password berhasil direset. Silakan login.' });
});
Pesan error harus jelas dan menawarkan jalan keluar: "Minta link baru", bukan error kosong.
5. Kegunaan nyata di bisnis Indonesia
- E-wallet & investasi retail (pola seperti DANA atau Bibit): saldo di akun berarti reset password harus sekali pakai + invalidate sesi aktif. Tanpa itu, penyerang yang punya akses singkat bisa bertahan.
- Marketplace (pola Tokopedia): penjual yang terkunci = gudang berhenti jual. Link yang sampai ke inbox (bukan spam) langsung memotong ticket support.
- SaaS B2B lokal: tim 10 orang dengan satu admin — email reset harus sampai ke inbox admin, bukan quarantine. Deliverability bukan bonus, itu kebutuhan.
Dari sisi biaya: email reset tipis, HTML-nya kecil. Di MailAnvil, 3.000 email pertama gratis, dan paket Pro Rp249rb/bulan untuk 50.000 email — cukup untuk aplikasi dengan puluhan ribu user aktif, dibayar QRIS atau GoPay tanpa kartu kredit.
Checklist sebelum production
- [ ] Token 256-bit acak, simpan hash-nya saja
- [ ] Expiry maksimal 1 jam
- [ ] Sekali pakai (
used = 1setelah reset) - [ ] Endpoint lupa-password anti-enumerasi
- [ ]
Idempotency-Keysaat kirim email - [ ] Domain terverifikasi DKIM di MailAnvil
- [ ] Invalidate sesi aktif setelah reset
Coba sendiri: daftar gratis di mailanvil.com, verifikasi domain, dan alur reset password kamu siap menerima email pertama dalam 5 menit.