yatra-x-user / controllers /auth.controller.js
imkrish's picture
Upload folder using huggingface_hub
db00c33 verified
Raw History Blame Contribute Delete
14.6 kB
/**
* Auth Controller - Authentication logic (PostgreSQL)
*/
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { getPool } = require('../db');
const googleService = require('../services/google.service');
const emailService = require('../services/email.service');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m'; // Short-lived access token
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || '30d'; // Long refresh token
// Security check: Ensure JWT_SECRET is configured
if (!JWT_SECRET) {
console.error('⚠️ SECURITY WARNING: JWT_SECRET environment variable is not set!');
if (process.env.NODE_ENV === 'production') {
console.error('❌ FATAL: Cannot run in production without JWT_SECRET');
process.exit(1);
}
}
/**
* Generate JWT tokens
*/
function generateTokens(user) {
const accessToken = jwt.sign(
{ id: user.id, sub: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
const refreshToken = jwt.sign(
{ id: user.id, type: 'refresh' },
JWT_SECRET,
{ expiresIn: JWT_REFRESH_EXPIRES_IN }
);
return { accessToken, refreshToken };
}
/**
* Register new user
*/
async function register(req, res) {
const { name, email, password } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
// Check if user exists
const existingResult = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
if (existingResult.rows.length > 0) {
return res.status(400).json({ error: 'Email already registered' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const query = `
INSERT INTO users (name, email, password_hash, role)
VALUES ($1, $2, $3, $4)
RETURNING id, name, email, role, created_at
`;
const result = await pool.query(query, [name, email, hashedPassword, 'tourist']);
const user = result.rows[0];
const tokens = generateTokens(user);
res.status(201).json({ user, tokens });
} catch (error) {
console.error('[Register Error]', error.message);
res.status(500).json({ error: 'Registration failed' });
}
}
// In-memory OTP store (use Redis in production)
const otpStore = new Map();
/**
* Send OTP to email for verification
*/
async function sendOtp(req, res) {
const { email } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
try {
// Check if email already registered
const existingResult = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
if (existingResult.rows.length > 0) {
return res.status(400).json({ error: 'Email already registered' });
}
// Generate 6-digit OTP
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
// Store OTP
otpStore.set(email, { otp, expiresAt });
// Send email with OTP
try {
await emailService.sendOtpEmail(email, otp);
console.log(`[SendOTP] OTP sent to ${email}`);
} catch (emailErr) {
console.error('[SendOTP] Email send failed:', emailErr.message);
// Still return success if email fails in dev - OTP is logged
if (process.env.NODE_ENV === 'production') {
return res.status(500).json({ error: 'Failed to send verification email' });
}
}
// Return success (hide OTP in production)
res.json({
success: true,
message: 'OTP sent to email',
// DEV ONLY - remove in production:
_devOtp: process.env.NODE_ENV !== 'production' ? otp : undefined
});
} catch (error) {
console.error('[SendOTP Error]', error.message);
res.status(500).json({ error: 'Failed to send OTP' });
}
}
/**
* Verify OTP (internal helper)
*/
function verifyOtp(email, otp) {
const stored = otpStore.get(email);
if (!stored) return { valid: false, error: 'OTP not found. Please request a new one.' };
if (Date.now() > stored.expiresAt) {
otpStore.delete(email);
return { valid: false, error: 'OTP expired. Please request a new one.' };
}
if (stored.otp !== otp) return { valid: false, error: 'Invalid OTP' };
otpStore.delete(email); // Clear after verification
return { valid: true };
}
/**
* Login with email/password
*/
async function login(req, res) {
const { email, password } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const validPassword = await bcrypt.compare(password, user.password_hash || '');
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user);
// Remove password from response
delete user.password_hash;
res.json({ user, tokens });
} catch (error) {
console.error('[Login Error]', error.message);
res.status(500).json({ error: 'Login failed' });
}
}
/**
* Google OAuth login
*/
async function googleLogin(req, res) {
const { accessToken } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
// Verify Google token and get profile
const profile = await googleService.verifyToken(accessToken);
if (!profile || !profile.email) {
return res.status(401).json({ error: 'Invalid Google token' });
}
// Find or create user
const existingResult = await pool.query('SELECT * FROM users WHERE email = $1', [profile.email]);
let user;
if (existingResult.rows.length === 0) {
// Create new user from Google profile
const query = `
INSERT INTO users (name, email, avatar, google_id, role, auth_provider)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, email, avatar, role, created_at
`;
const newUserResult = await pool.query(query, [
profile.name || 'Google User',
profile.email,
profile.picture || null,
profile.sub,
'tourist',
'google'
]);
user = newUserResult.rows[0];
} else {
user = existingResult.rows[0];
delete user.password_hash;
}
const tokens = generateTokens(user);
res.json({ user, tokens });
} catch (error) {
console.error('[Google Login Error]', error.message);
res.status(500).json({ error: 'Google login failed' });
}
}
/**
* Refresh tokens
*/
async function refresh(req, res) {
const { refreshToken } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
const decoded = jwt.verify(refreshToken, JWT_SECRET);
if (decoded.type !== 'refresh') {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const result = await pool.query('SELECT id, email, name, role FROM users WHERE id = $1', [decoded.id]);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'User not found' });
}
const user = result.rows[0];
const tokens = generateTokens(user);
res.json({ user, tokens });
} catch (error) {
res.status(401).json({ error: 'Invalid or expired refresh token' });
}
}
/**
* Get current user
*/
async function currentUser(req, res) {
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
const result = await pool.query('SELECT id, email, name, avatar, role, created_at FROM users WHERE id = $1', [req.user.id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user: result.rows[0] });
} catch (error) {
console.error('[CurrentUser Error]', error.message);
res.status(500).json({ error: 'Failed to fetch user' });
}
}
/**
* Request password reset - generates token and stores in DB
* In production, send email with reset link
*/
async function forgotPassword(req, res) {
const { email } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
try {
const result = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
// Always return success to prevent email enumeration
if (result.rows.length === 0) {
return res.json({ success: true, message: 'If a user exists with this email, a reset link will be sent.' });
}
const userId = result.rows[0].id;
const crypto = require('crypto');
const resetToken = crypto.randomBytes(32).toString('hex');
const hashedToken = await bcrypt.hash(resetToken, 10);
const expiresAt = new Date(Date.now() + 3600000); // 1 hour
// Store reset token in database (create table if needed)
await pool.query(`
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
`);
// Delete old tokens for this user
await pool.query('DELETE FROM password_reset_tokens WHERE user_id = $1', [userId]);
// Insert new token
await pool.query(
'INSERT INTO password_reset_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)',
[userId, hashedToken, expiresAt]
);
// TODO: Send email with reset link containing resetToken
console.log(`[ForgotPassword] Reset token generated for ${email}. Token: ${resetToken}`);
res.json({
success: true,
message: 'If a user exists with this email, a reset link will be sent.',
// DEV ONLY - remove in production:
_devToken: process.env.NODE_ENV !== 'production' ? resetToken : undefined
});
} catch (error) {
console.error('[ForgotPassword Error]', error.message);
res.status(500).json({ error: 'Password reset request failed' });
}
}
/**
* Reset password using token
*/
async function resetPassword(req, res) {
const { token, email, newPassword } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
if (!token || !email || !newPassword) {
return res.status(400).json({ error: 'Token, email, and new password are required' });
}
try {
// Find user and their reset token
const userResult = await pool.query('SELECT id FROM users WHERE email = $1', [email]);
if (userResult.rows.length === 0) {
return res.status(400).json({ error: 'Invalid or expired reset token' });
}
const userId = userResult.rows[0].id;
const tokenResult = await pool.query(
'SELECT token_hash, expires_at FROM password_reset_tokens WHERE user_id = $1 AND used = false',
[userId]
);
if (tokenResult.rows.length === 0) {
return res.status(400).json({ error: 'Invalid or expired reset token' });
}
const { token_hash, expires_at } = tokenResult.rows[0];
// Check expiry
if (new Date() > new Date(expires_at)) {
return res.status(400).json({ error: 'Reset token has expired' });
}
// Verify token
const validToken = await bcrypt.compare(token, token_hash);
if (!validToken) {
return res.status(400).json({ error: 'Invalid or expired reset token' });
}
// Update password
const hashedPassword = await bcrypt.hash(newPassword, 12);
await pool.query('UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2', [hashedPassword, userId]);
// Mark token as used
await pool.query('UPDATE password_reset_tokens SET used = true WHERE user_id = $1', [userId]);
res.json({ success: true, message: 'Password has been reset successfully' });
} catch (error) {
console.error('[ResetPassword Error]', error.message);
res.status(500).json({ error: 'Password reset failed' });
}
}
/**
* Change password (authenticated)
*/
async function changePassword(req, res) {
const { currentPassword, newPassword } = req.body;
const pool = getPool();
if (!pool) return res.status(500).json({ error: 'Database not connected' });
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Current password and new password are required' });
}
try {
const userId = req.user.id || req.user.sub;
const result = await pool.query('SELECT password_hash FROM users WHERE id = $1', [userId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
const validPassword = await bcrypt.compare(currentPassword, result.rows[0].password_hash || '');
if (!validPassword) {
return res.status(401).json({ error: 'Current password is incorrect' });
}
const hashedPassword = await bcrypt.hash(newPassword, 12);
await pool.query('UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2', [hashedPassword, userId]);
res.json({ success: true, message: 'Password changed successfully' });
} catch (error) {
console.error('[ChangePassword Error]', error.message);
res.status(500).json({ error: 'Password change failed' });
}
}
/**
* Logout - For stateless JWT, just acknowledge (client removes token)
* Could implement token blacklist if needed
*/
async function logout(req, res) {
// Stateless JWT - just acknowledge
// For actual token invalidation, implement a blacklist in Redis/DB
res.json({ success: true, message: 'Logged out successfully' });
}
module.exports = {
register,
sendOtp,
login,
googleLogin,
refresh,
currentUser,
forgotPassword,
resetPassword,
changePassword,
logout
};