282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const argon2 = require('argon2');
|
|
const randomstring = require('randomstring');
|
|
const {User} = require('../models'); // Pastikan sesuai dengan struktur project
|
|
require('dotenv').config();
|
|
const nodemailer = require('nodemailer');
|
|
const sgMail = require('@sendgrid/mail');
|
|
|
|
// Fungsi untuk membuat token JWT
|
|
const generateToken = (user) => {
|
|
return jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role }, // id bukan _id untuk Sequelize
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '1d' }
|
|
);
|
|
};
|
|
|
|
// Menambahkan user baru dengan hashing Argon2
|
|
exports.register = async (req, res) => {
|
|
try {
|
|
const { name, email, password, alamat, role } = req.body;
|
|
|
|
// Cek apakah email sudah terdaftar
|
|
const existingUser = await User.findOne({ where: { email } });
|
|
if (existingUser) return res.status(400).json({ message: 'Email already exists' });
|
|
|
|
// Hash password menggunakan Argon2
|
|
const hashedPassword = await argon2.hash(password);
|
|
|
|
const newUser = await User.create({
|
|
name,
|
|
email,
|
|
password: hashedPassword,
|
|
alamat,
|
|
role: 'user'
|
|
});
|
|
|
|
res.status(201).json({ message: 'User created successfully', user: newUser });
|
|
} catch (error) {
|
|
res.status(500).json({ message: 'Error creating user', error });
|
|
}
|
|
};
|
|
|
|
|
|
// Login
|
|
exports.login = async (req, res) => {
|
|
try {
|
|
const { email, password } = req.body;
|
|
|
|
// 🔹 Cek apakah user dengan email tersebut ada
|
|
const user = await User.findOne({ where: { email } });
|
|
if (!user) {
|
|
return res.status(401).json({ message: "Email atau password salah" });
|
|
}
|
|
|
|
// 🔹 Verifikasi password
|
|
const isPasswordValid = await argon2.verify(user.password, password);
|
|
if (!isPasswordValid) {
|
|
return res.status(401).json({ message: "Email atau password salah" });
|
|
}
|
|
|
|
// 🔹 Buat token JWT
|
|
const token = jwt.sign(
|
|
{ id: user.id, email: user.email, role: user.role },
|
|
process.env.JWT_SECRET,
|
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '1d' }
|
|
);
|
|
|
|
console.log("User ID dari backend:", user.id);
|
|
|
|
// 🔹 Kirim response dengan token dan role
|
|
res.status(200).json({
|
|
message: "Login berhasil",
|
|
token,
|
|
role: user.role // Ini penting untuk Flutter agar bisa menentukan halaman tujuan
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ message: "Terjadi kesalahan", error });
|
|
}
|
|
};
|
|
|
|
// Buat transporter Nodemailer dengan Gmail
|
|
const createGmailTransporter = () => {
|
|
return nodemailer.createTransport({
|
|
service: 'gmail',
|
|
auth: {
|
|
user: process.env.EMAIL_FROM, // sibayam52@gmail.com dari .env yang sudah ada
|
|
pass: process.env.EMAIL_PASS, // Gunakan App Password, bukan password biasa!
|
|
},
|
|
// Pool koneksi untuk menghindari rate limiting
|
|
pool: true,
|
|
maxConnections: 3,
|
|
maxMessages: 50,
|
|
rateDelta: 1500,
|
|
rateLimit: 3
|
|
});
|
|
};
|
|
|
|
exports.sendResetCodeWithGmail = async (req, res) => {
|
|
const { email } = req.body;
|
|
|
|
try {
|
|
// Validasi email
|
|
if (!email || !email.includes('@')) {
|
|
return res.status(400).json({ message: 'Email tidak valid' });
|
|
}
|
|
|
|
const user = await User.findOne({ where: { email } });
|
|
if (!user) return res.status(404).json({ message: 'User tidak ditemukan' });
|
|
|
|
// Generate 6 digit random code
|
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
|
|
|
// Konversi ke waktu Indonesia (GMT+7)
|
|
const expiresAtWIB = new Date(expiresAt.getTime() + 7 * 60 * 60 * 1000);
|
|
|
|
// Format manual jadi ISO-like (tanpa Z karena bukan UTC)
|
|
const isoWIB = expiresAtWIB.toISOString().replace( '+07:00');
|
|
|
|
await user.update({
|
|
resetToken: code,
|
|
resetTokenExpiry: isoWIB,
|
|
});
|
|
|
|
// Nama aplikasi yang konsisten
|
|
const appName = process.env.SENDGRID_SENDER_NAME || 'SistemPakar SIBAYAM';
|
|
|
|
// Coba buat transporter setiap kali untuk menghindari koneksi mati
|
|
const transporter = createGmailTransporter();
|
|
|
|
// Email sangat sederhana tetapi efektif
|
|
const mailOptions = {
|
|
from: `"${appName}" <${process.env.EMAIL_FROM}>`, // Menggunakan EMAIL_FROM dari .env
|
|
to: email,
|
|
subject: `[${code}] Kode Verifikasi ${appName}`, // Tanda kode di subject membantu visibilitas
|
|
html: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 5px;">
|
|
<h2 style="color: #333; margin-bottom: 20px;">Reset Password</h2>
|
|
<p>Halo ${user.name || 'Pengguna'},</p>
|
|
<p style="color: #666; margin-bottom: 15px;">Anda telah meminta untuk mereset password akun SIBAYAM Anda.</p>
|
|
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<p style="font-size: 16px; margin: 0;">Kode verifikasi Anda:</p>
|
|
<h1 style="color: #4CAF50; margin: 10px 0; letter-spacing: 5px;">${code}</h1>
|
|
</div>
|
|
<p style="color: #666; margin-bottom: 15px;">Kode ini akan kadaluarsa dalam 10 menit.</p>
|
|
<p style="color: #999; font-size: 12px; margin-top: 30px;">Jika Anda tidak meminta reset password, abaikan email ini.</p>
|
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;"/>
|
|
<p style="color: #999; font-size: 12px;">Email ini dikirim oleh sistem SIBAYAM. Mohon jangan membalas email ini.</p>
|
|
</div>
|
|
`
|
|
};
|
|
|
|
// Tambahkan debugging
|
|
console.log('Mengirim email ke:', email);
|
|
console.log('Menggunakan akun:', process.env.EMAIL_FROM);
|
|
console.log('Dengan transporter:', transporter ? 'Berhasil dibuat' : 'Gagal dibuat');
|
|
|
|
// Kirim email dengan try-catch terpisah untuk debugging
|
|
try {
|
|
const info = await transporter.sendMail(mailOptions);
|
|
console.log('Email reset password berhasil dikirim via Gmail:', info.messageId);
|
|
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
|
|
|
|
return res.json({
|
|
message: 'Kode verifikasi telah dikirim ke email Anda.',
|
|
expiresIn: '10 menit',
|
|
});
|
|
} catch (emailError) {
|
|
console.error('Error saat mengirim email:', emailError);
|
|
return res.status(500).json({
|
|
message: 'Gagal mengirim kode verifikasi via email',
|
|
error: process.env.NODE_ENV === 'development' ? emailError.toString() : undefined,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in sendResetCodeWithGmail:', error);
|
|
return res.status(500).json({
|
|
message: 'Gagal memproses permintaan reset password',
|
|
error: process.env.NODE_ENV === 'development' ? error.toString() : undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
|
|
|
exports.resetPasswordWithCode = async (req, res) => {
|
|
const { code, password } = req.body;
|
|
|
|
try {
|
|
// Validasi input
|
|
if (!code || !password) {
|
|
return res.status(400).json({
|
|
message: "Kode dan password baru wajib diisi"
|
|
});
|
|
}
|
|
|
|
// Validasi password yang lebih ketat
|
|
if (password.length < 8) {
|
|
return res.status(400).json({
|
|
message: "Password harus minimal 8 karakter"
|
|
});
|
|
}
|
|
|
|
const user = await User.findOne({
|
|
where: {
|
|
resetToken: code
|
|
}
|
|
});
|
|
|
|
if (!user) {
|
|
return res.status(400).json({
|
|
message: "Kode verifikasi salah"
|
|
});
|
|
}
|
|
|
|
// Check if code is expired
|
|
const now = new Date();
|
|
if (now > user.resetTokenExpiry) {
|
|
return res.status(400).json({
|
|
message: "Kode verifikasi sudah kadaluarsa. Silakan minta kode baru."
|
|
});
|
|
}
|
|
|
|
const hashedPassword = await argon2.hash(password);
|
|
|
|
await user.update({
|
|
password: hashedPassword,
|
|
resetToken: null,
|
|
resetTokenExpiry: null
|
|
});
|
|
|
|
// Kirim email konfirmasi menggunakan Gmail
|
|
try {
|
|
const appName = process.env.SENDGRID_SENDER_NAME || 'SistemPakar SIBAYAM';
|
|
const transporter = createGmailTransporter();
|
|
|
|
const mailOptions = {
|
|
from: `"${appName}" <${process.env.EMAIL_FROM}>`,
|
|
to: user.email,
|
|
subject: `Password Berhasil Diubah - ${appName}`,
|
|
html: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 5px;">
|
|
<h2 style="color: #333; margin-bottom: 20px;">Password Berhasil Diubah</h2>
|
|
<p>Halo ${user.name || 'Pengguna'},</p>
|
|
<p style="color: #666; margin-bottom: 15px;">Password akun SIBAYAM Anda telah berhasil diubah.</p>
|
|
<div style="background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<p style="font-size: 16px; margin: 0; color: #4CAF50;">✓ Password telah diperbarui</p>
|
|
</div>
|
|
<p style="color: #666; margin-bottom: 15px;">Jika Anda tidak melakukan perubahan ini, segera hubungi tim dukungan kami.</p>
|
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;"/>
|
|
<p style="color: #999; font-size: 12px;">Email ini dikirim oleh sistem SIBAYAM. Mohon jangan membalas email ini.</p>
|
|
</div>
|
|
`
|
|
};
|
|
|
|
// Tambahkan debugging
|
|
console.log('Mengirim email konfirmasi ke:', user.email);
|
|
console.log('Menggunakan akun:', process.env.EMAIL_FROM);
|
|
|
|
const info = await transporter.sendMail(mailOptions);
|
|
console.log('Email konfirmasi berhasil dikirim via Gmail:', info.messageId);
|
|
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
|
|
|
|
} catch (emailError) {
|
|
console.error('Error saat mengirim email konfirmasi:', emailError);
|
|
// Tidak menghentikan proses meski email konfirmasi gagal
|
|
}
|
|
|
|
return res.json({
|
|
message: "Password berhasil direset",
|
|
success: true
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error("Error in resetPasswordWithCode:", error);
|
|
return res.status(500).json({
|
|
message: "Terjadi kesalahan pada server",
|
|
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
});
|
|
}
|
|
}; |