mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/user-management
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-super-secret-jwt-key-here
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# CORS Configuration
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Rate Limiting Configuration
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# Password Configuration
|
||||
BCRYPT_SALT_ROUNDS=12
|
||||
|
||||
# Email Configuration (if implementing email features)
|
||||
# SMTP_HOST=smtp.gmail.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USER=your-email@gmail.com
|
||||
# SMTP_PASS=your-app-password
|
||||
@@ -0,0 +1,313 @@
|
||||
# User Management System
|
||||
|
||||
A comprehensive user management system built with Node.js, Express, and MongoDB. This project demonstrates enterprise-level patterns for user authentication, authorization, and management.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
- **User Registration & Authentication**: Secure user registration with JWT-based authentication
|
||||
- **Role-Based Access Control (RBAC)**: Admin, User, and Guest roles with permission system
|
||||
- **Password Security**: BCrypt hashing with configurable salt rounds
|
||||
- **Account Management**: User activation, deactivation, suspension, and soft deletion
|
||||
- **Profile Management**: User profile updates with validation
|
||||
- **Permission System**: Granular permissions for fine-grained access control
|
||||
|
||||
### Security Features
|
||||
- **Rate Limiting**: Configurable rate limits for different endpoints
|
||||
- **Input Validation**: Comprehensive validation using express-validator
|
||||
- **Security Headers**: Helmet.js for security headers
|
||||
- **CORS Protection**: Configurable CORS policies
|
||||
- **Account Lockout**: Automatic account lockout after failed login attempts
|
||||
- **JWT Security**: Secure token generation and validation
|
||||
|
||||
### API Features
|
||||
- **RESTful API**: Clean REST API design with proper HTTP methods
|
||||
- **Pagination**: Efficient pagination for large datasets
|
||||
- **Search Functionality**: Full-text search across user fields
|
||||
- **Filtering**: Role-based and status-based filtering
|
||||
- **Export Functionality**: User data export capabilities
|
||||
- **Statistics**: User statistics and analytics
|
||||
|
||||
### Development Features
|
||||
- **Error Handling**: Comprehensive error handling with custom error classes
|
||||
- **Logging**: Structured logging with Winston
|
||||
- **Documentation**: Detailed API documentation
|
||||
- **Testing**: Unit and integration tests with Jest
|
||||
- **Code Quality**: ESLint and Prettier configuration
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Runtime**: Node.js 16+
|
||||
- **Framework**: Express.js
|
||||
- **Database**: MongoDB with Mongoose ODM
|
||||
- **Authentication**: JSON Web Tokens (JWT)
|
||||
- **Password hashing**: BCrypt
|
||||
- **Validation**: Joi and express-validator
|
||||
- **Logging**: Winston
|
||||
- **Testing**: Jest and Supertest
|
||||
- **Security**: Helmet, CORS, Rate limiting
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (v16 or higher)
|
||||
- MongoDB (v4.4 or higher)
|
||||
- npm or yarn
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd user-management
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Environment configuration**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Update the `.env` file with your configuration:
|
||||
```env
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
MONGODB_URI=mongodb://localhost:27017/user-management
|
||||
JWT_SECRET=your-super-secret-jwt-key-here
|
||||
JWT_EXPIRES_IN=24h
|
||||
```
|
||||
|
||||
4. **Start MongoDB**
|
||||
```bash
|
||||
# Using MongoDB service
|
||||
sudo systemctl start mongod
|
||||
|
||||
# Or using Docker
|
||||
docker run -d -p 27017:27017 --name mongodb mongo:latest
|
||||
```
|
||||
|
||||
5. **Run the application**
|
||||
```bash
|
||||
# Development mode
|
||||
npm run dev
|
||||
|
||||
# Production mode
|
||||
npm start
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### Base URL
|
||||
```
|
||||
http://localhost:3000/api
|
||||
```
|
||||
|
||||
### Authentication
|
||||
Most endpoints require authentication. Include the JWT token in the Authorization header:
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### User Management
|
||||
- `POST /users` - Create new user
|
||||
- `GET /users` - Get all users (with pagination)
|
||||
- `GET /users/:id` - Get user by ID
|
||||
- `PUT /users/:id` - Update user
|
||||
- `DELETE /users/:id` - Delete user (soft delete)
|
||||
- `DELETE /users/:id/hard` - Permanently delete user
|
||||
|
||||
#### Authentication
|
||||
- `POST /users/auth` - User login
|
||||
- `PUT /users/:id/password` - Change password
|
||||
- `PUT /users/:id/reset-password` - Reset password (admin only)
|
||||
|
||||
#### Search & Filtering
|
||||
- `GET /users/search?q=query` - Search users
|
||||
- `GET /users/active` - Get active users
|
||||
- `GET /users/role/:role` - Get users by role
|
||||
|
||||
#### Permissions
|
||||
- `PUT /users/:id/permissions` - Add permission
|
||||
- `DELETE /users/:id/permissions` - Remove permission
|
||||
|
||||
#### Analytics
|
||||
- `GET /users/stats` - Get user statistics
|
||||
- `GET /users/export` - Export user data
|
||||
- `GET /users/:id/activity` - Get user activity
|
||||
|
||||
### Example Requests
|
||||
|
||||
#### Create User
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "john_doe",
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com",
|
||||
"password": "securepassword123",
|
||||
"age": 30,
|
||||
"role": "user"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Authenticate User
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/users/auth \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "john_doe",
|
||||
"password": "securepassword123"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Get Users (with authentication)
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/users \
|
||||
-H "Authorization: Bearer <your-jwt-token>"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
```
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── utils/
|
||||
├── integration/
|
||||
│ └── routes/
|
||||
└── setup/
|
||||
└── testSetup.js
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Linting
|
||||
npm run lint
|
||||
|
||||
# Formatting
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
src/
|
||||
├── config/
|
||||
│ └── database.js
|
||||
├── middleware/
|
||||
│ ├── auth.js
|
||||
│ ├── rateLimiter.js
|
||||
│ └── validate.js
|
||||
├── models/
|
||||
│ └── User.js
|
||||
├── routes/
|
||||
│ └── userRoutes.js
|
||||
├── services/
|
||||
│ └── UserService.js
|
||||
├── utils/
|
||||
│ ├── errors.js
|
||||
│ └── logger.js
|
||||
└── server.js
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
#### User Schema
|
||||
```javascript
|
||||
{
|
||||
id: String, // UUID
|
||||
username: String, // Unique, 3-20 chars
|
||||
email: String, // Optional, unique
|
||||
name: String, // Required, 1-100 chars
|
||||
age: Number, // Optional, 0-150
|
||||
password: String, // Hashed, min 8 chars
|
||||
role: String, // admin, user, guest
|
||||
status: String, // active, inactive, suspended, deleted
|
||||
lastLogin: Date, // Last login timestamp
|
||||
loginAttempts: Number, // Failed login counter
|
||||
permissions: [String], // Array of permissions
|
||||
metadata: Object, // Flexible metadata
|
||||
createdAt: Date, // Auto-generated
|
||||
updatedAt: Date // Auto-generated
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|-------|
|
||||
| `PORT` | Server port | 3000 |
|
||||
| `NODE_ENV` | Environment | development |
|
||||
| `MONGODB_URI` | MongoDB connection string | mongodb://localhost:27017/user-management |
|
||||
| `JWT_SECRET` | JWT secret key | Required |
|
||||
| `JWT_EXPIRES_IN` | JWT expiration time | 24h |
|
||||
| `ALLOWED_ORIGINS` | CORS allowed origins | http://localhost:3000 |
|
||||
| `LOG_LEVEL` | Logging level | info |
|
||||
| `BCRYPT_SALT_ROUNDS` | BCrypt salt rounds | 12 |
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Environment Variables**: Never commit sensitive data to version control
|
||||
2. **JWT Secret**: Use a strong, random JWT secret in production
|
||||
3. **Rate Limiting**: Adjust rate limits based on your requirements
|
||||
4. **Input Validation**: All inputs are validated and sanitized
|
||||
5. **Password Security**: Passwords are hashed using BCrypt with salt rounds
|
||||
6. **Account Lockout**: Accounts are locked after 5 failed login attempts
|
||||
7. **CORS**: Configure CORS origins for production
|
||||
8. **Security Headers**: Helmet.js provides security headers
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
1. **Database Indexing**: Indexes on frequently queried fields
|
||||
2. **Pagination**: Efficient pagination for large datasets
|
||||
3. **Connection Pooling**: MongoDB connection pooling
|
||||
4. **Compression**: Gzip compression for responses
|
||||
5. **Caching**: Ready for Redis integration
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests for new functionality
|
||||
5. Ensure all tests pass
|
||||
6. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Support
|
||||
|
||||
For support, please open an issue in the GitHub repository or contact the development team.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.0.0
|
||||
- Initial release
|
||||
- User management functionality
|
||||
- Authentication and authorization
|
||||
- API endpoints
|
||||
- Security features
|
||||
- Testing suite
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "user-management",
|
||||
"version": "1.0.0",
|
||||
"description": "A comprehensive user management system for testing Code Index MCP",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/ --fix",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"keywords": [
|
||||
"user-management",
|
||||
"nodejs",
|
||||
"express",
|
||||
"authentication",
|
||||
"api"
|
||||
],
|
||||
"author": "Test Author",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^7.4.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"joi": "^17.9.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.0.0",
|
||||
"express-rate-limit": "^6.8.1",
|
||||
"winston": "^3.10.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"uuid": "^9.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"compression": "^1.7.4",
|
||||
"express-validator": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^29.6.1",
|
||||
"supertest": "^6.3.3",
|
||||
"eslint": "^8.45.0",
|
||||
"prettier": "^3.0.0",
|
||||
"mongodb-memory-server": "^8.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"coverageDirectory": "coverage",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/server.js"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-unused-vars": "error",
|
||||
"prefer-const": "error",
|
||||
"no-var": "error"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Database connection configuration
|
||||
*/
|
||||
class Database {
|
||||
constructor() {
|
||||
this.mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-management';
|
||||
this.options = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
family: 4,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MongoDB
|
||||
*/
|
||||
async connect() {
|
||||
try {
|
||||
await mongoose.connect(this.mongoURI, this.options);
|
||||
logger.info('MongoDB connected successfully');
|
||||
|
||||
// Handle connection events
|
||||
mongoose.connection.on('error', (err) => {
|
||||
logger.error('MongoDB connection error:', err);
|
||||
});
|
||||
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
logger.warn('MongoDB disconnected');
|
||||
});
|
||||
|
||||
mongoose.connection.on('reconnected', () => {
|
||||
logger.info('MongoDB reconnected');
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', this.gracefulShutdown.bind(this));
|
||||
process.on('SIGTERM', this.gracefulShutdown.bind(this));
|
||||
|
||||
} catch (error) {
|
||||
logger.error('MongoDB connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from MongoDB
|
||||
*/
|
||||
async disconnect() {
|
||||
try {
|
||||
await mongoose.disconnect();
|
||||
logger.info('MongoDB disconnected successfully');
|
||||
} catch (error) {
|
||||
logger.error('MongoDB disconnection error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown
|
||||
*/
|
||||
async gracefulShutdown(signal) {
|
||||
logger.info(`Received ${signal}. Graceful shutdown...`);
|
||||
try {
|
||||
await this.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Error during graceful shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
getConnectionStatus() {
|
||||
const states = {
|
||||
0: 'disconnected',
|
||||
1: 'connected',
|
||||
2: 'connecting',
|
||||
3: 'disconnecting',
|
||||
};
|
||||
return states[mongoose.connection.readyState] || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database is connected
|
||||
*/
|
||||
isConnected() {
|
||||
return mongoose.connection.readyState === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database (for testing)
|
||||
*/
|
||||
async dropDatabase() {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
try {
|
||||
await mongoose.connection.db.dropDatabase();
|
||||
logger.info('Test database dropped');
|
||||
} catch (error) {
|
||||
logger.error('Error dropping test database:', error);
|
||||
}
|
||||
} else {
|
||||
logger.warn('Database drop attempted in non-test environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
async getStats() {
|
||||
try {
|
||||
const stats = await mongoose.connection.db.stats();
|
||||
return {
|
||||
database: mongoose.connection.name,
|
||||
collections: stats.collections,
|
||||
dataSize: stats.dataSize,
|
||||
storageSize: stats.storageSize,
|
||||
indexes: stats.indexes,
|
||||
indexSize: stats.indexSize,
|
||||
objects: stats.objects,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting database stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const database = new Database();
|
||||
|
||||
module.exports = database;
|
||||
@@ -0,0 +1,165 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User } = require('../models/User');
|
||||
const { AuthenticationError, AuthorizationError } = require('../utils/errors');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Verifies JWT token and attaches user to request object
|
||||
*/
|
||||
const auth = async (req, res, next) => {
|
||||
try {
|
||||
// Get token from header
|
||||
const authHeader = req.header('Authorization');
|
||||
const token = authHeader && authHeader.startsWith('Bearer ')
|
||||
? authHeader.substring(7)
|
||||
: null;
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError('Access denied. No token provided.');
|
||||
}
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret');
|
||||
|
||||
// Get user from database
|
||||
const user = await User.findOne({ id: decoded.id });
|
||||
if (!user) {
|
||||
throw new AuthenticationError('Invalid token. User not found.');
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if (!user.isActive) {
|
||||
throw new AuthenticationError('User account is not active.');
|
||||
}
|
||||
|
||||
// Attach user to request object
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
logger.warn('Invalid JWT token attempted');
|
||||
next(new AuthenticationError('Invalid token'));
|
||||
} else if (error.name === 'TokenExpiredError') {
|
||||
logger.warn('Expired JWT token attempted');
|
||||
next(new AuthenticationError('Token expired'));
|
||||
} else {
|
||||
logger.error('Authentication error:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Authorization middleware factory
|
||||
* Creates middleware that checks if user has required role
|
||||
*/
|
||||
const authorize = (roles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AuthenticationError('Authentication required'));
|
||||
}
|
||||
|
||||
// Convert single role to array
|
||||
const allowedRoles = Array.isArray(roles) ? roles : [roles];
|
||||
|
||||
// Check if user has required role
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
logger.warn(`User ${req.user.username} attempted to access resource requiring roles: ${allowedRoles.join(', ')}`);
|
||||
return next(new AuthorizationError('Insufficient permissions'));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission-based authorization middleware
|
||||
* Checks if user has specific permission
|
||||
*/
|
||||
const requirePermission = (permission) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AuthenticationError('Authentication required'));
|
||||
}
|
||||
|
||||
if (!req.user.hasPermission(permission)) {
|
||||
logger.warn(`User ${req.user.username} attempted to access resource requiring permission: ${permission}`);
|
||||
return next(new AuthorizationError('Insufficient permissions'));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Self or admin middleware
|
||||
* Allows access if user is accessing their own data or is an admin
|
||||
*/
|
||||
const selfOrAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next(new AuthenticationError('Authentication required'));
|
||||
}
|
||||
|
||||
const targetUserId = req.params.id;
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const isSelf = req.user.id === targetUserId;
|
||||
|
||||
if (!isAdmin && !isSelf) {
|
||||
logger.warn(`User ${req.user.username} attempted to access another user's data`);
|
||||
return next(new AuthorizationError('Access denied'));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin only middleware
|
||||
* Allows access only for admin users
|
||||
*/
|
||||
const adminOnly = authorize(['admin']);
|
||||
|
||||
/**
|
||||
* User or admin middleware
|
||||
* Allows access for user role and above
|
||||
*/
|
||||
const userOrAdmin = authorize(['user', 'admin']);
|
||||
|
||||
/**
|
||||
* Optional authentication middleware
|
||||
* Authenticates user if token is provided, but doesn't require it
|
||||
*/
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.header('Authorization');
|
||||
const token = authHeader && authHeader.startsWith('Bearer ')
|
||||
? authHeader.substring(7)
|
||||
: null;
|
||||
|
||||
if (!token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret');
|
||||
const user = await User.findOne({ id: decoded.id });
|
||||
|
||||
if (user && user.isActive) {
|
||||
req.user = user;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Don't fail on optional auth, just continue without user
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
auth,
|
||||
authorize,
|
||||
requirePermission,
|
||||
selfOrAdmin,
|
||||
adminOnly,
|
||||
userOrAdmin,
|
||||
optionalAuth,
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { RateLimitError } = require('../utils/errors');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* General rate limiter
|
||||
* Limits requests per IP address
|
||||
*/
|
||||
const generalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many requests from this IP, please try again later.');
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Authentication rate limiter
|
||||
* Stricter limits for login attempts
|
||||
*/
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // limit each IP to 5 login attempts per windowMs
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many login attempts from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Authentication rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many login attempts from this IP, please try again later.');
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* User creation rate limiter
|
||||
* Moderate limits for user registration
|
||||
*/
|
||||
const createUserLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10, // limit each IP to 10 user creations per hour
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many accounts created from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
logger.warn(`User creation rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many accounts created from this IP, please try again later.');
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Password reset rate limiter
|
||||
* Limits password reset attempts
|
||||
*/
|
||||
const passwordResetLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 3, // limit each IP to 3 password reset attempts per hour
|
||||
message: {
|
||||
error: {
|
||||
message: 'Too many password reset attempts from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
logger.warn(`Password reset rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many password reset attempts from this IP, please try again later.');
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
generalLimiter,
|
||||
authLimiter,
|
||||
createUserLimiter,
|
||||
passwordResetLimiter,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const { validationResult } = require('express-validator');
|
||||
const { ValidationError } = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* Validation middleware
|
||||
* Checks for validation errors and returns appropriate error response
|
||||
*/
|
||||
const validate = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
const errorMessages = errors.array().map(error => ({
|
||||
field: error.path,
|
||||
message: error.msg,
|
||||
value: error.value,
|
||||
}));
|
||||
|
||||
const validationError = new ValidationError(
|
||||
'Validation failed',
|
||||
errorMessages
|
||||
);
|
||||
|
||||
return next(validationError);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = validate;
|
||||
@@ -0,0 +1,333 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// User roles enumeration
|
||||
const USER_ROLES = {
|
||||
ADMIN: 'admin',
|
||||
USER: 'user',
|
||||
GUEST: 'guest',
|
||||
};
|
||||
|
||||
// User status enumeration
|
||||
const USER_STATUS = {
|
||||
ACTIVE: 'active',
|
||||
INACTIVE: 'inactive',
|
||||
SUSPENDED: 'suspended',
|
||||
DELETED: 'deleted',
|
||||
};
|
||||
|
||||
// User schema definition
|
||||
const userSchema = new mongoose.Schema(
|
||||
{
|
||||
id: {
|
||||
type: String,
|
||||
default: uuidv4,
|
||||
unique: true,
|
||||
required: true,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: [true, 'Username is required'],
|
||||
unique: true,
|
||||
minlength: [3, 'Username must be at least 3 characters'],
|
||||
maxlength: [20, 'Username cannot exceed 20 characters'],
|
||||
match: [/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'],
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email'],
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: [true, 'Name is required'],
|
||||
minlength: [1, 'Name is required'],
|
||||
maxlength: [100, 'Name cannot exceed 100 characters'],
|
||||
},
|
||||
age: {
|
||||
type: Number,
|
||||
min: [0, 'Age cannot be negative'],
|
||||
max: [150, 'Age cannot exceed 150'],
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: [true, 'Password is required'],
|
||||
minlength: [8, 'Password must be at least 8 characters'],
|
||||
select: false, // Don't include in queries by default
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: Object.values(USER_ROLES),
|
||||
default: USER_ROLES.USER,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: Object.values(USER_STATUS),
|
||||
default: USER_STATUS.ACTIVE,
|
||||
},
|
||||
lastLogin: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
loginAttempts: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
metadata: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Virtual for user response (without sensitive data)
|
||||
userSchema.virtual('response').get(function() {
|
||||
return {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
name: this.name,
|
||||
age: this.age,
|
||||
role: this.role,
|
||||
status: this.status,
|
||||
lastLogin: this.lastLogin,
|
||||
permissions: this.permissions,
|
||||
metadata: this.metadata,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Virtual for checking if user is active
|
||||
userSchema.virtual('isActive').get(function() {
|
||||
return this.status === USER_STATUS.ACTIVE;
|
||||
});
|
||||
|
||||
// Virtual for checking if user is admin
|
||||
userSchema.virtual('isAdmin').get(function() {
|
||||
return this.role === USER_ROLES.ADMIN;
|
||||
});
|
||||
|
||||
// Virtual for checking if user is locked
|
||||
userSchema.virtual('isLocked').get(function() {
|
||||
return this.loginAttempts >= 5 || this.status === USER_STATUS.SUSPENDED;
|
||||
});
|
||||
|
||||
// Pre-save middleware to hash password
|
||||
userSchema.pre('save', async function(next) {
|
||||
// Only hash the password if it has been modified (or is new)
|
||||
if (!this.isModified('password')) return next();
|
||||
|
||||
try {
|
||||
// Hash password with cost of 12
|
||||
const hashedPassword = await bcrypt.hash(this.password, 12);
|
||||
this.password = hashedPassword;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Instance method to check password
|
||||
userSchema.methods.checkPassword = async function(candidatePassword) {
|
||||
return await bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
// Instance method to generate JWT token
|
||||
userSchema.methods.generateToken = function() {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
role: this.role
|
||||
},
|
||||
process.env.JWT_SECRET || 'fallback-secret',
|
||||
{
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
||||
issuer: 'user-management-system'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Instance method to add permission
|
||||
userSchema.methods.addPermission = function(permission) {
|
||||
if (!this.permissions.includes(permission)) {
|
||||
this.permissions.push(permission);
|
||||
}
|
||||
};
|
||||
|
||||
// Instance method to remove permission
|
||||
userSchema.methods.removePermission = function(permission) {
|
||||
this.permissions = this.permissions.filter(p => p !== permission);
|
||||
};
|
||||
|
||||
// Instance method to check permission
|
||||
userSchema.methods.hasPermission = function(permission) {
|
||||
return this.permissions.includes(permission);
|
||||
};
|
||||
|
||||
// Instance method to record successful login
|
||||
userSchema.methods.recordLogin = function() {
|
||||
this.lastLogin = new Date();
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to record failed login attempt
|
||||
userSchema.methods.recordFailedLogin = function() {
|
||||
this.loginAttempts += 1;
|
||||
if (this.loginAttempts >= 5) {
|
||||
this.status = USER_STATUS.SUSPENDED;
|
||||
}
|
||||
};
|
||||
|
||||
// Instance method to reset login attempts
|
||||
userSchema.methods.resetLoginAttempts = function() {
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to activate user
|
||||
userSchema.methods.activate = function() {
|
||||
this.status = USER_STATUS.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to deactivate user
|
||||
userSchema.methods.deactivate = function() {
|
||||
this.status = USER_STATUS.INACTIVE;
|
||||
};
|
||||
|
||||
// Instance method to suspend user
|
||||
userSchema.methods.suspend = function() {
|
||||
this.status = USER_STATUS.SUSPENDED;
|
||||
};
|
||||
|
||||
// Instance method to delete user (soft delete)
|
||||
userSchema.methods.delete = function() {
|
||||
this.status = USER_STATUS.DELETED;
|
||||
};
|
||||
|
||||
// Instance method to get metadata
|
||||
userSchema.methods.getMetadata = function(key, defaultValue = null) {
|
||||
return this.metadata[key] || defaultValue;
|
||||
};
|
||||
|
||||
// Instance method to set metadata
|
||||
userSchema.methods.setMetadata = function(key, value) {
|
||||
this.metadata[key] = value;
|
||||
};
|
||||
|
||||
// Instance method to remove metadata
|
||||
userSchema.methods.removeMetadata = function(key) {
|
||||
delete this.metadata[key];
|
||||
};
|
||||
|
||||
// Instance method to validate user data
|
||||
userSchema.methods.validateUser = function() {
|
||||
const errors = [];
|
||||
|
||||
if (!this.username || this.username.length < 3) {
|
||||
errors.push('Username must be at least 3 characters');
|
||||
}
|
||||
|
||||
if (!this.name || this.name.length === 0) {
|
||||
errors.push('Name is required');
|
||||
}
|
||||
|
||||
if (this.age && (this.age < 0 || this.age > 150)) {
|
||||
errors.push('Age must be between 0 and 150');
|
||||
}
|
||||
|
||||
if (this.email && !this.email.match(/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/)) {
|
||||
errors.push('Email format is invalid');
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
// Static method to find by username
|
||||
userSchema.statics.findByUsername = function(username) {
|
||||
return this.findOne({ username });
|
||||
};
|
||||
|
||||
// Static method to find by email
|
||||
userSchema.statics.findByEmail = function(email) {
|
||||
return this.findOne({ email });
|
||||
};
|
||||
|
||||
// Static method to find active users
|
||||
userSchema.statics.findActive = function() {
|
||||
return this.find({ status: USER_STATUS.ACTIVE });
|
||||
};
|
||||
|
||||
// Static method to find by role
|
||||
userSchema.statics.findByRole = function(role) {
|
||||
return this.find({ role });
|
||||
};
|
||||
|
||||
// Static method to search users
|
||||
userSchema.statics.searchUsers = function(query, options = {}) {
|
||||
const searchRegex = new RegExp(query, 'i');
|
||||
const searchQuery = {
|
||||
$or: [
|
||||
{ username: searchRegex },
|
||||
{ name: searchRegex },
|
||||
{ email: searchRegex },
|
||||
],
|
||||
};
|
||||
|
||||
return this.find(searchQuery, null, options);
|
||||
};
|
||||
|
||||
// Static method to get user statistics
|
||||
userSchema.statics.getUserStats = async function() {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: 1 },
|
||||
active: { $sum: { $cond: [{ $eq: ['$status', USER_STATUS.ACTIVE] }, 1, 0] } },
|
||||
admin: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.ADMIN] }, 1, 0] } },
|
||||
user: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.USER] }, 1, 0] } },
|
||||
guest: { $sum: { $cond: [{ $eq: ['$role', USER_ROLES.GUEST] }, 1, 0] } },
|
||||
withEmail: { $sum: { $cond: [{ $ne: ['$email', null] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return stats[0] || {
|
||||
total: 0,
|
||||
active: 0,
|
||||
admin: 0,
|
||||
user: 0,
|
||||
guest: 0,
|
||||
withEmail: 0,
|
||||
};
|
||||
};
|
||||
|
||||
// Index for performance
|
||||
userSchema.index({ username: 1 });
|
||||
userSchema.index({ email: 1 });
|
||||
userSchema.index({ role: 1 });
|
||||
userSchema.index({ status: 1 });
|
||||
userSchema.index({ createdAt: -1 });
|
||||
|
||||
// Export model and constants
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
USER_ROLES,
|
||||
USER_STATUS,
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
const express = require('express');
|
||||
const { body, param, query } = require('express-validator');
|
||||
const UserService = require('../services/UserService');
|
||||
const { asyncHandler, createSuccessResponse, createErrorResponse } = require('../utils/errors');
|
||||
const auth = require('../middleware/auth');
|
||||
const validate = require('../middleware/validate');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const router = express.Router();
|
||||
const userService = new UserService();
|
||||
|
||||
// User creation validation
|
||||
const createUserValidation = [
|
||||
body('username')
|
||||
.isLength({ min: 3, max: 20 })
|
||||
.withMessage('Username must be between 3 and 20 characters')
|
||||
.matches(/^[a-zA-Z0-9_]+$/)
|
||||
.withMessage('Username can only contain letters, numbers, and underscores'),
|
||||
body('name')
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Name must be between 1 and 100 characters'),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('Please provide a valid email address'),
|
||||
body('age')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 150 })
|
||||
.withMessage('Age must be between 0 and 150'),
|
||||
body('password')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Password must be at least 8 characters long'),
|
||||
body('role')
|
||||
.optional()
|
||||
.isIn(['admin', 'user', 'guest'])
|
||||
.withMessage('Role must be admin, user, or guest'),
|
||||
];
|
||||
|
||||
// User update validation
|
||||
const updateUserValidation = [
|
||||
param('id').notEmpty().withMessage('User ID is required'),
|
||||
body('username')
|
||||
.optional()
|
||||
.isLength({ min: 3, max: 20 })
|
||||
.withMessage('Username must be between 3 and 20 characters')
|
||||
.matches(/^[a-zA-Z0-9_]+$/)
|
||||
.withMessage('Username can only contain letters, numbers, and underscores'),
|
||||
body('name')
|
||||
.optional()
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('Name must be between 1 and 100 characters'),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('Please provide a valid email address'),
|
||||
body('age')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 150 })
|
||||
.withMessage('Age must be between 0 and 150'),
|
||||
body('role')
|
||||
.optional()
|
||||
.isIn(['admin', 'user', 'guest'])
|
||||
.withMessage('Role must be admin, user, or guest'),
|
||||
];
|
||||
|
||||
// Password change validation
|
||||
const passwordChangeValidation = [
|
||||
param('id').notEmpty().withMessage('User ID is required'),
|
||||
body('currentPassword')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('Current password is required'),
|
||||
body('newPassword')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('New password must be at least 8 characters long'),
|
||||
];
|
||||
|
||||
// Authentication validation
|
||||
const authValidation = [
|
||||
body('username')
|
||||
.isLength({ min: 3 })
|
||||
.withMessage('Username must be at least 3 characters'),
|
||||
body('password')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('Password is required'),
|
||||
];
|
||||
|
||||
// Search validation
|
||||
const searchValidation = [
|
||||
query('q')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('Search query is required'),
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1 })
|
||||
.withMessage('Page must be a positive integer'),
|
||||
query('limit')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: 100 })
|
||||
.withMessage('Limit must be between 1 and 100'),
|
||||
];
|
||||
|
||||
// @route POST /api/users
|
||||
// @desc Create a new user
|
||||
// @access Public
|
||||
router.post('/', createUserValidation, validate, asyncHandler(async (req, res) => {
|
||||
const user = await userService.createUser(req.body);
|
||||
logger.info(`User created via API: ${user.username}`);
|
||||
res.status(201).json(createSuccessResponse(user, 'User created successfully'));
|
||||
}));
|
||||
|
||||
// @route POST /api/users/auth
|
||||
// @desc Authenticate user
|
||||
// @access Public
|
||||
router.post('/auth', authValidation, validate, asyncHandler(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const result = await userService.authenticateUser(username, password);
|
||||
logger.info(`User authenticated via API: ${username}`);
|
||||
res.json(createSuccessResponse(result, 'Authentication successful'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users
|
||||
// @desc Get all users with pagination
|
||||
// @access Private (Admin only)
|
||||
router.get('/', auth, asyncHandler(async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const filter = {};
|
||||
|
||||
// Add filtering by role if provided
|
||||
if (req.query.role) {
|
||||
filter.role = req.query.role;
|
||||
}
|
||||
|
||||
// Add filtering by status if provided
|
||||
if (req.query.status) {
|
||||
filter.status = req.query.status;
|
||||
}
|
||||
|
||||
const result = await userService.getAllUsers(page, limit, filter);
|
||||
res.json(createSuccessResponse(result, 'Users retrieved successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/search
|
||||
// @desc Search users
|
||||
// @access Private
|
||||
router.get('/search', auth, searchValidation, validate, asyncHandler(async (req, res) => {
|
||||
const { q: query, page = 1, limit = 20 } = req.query;
|
||||
const result = await userService.searchUsers(query, parseInt(page), parseInt(limit));
|
||||
res.json(createSuccessResponse(result, 'Search completed successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/stats
|
||||
// @desc Get user statistics
|
||||
// @access Private (Admin only)
|
||||
router.get('/stats', auth, asyncHandler(async (req, res) => {
|
||||
const stats = await userService.getUserStats();
|
||||
res.json(createSuccessResponse(stats, 'Statistics retrieved successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/export
|
||||
// @desc Export all users
|
||||
// @access Private (Admin only)
|
||||
router.get('/export', auth, asyncHandler(async (req, res) => {
|
||||
const users = await userService.exportUsers();
|
||||
res.json(createSuccessResponse(users, 'Users exported successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/active
|
||||
// @desc Get active users
|
||||
// @access Private
|
||||
router.get('/active', auth, asyncHandler(async (req, res) => {
|
||||
const users = await userService.getActiveUsers();
|
||||
res.json(createSuccessResponse(users, 'Active users retrieved successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/role/:role
|
||||
// @desc Get users by role
|
||||
// @access Private (Admin only)
|
||||
router.get('/role/:role', auth, asyncHandler(async (req, res) => {
|
||||
const { role } = req.params;
|
||||
const users = await userService.getUsersByRole(role);
|
||||
res.json(createSuccessResponse(users, `Users with role ${role} retrieved successfully`));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/:id
|
||||
// @desc Get user by ID
|
||||
// @access Private
|
||||
router.get('/:id', auth, asyncHandler(async (req, res) => {
|
||||
const user = await userService.getUserById(req.params.id);
|
||||
res.json(createSuccessResponse(user, 'User retrieved successfully'));
|
||||
}));
|
||||
|
||||
// @route GET /api/users/:id/activity
|
||||
// @desc Get user activity
|
||||
// @access Private (Admin or same user)
|
||||
router.get('/:id/activity', auth, asyncHandler(async (req, res) => {
|
||||
const activity = await userService.getUserActivity(req.params.id);
|
||||
res.json(createSuccessResponse(activity, 'User activity retrieved successfully'));
|
||||
}));
|
||||
|
||||
// @route PUT /api/users/:id
|
||||
// @desc Update user
|
||||
// @access Private (Admin or same user)
|
||||
router.put('/:id', auth, updateUserValidation, validate, asyncHandler(async (req, res) => {
|
||||
const user = await userService.updateUser(req.params.id, req.body);
|
||||
logger.info(`User updated via API: ${user.username}`);
|
||||
res.json(createSuccessResponse(user, 'User updated successfully'));
|
||||
}));
|
||||
|
||||
// @route PUT /api/users/:id/password
|
||||
// @desc Change user password
|
||||
// @access Private (Admin or same user)
|
||||
router.put('/:id/password', auth, passwordChangeValidation, validate, asyncHandler(async (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
await userService.changePassword(req.params.id, currentPassword, newPassword);
|
||||
logger.info(`Password changed via API for user: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'Password changed successfully'));
|
||||
}));
|
||||
|
||||
// @route PUT /api/users/:id/reset-password
|
||||
// @desc Reset user password (Admin only)
|
||||
// @access Private (Admin only)
|
||||
router.put('/:id/reset-password', auth, asyncHandler(async (req, res) => {
|
||||
const { newPassword } = req.body;
|
||||
await userService.resetPassword(req.params.id, newPassword);
|
||||
logger.info(`Password reset via API for user: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'Password reset successfully'));
|
||||
}));
|
||||
|
||||
// @route PUT /api/users/:id/permissions
|
||||
// @desc Add permission to user
|
||||
// @access Private (Admin only)
|
||||
router.put('/:id/permissions', auth, asyncHandler(async (req, res) => {
|
||||
const { permission } = req.body;
|
||||
await userService.addPermission(req.params.id, permission);
|
||||
logger.info(`Permission added via API for user: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'Permission added successfully'));
|
||||
}));
|
||||
|
||||
// @route DELETE /api/users/:id/permissions
|
||||
// @desc Remove permission from user
|
||||
// @access Private (Admin only)
|
||||
router.delete('/:id/permissions', auth, asyncHandler(async (req, res) => {
|
||||
const { permission } = req.body;
|
||||
await userService.removePermission(req.params.id, permission);
|
||||
logger.info(`Permission removed via API for user: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'Permission removed successfully'));
|
||||
}));
|
||||
|
||||
// @route DELETE /api/users/:id
|
||||
// @desc Delete user (soft delete)
|
||||
// @access Private (Admin only)
|
||||
router.delete('/:id', auth, asyncHandler(async (req, res) => {
|
||||
await userService.deleteUser(req.params.id);
|
||||
logger.info(`User deleted via API: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'User deleted successfully'));
|
||||
}));
|
||||
|
||||
// @route DELETE /api/users/:id/hard
|
||||
// @desc Hard delete user (permanent)
|
||||
// @access Private (Admin only)
|
||||
router.delete('/:id/hard', auth, asyncHandler(async (req, res) => {
|
||||
await userService.hardDeleteUser(req.params.id);
|
||||
logger.info(`User permanently deleted via API: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'User permanently deleted'));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,190 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const morgan = require('morgan');
|
||||
|
||||
const database = require('./config/database');
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const { generalLimiter } = require('./middleware/rateLimiter');
|
||||
const { globalErrorHandler, handleNotFound } = require('./utils/errors');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
/**
|
||||
* Express application setup
|
||||
*/
|
||||
class App {
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.port = process.env.PORT || 3000;
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
this.setupErrorHandling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup middleware
|
||||
*/
|
||||
setupMiddleware() {
|
||||
// Security middleware
|
||||
this.app.use(helmet());
|
||||
|
||||
// CORS configuration
|
||||
this.app.use(cors({
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Compression middleware
|
||||
this.app.use(compression());
|
||||
|
||||
// Body parsing middleware
|
||||
this.app.use(express.json({ limit: '10mb' }));
|
||||
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Logging middleware
|
||||
this.app.use(morgan('combined', { stream: logger.stream }));
|
||||
|
||||
// Rate limiting
|
||||
this.app.use(generalLimiter);
|
||||
|
||||
// Request ID middleware
|
||||
this.app.use((req, res, next) => {
|
||||
req.requestId = Math.random().toString(36).substr(2, 9);
|
||||
res.set('X-Request-ID', req.requestId);
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
database: database.getConnectionStatus(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup routes
|
||||
*/
|
||||
setupRoutes() {
|
||||
// API routes
|
||||
this.app.use('/api/users', userRoutes);
|
||||
|
||||
// Root endpoint
|
||||
this.app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'User Management API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
users: '/api/users',
|
||||
auth: '/api/users/auth',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup error handling
|
||||
*/
|
||||
setupErrorHandling() {
|
||||
// Handle 404 for unknown routes
|
||||
this.app.use(handleNotFound);
|
||||
|
||||
// Global error handler
|
||||
this.app.use(globalErrorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
async start() {
|
||||
try {
|
||||
// Connect to database
|
||||
await database.connect();
|
||||
|
||||
// Start server
|
||||
this.server = this.app.listen(this.port, () => {
|
||||
logger.info(`Server running on port ${this.port}`);
|
||||
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
logger.info(`Health check: http://localhost:${this.port}/health`);
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
this.server.on('error', (error) => {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof this.port === 'string' ? `Pipe ${this.port}` : `Port ${this.port}`;
|
||||
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
logger.error(`${bind} requires elevated privileges`);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
logger.error(`${bind} is already in use`);
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', this.gracefulShutdown.bind(this));
|
||||
process.on('SIGINT', this.gracefulShutdown.bind(this));
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown
|
||||
*/
|
||||
async gracefulShutdown(signal) {
|
||||
logger.info(`Received ${signal}. Graceful shutdown...`);
|
||||
|
||||
if (this.server) {
|
||||
this.server.close(async () => {
|
||||
logger.info('HTTP server closed');
|
||||
|
||||
try {
|
||||
await database.disconnect();
|
||||
logger.info('Database disconnected');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Error during graceful shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Express app instance
|
||||
*/
|
||||
getApp() {
|
||||
return this.app;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and start the application
|
||||
const app = new App();
|
||||
|
||||
// Start server if not in test environment
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.start();
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
module.exports = app.getApp();
|
||||
@@ -0,0 +1,493 @@
|
||||
const { User, USER_ROLES, USER_STATUS } = require('../models/User');
|
||||
const { AppError } = require('../utils/errors');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* UserService class handles all user-related business logic
|
||||
*/
|
||||
class UserService {
|
||||
/**
|
||||
* Create a new user
|
||||
* @param {Object} userData - User data object
|
||||
* @returns {Promise<Object>} Created user response
|
||||
*/
|
||||
async createUser(userData) {
|
||||
try {
|
||||
// Check if username already exists
|
||||
const existingUsername = await User.findByUsername(userData.username);
|
||||
if (existingUsername) {
|
||||
throw new AppError('Username already exists', 400);
|
||||
}
|
||||
|
||||
// Check if email already exists (if provided)
|
||||
if (userData.email) {
|
||||
const existingEmail = await User.findByEmail(userData.email);
|
||||
if (existingEmail) {
|
||||
throw new AppError('Email already exists', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new user
|
||||
const user = new User(userData);
|
||||
|
||||
// Validate user data
|
||||
const validationErrors = user.validateUser();
|
||||
if (validationErrors.length > 0) {
|
||||
throw new AppError(validationErrors.join(', '), 400);
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
logger.info(`User created successfully: ${user.username}`);
|
||||
return user.response;
|
||||
} catch (error) {
|
||||
logger.error('Error creating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
* @param {string} id - User ID
|
||||
* @returns {Promise<Object>} User response
|
||||
*/
|
||||
async getUserById(id) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
return user.response;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user by ID:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by username
|
||||
* @param {string} username - Username
|
||||
* @returns {Promise<Object>} User response
|
||||
*/
|
||||
async getUserByUsername(username) {
|
||||
try {
|
||||
const user = await User.findByUsername(username);
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
return user.response;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user by username:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by email
|
||||
* @param {string} email - Email address
|
||||
* @returns {Promise<Object>} User response
|
||||
*/
|
||||
async getUserByEmail(email) {
|
||||
try {
|
||||
const user = await User.findByEmail(email);
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
return user.response;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user by email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user
|
||||
* @param {string} id - User ID
|
||||
* @param {Object} updateData - Update data
|
||||
* @returns {Promise<Object>} Updated user response
|
||||
*/
|
||||
async updateUser(id, updateData) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
Object.keys(updateData).forEach(key => {
|
||||
if (key !== 'password' && key !== 'id') {
|
||||
user[key] = updateData[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Validate updated data
|
||||
const validationErrors = user.validateUser();
|
||||
if (validationErrors.length > 0) {
|
||||
throw new AppError(validationErrors.join(', '), 400);
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
logger.info(`User updated successfully: ${user.username}`);
|
||||
return user.response;
|
||||
} catch (error) {
|
||||
logger.error('Error updating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user (soft delete)
|
||||
* @param {string} id - User ID
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async deleteUser(id) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
user.delete();
|
||||
await user.save();
|
||||
|
||||
logger.info(`User deleted successfully: ${user.username}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error deleting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard delete user (permanent deletion)
|
||||
* @param {string} id - User ID
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async hardDeleteUser(id) {
|
||||
try {
|
||||
const result = await User.deleteOne({ id });
|
||||
if (result.deletedCount === 0) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
logger.info(`User permanently deleted: ${id}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error hard deleting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users with pagination
|
||||
* @param {number} page - Page number
|
||||
* @param {number} limit - Items per page
|
||||
* @param {Object} filter - Filter criteria
|
||||
* @returns {Promise<Object>} Paginated users response
|
||||
*/
|
||||
async getAllUsers(page = 1, limit = 20, filter = {}) {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const users = await User.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
|
||||
const total = await User.countDocuments(filter);
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
users: users.map(user => user.response),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting all users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active users
|
||||
* @returns {Promise<Array>} Active users
|
||||
*/
|
||||
async getActiveUsers() {
|
||||
try {
|
||||
const users = await User.findActive();
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error getting active users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users by role
|
||||
* @param {string} role - User role
|
||||
* @returns {Promise<Array>} Users with specified role
|
||||
*/
|
||||
async getUsersByRole(role) {
|
||||
try {
|
||||
const users = await User.findByRole(role);
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error getting users by role:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users
|
||||
* @param {string} query - Search query
|
||||
* @param {number} page - Page number
|
||||
* @param {number} limit - Items per page
|
||||
* @returns {Promise<Object>} Search results
|
||||
*/
|
||||
async searchUsers(query, page = 1, limit = 20) {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const users = await User.searchUsers(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort: { createdAt: -1 },
|
||||
});
|
||||
|
||||
// Count total matching users
|
||||
const totalUsers = await User.searchUsers(query);
|
||||
const total = totalUsers.length;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
users: users.map(user => user.response),
|
||||
query,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error searching users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
* @returns {Promise<Object>} User statistics
|
||||
*/
|
||||
async getUserStats() {
|
||||
try {
|
||||
const stats = await User.getUserStats();
|
||||
return stats;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user statistics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user
|
||||
* @param {string} username - Username
|
||||
* @param {string} password - Password
|
||||
* @returns {Promise<Object>} Authentication result
|
||||
*/
|
||||
async authenticateUser(username, password) {
|
||||
try {
|
||||
const user = await User.findByUsername(username).select('+password');
|
||||
|
||||
if (!user || !(await user.checkPassword(password))) {
|
||||
// Record failed login attempt if user exists
|
||||
if (user) {
|
||||
user.recordFailedLogin();
|
||||
await user.save();
|
||||
}
|
||||
throw new AppError('Invalid username or password', 401);
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new AppError('User account is not active', 401);
|
||||
}
|
||||
|
||||
if (user.isLocked) {
|
||||
throw new AppError('User account is locked', 401);
|
||||
}
|
||||
|
||||
// Record successful login
|
||||
user.recordLogin();
|
||||
await user.save();
|
||||
|
||||
// Generate token
|
||||
const token = user.generateToken();
|
||||
|
||||
logger.info(`User authenticated successfully: ${user.username}`);
|
||||
|
||||
return {
|
||||
user: user.response,
|
||||
token,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error authenticating user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
* @param {string} id - User ID
|
||||
* @param {string} currentPassword - Current password
|
||||
* @param {string} newPassword - New password
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async changePassword(id, currentPassword, newPassword) {
|
||||
try {
|
||||
const user = await User.findOne({ id }).select('+password');
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
if (!(await user.checkPassword(currentPassword))) {
|
||||
throw new AppError('Current password is incorrect', 400);
|
||||
}
|
||||
|
||||
user.password = newPassword;
|
||||
await user.save();
|
||||
|
||||
logger.info(`Password changed successfully for user: ${user.username}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error changing password:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password (admin function)
|
||||
* @param {string} id - User ID
|
||||
* @param {string} newPassword - New password
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async resetPassword(id, newPassword) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
user.password = newPassword;
|
||||
user.resetLoginAttempts();
|
||||
await user.save();
|
||||
|
||||
logger.info(`Password reset successfully for user: ${user.username}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error resetting password:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permission to user
|
||||
* @param {string} id - User ID
|
||||
* @param {string} permission - Permission to add
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async addPermission(id, permission) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
user.addPermission(permission);
|
||||
await user.save();
|
||||
|
||||
logger.info(`Permission added to user ${user.username}: ${permission}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error adding permission:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove permission from user
|
||||
* @param {string} id - User ID
|
||||
* @param {string} permission - Permission to remove
|
||||
* @returns {Promise<boolean>} Success status
|
||||
*/
|
||||
async removePermission(id, permission) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
user.removePermission(permission);
|
||||
await user.save();
|
||||
|
||||
logger.info(`Permission removed from user ${user.username}: ${permission}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Error removing permission:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export users data
|
||||
* @returns {Promise<Array>} Users data for export
|
||||
*/
|
||||
async exportUsers() {
|
||||
try {
|
||||
const users = await User.find().sort({ createdAt: -1 });
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error exporting users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user activity
|
||||
* @param {string} id - User ID
|
||||
* @returns {Promise<Object>} User activity data
|
||||
*/
|
||||
async getUserActivity(id) {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
lastLogin: user.lastLogin,
|
||||
loginAttempts: user.loginAttempts,
|
||||
isActive: user.isActive,
|
||||
isLocked: user.isLocked,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting user activity:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserService;
|
||||
// AUTO_REINDEX_MARKER: ci_auto_reindex_test_token_js
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Custom error classes for the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base application error class
|
||||
*/
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode = 500, isOperational = true) {
|
||||
super(message);
|
||||
|
||||
this.statusCode = statusCode;
|
||||
this.isOperational = isOperational;
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
|
||||
|
||||
// Capture stack trace
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error class
|
||||
*/
|
||||
class ValidationError extends AppError {
|
||||
constructor(message, errors = []) {
|
||||
super(message, 400);
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication error class
|
||||
*/
|
||||
class AuthenticationError extends AppError {
|
||||
constructor(message = 'Authentication failed') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization error class
|
||||
*/
|
||||
class AuthorizationError extends AppError {
|
||||
constructor(message = 'Access denied') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not found error class
|
||||
*/
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message = 'Resource not found') {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict error class
|
||||
*/
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Resource conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit error class
|
||||
*/
|
||||
class RateLimitError extends AppError {
|
||||
constructor(message = 'Too many requests') {
|
||||
super(message, 429);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database error class
|
||||
*/
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message = 'Database error') {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External service error class
|
||||
*/
|
||||
class ExternalServiceError extends AppError {
|
||||
constructor(message = 'External service error') {
|
||||
super(message, 502);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error handler for Express
|
||||
*/
|
||||
const globalErrorHandler = (err, req, res, next) => {
|
||||
// Default error values
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// Log error
|
||||
console.error('Error:', err);
|
||||
|
||||
// Mongoose bad ObjectId
|
||||
if (err.name === 'CastError') {
|
||||
const message = 'Resource not found';
|
||||
error = new NotFoundError(message);
|
||||
}
|
||||
|
||||
// Mongoose duplicate key
|
||||
if (err.code === 11000) {
|
||||
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
|
||||
const message = `Duplicate field value: ${value}. Please use another value`;
|
||||
error = new ConflictError(message);
|
||||
}
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const errors = Object.values(err.errors).map(val => val.message);
|
||||
error = new ValidationError('Validation failed', errors);
|
||||
}
|
||||
|
||||
// JWT errors
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
error = new AuthenticationError('Invalid token');
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
error = new AuthenticationError('Token expired');
|
||||
}
|
||||
|
||||
// Send error response
|
||||
res.status(error.statusCode || 500).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
|
||||
...(error.errors && { errors: error.errors }),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Async error handler wrapper
|
||||
*/
|
||||
const asyncHandler = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create error response
|
||||
*/
|
||||
const createErrorResponse = (message, statusCode = 500, errors = null) => {
|
||||
const response = {
|
||||
success: false,
|
||||
error: {
|
||||
message,
|
||||
statusCode,
|
||||
},
|
||||
};
|
||||
|
||||
if (errors) {
|
||||
response.error.errors = errors;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create success response
|
||||
*/
|
||||
const createSuccessResponse = (data, message = 'Success') => {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle 404 for unknown routes
|
||||
*/
|
||||
const handleNotFound = (req, res, next) => {
|
||||
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
RateLimitError,
|
||||
DatabaseError,
|
||||
ExternalServiceError,
|
||||
globalErrorHandler,
|
||||
asyncHandler,
|
||||
createErrorResponse,
|
||||
createSuccessResponse,
|
||||
handleNotFound,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
const winston = require('winston');
|
||||
|
||||
// Define log levels
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
};
|
||||
|
||||
// Define colors for each level
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'white',
|
||||
};
|
||||
|
||||
// Tell winston about the colors
|
||||
winston.addColors(colors);
|
||||
|
||||
// Format function
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
(info) => `${info.timestamp} ${info.level}: ${info.message}`
|
||||
)
|
||||
);
|
||||
|
||||
// Define which transports the logger must use
|
||||
const transports = [
|
||||
// Console transport
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
),
|
||||
}),
|
||||
|
||||
// File transport for errors
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
}),
|
||||
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: 'logs/combined.log',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
// Create the logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
});
|
||||
|
||||
// Create a stream object with a 'write' function that will be used by Morgan
|
||||
logger.stream = {
|
||||
write: (message) => {
|
||||
// Remove the trailing newline
|
||||
logger.http(message.trim());
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
Reference in New Issue
Block a user