48hr.email/infrastructure/web/routes/auth.js

259 lines
9.3 KiB
JavaScript

const express = require('express')
const router = new express.Router()
const { body, validationResult } = require('express-validator')
const debug = require('debug')('48hr-email:auth-routes')
const { redirectIfAuthenticated } = require('../middleware/auth')
const config = require('../../../application/config')
const templateContext = require('../template-context')
// Simple in-memory rate limiters for registration and login
const registrationRateLimitStore = new Map()
const loginRateLimitStore = new Map()
// Registration rate limiter: 5 attempts per IP per hour
const registrationRateLimiter = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress
const now = Date.now()
const windowMs = 60 * 60 * 1000 // 1 hour
const maxRequests = 5
// Clean up old entries
for (const [key, data] of registrationRateLimitStore.entries()) {
if (now - data.resetTime > windowMs) {
registrationRateLimitStore.delete(key)
}
}
// Get or create entry for this IP
let ipData = registrationRateLimitStore.get(ip)
if (!ipData || now - ipData.resetTime > windowMs) {
ipData = { count: 0, resetTime: now }
registrationRateLimitStore.set(ip, ipData)
}
// Check if limit exceeded
if (ipData.count >= maxRequests) {
debug(`Registration rate limit exceeded for IP ${ip}`)
req.session.errorMessage = 'Too many registration attempts. Please try again after 1 hour.'
return res.redirect('/register')
}
// Increment counter
ipData.count++
next()
}
// Login rate limiter: 10 attempts per IP per 15 minutes
const loginRateLimiter = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress
const now = Date.now()
const windowMs = 15 * 60 * 1000 // 15 minutes
const maxRequests = 10
// Clean up old entries
for (const [key, data] of loginRateLimitStore.entries()) {
if (now - data.resetTime > windowMs) {
loginRateLimitStore.delete(key)
}
}
// Get or create entry for this IP
let ipData = loginRateLimitStore.get(ip)
if (!ipData || now - ipData.resetTime > windowMs) {
ipData = { count: 0, resetTime: now }
loginRateLimitStore.set(ip, ipData)
}
// Check if limit exceeded
if (ipData.count >= maxRequests) {
debug(`Login rate limit exceeded for IP ${ip}`)
req.session.errorMessage = 'Too many login attempts. Please try again after 15 minutes.'
return res.redirect('/login')
}
// Increment counter
ipData.count++
next()
}
// Middleware to capture redirect URL
router.use((req, res, next) => {
if (req.method === 'GET' && req.path === '/auth') {
const referer = req.get('Referer')
const redirectParam = req.query.redirect
const redirectUrl = redirectParam || (referer && !referer.includes('/auth') ? referer : null)
if (redirectUrl) {
req.session.redirectAfterLogin = redirectUrl
}
}
next()
})
// GET /auth - Show unified auth page (login or register)
router.get('/auth', redirectIfAuthenticated, (req, res) => {
const config = req.app.get('config')
const successMessage = req.session.successMessage
delete req.session.successMessage
res.render('auth', templateContext.build(req, {
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
successMessage
}))
})
// POST /register - Process registration
router.post('/register',
redirectIfAuthenticated,
registrationRateLimiter,
body('username').trim().notEmpty().withMessage('Username is required'),
body('password').notEmpty().withMessage('Password is required'),
body('confirmPassword').notEmpty().withMessage('Password confirmation is required'),
async(req, res) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
const firstError = errors.array()[0].msg
debug(`Registration validation failed: ${firstError}`)
req.session.errorMessage = firstError
return res.redirect('/auth')
}
const { username, password, confirmPassword } = req.body
// Check if passwords match
if (password !== confirmPassword) {
debug('Registration failed: Passwords do not match')
req.session.errorMessage = 'Passwords do not match'
return res.redirect('/auth')
}
const authService = req.app.get('authService')
const result = await authService.register(username, password)
if (result.success) {
debug(`User registered successfully: ${username}`)
req.session.successMessage = 'Registration successful! Please log in.'
return res.redirect('/auth')
} else {
debug(`Registration failed: ${result.error}`)
req.session.errorMessage = result.error
return res.redirect('/auth')
}
} catch (error) {
debug(`Registration error: ${error.message}`)
console.error('Error during registration', error)
req.session.errorMessage = 'An unexpected error occurred. Please try again.'
res.redirect('/auth')
}
}
)
// POST /login - Process login
router.post('/login',
redirectIfAuthenticated,
loginRateLimiter,
body('username').trim().notEmpty().withMessage('Username is required'),
body('password').notEmpty().withMessage('Password is required'),
async(req, res) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
const firstError = errors.array()[0].msg
debug(`Login validation failed: ${firstError}`)
req.session.errorMessage = firstError
return res.redirect('/auth')
}
const { username, password } = req.body
const authService = req.app.get('authService')
const result = await authService.login(username, password)
if (result.success) {
debug(`User logged in successfully: ${username}`)
// Store redirect URL before regenerating session
const redirectUrl = req.session.redirectAfterLogin || '/'
// Regenerate session to prevent fixation attacks
req.session.regenerate((err) => {
if (err) {
debug(`Session regeneration error: ${err.message}`)
req.session.errorMessage = 'Login failed. Please try again.'
return res.redirect('/auth')
}
// Set session data
req.session.userId = result.user.id
req.session.username = result.user.username
req.session.isAuthenticated = true
req.session.createdAt = result.user.created_at
req.session.save((err) => {
if (err) {
debug(`Session save error: ${err.message}`)
req.session.errorMessage = 'Login failed. Please try again.'
return res.redirect('/auth')
}
debug(`Session created for user: ${username}, redirecting to: ${redirectUrl}`)
res.redirect(redirectUrl)
})
})
} else {
debug(`Login failed: ${result.error}`)
req.session.errorMessage = result.error
return res.redirect('/auth')
}
} catch (error) {
debug(`Login error: ${error.message}`)
console.error('Error during login', error)
req.session.errorMessage = 'An unexpected error occurred. Please try again.'
res.redirect('/auth')
}
}
)
// GET /logout - Logout user
router.get('/logout', (req, res) => {
// Store redirect URL before destroying session
const redirectUrl = req.query.redirect || req.get('Referer') || '/'
debug(`Logout requested with redirect: ${redirectUrl}`)
if (req.session) {
const username = req.session.username
req.session.destroy((err) => {
if (err) {
debug(`Logout error: ${err.message}`)
console.error('Error during logout', err)
return res.redirect('/')
}
debug(`User logged out: ${username}, redirecting to: ${redirectUrl}`)
// Clear cookie explicitly
res.clearCookie('connect.sid')
res.redirect(redirectUrl)
})
} else {
debug(`No session found, redirecting to: ${redirectUrl}`)
res.redirect(redirectUrl)
}
}) // GET /auth/check - JSON endpoint for checking auth status (AJAX)
router.get('/auth/check', (req, res) => {
if (req.session && req.session.userId && req.session.isAuthenticated) {
res.json({
authenticated: true,
user: {
id: req.session.userId,
username: req.session.username
}
})
} else {
res.json({
authenticated: false
})
}
})
module.exports = router