Refactor code structure for improved readability and maintainability

This commit is contained in:
catlog22
2025-12-12 11:19:58 +08:00
parent 77de8d857b
commit b74a90b416
169 changed files with 29206 additions and 369 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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;