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
464 lines
11 KiB
TypeScript
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'
|
|
});
|
|
}
|
|
}
|