Spaces:
Paused
Paused
Download controllers/auth.controller.js from imkrish/yatra-x-user: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/imkrish/yatra-x-user/resolve/main/controllers/auth.controller.js
- Command line
-
hf download hf://spaces/imkrish/yatra-x-user/controllers/auth.controller.js
-
curl -L -o auth.controller.js https://huggingface.co/spaces/imkrish/yatra-x-user/resolve/main/controllers/auth.controller.js
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 | |
| }; | |