Files
cim_summary/backend/src/controllers/authController.ts
Jon 5a3c961bfc feat: Complete implementation of Tasks 1-5 - CIM Document Processor
Backend Infrastructure:
- Complete Express server setup with security middleware (helmet, CORS, rate limiting)
- Comprehensive error handling and logging with Winston
- Authentication system with JWT tokens and session management
- Database models and migrations for Users, Documents, Feedback, and Processing Jobs
- API routes structure for authentication and document management
- Integration tests for all server components (86 tests passing)

Frontend Infrastructure:
- React application with TypeScript and Vite
- Authentication UI with login form, protected routes, and logout functionality
- Authentication context with proper async state management
- Component tests with proper async handling (25 tests passing)
- Tailwind CSS styling and responsive design

Key Features:
- User registration, login, and authentication
- Protected routes with role-based access control
- Comprehensive error handling and user feedback
- Database schema with proper relationships
- Security middleware and validation
- Production-ready build configuration

Test Coverage: 111/111 tests passing
Tasks Completed: 1-5 (Project setup, Database, Auth system, Frontend UI, Backend infrastructure)

Ready for Task 6: File upload backend infrastructure
2025-07-27 13:29:26 -04:00

464 lines
11 KiB
TypeScript

import { Request, Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth';
import { UserModel } from '../models/UserModel';
import {
generateAuthTokens,
verifyRefreshToken,
hashPassword,
comparePassword,
validatePassword
} from '../utils/auth';
import { sessionService } from '../services/sessionService';
import logger from '../utils/logger';
export interface RegisterRequest extends Request {
body: {
email: string;
name: string;
password: string;
};
}
export interface LoginRequest extends Request {
body: {
email: string;
password: string;
};
}
export interface RefreshTokenRequest extends Request {
body: {
refreshToken: string;
};
}
/**
* Register a new user
*/
export async function register(req: RegisterRequest, res: Response): Promise<void> {
try {
const { email, name, password } = req.body;
// Validate input
if (!email || !name || !password) {
res.status(400).json({
success: false,
message: 'Email, name, and password are required'
});
return;
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
res.status(400).json({
success: false,
message: 'Invalid email format'
});
return;
}
// Validate password strength
const passwordValidation = validatePassword(password);
if (!passwordValidation.isValid) {
res.status(400).json({
success: false,
message: 'Password does not meet requirements',
errors: passwordValidation.errors
});
return;
}
// Check if user already exists
const existingUser = await UserModel.findByEmail(email);
if (existingUser) {
res.status(409).json({
success: false,
message: 'User with this email already exists'
});
return;
}
// Hash password
const hashedPassword = await hashPassword(password);
// Create user
const user = await UserModel.create({
email,
name,
password: hashedPassword,
role: 'user'
});
// Generate tokens
const tokens = generateAuthTokens({
userId: user.id,
email: user.email,
role: user.role
});
// Store session
await sessionService.storeSession(user.id, {
userId: user.id,
email: user.email,
role: user.role,
refreshToken: tokens.refreshToken
});
logger.info(`New user registered: ${email}`);
res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
tokens: {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn
}
}
});
} catch (error) {
logger.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Internal server error during registration'
});
}
}
/**
* Login user
*/
export async function login(req: LoginRequest, res: Response): Promise<void> {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
res.status(400).json({
success: false,
message: 'Email and password are required'
});
return;
}
// Find user by email
const user = await UserModel.findByEmail(email);
if (!user) {
res.status(401).json({
success: false,
message: 'Invalid email or password'
});
return;
}
// Check if user is active
if (!user.is_active) {
res.status(401).json({
success: false,
message: 'Account is deactivated'
});
return;
}
// Verify password
const isPasswordValid = await comparePassword(password, user.password_hash);
if (!isPasswordValid) {
res.status(401).json({
success: false,
message: 'Invalid email or password'
});
return;
}
// Generate tokens
const tokens = generateAuthTokens({
userId: user.id,
email: user.email,
role: user.role
});
// Store session
await sessionService.storeSession(user.id, {
userId: user.id,
email: user.email,
role: user.role,
refreshToken: tokens.refreshToken
});
// Update last login
await UserModel.updateLastLogin(user.id);
logger.info(`User logged in: ${email}`);
res.status(200).json({
success: true,
message: 'Login successful',
data: {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
},
tokens: {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn
}
}
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Internal server error during login'
});
}
}
/**
* Logout user
*/
export async function logout(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
message: 'Authentication required'
});
return;
}
// Get the token from header for blacklisting
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
if (token) {
// Blacklist the access token
await sessionService.blacklistToken(token, 3600); // 1 hour
}
}
// Remove session
await sessionService.removeSession(req.user.userId);
logger.info(`User logged out: ${req.user.email}`);
res.status(200).json({
success: true,
message: 'Logout successful'
});
} catch (error) {
logger.error('Logout error:', error);
res.status(500).json({
success: false,
message: 'Internal server error during logout'
});
}
}
/**
* Refresh access token
*/
export async function refreshToken(req: RefreshTokenRequest, res: Response): Promise<void> {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
res.status(400).json({
success: false,
message: 'Refresh token is required'
});
return;
}
// Verify refresh token
const decoded = verifyRefreshToken(refreshToken);
// Check if user exists and is active
const user = await UserModel.findById(decoded.userId);
if (!user || !user.is_active) {
res.status(401).json({
success: false,
message: 'Invalid refresh token'
});
return;
}
// Check if session exists and matches
const session = await sessionService.getSession(decoded.userId);
if (!session || session.refreshToken !== refreshToken) {
res.status(401).json({
success: false,
message: 'Invalid refresh token'
});
return;
}
// Generate new tokens
const tokens = generateAuthTokens({
userId: user.id,
email: user.email,
role: user.role
});
// Update session with new refresh token
await sessionService.storeSession(user.id, {
userId: user.id,
email: user.email,
role: user.role,
refreshToken: tokens.refreshToken
});
// Blacklist old refresh token
await sessionService.blacklistToken(refreshToken, 86400); // 24 hours
logger.info(`Token refreshed for user: ${user.email}`);
res.status(200).json({
success: true,
message: 'Token refreshed successfully',
data: {
tokens: {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn
}
}
});
} catch (error) {
logger.error('Token refresh error:', error);
res.status(401).json({
success: false,
message: 'Invalid refresh token'
});
}
}
/**
* Get current user profile
*/
export async function getProfile(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
message: 'Authentication required'
});
return;
}
const user = await UserModel.findById(req.user.userId);
if (!user) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
created_at: user.created_at,
last_login: user.last_login
}
}
});
} catch (error) {
logger.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}
/**
* Update user profile
*/
export async function updateProfile(req: AuthenticatedRequest, res: Response): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
message: 'Authentication required'
});
return;
}
const { name, email } = req.body;
// Validate input
if (email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
res.status(400).json({
success: false,
message: 'Invalid email format'
});
return;
}
// Check if email is already taken by another user
const existingUser = await UserModel.findByEmail(email);
if (existingUser && existingUser.id !== req.user.userId) {
res.status(409).json({
success: false,
message: 'Email is already taken'
});
return;
}
}
// Update user
const updatedUser = await UserModel.update(req.user.userId, {
name: name || undefined,
email: email || undefined
});
if (!updatedUser) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
logger.info(`Profile updated for user: ${req.user.email}`);
res.status(200).json({
success: true,
message: 'Profile updated successfully',
data: {
user: {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
role: updatedUser.role,
created_at: updatedUser.created_at,
last_login: updatedUser.last_login
}
}
});
} catch (error) {
logger.error('Update profile error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
}