// backend/routes/auth/settings.js const express = require('express'); const bcrypt = require('bcryptjs'); const path = require('path'); const fs = require('fs'); const User = require('../../models/User'); const { requireAuth, makeToken, setCookieToken, upload, getUploadsDir } = require('./helpers'); const router = express.Router(); // ─── POST /api/auth/change-password ────────────────────────────────────────── router.post('/change-password', requireAuth, async (req, res) => { try { const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword) { return res.status(400).json({ error: 'Current password and new password are required' }); } const user = await User.findById(req.user.id).select('+password_hash'); if (!user) return res.status(404).json({ error: 'User not found' }); if (!bcrypt.compareSync(currentPassword, user.password_hash)) { return res.status(400).json({ error: 'Password saat ini salah' }); } const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/; if (!passwordRegex.test(newPassword)) { return res.status(400).json({ error: 'Password baru tidak memenuhi kriteria: minimal 6 karakter, serta mengandung huruf besar, huruf kecil, dan angka.' }); } user.password_hash = bcrypt.hashSync(newPassword, 10); await user.save(); res.json({ ok: true, message: 'Password berhasil diubah!' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── POST /api/auth/change-username ────────────────────────────────────────── router.post('/change-username', requireAuth, async (req, res) => { try { const { currentPassword, newUsername } = req.body; if (!currentPassword || !newUsername) { return res.status(400).json({ error: 'Current password and new username are required' }); } if (newUsername.length < 4 || /[^a-zA-Z0-9_]/.test(newUsername)) { return res.status(400).json({ error: 'Username baru tidak valid (minimal 4 karakter, hanya huruf, angka, dan underscore).' }); } const user = await User.findById(req.user.id).select('+password_hash'); if (!user) return res.status(404).json({ error: 'User not found' }); if (!bcrypt.compareSync(currentPassword, user.password_hash)) { return res.status(400).json({ error: 'Password saat ini salah' }); } const existing = await User.findOne({ username: newUsername }); if (existing) return res.status(400).json({ error: 'Username sudah digunakan oleh akun lain' }); user.username = newUsername; await user.save(); const newToken = makeToken(user); setCookieToken(res, newToken); res.json({ ok: true, message: 'Username berhasil diubah!', newUsername }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── POST /api/auth/change-account-name ────────────────────────────────────── router.post('/change-account-name', requireAuth, async (req, res) => { try { const { currentPassword, newAccountName } = req.body; if (!currentPassword || newAccountName == null) { return res.status(400).json({ error: 'Current password and new account name are required' }); } if (!newAccountName.trim()) { return res.status(400).json({ error: 'Nama akun tidak boleh kosong' }); } const user = await User.findById(req.user.id).select('+password_hash'); if (!user) return res.status(404).json({ error: 'User not found' }); if (!bcrypt.compareSync(currentPassword, user.password_hash)) { return res.status(400).json({ error: 'Password saat ini salah' }); } user.account_name = newAccountName.trim(); await user.save(); const newToken = makeToken(user); setCookieToken(res, newToken); res.json({ ok: true, message: 'Nama akun berhasil diubah!', newAccountName: user.account_name }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── POST /api/auth/upload-profile-picture ─────────────────────────────────── // Menerima JSON: { profile_picture_base64: "data:image/png;base64,...", user_id? } // Menghindari multipart/form-data yang bermasalah melalui Apache proxy layer router.post('/upload-profile-picture', requireAuth, async (req, res) => { try { const { profile_picture_base64, user_id } = req.body; if (!profile_picture_base64) { return res.status(400).json({ error: 'No image data provided. Please select an image file first.' }); } // Validasi format base64 data URL const matches = profile_picture_base64.match(/^data:image\/(png|jpg|jpeg|gif|webp);base64,(.+)$/); if (!matches) { return res.status(400).json({ error: 'Invalid image format. Only PNG, JPG, GIF, WEBP are allowed.' }); } const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1]; const base64Data = matches[2]; // Validasi ukuran (max 5MB uncompressed) const fileSizeBytes = Buffer.byteLength(base64Data, 'base64'); if (fileSizeBytes > 5 * 1024 * 1024) { return res.status(400).json({ error: 'Image too large. Maximum size is 5MB.' }); } // Tentukan target user (self atau admin update user lain) const targetId = user_id || req.user.id; const user = await User.findById(targetId); if (!user) return res.status(404).json({ error: 'User not found' }); // Hapus foto profil lama jika ada if (user.profile_picture) { const oldPath = path.join(getUploadsDir(), user.profile_picture); if (fs.existsSync(oldPath)) { try { fs.unlinkSync(oldPath); } catch (_) {} } } // Simpan file baru const filename = `profile-${targetId}-${Date.now()}.${ext}`; const filePath = path.join(getUploadsDir(), filename); fs.writeFileSync(filePath, base64Data, 'base64'); user.profile_picture = filename; await user.save(); // Perbarui token hanya jika user mengupdate foto dirinya sendiri if (String(targetId) === String(req.user.id)) { const newToken = makeToken(user); setCookieToken(res, newToken); } res.json({ ok: true, message: 'Profile picture updated successfully.', profile_picture: filename }); } catch (err) { console.error('[Upload Error]', err); res.status(500).json({ error: err.message }); } }); // ─── POST /api/auth/remove-profile-picture ─────────────────────────────────── router.post('/remove-profile-picture', requireAuth, async (req, res) => { try { const user = await User.findById(req.user.id); if (!user) return res.status(404).json({ error: 'User not found' }); if (user.profile_picture) { const filePath = path.join(getUploadsDir(), user.profile_picture); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } user.profile_picture = null; await user.save(); const newToken = makeToken(user); setCookieToken(res, newToken); res.json({ ok: true, message: 'Foto profil berhasil dihapus' }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;