48hr.email/infrastructure/web/routes/auth.js
ClaraCrazy 598cea9b9c
[Feat]: Add User Registration
Add User table to sql, add user-repository, add registration and login routes, update config
2026-01-02 16:27:43 +01:00

259 lines
8.9 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')
// 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()
}
// GET /register - Show registration form
router.get('/register', redirectIfAuthenticated, (req, res) => {
const config = req.app.get('config')
const errorMessage = req.session.errorMessage
const successMessage = req.session.successMessage
// Clear messages after reading
delete req.session.errorMessage
delete req.session.successMessage
res.render('register', {
title: `Register | ${config.http.branding[0]}`,
branding: config.http.branding,
errorMessage,
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('/register')
}
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('/register')
}
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('/login')
} else {
debug(`Registration failed: ${result.error}`)
req.session.errorMessage = result.error
return res.redirect('/register')
}
} 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('/register')
}
}
)
// GET /login - Show login form
router.get('/login', redirectIfAuthenticated, (req, res) => {
const config = req.app.get('config')
const errorMessage = req.session.errorMessage
const successMessage = req.session.successMessage
// Clear messages after reading
delete req.session.errorMessage
delete req.session.successMessage
res.render('login-auth', {
title: `Login | ${config.http.branding[0]}`,
branding: config.http.branding,
errorMessage,
successMessage
})
})
// 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('/login')
}
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}`)
// Regenerate session to prevent fixation attacks
const redirectUrl = req.session.redirectAfterLogin || '/'
req.session.regenerate((err) => {
if (err) {
debug(`Session regeneration error: ${err.message}`)
req.session.errorMessage = 'Login failed. Please try again.'
return res.redirect('/login')
}
// 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('/login')
}
debug(`Session created for user: ${username}`)
res.redirect(redirectUrl)
})
})
} else {
debug(`Login failed: ${result.error}`)
req.session.errorMessage = result.error
return res.redirect('/login')
}
} 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('/login')
}
}
)
// GET /logout - Logout user
router.get('/logout', (req, res) => {
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)
} else {
debug(`User logged out: ${username}`)
}
res.redirect('/')
})
} else {
res.redirect('/')
}
})
// 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