← back to Watches
middleware/auth.js
337 lines
/**
* Authentication Middleware
* Session validation, user loading, and route protection
*/
import pool from '../database/connection.js';
/**
* Load user from session token
* Adds req.user if valid session exists
*/
export async function loadUser(req, res, next) {
try {
const sessionToken = req.cookies?.session_token;
if (!sessionToken) {
req.user = null;
return next();
}
// Validate session and get user
const result = await pool.query(
`SELECT u.id, u.email, u.name, u.avatar_url, u.role, u.provider,
u.email_verified, u.created_at
FROM users u
JOIN user_sessions s ON u.id = s.user_id
WHERE s.session_token = $1
AND s.is_valid = true
AND s.expires_at > NOW()
AND u.is_active = true`,
[sessionToken]
);
if (result.rows.length === 0) {
// Invalid or expired session - clear cookie
res.clearCookie('session_token');
req.user = null;
return next();
}
// Update session last_active_at
await pool.query(
`UPDATE user_sessions SET last_active_at = NOW() WHERE session_token = $1`,
[sessionToken]
);
req.user = result.rows[0];
next();
} catch (error) {
console.error('Auth middleware error:', error);
req.user = null;
next();
}
}
/**
* Require authentication
* Returns 401 if not authenticated
*/
export function requireAuth(req, res, next) {
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
next();
}
/**
* Require specific role
* @param {string[]} roles - Allowed roles
*/
export function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required',
code: 'AUTH_REQUIRED'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions',
code: 'FORBIDDEN'
});
}
next();
};
}
/**
* Require admin role
*/
export const requireAdmin = requireRole('admin');
/**
* Optional auth - loads user if present but doesn't require it
*/
export function optionalAuth(req, res, next) {
// loadUser already handles this, just continue
next();
}
/**
* Logout - invalidate session
*/
export async function logout(req, res) {
try {
const sessionToken = req.cookies?.session_token;
if (sessionToken) {
// Invalidate session in database
await pool.query(
`UPDATE user_sessions
SET is_valid = false, revoked_at = NOW(), revoke_reason = 'logout'
WHERE session_token = $1`,
[sessionToken]
);
}
// Clear cookie
res.clearCookie('session_token');
res.json({ success: true, message: 'Logged out successfully' });
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ success: false, error: 'Logout failed' });
}
}
/**
* Logout all sessions for a user
*/
export async function logoutAll(req, res) {
try {
if (!req.user) {
return res.status(401).json({ success: false, error: 'Not authenticated' });
}
// Invalidate all sessions
await pool.query(
`UPDATE user_sessions
SET is_valid = false, revoked_at = NOW(), revoke_reason = 'logout_all'
WHERE user_id = $1 AND is_valid = true`,
[req.user.id]
);
// Clear current cookie
res.clearCookie('session_token');
res.json({ success: true, message: 'All sessions logged out' });
} catch (error) {
console.error('Logout all error:', error);
res.status(500).json({ success: false, error: 'Logout failed' });
}
}
/**
* Refresh session token
*/
export async function refreshSession(req, res) {
try {
const refreshToken = req.body.refreshToken;
if (!refreshToken) {
return res.status(400).json({ success: false, error: 'Refresh token required' });
}
// Find valid refresh token
const result = await pool.query(
`SELECT s.*, u.id as user_id, u.email, u.name, u.avatar_url
FROM user_sessions s
JOIN users u ON s.user_id = u.id
WHERE s.refresh_token = $1
AND s.is_valid = true
AND s.refresh_expires_at > NOW()
AND u.is_active = true`,
[refreshToken]
);
if (result.rows.length === 0) {
return res.status(401).json({ success: false, error: 'Invalid refresh token' });
}
const session = result.rows[0];
// Generate new session token
const { v4: uuidv4 } = await import('uuid');
const newSessionToken = uuidv4();
const newExpiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
// Update session
await pool.query(
`UPDATE user_sessions
SET session_token = $1, expires_at = $2, last_active_at = NOW()
WHERE id = $3`,
[newSessionToken, newExpiresAt, session.id]
);
// Set new cookie
res.cookie('session_token', newSessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'lax'
});
res.json({
success: true,
user: {
id: session.user_id,
email: session.email,
name: session.name,
avatar_url: session.avatar_url
}
});
} catch (error) {
console.error('Refresh session error:', error);
res.status(500).json({ success: false, error: 'Session refresh failed' });
}
}
/**
* Get current user
*/
export function getCurrentUser(req, res) {
if (!req.user) {
return res.json({ success: true, user: null });
}
res.json({
success: true,
user: {
id: req.user.id,
email: req.user.email,
name: req.user.name,
avatar_url: req.user.avatar_url,
role: req.user.role,
provider: req.user.provider,
email_verified: req.user.email_verified
}
});
}
/**
* Get user's active sessions
*/
export async function getSessions(req, res) {
try {
if (!req.user) {
return res.status(401).json({ success: false, error: 'Not authenticated' });
}
const result = await pool.query(
`SELECT id, user_agent, ip_address, device_type, created_at, last_active_at,
session_token = $2 as is_current
FROM user_sessions
WHERE user_id = $1 AND is_valid = true AND expires_at > NOW()
ORDER BY last_active_at DESC`,
[req.user.id, req.cookies?.session_token]
);
res.json({
success: true,
sessions: result.rows.map(s => ({
id: s.id,
device: s.device_type,
userAgent: s.user_agent,
ip: s.ip_address,
createdAt: s.created_at,
lastActive: s.last_active_at,
isCurrent: s.is_current
}))
});
} catch (error) {
console.error('Get sessions error:', error);
res.status(500).json({ success: false, error: 'Failed to get sessions' });
}
}
/**
* Revoke a specific session
*/
export async function revokeSession(req, res) {
try {
if (!req.user) {
return res.status(401).json({ success: false, error: 'Not authenticated' });
}
const { sessionId } = req.params;
// Only allow revoking own sessions
const result = await pool.query(
`UPDATE user_sessions
SET is_valid = false, revoked_at = NOW(), revoke_reason = 'user_revoked'
WHERE id = $1 AND user_id = $2 AND is_valid = true
RETURNING id`,
[sessionId, req.user.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Session not found' });
}
res.json({ success: true, message: 'Session revoked' });
} catch (error) {
console.error('Revoke session error:', error);
res.status(500).json({ success: false, error: 'Failed to revoke session' });
}
}
export default {
loadUser,
requireAuth,
requireRole,
requireAdmin,
optionalAuth,
logout,
logoutAll,
refreshSession,
getCurrentUser,
getSessions,
revokeSession
};