require('dotenv').config() const bcrypt = require('bcrypt') const jwt = require('jsonwebtoken') const moment = require('moment') const crypto = require('crypto') const { Sequelize } = require('sequelize') const { sequelize } = require('../../../models/migration.js') const response = require('../../helpers/responses') const { sendOTP, generateOTP, sendForgotPassword, normalizePhone } = require('../../helpers/helpers') const UserResource = require('../resources/user.resource') const { sendMail } = require('../../config/mail.config') const db = require('../../../models/migration.js') const { password } = require('../../config/db.config.js') const errorHandler = require('../../middlewares/errorHandler.js') const { v4: uuidv4 } = require("uuid"); const User = db.User const Branch = db.Branch const UserOtp = db.UserOtp const PasswordReset = db.PasswordReset const { Op } = require('sequelize') const { OAuth2Client } = require("google-auth-library"); const signIn = async (req, res) => { const client = new OAuth2Client("GOOGLE_CLIENT_ID"); try { const { email, phone, password, token: tokenGoogle, login_via } = req.body; let user; if (email) { user = await User.findOne({ where: { email } }); if (login_via == 'GOOGLE') { // Verify Google token const ticket = await client.verifyIdToken({ idToken: tokenGoogle, // Changed from tokenGoogle to idToken audience: process.env.GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); const googleEmail = payload.email; // Find or create user with Google credentials user = await User.findOne({ where: { email: googleEmail } }); if (!user) { // Create new user if doesn't exist user = await User.create({ email: googleEmail, name: payload.name, login_via: 'GOOGLE', token: tokenGoogle, google_id: payload.sub, avatar_url: payload.picture, }); }else{ // Update user's Google token user.google_id = payload.sub; user.avatar_url = payload.picture; user.token = tokenGoogle; await user.save(); } } } else if (phone) { const normalizedPhone = normalizePhone(phone); user = await User.findOne({ where: { phone: normalizedPhone } }); } if (login_via === 'GOOGLE') { } else { if (!user || !(await bcrypt.compare(password, user.password))) { return res.status(400).json({ error: "Email / Phone atau Password salah" }); } } if (user.is_suspended) { return res.status(403).json({ error: "Akun Anda telah ditangguhkan" }); } const now = new Date(); // === Generate JWT mirip Supabase === const token = jwt.sign( { name: user.name, id: user.id, email: user.email, phone: user.phone || "", role: user.role || "user", }, process.env.JWT_SECRET_KEY ); // Update last login user.last_login = now; user.is_first_login = false; await user.save(); return res.json({ access_token: token, token_type: "bearer", user: { id: user.id, name: user.name, email: user.email, role: user.role || "user", created_at: user.created_at.toISOString(), }, }); } catch (error) { errorHandler(error, req, res) return response.failed(res, 500, error.message) } }; const sendOtp = async (req, res) => { try { const { email, phone, via } = req.body const otp = generateOTP(6) const hash = await bcrypt.hash(otp, 10) const data = via === 'WHATSAPP' ? email : phone const otpData = await UserOtp.create({ data, otp, token: hash, expire_in: 60, via }) await sendOTP('OTP', otp, via, phone, email) return response.success(res, { otpData }, `OTP dikirim ${via}`) } catch (error) { return response.failed(res, 500, error.message) } } const checkOtp = async (req, res) => { try { const { otp, token } = req.body const data = await UserOtp.findOne({ where: { otp, token } }) if (!data) { return response.failed(res, 404, 'OTP tidak valid') } const userCondition = data.via === 'WHATSAPP' ? { email: data.data } : { phone: data.data } let user = await User.findOne({ where: userCondition }) if (!user) { user = await User.create(userCondition) } const jwtToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '1d' }) return response.success(res, { user: new UserResource(user), token: jwtToken }, 'Login OTP berhasil') } catch (error) { return response.failed(res, 500, error.message) } } const signUp = async (req, res) => { try { const { name, email, password, role, branch_id } = req.body; // Cek apakah email / phone sudah ada const existingUser = await User.findOne({ where: { [Sequelize.Op.or]: [{ email }], }, }); if (existingUser) { return res.status(400).json({ error: "Email sudah terdaftar" }); } const branch = await Branch.findOne({ where: { id: branch_id } }); if (!branch) { return response.failed(res, 404, "Branch tidak ditemukan"); } // Hash password const hashedPassword = await bcrypt.hash(password, 10); // Buat user baru dalam transaksi const user = await sequelize.transaction(async (t) => { const newUser = await User.create( { name, email, branch_id, password: hashedPassword, role: role || "user", }, { transaction: t } ); return newUser; }); // Generate token JWT const token = jwt.sign( { name: user.name, id: user.id, email: user.email, phone: user.phone || "", role: user.role || "user", }, process.env.JWT_SECRET_KEY ); const refreshToken = uuidv4(); const now = new Date(); // Update last_login setelah register user.last_login = now; user.is_first_login = false; await user.save(); return res.json({ access_token: token, token_type: "bearer", user: { id: user.id, name: user.name, email: user.email, branch: branch.name, role: user.role || "user", created_at: user.created_at.toISOString(), }, }); } catch (error) { errorHandler(error, req, res) return response.failed(res, 500, error.message) } }; const getUserLogin = async (token, res) => { try { const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY) const user = await User.findByPk(decoded.id, { attributes: { exclude: ['password'] } }) if (!user) { return response.failed(res, 404, 'User Tidak Ditemukan') } return response.success(res, new UserResource(user), 'User ditemukan') } catch (error) { return response.failed(res, 401, 'Token Tidak Valid atau expired') } } const forgotPassword = async (body, res) => { const [user, resetPassword] = await Promise.all([ User.findOne({ where: { email: body.email, }, }), PasswordReset.findOne({ where: { email: body.email, is_used: false, }, }), ]); if (!user) { return response.error(res, 1307, 400); } if (resetPassword) { resetPassword.token = crypto.randomBytes(20).toString('hex'); resetPassword.expires_at = moment().add(15, 'minutes').toISOString(); await resetPassword.save(); await sequelize.transaction(async () => { await sendForgotPassword(body.email, resetPassword.token); }); const tokenUpdated = { user, forgot_password: resetPassword, }; return response.success(res, tokenUpdated, 'Token Updated'); } const emailToken = crypto.randomBytes(20).toString('hex'); const expiresAt = moment().add(15, 'minutes').toISOString(); const dataPasswordReset = await PasswordReset.create({ email: body.email, token: emailToken, expires_at: expiresAt, }); const tokenCreated = { user, forgot_password: dataPasswordReset, }; return response.success(res, tokenCreated, 'Token Created'); }; const resetPassword = async (body, token, res) => { try { const resetPassword = await PasswordReset.findOne({ where: { is_used: false, token: token, }, }); console.log('resetPassword data:', resetPassword); if (!resetPassword) { return response.error(res, 1306, 400); } const now = moment(); const expiresAt = moment(resetPassword.expires_at); if (now.isAfter(expiresAt)) { return response.error(res, 1306, 400); } const password = body.new_password; const confirm_password = body.confirm_password; if (password !== confirm_password) { return response.failed(res, 1105, 400); } const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(password, salt); const user = await User.findOne({ where: { email: resetPassword.email, }, }); user.password = hash; user.is_default = false; await user.save(); resetPassword.is_used = true; await resetPassword.save(); return response.success(res, null, 'Password reset successfully'); } catch (error) { console.error('Reset password error:', error); return response.failed(res, 500, error.message || 'Something went wrong'); } }; module.exports = { signIn, sendOtp, checkOtp, signUp, getUserLogin, resetPassword, forgotPassword }