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,324 @@
|
||||
# User Management System (Go)
|
||||
|
||||
A comprehensive user management system built in Go for testing Code Index MCP's analysis capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **User Management**: Create, update, delete, and search users
|
||||
- **REST API**: Full HTTP API with JSON responses
|
||||
- **Authentication**: BCrypt password hashing and JWT tokens
|
||||
- **Authorization**: Role-based access control (Admin, User, Guest)
|
||||
- **Database**: SQLite with GORM ORM
|
||||
- **Pagination**: Efficient pagination for large datasets
|
||||
- **Search**: Full-text search across users
|
||||
- **Export**: JSON export functionality
|
||||
- **Logging**: Structured logging with middleware
|
||||
- **CORS**: Cross-origin resource sharing support
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
user-management/
|
||||
├── cmd/
|
||||
│ ├── server/
|
||||
│ │ └── main.go # HTTP server entry point
|
||||
│ └── cli/
|
||||
│ └── main.go # CLI application
|
||||
├── internal/
|
||||
│ ├── models/
|
||||
│ │ └── user.go # User model and types
|
||||
│ ├── services/
|
||||
│ │ └── user_service.go # Business logic
|
||||
│ └── utils/
|
||||
│ └── types.go # Utility types and helpers
|
||||
├── pkg/
|
||||
│ └── api/
|
||||
│ └── user_handler.go # HTTP handlers
|
||||
├── go.mod # Go module file
|
||||
├── go.sum # Go dependencies
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- **Go 1.21**: Modern Go with generics and latest features
|
||||
- **Gin**: HTTP web framework
|
||||
- **GORM**: ORM for database operations
|
||||
- **SQLite**: Embedded database
|
||||
- **UUID**: Unique identifiers
|
||||
- **BCrypt**: Password hashing
|
||||
- **JWT**: JSON Web Tokens (planned)
|
||||
- **Viper**: Configuration management
|
||||
- **Cobra**: CLI framework
|
||||
|
||||
## Build and Run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.21 or higher
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
### Run HTTP Server
|
||||
|
||||
```bash
|
||||
go run cmd/server/main.go
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:8080`
|
||||
|
||||
### Run CLI
|
||||
|
||||
```bash
|
||||
go run cmd/cli/main.go
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Build server
|
||||
go build -o bin/server cmd/server/main.go
|
||||
|
||||
# Build CLI
|
||||
go build -o bin/cli cmd/cli/main.go
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Users
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/users` | Create a new user |
|
||||
| `GET` | `/api/v1/users` | Get all users (paginated) |
|
||||
| `GET` | `/api/v1/users/:id` | Get user by ID |
|
||||
| `PUT` | `/api/v1/users/:id` | Update user |
|
||||
| `DELETE` | `/api/v1/users/:id` | Delete user |
|
||||
| `GET` | `/api/v1/users/search` | Search users |
|
||||
| `GET` | `/api/v1/users/stats` | Get user statistics |
|
||||
| `GET` | `/api/v1/users/export` | Export users |
|
||||
|
||||
### Authentication
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/auth/login` | User login |
|
||||
| `POST` | `/api/v1/auth/logout` | User logout |
|
||||
| `POST` | `/api/v1/auth/change-password` | Change password |
|
||||
|
||||
### Admin
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/admin/users/:id/reset-password` | Reset user password |
|
||||
| `POST` | `/api/v1/admin/users/:id/permissions` | Add permission |
|
||||
| `DELETE` | `/api/v1/admin/users/:id/permissions` | Remove permission |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Create User
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "johndoe",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Users
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/users?page=1&page_size=10
|
||||
```
|
||||
|
||||
### Search Users
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/users/search?q=john&page=1&page_size=10
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Statistics
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/users/stats
|
||||
```
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/example/user-management/internal/models"
|
||||
"github.com/example/user-management/internal/services"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize database
|
||||
db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
db.AutoMigrate(&models.User{})
|
||||
|
||||
// Initialize service
|
||||
userService := services.NewUserService(db)
|
||||
|
||||
// Create user
|
||||
req := &models.UserRequest{
|
||||
Username: "alice",
|
||||
Email: "alice@example.com",
|
||||
Name: "Alice Smith",
|
||||
Age: 25,
|
||||
Password: "password123",
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
|
||||
user, err := userService.CreateUser(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
authUser, err := userService.AuthenticateUser("alice", "password123")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
stats, err := userService.GetUserStats()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Features
|
||||
|
||||
This project tests the following Go language features:
|
||||
|
||||
### Core Language Features
|
||||
- **Structs and Methods**: User model with associated methods
|
||||
- **Interfaces**: Service and handler interfaces
|
||||
- **Pointers**: Efficient memory management
|
||||
- **Error Handling**: Comprehensive error handling patterns
|
||||
- **Packages**: Modular code organization
|
||||
- **Imports**: Internal and external package imports
|
||||
|
||||
### Modern Go Features
|
||||
- **Generics**: Type-safe collections (Go 1.18+)
|
||||
- **Modules**: Dependency management with go.mod
|
||||
- **Context**: Request context handling
|
||||
- **Channels**: Concurrent programming (in background tasks)
|
||||
- **Goroutines**: Concurrent execution
|
||||
- **JSON Tags**: Struct field mapping
|
||||
|
||||
### Advanced Features
|
||||
- **Reflection**: GORM model reflection
|
||||
- **Build Tags**: Conditional compilation
|
||||
- **Embedding**: Struct embedding for composition
|
||||
- **Type Assertions**: Interface type checking
|
||||
- **Panic/Recover**: Error recovery mechanisms
|
||||
|
||||
### Framework Integration
|
||||
- **Gin**: HTTP router and middleware
|
||||
- **GORM**: ORM with hooks and associations
|
||||
- **UUID**: Unique identifier generation
|
||||
- **BCrypt**: Cryptographic hashing
|
||||
- **SQLite**: Embedded database
|
||||
|
||||
### Design Patterns
|
||||
- **Repository Pattern**: Data access layer
|
||||
- **Service Layer**: Business logic separation
|
||||
- **Dependency Injection**: Service composition
|
||||
- **Middleware Pattern**: HTTP request processing
|
||||
- **Factory Pattern**: Service creation
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
- **gin-gonic/gin**: Web framework
|
||||
- **gorm.io/gorm**: ORM
|
||||
- **gorm.io/driver/sqlite**: SQLite driver
|
||||
- **google/uuid**: UUID generation
|
||||
- **golang.org/x/crypto**: Cryptographic functions
|
||||
|
||||
### CLI Dependencies
|
||||
- **spf13/cobra**: CLI framework
|
||||
- **spf13/viper**: Configuration management
|
||||
|
||||
### Development Dependencies
|
||||
- **testify**: Testing framework
|
||||
- **mockery**: Mock generation
|
||||
|
||||
## Configuration
|
||||
|
||||
The application can be configured using environment variables or a configuration file:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
driver: sqlite
|
||||
database: users.db
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
host: localhost
|
||||
|
||||
jwt:
|
||||
secret_key: your-secret-key
|
||||
expiration_hours: 24
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Generate Mocks
|
||||
|
||||
```bash
|
||||
mockery --all
|
||||
```
|
||||
|
||||
### Format Code
|
||||
|
||||
```bash
|
||||
gofmt -w .
|
||||
```
|
||||
|
||||
### Lint Code
|
||||
|
||||
```bash
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT License - This is a sample project for testing purposes.
|
||||
@@ -0,0 +1,294 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/example/user-management/internal/models"
|
||||
"github.com/example/user-management/internal/services"
|
||||
"github.com/example/user-management/internal/utils"
|
||||
"github.com/example/user-management/pkg/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize database
|
||||
db, err := initDatabase()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize database:", err)
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
userService := services.NewUserService(db)
|
||||
|
||||
// Initialize API handlers
|
||||
userHandler := api.NewUserHandler(userService)
|
||||
|
||||
// Setup routes
|
||||
router := setupRoutes(userHandler)
|
||||
|
||||
// Create sample data
|
||||
createSampleData(userService)
|
||||
|
||||
// Start server
|
||||
log.Println("Starting server on :8080")
|
||||
if err := router.Run(":8080"); err != nil {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initDatabase() (*gorm.DB, error) {
|
||||
db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Auto migrate
|
||||
if err := db.AutoMigrate(&models.User{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func setupRoutes(userHandler *api.UserHandler) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
// Middleware
|
||||
router.Use(corsMiddleware())
|
||||
router.Use(loggingMiddleware())
|
||||
|
||||
// Health check
|
||||
router.GET("/health", healthCheck)
|
||||
|
||||
// API routes
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
users := v1.Group("/users")
|
||||
{
|
||||
users.POST("", userHandler.CreateUser)
|
||||
users.GET("", userHandler.GetUsers)
|
||||
users.GET("/:id", userHandler.GetUser)
|
||||
users.PUT("/:id", userHandler.UpdateUser)
|
||||
users.DELETE("/:id", userHandler.DeleteUser)
|
||||
users.GET("/search", userHandler.SearchUsers)
|
||||
users.GET("/stats", userHandler.GetUserStats)
|
||||
users.GET("/export", userHandler.ExportUsers)
|
||||
}
|
||||
|
||||
auth := v1.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", userHandler.Login)
|
||||
auth.POST("/logout", userHandler.Logout)
|
||||
auth.POST("/change-password", userHandler.ChangePassword)
|
||||
}
|
||||
|
||||
admin := v1.Group("/admin")
|
||||
{
|
||||
admin.POST("/users/:id/reset-password", userHandler.ResetPassword)
|
||||
admin.POST("/users/:id/permissions", userHandler.AddPermission)
|
||||
admin.DELETE("/users/:id/permissions", userHandler.RemovePermission)
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func healthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC(),
|
||||
"version": "1.0.0",
|
||||
})
|
||||
}
|
||||
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func loggingMiddleware() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format(time.RFC1123),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.Request.UserAgent(),
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func createSampleData(userService *services.UserService) {
|
||||
// Check if admin user already exists
|
||||
if _, err := userService.GetUserByUsername("admin"); err == nil {
|
||||
return // Admin user already exists
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
adminReq := &models.UserRequest{
|
||||
Username: "admin",
|
||||
Email: "admin@example.com",
|
||||
Name: "System Administrator",
|
||||
Age: 30,
|
||||
Password: "admin123",
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
|
||||
admin, err := userService.CreateUser(adminReq)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create admin user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add admin permissions
|
||||
permissions := []string{
|
||||
"user_management",
|
||||
"system_admin",
|
||||
"user_read",
|
||||
"user_write",
|
||||
"user_delete",
|
||||
}
|
||||
|
||||
for _, perm := range permissions {
|
||||
if err := userService.AddPermission(admin.ID, perm); err != nil {
|
||||
log.Printf("Failed to add permission %s to admin: %v", perm, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create sample users
|
||||
sampleUsers := []*models.UserRequest{
|
||||
{
|
||||
Username: "john_doe",
|
||||
Email: "john@example.com",
|
||||
Name: "John Doe",
|
||||
Age: 25,
|
||||
Password: "password123",
|
||||
Role: models.RoleUser,
|
||||
},
|
||||
{
|
||||
Username: "jane_smith",
|
||||
Email: "jane@example.com",
|
||||
Name: "Jane Smith",
|
||||
Age: 28,
|
||||
Password: "password123",
|
||||
Role: models.RoleUser,
|
||||
},
|
||||
{
|
||||
Username: "guest_user",
|
||||
Email: "guest@example.com",
|
||||
Name: "Guest User",
|
||||
Age: 22,
|
||||
Password: "password123",
|
||||
Role: models.RoleGuest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, userReq := range sampleUsers {
|
||||
if _, err := userService.CreateUser(userReq); err != nil {
|
||||
log.Printf("Failed to create user %s: %v", userReq.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Sample data created successfully")
|
||||
}
|
||||
|
||||
// Helper functions for demo
|
||||
func printUserStats(userService *services.UserService) {
|
||||
stats, err := userService.GetUserStats()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get user stats: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("User Statistics:")
|
||||
log.Printf(" Total: %d", stats.Total)
|
||||
log.Printf(" Active: %d", stats.Active)
|
||||
log.Printf(" Admin: %d", stats.Admin)
|
||||
log.Printf(" User: %d", stats.User)
|
||||
log.Printf(" Guest: %d", stats.Guest)
|
||||
log.Printf(" With Email: %d", stats.WithEmail)
|
||||
}
|
||||
|
||||
func demonstrateUserOperations(userService *services.UserService) {
|
||||
log.Println("\n=== User Management Demo ===")
|
||||
|
||||
// Get all users
|
||||
users, total, err := userService.GetAllUsers(1, 10)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get users: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Found %d users (total: %d):", len(users), total)
|
||||
for _, user := range users {
|
||||
log.Printf(" - %s (%s) - %s [%s]",
|
||||
user.Username, user.Name, user.Role, user.Status)
|
||||
}
|
||||
|
||||
// Test authentication
|
||||
log.Println("\n=== Authentication Test ===")
|
||||
user, err := userService.AuthenticateUser("admin", "admin123")
|
||||
if err != nil {
|
||||
log.Printf("Authentication failed: %v", err)
|
||||
} else {
|
||||
log.Printf("Authentication successful for: %s", user.Username)
|
||||
log.Printf("Last login: %v", user.LastLogin)
|
||||
}
|
||||
|
||||
// Test search
|
||||
log.Println("\n=== Search Test ===")
|
||||
searchResults, _, err := userService.SearchUsers("john", 1, 10)
|
||||
if err != nil {
|
||||
log.Printf("Search failed: %v", err)
|
||||
} else {
|
||||
log.Printf("Search results for 'john': %d users", len(searchResults))
|
||||
for _, user := range searchResults {
|
||||
log.Printf(" - %s (%s)", user.Username, user.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Print stats
|
||||
log.Println("\n=== Statistics ===")
|
||||
printUserStats(userService)
|
||||
}
|
||||
|
||||
// Run demo if not in server mode
|
||||
func runDemo() {
|
||||
log.Println("Running User Management Demo...")
|
||||
|
||||
// Initialize database
|
||||
db, err := initDatabase()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize database:", err)
|
||||
}
|
||||
|
||||
// Initialize services
|
||||
userService := services.NewUserService(db)
|
||||
|
||||
// Create sample data
|
||||
createSampleData(userService)
|
||||
|
||||
// Demonstrate operations
|
||||
demonstrateUserOperations(userService)
|
||||
|
||||
log.Println("\nDemo completed!")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
module github.com/example/user-management
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.16.0
|
||||
golang.org/x/crypto v0.11.0
|
||||
gorm.io/driver/sqlite v1.5.2
|
||||
gorm.io/gorm v1.25.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,310 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserRole represents the role of a user
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleUser UserRole = "user"
|
||||
RoleGuest UserRole = "guest"
|
||||
)
|
||||
|
||||
// UserStatus represents the status of a user
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
StatusActive UserStatus = "active"
|
||||
StatusInactive UserStatus = "inactive"
|
||||
StatusSuspended UserStatus = "suspended"
|
||||
StatusDeleted UserStatus = "deleted"
|
||||
)
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key"`
|
||||
Username string `json:"username" gorm:"uniqueIndex;not null"`
|
||||
Email string `json:"email" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Age int `json:"age"`
|
||||
PasswordHash string `json:"-" gorm:"not null"`
|
||||
Role UserRole `json:"role" gorm:"default:user"`
|
||||
Status UserStatus `json:"status" gorm:"default:active"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
LoginAttempts int `json:"login_attempts" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// Permissions is a JSON field containing user permissions
|
||||
Permissions []string `json:"permissions" gorm:"type:json"`
|
||||
|
||||
// Metadata for additional user information
|
||||
Metadata map[string]interface{} `json:"metadata" gorm:"type:json"`
|
||||
}
|
||||
|
||||
// UserRequest represents a request to create or update a user
|
||||
type UserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=20"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Name string `json:"name" binding:"required,min=1,max=100"`
|
||||
Age int `json:"age" binding:"min=0,max=150"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Role UserRole `json:"role" binding:"omitempty,oneof=admin user guest"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// UserResponse represents a user response (without sensitive data)
|
||||
type UserResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Role UserRole `json:"role"`
|
||||
Status UserStatus `json:"status"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// BeforeCreate is a GORM hook that runs before creating a user
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == uuid.Nil {
|
||||
u.ID = uuid.New()
|
||||
}
|
||||
|
||||
if u.Permissions == nil {
|
||||
u.Permissions = []string{}
|
||||
}
|
||||
|
||||
if u.Metadata == nil {
|
||||
u.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPassword hashes and sets the user's password
|
||||
func (u *User) SetPassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return errors.New("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.PasswordHash = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyPassword checks if the provided password matches the user's password
|
||||
func (u *User) VerifyPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// HasPermission checks if the user has a specific permission
|
||||
func (u *User) HasPermission(permission string) bool {
|
||||
for _, p := range u.Permissions {
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddPermission adds a permission to the user
|
||||
func (u *User) AddPermission(permission string) {
|
||||
if !u.HasPermission(permission) {
|
||||
u.Permissions = append(u.Permissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePermission removes a permission from the user
|
||||
func (u *User) RemovePermission(permission string) {
|
||||
for i, p := range u.Permissions {
|
||||
if p == permission {
|
||||
u.Permissions = append(u.Permissions[:i], u.Permissions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsActive checks if the user is active
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Status == StatusActive
|
||||
}
|
||||
|
||||
// IsAdmin checks if the user is an admin
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// IsLocked checks if the user is locked due to too many failed login attempts
|
||||
func (u *User) IsLocked() bool {
|
||||
return u.LoginAttempts >= 5 || u.Status == StatusSuspended
|
||||
}
|
||||
|
||||
// Login records a successful login
|
||||
func (u *User) Login() error {
|
||||
if !u.IsActive() {
|
||||
return errors.New("user is not active")
|
||||
}
|
||||
|
||||
if u.IsLocked() {
|
||||
return errors.New("user is locked")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
u.LastLogin = &now
|
||||
u.LoginAttempts = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailedLoginAttempt records a failed login attempt
|
||||
func (u *User) FailedLoginAttempt() {
|
||||
u.LoginAttempts++
|
||||
if u.LoginAttempts >= 5 {
|
||||
u.Status = StatusSuspended
|
||||
}
|
||||
}
|
||||
|
||||
// ResetLoginAttempts resets the login attempts counter
|
||||
func (u *User) ResetLoginAttempts() {
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// Activate activates the user account
|
||||
func (u *User) Activate() {
|
||||
u.Status = StatusActive
|
||||
u.LoginAttempts = 0
|
||||
}
|
||||
|
||||
// Deactivate deactivates the user account
|
||||
func (u *User) Deactivate() {
|
||||
u.Status = StatusInactive
|
||||
}
|
||||
|
||||
// Suspend suspends the user account
|
||||
func (u *User) Suspend() {
|
||||
u.Status = StatusSuspended
|
||||
}
|
||||
|
||||
// Delete marks the user as deleted
|
||||
func (u *User) Delete() {
|
||||
u.Status = StatusDeleted
|
||||
}
|
||||
|
||||
// ToResponse converts a User to a UserResponse
|
||||
func (u *User) ToResponse() *UserResponse {
|
||||
return &UserResponse{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Name: u.Name,
|
||||
Age: u.Age,
|
||||
Role: u.Role,
|
||||
Status: u.Status,
|
||||
LastLogin: u.LastLogin,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
Permissions: u.Permissions,
|
||||
Metadata: u.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// FromRequest creates a User from a UserRequest
|
||||
func (u *User) FromRequest(req *UserRequest) error {
|
||||
u.Username = req.Username
|
||||
u.Email = req.Email
|
||||
u.Name = req.Name
|
||||
u.Age = req.Age
|
||||
u.Role = req.Role
|
||||
u.Metadata = req.Metadata
|
||||
|
||||
if req.Password != "" {
|
||||
return u.SetPassword(req.Password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON customizes JSON marshaling for User
|
||||
func (u *User) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(u.ToResponse())
|
||||
}
|
||||
|
||||
// Validate validates the user model
|
||||
func (u *User) Validate() error {
|
||||
if len(u.Username) < 3 || len(u.Username) > 20 {
|
||||
return errors.New("username must be between 3 and 20 characters")
|
||||
}
|
||||
|
||||
if len(u.Name) == 0 || len(u.Name) > 100 {
|
||||
return errors.New("name must be between 1 and 100 characters")
|
||||
}
|
||||
|
||||
if u.Age < 0 || u.Age > 150 {
|
||||
return errors.New("age must be between 0 and 150")
|
||||
}
|
||||
|
||||
if u.Role != RoleAdmin && u.Role != RoleUser && u.Role != RoleGuest {
|
||||
return errors.New("invalid role")
|
||||
}
|
||||
|
||||
if u.Status != StatusActive && u.Status != StatusInactive &&
|
||||
u.Status != StatusSuspended && u.Status != StatusDeleted {
|
||||
return errors.New("invalid status")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TableName returns the table name for GORM
|
||||
func (u *User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// GetMetadata gets a metadata value by key
|
||||
func (u *User) GetMetadata(key string) (interface{}, bool) {
|
||||
if u.Metadata == nil {
|
||||
return nil, false
|
||||
}
|
||||
value, exists := u.Metadata[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// SetMetadata sets a metadata value
|
||||
func (u *User) SetMetadata(key string, value interface{}) {
|
||||
if u.Metadata == nil {
|
||||
u.Metadata = make(map[string]interface{})
|
||||
}
|
||||
u.Metadata[key] = value
|
||||
}
|
||||
|
||||
// RemoveMetadata removes a metadata key
|
||||
func (u *User) RemoveMetadata(key string) {
|
||||
if u.Metadata != nil {
|
||||
delete(u.Metadata, key)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representation of the user
|
||||
func (u *User) String() string {
|
||||
return u.Username + " (" + u.Name + ")"
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/example/user-management/internal/models"
|
||||
"github.com/example/user-management/internal/utils"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserService handles user-related business logic
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserService creates a new user service
|
||||
func NewUserService(db *gorm.DB) *UserService {
|
||||
return &UserService{db: db}
|
||||
}
|
||||
|
||||
// CreateUser creates a new user
|
||||
func (s *UserService) CreateUser(req *models.UserRequest) (*models.User, error) {
|
||||
// Check if username already exists
|
||||
var existingUser models.User
|
||||
if err := s.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
// Check if email already exists (if provided)
|
||||
if req.Email != "" {
|
||||
if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Create new user
|
||||
user := &models.User{
|
||||
Role: models.RoleUser,
|
||||
Status: models.StatusActive,
|
||||
}
|
||||
|
||||
if err := user.FromRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("failed to create user from request: %w", err)
|
||||
}
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("user validation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Create(user).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by ID
|
||||
func (s *UserService) GetUserByID(id uuid.UUID) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.First(&user, "id = ?", id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByUsername retrieves a user by username
|
||||
func (s *UserService) GetUserByUsername(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail retrieves a user by email
|
||||
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user
|
||||
func (s *UserService) UpdateUser(id uuid.UUID, updates map[string]interface{}) (*models.User, error) {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
for key, value := range updates {
|
||||
switch key {
|
||||
case "name":
|
||||
if name, ok := value.(string); ok {
|
||||
user.Name = name
|
||||
}
|
||||
case "age":
|
||||
if age, ok := value.(int); ok {
|
||||
user.Age = age
|
||||
}
|
||||
case "email":
|
||||
if email, ok := value.(string); ok {
|
||||
user.Email = email
|
||||
}
|
||||
case "role":
|
||||
if role, ok := value.(models.UserRole); ok {
|
||||
user.Role = role
|
||||
}
|
||||
case "status":
|
||||
if status, ok := value.(models.UserStatus); ok {
|
||||
user.Status = status
|
||||
}
|
||||
case "metadata":
|
||||
if metadata, ok := value.(map[string]interface{}); ok {
|
||||
user.Metadata = metadata
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("user validation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeleteUser soft deletes a user
|
||||
func (s *UserService) DeleteUser(id uuid.UUID) error {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Delete()
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HardDeleteUser permanently deletes a user
|
||||
func (s *UserService) HardDeleteUser(id uuid.UUID) error {
|
||||
if err := s.db.Unscoped().Delete(&models.User{}, id).Error; err != nil {
|
||||
return fmt.Errorf("failed to hard delete user: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllUsers retrieves all users with pagination
|
||||
func (s *UserService) GetAllUsers(page, pageSize int) ([]*models.User, int64, error) {
|
||||
var users []*models.User
|
||||
var total int64
|
||||
|
||||
// Count total users
|
||||
if err := s.db.Model(&models.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count users: %w", err)
|
||||
}
|
||||
|
||||
// Get users with pagination
|
||||
offset := (page - 1) * pageSize
|
||||
if err := s.db.Limit(pageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// GetActiveUsers retrieves all active users
|
||||
func (s *UserService) GetActiveUsers() ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := s.db.Where("status = ?", models.StatusActive).Find(&users).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get active users: %w", err)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetUsersByRole retrieves users by role
|
||||
func (s *UserService) GetUsersByRole(role models.UserRole) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := s.db.Where("role = ?", role).Find(&users).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get users by role: %w", err)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// SearchUsers searches for users by name or username
|
||||
func (s *UserService) SearchUsers(query string, page, pageSize int) ([]*models.User, int64, error) {
|
||||
var users []*models.User
|
||||
var total int64
|
||||
|
||||
searchQuery := "%" + strings.ToLower(query) + "%"
|
||||
|
||||
// Count total matching users
|
||||
if err := s.db.Model(&models.User{}).Where(
|
||||
"LOWER(name) LIKE ? OR LOWER(username) LIKE ? OR LOWER(email) LIKE ?",
|
||||
searchQuery, searchQuery, searchQuery,
|
||||
).Count(&total).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count search results: %w", err)
|
||||
}
|
||||
|
||||
// Get matching users with pagination
|
||||
offset := (page - 1) * pageSize
|
||||
if err := s.db.Where(
|
||||
"LOWER(name) LIKE ? OR LOWER(username) LIKE ? OR LOWER(email) LIKE ?",
|
||||
searchQuery, searchQuery, searchQuery,
|
||||
).Limit(pageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to search users: %w", err)
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// GetUserStats returns user statistics
|
||||
func (s *UserService) GetUserStats() (*utils.UserStats, error) {
|
||||
var stats utils.UserStats
|
||||
|
||||
// Total users
|
||||
if err := s.db.Model(&models.User{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count total users: %w", err)
|
||||
}
|
||||
|
||||
// Active users
|
||||
if err := s.db.Model(&models.User{}).Where("status = ?", models.StatusActive).Count(&stats.Active).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count active users: %w", err)
|
||||
}
|
||||
|
||||
// Admin users
|
||||
if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleAdmin).Count(&stats.Admin).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count admin users: %w", err)
|
||||
}
|
||||
|
||||
// Regular users
|
||||
if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleUser).Count(&stats.User).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count regular users: %w", err)
|
||||
}
|
||||
|
||||
// Guest users
|
||||
if err := s.db.Model(&models.User{}).Where("role = ?", models.RoleGuest).Count(&stats.Guest).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count guest users: %w", err)
|
||||
}
|
||||
|
||||
// Users with email
|
||||
if err := s.db.Model(&models.User{}).Where("email != ''").Count(&stats.WithEmail).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count users with email: %w", err)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// AuthenticateUser authenticates a user with username and password
|
||||
func (s *UserService) AuthenticateUser(username, password string) (*models.User, error) {
|
||||
user, err := s.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
if !user.IsActive() {
|
||||
return nil, errors.New("user account is not active")
|
||||
}
|
||||
|
||||
if user.IsLocked() {
|
||||
return nil, errors.New("user account is locked")
|
||||
}
|
||||
|
||||
if !user.VerifyPassword(password) {
|
||||
user.FailedLoginAttempt()
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update failed login attempt: %w", err)
|
||||
}
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
// Successful login
|
||||
if err := user.Login(); err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to update login info: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword changes a user's password
|
||||
func (s *UserService) ChangePassword(id uuid.UUID, currentPassword, newPassword string) error {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !user.VerifyPassword(currentPassword) {
|
||||
return errors.New("current password is incorrect")
|
||||
}
|
||||
|
||||
if err := user.SetPassword(newPassword); err != nil {
|
||||
return fmt.Errorf("failed to set new password: %w", err)
|
||||
}
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword resets a user's password (admin function)
|
||||
func (s *UserService) ResetPassword(id uuid.UUID, newPassword string) error {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.SetPassword(newPassword); err != nil {
|
||||
return fmt.Errorf("failed to set new password: %w", err)
|
||||
}
|
||||
|
||||
user.ResetLoginAttempts()
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPermission adds a permission to a user
|
||||
func (s *UserService) AddPermission(id uuid.UUID, permission string) error {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.AddPermission(permission)
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to add permission: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePermission removes a permission from a user
|
||||
func (s *UserService) RemovePermission(id uuid.UUID, permission string) error {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.RemovePermission(permission)
|
||||
|
||||
if err := s.db.Save(user).Error; err != nil {
|
||||
return fmt.Errorf("failed to remove permission: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportUsers exports users to JSON
|
||||
func (s *UserService) ExportUsers() ([]byte, error) {
|
||||
users, _, err := s.GetAllUsers(1, 1000) // Get all users (limit to 1000 for safety)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get users for export: %w", err)
|
||||
}
|
||||
|
||||
var responses []*models.UserResponse
|
||||
for _, user := range users {
|
||||
responses = append(responses, user.ToResponse())
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(responses, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal users: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetUserActivity returns user activity information
|
||||
func (s *UserService) GetUserActivity(id uuid.UUID) (*utils.UserActivity, error) {
|
||||
user, err := s.GetUserByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activity := &utils.UserActivity{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
LastLogin: user.LastLogin,
|
||||
LoginAttempts: user.LoginAttempts,
|
||||
IsActive: user.IsActive(),
|
||||
IsLocked: user.IsLocked(),
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
return activity, nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserStats represents user statistics
|
||||
type UserStats struct {
|
||||
Total int64 `json:"total"`
|
||||
Active int64 `json:"active"`
|
||||
Admin int64 `json:"admin"`
|
||||
User int64 `json:"user"`
|
||||
Guest int64 `json:"guest"`
|
||||
WithEmail int64 `json:"with_email"`
|
||||
}
|
||||
|
||||
// UserActivity represents user activity information
|
||||
type UserActivity struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
LoginAttempts int `json:"login_attempts"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PaginatedResponse represents a paginated response
|
||||
type PaginatedResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// NewPaginatedResponse creates a new paginated response
|
||||
func NewPaginatedResponse(data interface{}, page, pageSize int, total int64) *PaginatedResponse {
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
return &PaginatedResponse{
|
||||
Data: data,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
}
|
||||
|
||||
// APIResponse represents a standard API response
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewSuccessResponse creates a new success response
|
||||
func NewSuccessResponse(message string, data interface{}) *APIResponse {
|
||||
return &APIResponse{
|
||||
Success: true,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorResponse creates a new error response
|
||||
func NewErrorResponse(message string, err error) *APIResponse {
|
||||
resp := &APIResponse{
|
||||
Success: false,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// ValidationError represents a validation error
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ValidationErrors represents multiple validation errors
|
||||
type ValidationErrors struct {
|
||||
Errors []ValidationError `json:"errors"`
|
||||
}
|
||||
|
||||
// NewValidationErrors creates a new validation errors instance
|
||||
func NewValidationErrors() *ValidationErrors {
|
||||
return &ValidationErrors{
|
||||
Errors: make([]ValidationError, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a validation error
|
||||
func (ve *ValidationErrors) Add(field, message string) {
|
||||
ve.Errors = append(ve.Errors, ValidationError{
|
||||
Field: field,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// HasErrors returns true if there are validation errors
|
||||
func (ve *ValidationErrors) HasErrors() bool {
|
||||
return len(ve.Errors) > 0
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (ve *ValidationErrors) Error() string {
|
||||
if len(ve.Errors) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(ve.Errors) == 1 {
|
||||
return ve.Errors[0].Message
|
||||
}
|
||||
|
||||
return "multiple validation errors"
|
||||
}
|
||||
|
||||
// DatabaseConfig represents database configuration
|
||||
type DatabaseConfig struct {
|
||||
Driver string `json:"driver"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
SSLMode string `json:"ssl_mode"`
|
||||
}
|
||||
|
||||
// ServerConfig represents server configuration
|
||||
type ServerConfig struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
ReadTimeout int `json:"read_timeout"`
|
||||
WriteTimeout int `json:"write_timeout"`
|
||||
IdleTimeout int `json:"idle_timeout"`
|
||||
}
|
||||
|
||||
// JWTConfig represents JWT configuration
|
||||
type JWTConfig struct {
|
||||
SecretKey string `json:"secret_key"`
|
||||
ExpirationHours int `json:"expiration_hours"`
|
||||
RefreshHours int `json:"refresh_hours"`
|
||||
Issuer string `json:"issuer"`
|
||||
SigningAlgorithm string `json:"signing_algorithm"`
|
||||
}
|
||||
|
||||
// Config represents application configuration
|
||||
type Config struct {
|
||||
Database DatabaseConfig `json:"database"`
|
||||
Server ServerConfig `json:"server"`
|
||||
JWT JWTConfig `json:"jwt"`
|
||||
LogLevel string `json:"log_level"`
|
||||
Debug bool `json:"debug"`
|
||||
}
|
||||
|
||||
// SearchParams represents search parameters
|
||||
type SearchParams struct {
|
||||
Query string `json:"query"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
SortBy string `json:"sort_by"`
|
||||
SortDir string `json:"sort_dir"`
|
||||
}
|
||||
|
||||
// NewSearchParams creates new search parameters with defaults
|
||||
func NewSearchParams() *SearchParams {
|
||||
return &SearchParams{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
SortBy: "created_at",
|
||||
SortDir: "desc",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates search parameters
|
||||
func (sp *SearchParams) Validate() error {
|
||||
if sp.Page < 1 {
|
||||
sp.Page = 1
|
||||
}
|
||||
|
||||
if sp.PageSize < 1 {
|
||||
sp.PageSize = 20
|
||||
}
|
||||
|
||||
if sp.PageSize > 100 {
|
||||
sp.PageSize = 100
|
||||
}
|
||||
|
||||
if sp.SortBy == "" {
|
||||
sp.SortBy = "created_at"
|
||||
}
|
||||
|
||||
if sp.SortDir != "asc" && sp.SortDir != "desc" {
|
||||
sp.SortDir = "desc"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterParams represents filter parameters
|
||||
type FilterParams struct {
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
AgeMin int `json:"age_min"`
|
||||
AgeMax int `json:"age_max"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AuditLog represents an audit log entry
|
||||
type AuditLog struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Session represents a user session
|
||||
type Session struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// IsExpired checks if the session is expired
|
||||
func (s *Session) IsExpired() bool {
|
||||
return time.Now().After(s.ExpiresAt)
|
||||
}
|
||||
|
||||
// ExtendSession extends the session expiration
|
||||
func (s *Session) ExtendSession(duration time.Duration) {
|
||||
s.ExpiresAt = time.Now().Add(duration)
|
||||
s.UpdatedAt = time.Now()
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/example/user-management/internal/models"
|
||||
"github.com/example/user-management/internal/services"
|
||||
"github.com/example/user-management/internal/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserHandler handles user-related HTTP requests
|
||||
type UserHandler struct {
|
||||
userService *services.UserService
|
||||
}
|
||||
|
||||
// NewUserHandler creates a new user handler
|
||||
func NewUserHandler(userService *services.UserService) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser handles user creation
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req models.UserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to create user", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, utils.NewSuccessResponse("User created successfully", user.ToResponse()))
|
||||
}
|
||||
|
||||
// GetUser handles getting a single user
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, utils.NewErrorResponse("User not found", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("User retrieved successfully", user.ToResponse()))
|
||||
}
|
||||
|
||||
// GetUsers handles getting users with pagination
|
||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
users, total, err := h.userService.GetAllUsers(page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to get users", err))
|
||||
return
|
||||
}
|
||||
|
||||
var responses []*models.UserResponse
|
||||
for _, user := range users {
|
||||
responses = append(responses, user.ToResponse())
|
||||
}
|
||||
|
||||
paginatedResponse := utils.NewPaginatedResponse(responses, page, pageSize, total)
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Users retrieved successfully", paginatedResponse))
|
||||
}
|
||||
|
||||
// UpdateUser handles user updates
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.UpdateUser(id, updates)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to update user", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("User updated successfully", user.ToResponse()))
|
||||
}
|
||||
|
||||
// DeleteUser handles user deletion
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.DeleteUser(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to delete user", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("User deleted successfully", nil))
|
||||
}
|
||||
|
||||
// SearchUsers handles user search
|
||||
func (h *UserHandler) SearchUsers(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
users, total, err := h.userService.SearchUsers(query, page, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to search users", err))
|
||||
return
|
||||
}
|
||||
|
||||
var responses []*models.UserResponse
|
||||
for _, user := range users {
|
||||
responses = append(responses, user.ToResponse())
|
||||
}
|
||||
|
||||
paginatedResponse := utils.NewPaginatedResponse(responses, page, pageSize, total)
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Search completed successfully", paginatedResponse))
|
||||
}
|
||||
|
||||
// GetUserStats handles getting user statistics
|
||||
func (h *UserHandler) GetUserStats(c *gin.Context) {
|
||||
stats, err := h.userService.GetUserStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to get user statistics", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Statistics retrieved successfully", stats))
|
||||
}
|
||||
|
||||
// ExportUsers handles user export
|
||||
func (h *UserHandler) ExportUsers(c *gin.Context) {
|
||||
data, err := h.userService.ExportUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, utils.NewErrorResponse("Failed to export users", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", "attachment; filename=users.json")
|
||||
c.Data(http.StatusOK, "application/json", data)
|
||||
}
|
||||
|
||||
// Login handles user authentication
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.AuthenticateUser(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, utils.NewErrorResponse("Authentication failed", err))
|
||||
return
|
||||
}
|
||||
|
||||
// In a real application, you would generate a JWT token here
|
||||
response := map[string]interface{}{
|
||||
"user": user.ToResponse(),
|
||||
"token": "dummy-jwt-token", // This would be a real JWT token
|
||||
"expires": "2024-12-31T23:59:59Z",
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Login successful", response))
|
||||
}
|
||||
|
||||
// Logout handles user logout
|
||||
func (h *UserHandler) Logout(c *gin.Context) {
|
||||
// In a real application, you would invalidate the JWT token here
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Logout successful", nil))
|
||||
}
|
||||
|
||||
// ChangePassword handles password change
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID uuid.UUID `json:"user_id" binding:"required"`
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.ChangePassword(req.UserID, req.CurrentPassword, req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to change password", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Password changed successfully", nil))
|
||||
}
|
||||
|
||||
// ResetPassword handles password reset (admin only)
|
||||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.ResetPassword(id, req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to reset password", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Password reset successfully", nil))
|
||||
}
|
||||
|
||||
// AddPermission handles adding permission to user
|
||||
func (h *UserHandler) AddPermission(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Permission string `json:"permission" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid request", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.AddPermission(id, req.Permission); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to add permission", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Permission added successfully", nil))
|
||||
}
|
||||
|
||||
// RemovePermission handles removing permission from user
|
||||
func (h *UserHandler) RemovePermission(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Invalid user ID", err))
|
||||
return
|
||||
}
|
||||
|
||||
permission := c.Query("permission")
|
||||
if permission == "" {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Permission parameter is required", nil))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userService.RemovePermission(id, permission); err != nil {
|
||||
c.JSON(http.StatusBadRequest, utils.NewErrorResponse("Failed to remove permission", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, utils.NewSuccessResponse("Permission removed successfully", nil))
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# User Management System (Java)
|
||||
|
||||
A comprehensive user management system built in Java for testing Code Index MCP's analysis capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **User Management**: Create, update, delete, and search users
|
||||
- **Authentication**: BCrypt password hashing and verification
|
||||
- **Authorization**: Role-based access control (Admin, User, Guest)
|
||||
- **Data Validation**: Input validation and sanitization
|
||||
- **Export/Import**: JSON and CSV export capabilities
|
||||
- **Persistence**: File-based storage with JSON serialization
|
||||
- **Logging**: SLF4J logging with Logback
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/main/java/com/example/usermanagement/
|
||||
├── models/
|
||||
│ ├── Person.java # Base person model
|
||||
│ ├── User.java # User model with auth features
|
||||
│ ├── UserRole.java # User role enumeration
|
||||
│ └── UserStatus.java # User status enumeration
|
||||
├── services/
|
||||
│ └── UserManager.java # User management service
|
||||
├── utils/
|
||||
│ ├── ValidationUtils.java # Validation utilities
|
||||
│ ├── UserNotFoundException.java # Custom exception
|
||||
│ └── DuplicateUserException.java # Custom exception
|
||||
└── Main.java # Main demo application
|
||||
```
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- **Java 11**: Modern Java features and APIs
|
||||
- **Jackson**: JSON processing and serialization
|
||||
- **BCrypt**: Secure password hashing
|
||||
- **Apache Commons**: Utility libraries (Lang3, CSV)
|
||||
- **SLF4J + Logback**: Logging framework
|
||||
- **Maven**: Build and dependency management
|
||||
- **JUnit 5**: Testing framework
|
||||
|
||||
## Build and Run
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Java 11 or higher
|
||||
- Maven 3.6+
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
mvn exec:java -Dexec.mainClass="com.example.usermanagement.Main"
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
### Package
|
||||
|
||||
```bash
|
||||
mvn package
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating Users
|
||||
|
||||
```java
|
||||
UserManager userManager = new UserManager();
|
||||
|
||||
// Create a basic user
|
||||
User user = userManager.createUser("John Doe", 30, "john_doe", "john@example.com");
|
||||
user.setPassword("SecurePass123!");
|
||||
|
||||
// Create an admin user
|
||||
User admin = userManager.createUser("Jane Smith", 35, "jane_admin",
|
||||
"jane@example.com", UserRole.ADMIN);
|
||||
admin.setPassword("AdminPass123!");
|
||||
admin.addPermission("user_management");
|
||||
```
|
||||
|
||||
### User Authentication
|
||||
|
||||
```java
|
||||
// Verify password
|
||||
boolean isValid = user.verifyPassword("SecurePass123!");
|
||||
|
||||
// Login
|
||||
if (user.login()) {
|
||||
System.out.println("Login successful!");
|
||||
System.out.println("Last login: " + user.getLastLogin());
|
||||
}
|
||||
```
|
||||
|
||||
### User Management
|
||||
|
||||
```java
|
||||
// Search users
|
||||
List<User> results = userManager.searchUsers("john");
|
||||
|
||||
// Filter users
|
||||
List<User> activeUsers = userManager.getActiveUsers();
|
||||
List<User> adminUsers = userManager.getUsersByRole(UserRole.ADMIN);
|
||||
List<User> olderUsers = userManager.getUsersOlderThan(25);
|
||||
|
||||
// Update user
|
||||
Map<String, Object> updates = Map.of("age", 31, "email", "newemail@example.com");
|
||||
userManager.updateUser("john_doe", updates);
|
||||
|
||||
// Export users
|
||||
String jsonData = userManager.exportUsers("json");
|
||||
String csvData = userManager.exportUsers("csv");
|
||||
```
|
||||
|
||||
## Testing Features
|
||||
|
||||
This project tests the following Java language features:
|
||||
|
||||
### Core Language Features
|
||||
- **Classes and Inheritance**: Person and User class hierarchy
|
||||
- **Enums**: UserRole and UserStatus with methods
|
||||
- **Interfaces**: Custom exceptions and validation
|
||||
- **Generics**: Collections with type safety
|
||||
- **Annotations**: Jackson JSON annotations
|
||||
- **Exception Handling**: Custom exceptions and try-catch blocks
|
||||
|
||||
### Modern Java Features
|
||||
- **Streams API**: Filtering, mapping, and collecting
|
||||
- **Lambda Expressions**: Functional programming
|
||||
- **Method References**: Stream operations
|
||||
- **Optional**: Null-safe operations
|
||||
- **Time API**: LocalDateTime usage
|
||||
|
||||
### Advanced Features
|
||||
- **Concurrent Collections**: ConcurrentHashMap
|
||||
- **Reflection**: Jackson serialization
|
||||
- **File I/O**: NIO.2 Path and Files
|
||||
- **Logging**: SLF4J with parameterized messages
|
||||
- **Validation**: Input validation and sanitization
|
||||
|
||||
### Framework Integration
|
||||
- **Maven**: Build lifecycle and dependency management
|
||||
- **Jackson**: JSON serialization/deserialization
|
||||
- **BCrypt**: Password hashing
|
||||
- **Apache Commons**: Utility libraries
|
||||
- **SLF4J**: Structured logging
|
||||
|
||||
### Design Patterns
|
||||
- **Builder Pattern**: Object construction
|
||||
- **Factory Pattern**: User creation
|
||||
- **Repository Pattern**: Data access
|
||||
- **Service Layer**: Business logic separation
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
- **Jackson Databind**: JSON processing
|
||||
- **Jackson JSR310**: Java 8 time support
|
||||
- **BCrypt**: Password hashing
|
||||
- **Apache Commons Lang3**: Utilities
|
||||
- **Apache Commons CSV**: CSV processing
|
||||
|
||||
### Logging
|
||||
- **SLF4J API**: Logging facade
|
||||
- **Logback Classic**: Logging implementation
|
||||
|
||||
### Testing
|
||||
- **JUnit 5**: Testing framework
|
||||
- **Mockito**: Mocking framework
|
||||
|
||||
## License
|
||||
|
||||
MIT License - This is a sample project for testing purposes.
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>user-management</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>User Management System</name>
|
||||
<description>A sample user management system for testing Code Index MCP</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.9.2</junit.version>
|
||||
<jackson.version>2.15.2</jackson.version>
|
||||
<slf4j.version>2.0.7</slf4j.version>
|
||||
<logback.version>1.4.7</logback.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Jackson for JSON processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-csv</artifactId>
|
||||
<version>1.9.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- BCrypt for password hashing -->
|
||||
<dependency>
|
||||
<groupId>org.mindrot</groupId>
|
||||
<artifactId>jbcrypt</artifactId>
|
||||
<version>0.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<mainClass>com.example.usermanagement.Main</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,220 @@
|
||||
package com.example.usermanagement;
|
||||
|
||||
import com.example.usermanagement.models.User;
|
||||
import com.example.usermanagement.models.UserRole;
|
||||
import com.example.usermanagement.services.UserManager;
|
||||
import com.example.usermanagement.utils.UserNotFoundException;
|
||||
import com.example.usermanagement.utils.DuplicateUserException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Main class demonstrating the User Management System.
|
||||
*/
|
||||
public class Main {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Main.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=".repeat(50));
|
||||
System.out.println("User Management System Demo (Java)");
|
||||
System.out.println("=".repeat(50));
|
||||
|
||||
// Create user manager
|
||||
UserManager userManager = new UserManager();
|
||||
|
||||
// Create sample users
|
||||
System.out.println("\n1. Creating sample users...");
|
||||
createSampleUsers(userManager);
|
||||
|
||||
// Display all users
|
||||
System.out.println("\n2. Listing all users...");
|
||||
listAllUsers(userManager);
|
||||
|
||||
// Test user retrieval
|
||||
System.out.println("\n3. Testing user retrieval...");
|
||||
testUserRetrieval(userManager);
|
||||
|
||||
// Test user search
|
||||
System.out.println("\n4. Testing user search...");
|
||||
testUserSearch(userManager);
|
||||
|
||||
// Test user filtering
|
||||
System.out.println("\n5. Testing user filtering...");
|
||||
testUserFiltering(userManager);
|
||||
|
||||
// Test user updates
|
||||
System.out.println("\n6. Testing user updates...");
|
||||
testUserUpdates(userManager);
|
||||
|
||||
// Test authentication
|
||||
System.out.println("\n7. Testing authentication...");
|
||||
testAuthentication(userManager);
|
||||
|
||||
// Display statistics
|
||||
System.out.println("\n8. User statistics...");
|
||||
displayStatistics(userManager);
|
||||
|
||||
// Test export functionality
|
||||
System.out.println("\n9. Testing export functionality...");
|
||||
testExport(userManager);
|
||||
|
||||
// Test user permissions
|
||||
System.out.println("\n10. Testing user permissions...");
|
||||
testPermissions(userManager);
|
||||
|
||||
System.out.println("\n" + "=".repeat(50));
|
||||
System.out.println("Demo completed successfully!");
|
||||
System.out.println("=".repeat(50));
|
||||
}
|
||||
|
||||
private static void createSampleUsers(UserManager userManager) {
|
||||
try {
|
||||
// Create admin user
|
||||
User admin = userManager.createUser("Alice Johnson", 30, "alice_admin",
|
||||
"alice@example.com", UserRole.ADMIN);
|
||||
admin.setPassword("AdminPass123!");
|
||||
admin.addPermission("user_management");
|
||||
admin.addPermission("system_admin");
|
||||
|
||||
// Create regular users
|
||||
User user1 = userManager.createUser("Bob Smith", 25, "bob_user", "bob@example.com");
|
||||
user1.setPassword("UserPass123!");
|
||||
|
||||
User user2 = userManager.createUser("Charlie Brown", 35, "charlie", "charlie@example.com");
|
||||
user2.setPassword("CharliePass123!");
|
||||
|
||||
User user3 = userManager.createUser("Diana Prince", 28, "diana", "diana@example.com");
|
||||
user3.setPassword("DianaPass123!");
|
||||
|
||||
System.out.println("✓ Created " + userManager.getUserCount() + " users");
|
||||
|
||||
} catch (DuplicateUserException e) {
|
||||
System.out.println("✗ Error creating users: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
System.out.println("✗ Unexpected error: " + e.getMessage());
|
||||
logger.error("Error creating sample users", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void listAllUsers(UserManager userManager) {
|
||||
List<User> users = userManager.getAllUsers();
|
||||
|
||||
System.out.println("Found " + users.size() + " users:");
|
||||
users.forEach(user ->
|
||||
System.out.println(" • " + user.getUsername() + " (" + user.getName() +
|
||||
") - " + user.getRole().getDisplayName() +
|
||||
" [" + user.getStatus().getDisplayName() + "]")
|
||||
);
|
||||
}
|
||||
|
||||
private static void testUserRetrieval(UserManager userManager) {
|
||||
try {
|
||||
User user = userManager.getUser("alice_admin");
|
||||
System.out.println("✓ Retrieved user: " + user.getUsername() + " (" + user.getName() + ")");
|
||||
|
||||
User userByEmail = userManager.getUserByEmail("bob@example.com");
|
||||
if (userByEmail != null) {
|
||||
System.out.println("✓ Found user by email: " + userByEmail.getUsername());
|
||||
}
|
||||
|
||||
} catch (UserNotFoundException e) {
|
||||
System.out.println("✗ User retrieval failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testUserSearch(UserManager userManager) {
|
||||
List<User> searchResults = userManager.searchUsers("alice");
|
||||
System.out.println("Search results for 'alice': " + searchResults.size() + " users found");
|
||||
|
||||
searchResults.forEach(user ->
|
||||
System.out.println(" • " + user.getUsername() + " (" + user.getName() + ")")
|
||||
);
|
||||
}
|
||||
|
||||
private static void testUserFiltering(UserManager userManager) {
|
||||
List<User> olderUsers = userManager.getUsersOlderThan(30);
|
||||
System.out.println("Users older than 30: " + olderUsers.size() + " users");
|
||||
|
||||
olderUsers.forEach(user ->
|
||||
System.out.println(" • " + user.getUsername() + " (" + user.getName() + ") - age " + user.getAge())
|
||||
);
|
||||
|
||||
List<User> adminUsers = userManager.getUsersByRole(UserRole.ADMIN);
|
||||
System.out.println("Admin users: " + adminUsers.size() + " users");
|
||||
}
|
||||
|
||||
private static void testUserUpdates(UserManager userManager) {
|
||||
try {
|
||||
Map<String, Object> updates = Map.of("age", 26);
|
||||
User updatedUser = userManager.updateUser("bob_user", updates);
|
||||
System.out.println("✓ Updated " + updatedUser.getUsername() + "'s age to " + updatedUser.getAge());
|
||||
|
||||
} catch (UserNotFoundException e) {
|
||||
System.out.println("✗ Update failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testAuthentication(UserManager userManager) {
|
||||
try {
|
||||
User user = userManager.getUser("alice_admin");
|
||||
|
||||
// Test password verification
|
||||
boolean isValid = user.verifyPassword("AdminPass123!");
|
||||
System.out.println("✓ Password verification: " + (isValid ? "SUCCESS" : "FAILED"));
|
||||
|
||||
// Test login
|
||||
boolean loginSuccess = user.login();
|
||||
System.out.println("✓ Login attempt: " + (loginSuccess ? "SUCCESS" : "FAILED"));
|
||||
|
||||
if (loginSuccess) {
|
||||
System.out.println("✓ Last login: " + user.getLastLogin());
|
||||
}
|
||||
|
||||
} catch (UserNotFoundException e) {
|
||||
System.out.println("✗ Authentication test failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void displayStatistics(UserManager userManager) {
|
||||
Map<String, Integer> stats = userManager.getUserStats();
|
||||
|
||||
stats.forEach((key, value) ->
|
||||
System.out.println(" " + key.replace("_", " ").toUpperCase() + ": " + value)
|
||||
);
|
||||
}
|
||||
|
||||
private static void testExport(UserManager userManager) {
|
||||
try {
|
||||
String jsonExport = userManager.exportUsers("json");
|
||||
System.out.println("✓ JSON export: " + jsonExport.length() + " characters");
|
||||
|
||||
String csvExport = userManager.exportUsers("csv");
|
||||
System.out.println("✓ CSV export: " + csvExport.split("\n").length + " lines");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("✗ Export failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void testPermissions(UserManager userManager) {
|
||||
try {
|
||||
User admin = userManager.getUser("alice_admin");
|
||||
|
||||
System.out.println("Admin permissions: " + admin.getPermissions());
|
||||
System.out.println("Has user_management permission: " + admin.hasPermission("user_management"));
|
||||
System.out.println("Is admin: " + admin.isAdmin());
|
||||
|
||||
// Test role privileges
|
||||
System.out.println("Admin role can act on USER role: " +
|
||||
admin.getRole().canActOn(UserRole.USER));
|
||||
|
||||
} catch (UserNotFoundException e) {
|
||||
System.out.println("✗ Permission test failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.example.usermanagement.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents a person with basic information.
|
||||
* This class serves as the base class for more specific person types.
|
||||
*/
|
||||
public class Person {
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("age")
|
||||
private int age;
|
||||
|
||||
@JsonProperty("email")
|
||||
private String email;
|
||||
|
||||
@JsonProperty("created_at")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonProperty("metadata")
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* Default constructor for Jackson deserialization.
|
||||
*/
|
||||
public Person() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with name and age.
|
||||
*
|
||||
* @param name The person's name
|
||||
* @param age The person's age
|
||||
* @throws IllegalArgumentException if validation fails
|
||||
*/
|
||||
public Person(String name, int age) {
|
||||
this();
|
||||
setName(name);
|
||||
setAge(age);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with name, age, and email.
|
||||
*
|
||||
* @param name The person's name
|
||||
* @param age The person's age
|
||||
* @param email The person's email address
|
||||
* @throws IllegalArgumentException if validation fails
|
||||
*/
|
||||
public Person(String name, int age, String email) {
|
||||
this(name, age);
|
||||
setEmail(email);
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
throw new IllegalArgumentException("Name cannot be null or empty");
|
||||
}
|
||||
if (name.length() > 100) {
|
||||
throw new IllegalArgumentException("Name cannot exceed 100 characters");
|
||||
}
|
||||
this.name = name.trim();
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
if (age < 0) {
|
||||
throw new IllegalArgumentException("Age cannot be negative");
|
||||
}
|
||||
if (age > 150) {
|
||||
throw new IllegalArgumentException("Age cannot exceed 150");
|
||||
}
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
if (StringUtils.isNotBlank(email) && !isValidEmail(email)) {
|
||||
throw new IllegalArgumentException("Invalid email format");
|
||||
}
|
||||
this.email = StringUtils.isBlank(email) ? null : email.trim();
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Map<String, Object> getMetadata() {
|
||||
return new HashMap<>(metadata);
|
||||
}
|
||||
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new HashMap<>() : new HashMap<>(metadata);
|
||||
}
|
||||
|
||||
// Business methods
|
||||
|
||||
/**
|
||||
* Returns a greeting message for the person.
|
||||
*
|
||||
* @return A personalized greeting
|
||||
*/
|
||||
public String greet() {
|
||||
return String.format("Hello, I'm %s and I'm %d years old.", name, age);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the person has an email address.
|
||||
*
|
||||
* @return true if email is present and not empty
|
||||
*/
|
||||
public boolean hasEmail() {
|
||||
return StringUtils.isNotBlank(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the person's email address.
|
||||
*
|
||||
* @param newEmail The new email address
|
||||
* @throws IllegalArgumentException if email format is invalid
|
||||
*/
|
||||
public void updateEmail(String newEmail) {
|
||||
setEmail(newEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds metadata to the person.
|
||||
*
|
||||
* @param key The metadata key
|
||||
* @param value The metadata value
|
||||
*/
|
||||
public void addMetadata(String key, Object value) {
|
||||
if (StringUtils.isNotBlank(key)) {
|
||||
metadata.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets metadata value by key.
|
||||
*
|
||||
* @param key The metadata key
|
||||
* @return The metadata value or null if not found
|
||||
*/
|
||||
public Object getMetadata(String key) {
|
||||
return metadata.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets metadata value by key with default value.
|
||||
*
|
||||
* @param key The metadata key
|
||||
* @param defaultValue The default value if key is not found
|
||||
* @return The metadata value or default value
|
||||
*/
|
||||
public Object getMetadata(String key, Object defaultValue) {
|
||||
return metadata.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes metadata by key.
|
||||
*
|
||||
* @param key The metadata key to remove
|
||||
* @return The removed value or null if not found
|
||||
*/
|
||||
public Object removeMetadata(String key) {
|
||||
return metadata.remove(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all metadata.
|
||||
*/
|
||||
public void clearMetadata() {
|
||||
metadata.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email format using a simple regex.
|
||||
*
|
||||
* @param email The email to validate
|
||||
* @return true if email format is valid
|
||||
*/
|
||||
private boolean isValidEmail(String email) {
|
||||
String emailPattern = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
||||
return email.matches(emailPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Person instance from a map of data.
|
||||
*
|
||||
* @param data The data map
|
||||
* @return A new Person instance
|
||||
*/
|
||||
public static Person fromMap(Map<String, Object> data) {
|
||||
Person person = new Person();
|
||||
|
||||
if (data.containsKey("name")) {
|
||||
person.setName((String) data.get("name"));
|
||||
}
|
||||
|
||||
if (data.containsKey("age")) {
|
||||
person.setAge((Integer) data.get("age"));
|
||||
}
|
||||
|
||||
if (data.containsKey("email")) {
|
||||
person.setEmail((String) data.get("email"));
|
||||
}
|
||||
|
||||
if (data.containsKey("metadata")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> metadata = (Map<String, Object>) data.get("metadata");
|
||||
person.setMetadata(metadata);
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the person to a map representation.
|
||||
*
|
||||
* @return A map containing person data
|
||||
*/
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", name);
|
||||
map.put("age", age);
|
||||
map.put("email", email);
|
||||
map.put("created_at", createdAt);
|
||||
map.put("metadata", new HashMap<>(metadata));
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
|
||||
Person person = (Person) obj;
|
||||
return age == person.age &&
|
||||
Objects.equals(name, person.name) &&
|
||||
Objects.equals(email, person.email) &&
|
||||
Objects.equals(createdAt, person.createdAt) &&
|
||||
Objects.equals(metadata, person.metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, age, email, createdAt, metadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Person{name='%s', age=%d, email='%s', createdAt=%s}",
|
||||
name, age, email, createdAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package com.example.usermanagement.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* User class extending Person with authentication and authorization features.
|
||||
*/
|
||||
public class User extends Person {
|
||||
|
||||
@JsonProperty("username")
|
||||
private String username;
|
||||
|
||||
@JsonProperty("password_hash")
|
||||
private String passwordHash;
|
||||
|
||||
@JsonProperty("role")
|
||||
private UserRole role;
|
||||
|
||||
@JsonProperty("status")
|
||||
private UserStatus status;
|
||||
|
||||
@JsonProperty("last_login")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
@JsonProperty("login_attempts")
|
||||
private int loginAttempts;
|
||||
|
||||
@JsonProperty("permissions")
|
||||
private Set<String> permissions;
|
||||
|
||||
/**
|
||||
* Default constructor for Jackson deserialization.
|
||||
*/
|
||||
public User() {
|
||||
super();
|
||||
this.role = UserRole.USER;
|
||||
this.status = UserStatus.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
this.permissions = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with basic information.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
*/
|
||||
public User(String name, int age, String username) {
|
||||
super(name, age);
|
||||
setUsername(username);
|
||||
this.role = UserRole.USER;
|
||||
this.status = UserStatus.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
this.permissions = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with email.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
* @param email The email address
|
||||
*/
|
||||
public User(String name, int age, String username, String email) {
|
||||
super(name, age, email);
|
||||
setUsername(username);
|
||||
this.role = UserRole.USER;
|
||||
this.status = UserStatus.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
this.permissions = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with role.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
* @param email The email address
|
||||
* @param role The user role
|
||||
*/
|
||||
public User(String name, int age, String username, String email, UserRole role) {
|
||||
this(name, age, username, email);
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be null or empty");
|
||||
}
|
||||
if (username.length() < 3 || username.length() > 20) {
|
||||
throw new IllegalArgumentException("Username must be between 3 and 20 characters");
|
||||
}
|
||||
if (!username.matches("^[a-zA-Z0-9_]+$")) {
|
||||
throw new IllegalArgumentException("Username can only contain letters, numbers, and underscores");
|
||||
}
|
||||
this.username = username.trim();
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public UserRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(UserRole role) {
|
||||
this.role = role != null ? role : UserRole.USER;
|
||||
}
|
||||
|
||||
public UserStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(UserStatus status) {
|
||||
this.status = status != null ? status : UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLogin() {
|
||||
return lastLogin;
|
||||
}
|
||||
|
||||
public void setLastLogin(LocalDateTime lastLogin) {
|
||||
this.lastLogin = lastLogin;
|
||||
}
|
||||
|
||||
public int getLoginAttempts() {
|
||||
return loginAttempts;
|
||||
}
|
||||
|
||||
public void setLoginAttempts(int loginAttempts) {
|
||||
this.loginAttempts = Math.max(0, loginAttempts);
|
||||
}
|
||||
|
||||
public Set<String> getPermissions() {
|
||||
return new HashSet<>(permissions);
|
||||
}
|
||||
|
||||
public void setPermissions(Set<String> permissions) {
|
||||
this.permissions = permissions != null ? new HashSet<>(permissions) : new HashSet<>();
|
||||
}
|
||||
|
||||
// Authentication methods
|
||||
|
||||
/**
|
||||
* Sets the user's password using BCrypt hashing.
|
||||
*
|
||||
* @param password The plain text password
|
||||
* @throws IllegalArgumentException if password is invalid
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
if (StringUtils.isBlank(password)) {
|
||||
throw new IllegalArgumentException("Password cannot be null or empty");
|
||||
}
|
||||
if (password.length() < 8) {
|
||||
throw new IllegalArgumentException("Password must be at least 8 characters long");
|
||||
}
|
||||
|
||||
// Hash the password with BCrypt
|
||||
this.passwordHash = BCrypt.hashpw(password, BCrypt.gensalt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a password against the stored hash.
|
||||
*
|
||||
* @param password The plain text password to verify
|
||||
* @return true if password matches
|
||||
*/
|
||||
public boolean verifyPassword(String password) {
|
||||
if (StringUtils.isBlank(password) || StringUtils.isBlank(passwordHash)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return BCrypt.checkpw(password, passwordHash);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Permission methods
|
||||
|
||||
/**
|
||||
* Adds a permission to the user.
|
||||
*
|
||||
* @param permission The permission to add
|
||||
*/
|
||||
public void addPermission(String permission) {
|
||||
if (StringUtils.isNotBlank(permission)) {
|
||||
permissions.add(permission.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a permission from the user.
|
||||
*
|
||||
* @param permission The permission to remove
|
||||
*/
|
||||
public void removePermission(String permission) {
|
||||
permissions.remove(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has a specific permission.
|
||||
*
|
||||
* @param permission The permission to check
|
||||
* @return true if user has the permission
|
||||
*/
|
||||
public boolean hasPermission(String permission) {
|
||||
return permissions.contains(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all permissions.
|
||||
*/
|
||||
public void clearPermissions() {
|
||||
permissions.clear();
|
||||
}
|
||||
|
||||
// Status and role methods
|
||||
|
||||
/**
|
||||
* Checks if the user is an admin.
|
||||
*
|
||||
* @return true if user is admin
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return role == UserRole.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is active.
|
||||
*
|
||||
* @return true if user is active
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return status == UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is locked due to too many failed login attempts.
|
||||
*
|
||||
* @return true if user is locked
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
return status == UserStatus.SUSPENDED || loginAttempts >= 5;
|
||||
}
|
||||
|
||||
// Login methods
|
||||
|
||||
/**
|
||||
* Records a successful login.
|
||||
*
|
||||
* @return true if login was successful
|
||||
*/
|
||||
public boolean login() {
|
||||
if (!isActive() || isLocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.lastLogin = LocalDateTime.now();
|
||||
this.loginAttempts = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a failed login attempt.
|
||||
*/
|
||||
public void failedLoginAttempt() {
|
||||
this.loginAttempts++;
|
||||
if (this.loginAttempts >= 5) {
|
||||
this.status = UserStatus.SUSPENDED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets login attempts.
|
||||
*/
|
||||
public void resetLoginAttempts() {
|
||||
this.loginAttempts = 0;
|
||||
}
|
||||
|
||||
// Status change methods
|
||||
|
||||
/**
|
||||
* Activates the user account.
|
||||
*/
|
||||
public void activate() {
|
||||
this.status = UserStatus.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates the user account.
|
||||
*/
|
||||
public void deactivate() {
|
||||
this.status = UserStatus.INACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspends the user account.
|
||||
*/
|
||||
public void suspend() {
|
||||
this.status = UserStatus.SUSPENDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the user as deleted.
|
||||
*/
|
||||
public void delete() {
|
||||
this.status = UserStatus.DELETED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
if (!super.equals(obj)) return false;
|
||||
|
||||
User user = (User) obj;
|
||||
return loginAttempts == user.loginAttempts &&
|
||||
Objects.equals(username, user.username) &&
|
||||
Objects.equals(passwordHash, user.passwordHash) &&
|
||||
role == user.role &&
|
||||
status == user.status &&
|
||||
Objects.equals(lastLogin, user.lastLogin) &&
|
||||
Objects.equals(permissions, user.permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), username, passwordHash, role, status,
|
||||
lastLogin, loginAttempts, permissions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("User{username='%s', name='%s', role=%s, status=%s, lastLogin=%s}",
|
||||
username, getName(), role, status, lastLogin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.example.usermanagement.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Enumeration for user roles in the system.
|
||||
*/
|
||||
public enum UserRole {
|
||||
|
||||
/**
|
||||
* Administrator role with full system access.
|
||||
*/
|
||||
ADMIN("admin", "Administrator", "Full system access"),
|
||||
|
||||
/**
|
||||
* Regular user role with standard permissions.
|
||||
*/
|
||||
USER("user", "User", "Standard user permissions"),
|
||||
|
||||
/**
|
||||
* Guest role with limited permissions.
|
||||
*/
|
||||
GUEST("guest", "Guest", "Limited guest permissions");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* Constructor for UserRole enum.
|
||||
*
|
||||
* @param code The role code
|
||||
* @param displayName The display name
|
||||
* @param description The role description
|
||||
*/
|
||||
UserRole(String code, String displayName, String description) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role code.
|
||||
*
|
||||
* @return The role code
|
||||
*/
|
||||
@JsonValue
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name.
|
||||
*
|
||||
* @return The display name
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role description.
|
||||
*
|
||||
* @return The role description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a UserRole by its code.
|
||||
*
|
||||
* @param code The role code to search for
|
||||
* @return The UserRole or null if not found
|
||||
*/
|
||||
public static UserRole fromCode(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (UserRole role : values()) {
|
||||
if (role.code.equalsIgnoreCase(code)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this role has higher privilege than another role.
|
||||
*
|
||||
* @param other The other role to compare with
|
||||
* @return true if this role has higher privilege
|
||||
*/
|
||||
public boolean hasHigherPrivilegeThan(UserRole other) {
|
||||
return this.ordinal() < other.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this role has lower privilege than another role.
|
||||
*
|
||||
* @param other The other role to compare with
|
||||
* @return true if this role has lower privilege
|
||||
*/
|
||||
public boolean hasLowerPrivilegeThan(UserRole other) {
|
||||
return this.ordinal() > other.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this role can perform actions on another role.
|
||||
*
|
||||
* @param targetRole The target role
|
||||
* @return true if this role can act on the target role
|
||||
*/
|
||||
public boolean canActOn(UserRole targetRole) {
|
||||
// Admin can act on all roles
|
||||
if (this == ADMIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Users can only act on guests
|
||||
if (this == USER) {
|
||||
return targetRole == GUEST;
|
||||
}
|
||||
|
||||
// Guests cannot act on anyone
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.example.usermanagement.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Enumeration for user status in the system.
|
||||
*/
|
||||
public enum UserStatus {
|
||||
|
||||
/**
|
||||
* Active status - user can login and use the system.
|
||||
*/
|
||||
ACTIVE("active", "Active", "User can login and use the system"),
|
||||
|
||||
/**
|
||||
* Inactive status - user account is temporarily disabled.
|
||||
*/
|
||||
INACTIVE("inactive", "Inactive", "User account is temporarily disabled"),
|
||||
|
||||
/**
|
||||
* Suspended status - user account is suspended due to violations.
|
||||
*/
|
||||
SUSPENDED("suspended", "Suspended", "User account is suspended due to violations"),
|
||||
|
||||
/**
|
||||
* Deleted status - user account is marked for deletion.
|
||||
*/
|
||||
DELETED("deleted", "Deleted", "User account is marked for deletion");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* Constructor for UserStatus enum.
|
||||
*
|
||||
* @param code The status code
|
||||
* @param displayName The display name
|
||||
* @param description The status description
|
||||
*/
|
||||
UserStatus(String code, String displayName, String description) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status code.
|
||||
*
|
||||
* @return The status code
|
||||
*/
|
||||
@JsonValue
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name.
|
||||
*
|
||||
* @return The display name
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status description.
|
||||
*
|
||||
* @return The status description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a UserStatus by its code.
|
||||
*
|
||||
* @param code The status code to search for
|
||||
* @return The UserStatus or null if not found
|
||||
*/
|
||||
public static UserStatus fromCode(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (UserStatus status : values()) {
|
||||
if (status.code.equalsIgnoreCase(code)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this status allows user login.
|
||||
*
|
||||
* @return true if user can login with this status
|
||||
*/
|
||||
public boolean allowsLogin() {
|
||||
return this == ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this status indicates the user is disabled.
|
||||
*
|
||||
* @return true if user is disabled
|
||||
*/
|
||||
public boolean isDisabled() {
|
||||
return this == INACTIVE || this == SUSPENDED || this == DELETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this status indicates the user is deleted.
|
||||
*
|
||||
* @return true if user is deleted
|
||||
*/
|
||||
public boolean isDeleted() {
|
||||
return this == DELETED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this status can be changed to another status.
|
||||
*
|
||||
* @param targetStatus The target status
|
||||
* @return true if status change is allowed
|
||||
*/
|
||||
public boolean canChangeTo(UserStatus targetStatus) {
|
||||
// Cannot change from deleted status
|
||||
if (this == DELETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot change to same status
|
||||
if (this == targetStatus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All other changes are allowed
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package com.example.usermanagement.services;
|
||||
|
||||
import com.example.usermanagement.models.User;
|
||||
import com.example.usermanagement.models.UserRole;
|
||||
import com.example.usermanagement.models.UserStatus;
|
||||
import com.example.usermanagement.utils.UserNotFoundException;
|
||||
import com.example.usermanagement.utils.DuplicateUserException;
|
||||
import com.example.usermanagement.utils.ValidationUtils;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVPrinter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service class for managing users in the system.
|
||||
* Provides CRUD operations, search functionality, and data persistence.
|
||||
*/
|
||||
public class UserManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserManager.class);
|
||||
|
||||
private final Map<String, User> users;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String storagePath;
|
||||
|
||||
/**
|
||||
* Constructor with default storage path.
|
||||
*/
|
||||
public UserManager() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with custom storage path.
|
||||
*
|
||||
* @param storagePath The file path for user data storage
|
||||
*/
|
||||
public UserManager(String storagePath) {
|
||||
this.users = new ConcurrentHashMap<>();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
this.objectMapper.registerModule(new JavaTimeModule());
|
||||
this.storagePath = storagePath;
|
||||
|
||||
if (StringUtils.isNotBlank(storagePath)) {
|
||||
loadUsersFromFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user in the system.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
* @param email The email address (optional)
|
||||
* @param role The user role
|
||||
* @return The created user
|
||||
* @throws DuplicateUserException if username already exists
|
||||
* @throws IllegalArgumentException if validation fails
|
||||
*/
|
||||
public User createUser(String name, int age, String username, String email, UserRole role) {
|
||||
logger.debug("Creating user with username: {}", username);
|
||||
|
||||
if (users.containsKey(username)) {
|
||||
throw new DuplicateUserException("User with username '" + username + "' already exists");
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
ValidationUtils.validateUsername(username);
|
||||
if (StringUtils.isNotBlank(email)) {
|
||||
ValidationUtils.validateEmail(email);
|
||||
}
|
||||
|
||||
User user = new User(name, age, username, email, role);
|
||||
users.put(username, user);
|
||||
|
||||
saveUsersToFile();
|
||||
logger.info("User created successfully: {}", username);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user with default role.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
* @param email The email address (optional)
|
||||
* @return The created user
|
||||
*/
|
||||
public User createUser(String name, int age, String username, String email) {
|
||||
return createUser(name, age, username, email, UserRole.USER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user with minimal information.
|
||||
*
|
||||
* @param name The user's name
|
||||
* @param age The user's age
|
||||
* @param username The username
|
||||
* @return The created user
|
||||
*/
|
||||
public User createUser(String name, int age, String username) {
|
||||
return createUser(name, age, username, null, UserRole.USER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user by username.
|
||||
*
|
||||
* @param username The username
|
||||
* @return The user
|
||||
* @throws UserNotFoundException if user is not found
|
||||
*/
|
||||
public User getUser(String username) {
|
||||
User user = users.get(username);
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException("User with username '" + username + "' not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user by email address.
|
||||
*
|
||||
* @param email The email address
|
||||
* @return The user or null if not found
|
||||
*/
|
||||
public User getUserByEmail(String email) {
|
||||
return users.values().stream()
|
||||
.filter(user -> Objects.equals(user.getEmail(), email))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user information.
|
||||
*
|
||||
* @param username The username
|
||||
* @param updates A map of field updates
|
||||
* @return The updated user
|
||||
* @throws UserNotFoundException if user is not found
|
||||
*/
|
||||
public User updateUser(String username, Map<String, Object> updates) {
|
||||
User user = getUser(username);
|
||||
|
||||
updates.forEach((field, value) -> {
|
||||
switch (field.toLowerCase()) {
|
||||
case "name":
|
||||
user.setName((String) value);
|
||||
break;
|
||||
case "age":
|
||||
user.setAge((Integer) value);
|
||||
break;
|
||||
case "email":
|
||||
user.setEmail((String) value);
|
||||
break;
|
||||
case "role":
|
||||
if (value instanceof UserRole) {
|
||||
user.setRole((UserRole) value);
|
||||
} else if (value instanceof String) {
|
||||
user.setRole(UserRole.fromCode((String) value));
|
||||
}
|
||||
break;
|
||||
case "status":
|
||||
if (value instanceof UserStatus) {
|
||||
user.setStatus((UserStatus) value);
|
||||
} else if (value instanceof String) {
|
||||
user.setStatus(UserStatus.fromCode((String) value));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unknown field for update: {}", field);
|
||||
}
|
||||
});
|
||||
|
||||
saveUsersToFile();
|
||||
logger.info("User updated successfully: {}", username);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user (soft delete).
|
||||
*
|
||||
* @param username The username
|
||||
* @return true if user was deleted
|
||||
* @throws UserNotFoundException if user is not found
|
||||
*/
|
||||
public boolean deleteUser(String username) {
|
||||
User user = getUser(username);
|
||||
user.delete();
|
||||
|
||||
saveUsersToFile();
|
||||
logger.info("User deleted successfully: {}", username);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a user completely from the system.
|
||||
*
|
||||
* @param username The username
|
||||
* @return true if user was removed
|
||||
* @throws UserNotFoundException if user is not found
|
||||
*/
|
||||
public boolean removeUser(String username) {
|
||||
if (!users.containsKey(username)) {
|
||||
throw new UserNotFoundException("User with username '" + username + "' not found");
|
||||
}
|
||||
|
||||
users.remove(username);
|
||||
saveUsersToFile();
|
||||
logger.info("User removed completely: {}", username);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all users in the system.
|
||||
*
|
||||
* @return A list of all users
|
||||
*/
|
||||
public List<User> getAllUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all active users.
|
||||
*
|
||||
* @return A list of active users
|
||||
*/
|
||||
public List<User> getActiveUsers() {
|
||||
return users.values().stream()
|
||||
.filter(User::isActive)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users by role.
|
||||
*
|
||||
* @param role The user role
|
||||
* @return A list of users with the specified role
|
||||
*/
|
||||
public List<User> getUsersByRole(UserRole role) {
|
||||
return users.values().stream()
|
||||
.filter(user -> user.getRole() == role)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters users using a custom predicate.
|
||||
*
|
||||
* @param predicate The filter predicate
|
||||
* @return A list of filtered users
|
||||
*/
|
||||
public List<User> filterUsers(Predicate<User> predicate) {
|
||||
return users.values().stream()
|
||||
.filter(predicate)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches users by name or username.
|
||||
*
|
||||
* @param query The search query
|
||||
* @return A list of matching users
|
||||
*/
|
||||
public List<User> searchUsers(String query) {
|
||||
if (StringUtils.isBlank(query)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
String lowercaseQuery = query.toLowerCase();
|
||||
return users.values().stream()
|
||||
.filter(user ->
|
||||
user.getName().toLowerCase().contains(lowercaseQuery) ||
|
||||
user.getUsername().toLowerCase().contains(lowercaseQuery) ||
|
||||
(user.getEmail() != null && user.getEmail().toLowerCase().contains(lowercaseQuery)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users older than specified age.
|
||||
*
|
||||
* @param age The age threshold
|
||||
* @return A list of users older than the specified age
|
||||
*/
|
||||
public List<User> getUsersOlderThan(int age) {
|
||||
return filterUsers(user -> user.getAge() > age);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users with email addresses.
|
||||
*
|
||||
* @return A list of users with email addresses
|
||||
*/
|
||||
public List<User> getUsersWithEmail() {
|
||||
return filterUsers(User::hasEmail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets users with specific permission.
|
||||
*
|
||||
* @param permission The permission to check
|
||||
* @return A list of users with the specified permission
|
||||
*/
|
||||
public List<User> getUsersWithPermission(String permission) {
|
||||
return filterUsers(user -> user.hasPermission(permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total number of users.
|
||||
*
|
||||
* @return The user count
|
||||
*/
|
||||
public int getUserCount() {
|
||||
return users.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user statistics.
|
||||
*
|
||||
* @return A map of user statistics
|
||||
*/
|
||||
public Map<String, Integer> getUserStats() {
|
||||
Map<String, Integer> stats = new HashMap<>();
|
||||
|
||||
stats.put("total", users.size());
|
||||
stats.put("active", getActiveUsers().size());
|
||||
stats.put("admin", getUsersByRole(UserRole.ADMIN).size());
|
||||
stats.put("user", getUsersByRole(UserRole.USER).size());
|
||||
stats.put("guest", getUsersByRole(UserRole.GUEST).size());
|
||||
stats.put("with_email", getUsersWithEmail().size());
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports users to specified format.
|
||||
*
|
||||
* @param format The export format ("json" or "csv")
|
||||
* @return The exported data as string
|
||||
* @throws IllegalArgumentException if format is unsupported
|
||||
*/
|
||||
public String exportUsers(String format) {
|
||||
switch (format.toLowerCase()) {
|
||||
case "json":
|
||||
return exportToJson();
|
||||
case "csv":
|
||||
return exportToCsv();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported export format: " + format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports users to JSON format.
|
||||
*
|
||||
* @return JSON string representation of users
|
||||
*/
|
||||
private String exportToJson() {
|
||||
try {
|
||||
return objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(users.values());
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("Error exporting users to JSON", e);
|
||||
return "[]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports users to CSV format.
|
||||
*
|
||||
* @return CSV string representation of users
|
||||
*/
|
||||
private String exportToCsv() {
|
||||
try (StringWriter writer = new StringWriter();
|
||||
CSVPrinter printer = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(
|
||||
"Username", "Name", "Age", "Email", "Role", "Status", "Last Login"))) {
|
||||
|
||||
for (User user : users.values()) {
|
||||
printer.printRecord(
|
||||
user.getUsername(),
|
||||
user.getName(),
|
||||
user.getAge(),
|
||||
user.getEmail(),
|
||||
user.getRole().getCode(),
|
||||
user.getStatus().getCode(),
|
||||
user.getLastLogin()
|
||||
);
|
||||
}
|
||||
|
||||
return writer.toString();
|
||||
} catch (IOException e) {
|
||||
logger.error("Error exporting users to CSV", e);
|
||||
return "Username,Name,Age,Email,Role,Status,Last Login\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a username exists in the system.
|
||||
*
|
||||
* @param username The username to check
|
||||
* @return true if username exists
|
||||
*/
|
||||
public boolean userExists(String username) {
|
||||
return users.containsKey(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all users from the system.
|
||||
*/
|
||||
public void clearAllUsers() {
|
||||
users.clear();
|
||||
saveUsersToFile();
|
||||
logger.info("All users cleared from system");
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads users from file storage.
|
||||
*/
|
||||
private void loadUsersFromFile() {
|
||||
if (StringUtils.isBlank(storagePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(storagePath);
|
||||
if (!Files.exists(path)) {
|
||||
logger.debug("User storage file does not exist: {}", storagePath);
|
||||
return;
|
||||
}
|
||||
|
||||
String content = Files.readString(path);
|
||||
List<User> userList = Arrays.asList(objectMapper.readValue(content, User[].class));
|
||||
|
||||
users.clear();
|
||||
for (User user : userList) {
|
||||
users.put(user.getUsername(), user);
|
||||
}
|
||||
|
||||
logger.info("Loaded {} users from file: {}", users.size(), storagePath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error loading users from file: {}", storagePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves users to file storage.
|
||||
*/
|
||||
private void saveUsersToFile() {
|
||||
if (StringUtils.isBlank(storagePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = Paths.get(storagePath);
|
||||
Files.createDirectories(path.getParent());
|
||||
|
||||
String content = objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(users.values());
|
||||
Files.writeString(path, content);
|
||||
|
||||
logger.debug("Saved {} users to file: {}", users.size(), storagePath);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error saving users to file: {}", storagePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
// CI marker method to verify auto-reindex on change
|
||||
public String ciAddedSymbolMarker() {
|
||||
return "ci_symbol_java";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.usermanagement.utils;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a user that already exists.
|
||||
*/
|
||||
public class DuplicateUserException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new DuplicateUserException with the specified detail message.
|
||||
*
|
||||
* @param message the detail message
|
||||
*/
|
||||
public DuplicateUserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new DuplicateUserException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message
|
||||
* @param cause the cause
|
||||
*/
|
||||
public DuplicateUserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.example.usermanagement.utils;
|
||||
|
||||
/**
|
||||
* Exception thrown when a user is not found in the system.
|
||||
*/
|
||||
public class UserNotFoundException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new UserNotFoundException with the specified detail message.
|
||||
*
|
||||
* @param message the detail message
|
||||
*/
|
||||
public UserNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new UserNotFoundException with the specified detail message and cause.
|
||||
*
|
||||
* @param message the detail message
|
||||
* @param cause the cause
|
||||
*/
|
||||
public UserNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.example.usermanagement.utils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility class for validation operations.
|
||||
*/
|
||||
public final class ValidationUtils {
|
||||
|
||||
private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
||||
private static final String USERNAME_PATTERN = "^[a-zA-Z0-9_]+$";
|
||||
|
||||
/**
|
||||
* Private constructor to prevent instantiation.
|
||||
*/
|
||||
private ValidationUtils() {
|
||||
throw new UnsupportedOperationException("Utility class cannot be instantiated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates email format.
|
||||
*
|
||||
* @param email The email to validate
|
||||
* @throws IllegalArgumentException if email is invalid
|
||||
*/
|
||||
public static void validateEmail(String email) {
|
||||
if (StringUtils.isBlank(email)) {
|
||||
throw new IllegalArgumentException("Email cannot be null or empty");
|
||||
}
|
||||
|
||||
if (!email.matches(EMAIL_PATTERN)) {
|
||||
throw new IllegalArgumentException("Invalid email format");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates username format.
|
||||
*
|
||||
* @param username The username to validate
|
||||
* @throws IllegalArgumentException if username is invalid
|
||||
*/
|
||||
public static void validateUsername(String username) {
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be null or empty");
|
||||
}
|
||||
|
||||
if (username.length() < 3 || username.length() > 20) {
|
||||
throw new IllegalArgumentException("Username must be between 3 and 20 characters");
|
||||
}
|
||||
|
||||
if (!username.matches(USERNAME_PATTERN)) {
|
||||
throw new IllegalArgumentException("Username can only contain letters, numbers, and underscores");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if email format is valid.
|
||||
*
|
||||
* @param email The email to check
|
||||
* @return true if email is valid
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
return StringUtils.isNotBlank(email) && email.matches(EMAIL_PATTERN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if username format is valid.
|
||||
*
|
||||
* @param username The username to check
|
||||
* @return true if username is valid
|
||||
*/
|
||||
public static boolean isValidUsername(String username) {
|
||||
return StringUtils.isNotBlank(username) &&
|
||||
username.length() >= 3 &&
|
||||
username.length() <= 20 &&
|
||||
username.matches(USERNAME_PATTERN);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Person : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSString *name;
|
||||
@property (nonatomic, assign) NSInteger age;
|
||||
@property (nonatomic, strong) NSString *email;
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
|
||||
- (void)sayHello;
|
||||
- (void)updateEmail:(NSString *)email;
|
||||
+ (Person *)createDefaultPerson;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
#import "Person.h"
|
||||
|
||||
@implementation Person
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_name = name;
|
||||
_age = age;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)sayHello {
|
||||
NSLog(@"Hello, my name is %@", self.name);
|
||||
}
|
||||
|
||||
- (void)updateEmail:(NSString *)email {
|
||||
self.email = email;
|
||||
NSLog(@"Email updated to: %@", email);
|
||||
}
|
||||
|
||||
+ (Person *)createDefaultPerson {
|
||||
return [[Person alloc] initWithName:@"John Doe" age:30];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Person.h"
|
||||
|
||||
@interface UserManager : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray<Person *> *users;
|
||||
|
||||
+ (UserManager *)sharedManager;
|
||||
- (void)addUser:(Person *)user;
|
||||
- (Person *)findUserByName:(NSString *)name;
|
||||
- (void)removeUser:(Person *)user;
|
||||
- (NSInteger)userCount;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
#import "UserManager.h"
|
||||
|
||||
@implementation UserManager
|
||||
|
||||
+ (UserManager *)sharedManager {
|
||||
static UserManager *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [[UserManager alloc] init];
|
||||
sharedInstance.users = [[NSMutableArray alloc] init];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (void)addUser:(Person *)user {
|
||||
if (user) {
|
||||
[self.users addObject:user];
|
||||
NSLog(@"Added user: %@", user.name);
|
||||
}
|
||||
}
|
||||
|
||||
- (Person *)findUserByName:(NSString *)name {
|
||||
for (Person *user in self.users) {
|
||||
if ([user.name isEqualToString:name]) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)removeUser:(Person *)user {
|
||||
if (user) {
|
||||
[self.users removeObject:user];
|
||||
NSLog(@"Removed user: %@", user.name);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)userCount {
|
||||
return self.users.count;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,30 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Person.h"
|
||||
#import "UserManager.h"
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
// Create some users
|
||||
Person *alice = [[Person alloc] initWithName:@"Alice" age:25];
|
||||
Person *bob = [[Person alloc] initWithName:@"Bob" age:30];
|
||||
Person *charlie = [Person createDefaultPerson];
|
||||
|
||||
// Get shared manager
|
||||
UserManager *manager = [UserManager sharedManager];
|
||||
|
||||
// Add users
|
||||
[manager addUser:alice];
|
||||
[manager addUser:bob];
|
||||
[manager addUser:charlie];
|
||||
|
||||
// Find user
|
||||
Person *found = [manager findUserByName:@"Alice"];
|
||||
if (found) {
|
||||
[found sayHello];
|
||||
[found updateEmail:@"alice@example.com"];
|
||||
}
|
||||
|
||||
NSLog(@"Total users: %ld", (long)[manager userCount]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
# User Management System (Python)
|
||||
|
||||
A comprehensive user management system built in Python for testing Code Index MCP's analysis capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **User Management**: Create, update, delete, and search users
|
||||
- **Authentication**: Password-based authentication with session management
|
||||
- **Authorization**: Role-based access control (Admin, User, Guest)
|
||||
- **Data Validation**: Comprehensive input validation and sanitization
|
||||
- **Export/Import**: JSON and CSV export capabilities
|
||||
- **CLI Interface**: Command-line interface for system management
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
user_management/
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ ├── person.py # Basic Person model
|
||||
│ └── user.py # User model with auth features
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ ├── user_manager.py # User management service
|
||||
│ └── auth_service.py # Authentication service
|
||||
├── utils/
|
||||
│ ├── __init__.py
|
||||
│ ├── validators.py # Input validation utilities
|
||||
│ ├── exceptions.py # Custom exception classes
|
||||
│ └── helpers.py # Helper functions
|
||||
├── tests/ # Test directory (empty for now)
|
||||
├── __init__.py
|
||||
└── cli.py # Command-line interface
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Install the package in development mode:
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Demo
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Using the CLI
|
||||
|
||||
```bash
|
||||
# Create a new user
|
||||
user-cli create-user --name "John Doe" --username "john" --age 30 --email "john@example.com"
|
||||
|
||||
# List all users
|
||||
user-cli list-users
|
||||
|
||||
# Get user information
|
||||
user-cli get-user john
|
||||
|
||||
# Update user
|
||||
user-cli update-user john --age 31
|
||||
|
||||
# Delete user
|
||||
user-cli delete-user john
|
||||
|
||||
# Search users
|
||||
user-cli search "john"
|
||||
|
||||
# Show statistics
|
||||
user-cli stats
|
||||
|
||||
# Export users
|
||||
user-cli export --format json --output users.json
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```python
|
||||
from user_management import UserManager, UserRole
|
||||
from user_management.services.auth_service import AuthService
|
||||
|
||||
# Create user manager
|
||||
user_manager = UserManager()
|
||||
|
||||
# Create a user
|
||||
user = user_manager.create_user(
|
||||
name="Jane Smith",
|
||||
username="jane",
|
||||
age=28,
|
||||
email="jane@example.com",
|
||||
role=UserRole.USER
|
||||
)
|
||||
|
||||
# Set password
|
||||
user.set_password("SecurePass123!")
|
||||
|
||||
# Authenticate
|
||||
auth_service = AuthService(user_manager)
|
||||
authenticated_user = auth_service.authenticate("jane", "SecurePass123!")
|
||||
|
||||
# Create session
|
||||
session_id = auth_service.create_session(authenticated_user)
|
||||
```
|
||||
|
||||
## Testing Features
|
||||
|
||||
This project tests the following Python language features:
|
||||
|
||||
- **Classes and Inheritance**: Person and User classes with inheritance
|
||||
- **Dataclasses**: Modern Python data structures
|
||||
- **Enums**: Role and status enumerations
|
||||
- **Type Hints**: Comprehensive type annotations
|
||||
- **Properties**: Getter/setter methods
|
||||
- **Class Methods**: Factory methods and utilities
|
||||
- **Static Methods**: Utility functions
|
||||
- **Context Managers**: Resource management
|
||||
- **Decorators**: Method decorators
|
||||
- **Generators**: Iteration patterns
|
||||
- **Exception Handling**: Custom exceptions
|
||||
- **Package Structure**: Modules and imports
|
||||
- **CLI Development**: Click framework integration
|
||||
- **JSON/CSV Processing**: Data serialization
|
||||
- **Regular Expressions**: Input validation
|
||||
- **Datetime Handling**: Time-based operations
|
||||
- **Cryptography**: Password hashing
|
||||
- **File I/O**: Data persistence
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **click**: Command-line interface framework
|
||||
- **pytest**: Testing framework
|
||||
- **pydantic**: Data validation (optional)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - This is a sample project for testing purposes.
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Main entry point for the user management system demo.
|
||||
"""
|
||||
|
||||
from user_management import UserManager, Person, User
|
||||
from user_management.services.auth_service import AuthService
|
||||
from user_management.models.user import UserRole
|
||||
from user_management.utils.exceptions import UserNotFoundError, DuplicateUserError
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate the user management system."""
|
||||
print("=" * 50)
|
||||
print("User Management System Demo")
|
||||
print("=" * 50)
|
||||
|
||||
# Create user manager and auth service
|
||||
user_manager = UserManager()
|
||||
auth_service = AuthService(user_manager)
|
||||
|
||||
# Create some sample users
|
||||
print("\n1. Creating sample users...")
|
||||
|
||||
try:
|
||||
# Create admin user
|
||||
admin = user_manager.create_user(
|
||||
name="Alice Johnson",
|
||||
username="alice_admin",
|
||||
age=30,
|
||||
email="alice@example.com",
|
||||
role=UserRole.ADMIN
|
||||
)
|
||||
admin.set_password("AdminPass123!")
|
||||
admin.add_permission("user_management")
|
||||
admin.add_permission("system_admin")
|
||||
|
||||
# Create regular users
|
||||
user1 = user_manager.create_user(
|
||||
name="Bob Smith",
|
||||
username="bob_user",
|
||||
age=25,
|
||||
email="bob@example.com"
|
||||
)
|
||||
user1.set_password("UserPass123!")
|
||||
|
||||
user2 = user_manager.create_user(
|
||||
name="Charlie Brown",
|
||||
username="charlie",
|
||||
age=35,
|
||||
email="charlie@example.com"
|
||||
)
|
||||
user2.set_password("CharliePass123!")
|
||||
|
||||
print(f"✓ Created {user_manager.get_user_count()} users")
|
||||
|
||||
except DuplicateUserError as e:
|
||||
print(f"✗ Error creating users: {e}")
|
||||
|
||||
# Display all users
|
||||
print("\n2. Listing all users...")
|
||||
users = user_manager.get_all_users()
|
||||
for user in users:
|
||||
print(f" • {user.username} ({user.name}) - {user.role.value}")
|
||||
|
||||
# Test authentication
|
||||
print("\n3. Testing authentication...")
|
||||
try:
|
||||
authenticated_user = auth_service.authenticate("alice_admin", "AdminPass123!")
|
||||
print(f"✓ Authentication successful for {authenticated_user.username}")
|
||||
|
||||
# Create session
|
||||
session_id = auth_service.create_session(authenticated_user)
|
||||
print(f"✓ Session created: {session_id[:16]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Authentication failed: {e}")
|
||||
|
||||
# Test user search
|
||||
print("\n4. Testing user search...")
|
||||
search_results = user_manager.search_users("alice")
|
||||
print(f"Search results for 'alice': {len(search_results)} users found")
|
||||
for user in search_results:
|
||||
print(f" • {user.username} ({user.name})")
|
||||
|
||||
# Test filtering
|
||||
print("\n5. Testing user filtering...")
|
||||
older_users = user_manager.get_users_older_than(30)
|
||||
print(f"Users older than 30: {len(older_users)} users")
|
||||
for user in older_users:
|
||||
print(f" • {user.username} ({user.name}) - age {user.age}")
|
||||
|
||||
# Test user updates
|
||||
print("\n6. Testing user updates...")
|
||||
try:
|
||||
updated_user = user_manager.update_user("bob_user", age=26)
|
||||
print(f"✓ Updated {updated_user.username}'s age to {updated_user.age}")
|
||||
except UserNotFoundError as e:
|
||||
print(f"✗ Update failed: {e}")
|
||||
|
||||
# Display statistics
|
||||
print("\n7. User statistics...")
|
||||
stats = user_manager.get_user_stats()
|
||||
for key, value in stats.items():
|
||||
print(f" {key.replace('_', ' ').title()}: {value}")
|
||||
|
||||
# Test export functionality
|
||||
print("\n8. Testing export functionality...")
|
||||
try:
|
||||
json_export = user_manager.export_users('json')
|
||||
print(f"✓ JSON export: {len(json_export)} characters")
|
||||
|
||||
csv_export = user_manager.export_users('csv')
|
||||
print(f"✓ CSV export: {len(csv_export.splitlines())} lines")
|
||||
except Exception as e:
|
||||
print(f"✗ Export failed: {e}")
|
||||
|
||||
# Test password change
|
||||
print("\n9. Testing password change...")
|
||||
try:
|
||||
auth_service.change_password("bob_user", "UserPass123!", "NewUserPass123!")
|
||||
print("✓ Password changed successfully")
|
||||
|
||||
# Test with new password
|
||||
auth_service.authenticate("bob_user", "NewUserPass123!")
|
||||
print("✓ Authentication with new password successful")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Password change failed: {e}")
|
||||
|
||||
# Test session management
|
||||
print("\n10. Testing session management...")
|
||||
session_stats = auth_service.get_session_stats()
|
||||
print(f"Active sessions: {session_stats['total_sessions']}")
|
||||
|
||||
# Cleanup expired sessions
|
||||
expired_count = auth_service.cleanup_expired_sessions()
|
||||
print(f"Cleaned up {expired_count} expired sessions")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Demo completed successfully!")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
pytest>=7.0.0
|
||||
pydantic>=1.10.0
|
||||
click>=8.0.0
|
||||
@@ -0,0 +1,37 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="user-management",
|
||||
version="0.1.0",
|
||||
description="A sample user management system for testing Code Index MCP",
|
||||
author="Test Author",
|
||||
author_email="test@example.com",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"pydantic>=1.10.0",
|
||||
"click>=8.0.0",
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest>=7.0.0",
|
||||
"black>=22.0.0",
|
||||
"flake8>=4.0.0",
|
||||
]
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"user-cli=user_management.cli:main",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.8",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
User Management System
|
||||
|
||||
A sample application for testing Code Index MCP's Python analysis capabilities.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Test Author"
|
||||
|
||||
from .models.person import Person
|
||||
from .models.user import User
|
||||
from .services.user_manager import UserManager
|
||||
|
||||
__all__ = ["Person", "User", "UserManager"]
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Command line interface for user management system.
|
||||
"""
|
||||
|
||||
import click
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from .services.user_manager import UserManager
|
||||
from .services.auth_service import AuthService
|
||||
from .models.user import UserRole, UserStatus
|
||||
from .utils.exceptions import UserNotFoundError, DuplicateUserError
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
"""User Management System CLI."""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['user_manager'] = UserManager()
|
||||
ctx.obj['auth_service'] = AuthService(ctx.obj['user_manager'])
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--name', required=True, help='Full name of the user')
|
||||
@click.option('--username', required=True, help='Username for the user')
|
||||
@click.option('--age', required=True, type=int, help='Age of the user')
|
||||
@click.option('--email', help='Email address of the user')
|
||||
@click.option('--role', type=click.Choice(['user', 'admin', 'guest']), default='user', help='Role of the user')
|
||||
@click.option('--password', prompt=True, hide_input=True, help='Password for the user')
|
||||
@click.pass_context
|
||||
def create_user(ctx, name: str, username: str, age: int, email: Optional[str], role: str, password: str):
|
||||
"""Create a new user."""
|
||||
try:
|
||||
user_manager = ctx.obj['user_manager']
|
||||
user_role = UserRole(role)
|
||||
|
||||
user = user_manager.create_user(
|
||||
name=name,
|
||||
username=username,
|
||||
age=age,
|
||||
email=email,
|
||||
role=user_role
|
||||
)
|
||||
|
||||
user.set_password(password)
|
||||
|
||||
click.echo(f"User '{username}' created successfully!")
|
||||
click.echo(f"Name: {user.name}")
|
||||
click.echo(f"Age: {user.age}")
|
||||
click.echo(f"Email: {user.email or 'Not provided'}")
|
||||
click.echo(f"Role: {user.role.value}")
|
||||
|
||||
except DuplicateUserError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
except ValueError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('username')
|
||||
@click.pass_context
|
||||
def get_user(ctx, username: str):
|
||||
"""Get information about a user."""
|
||||
try:
|
||||
user_manager = ctx.obj['user_manager']
|
||||
user = user_manager.get_user(username)
|
||||
|
||||
click.echo(f"Username: {user.username}")
|
||||
click.echo(f"Name: {user.name}")
|
||||
click.echo(f"Age: {user.age}")
|
||||
click.echo(f"Email: {user.email or 'Not provided'}")
|
||||
click.echo(f"Role: {user.role.value}")
|
||||
click.echo(f"Status: {user.status.value}")
|
||||
click.echo(f"Created: {user.created_at}")
|
||||
click.echo(f"Last Login: {user.last_login or 'Never'}")
|
||||
click.echo(f"Permissions: {', '.join(user.permissions) if user.permissions else 'None'}")
|
||||
|
||||
except UserNotFoundError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def list_users(ctx):
|
||||
"""List all users."""
|
||||
user_manager = ctx.obj['user_manager']
|
||||
users = user_manager.get_all_users()
|
||||
|
||||
if not users:
|
||||
click.echo("No users found.")
|
||||
return
|
||||
|
||||
click.echo(f"Found {len(users)} users:")
|
||||
click.echo("-" * 60)
|
||||
|
||||
for user in users:
|
||||
status_indicator = "✓" if user.is_active() else "✗"
|
||||
click.echo(f"{status_indicator} {user.username:<15} {user.name:<20} {user.role.value:<10} {user.email or 'No email'}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('username')
|
||||
@click.option('--name', help='New name for the user')
|
||||
@click.option('--age', type=int, help='New age for the user')
|
||||
@click.option('--email', help='New email for the user')
|
||||
@click.option('--role', type=click.Choice(['user', 'admin', 'guest']), help='New role for the user')
|
||||
@click.pass_context
|
||||
def update_user(ctx, username: str, name: Optional[str], age: Optional[int],
|
||||
email: Optional[str], role: Optional[str]):
|
||||
"""Update user information."""
|
||||
try:
|
||||
user_manager = ctx.obj['user_manager']
|
||||
|
||||
updates = {}
|
||||
if name:
|
||||
updates['name'] = name
|
||||
if age is not None:
|
||||
updates['age'] = age
|
||||
if email:
|
||||
updates['email'] = email
|
||||
if role:
|
||||
updates['role'] = UserRole(role)
|
||||
|
||||
if not updates:
|
||||
click.echo("No updates provided.")
|
||||
return
|
||||
|
||||
user = user_manager.update_user(username, **updates)
|
||||
click.echo(f"User '{username}' updated successfully!")
|
||||
|
||||
except UserNotFoundError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
except ValueError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('username')
|
||||
@click.confirmation_option(prompt='Are you sure you want to delete this user?')
|
||||
@click.pass_context
|
||||
def delete_user(ctx, username: str):
|
||||
"""Delete a user."""
|
||||
try:
|
||||
user_manager = ctx.obj['user_manager']
|
||||
user_manager.delete_user(username)
|
||||
click.echo(f"User '{username}' deleted successfully!")
|
||||
|
||||
except UserNotFoundError as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('username')
|
||||
@click.option('--password', prompt=True, hide_input=True, help='Password for authentication')
|
||||
@click.pass_context
|
||||
def authenticate(ctx, username: str, password: str):
|
||||
"""Authenticate a user."""
|
||||
try:
|
||||
auth_service = ctx.obj['auth_service']
|
||||
user = auth_service.authenticate(username, password)
|
||||
|
||||
click.echo(f"Authentication successful!")
|
||||
click.echo(f"Welcome, {user.name}!")
|
||||
|
||||
# Create a session
|
||||
session_id = auth_service.create_session(user)
|
||||
click.echo(f"Session created: {session_id}")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Authentication failed: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def stats(ctx):
|
||||
"""Show user statistics."""
|
||||
user_manager = ctx.obj['user_manager']
|
||||
auth_service = ctx.obj['auth_service']
|
||||
|
||||
user_stats = user_manager.get_user_stats()
|
||||
session_stats = auth_service.get_session_stats()
|
||||
|
||||
click.echo("User Statistics:")
|
||||
click.echo(f" Total Users: {user_stats['total']}")
|
||||
click.echo(f" Active Users: {user_stats['active']}")
|
||||
click.echo(f" Admin Users: {user_stats['admin']}")
|
||||
click.echo(f" Regular Users: {user_stats['user']}")
|
||||
click.echo(f" Guest Users: {user_stats['guest']}")
|
||||
click.echo(f" Users with Email: {user_stats['with_email']}")
|
||||
|
||||
click.echo("\nSession Statistics:")
|
||||
click.echo(f" Active Sessions: {session_stats['total_sessions']}")
|
||||
click.echo(f" Recent Sessions: {session_stats['recent_sessions']}")
|
||||
click.echo(f" Old Sessions: {session_stats['old_sessions']}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--format', type=click.Choice(['json', 'csv']), default='json', help='Export format')
|
||||
@click.option('--output', help='Output file path')
|
||||
@click.pass_context
|
||||
def export(ctx, format: str, output: Optional[str]):
|
||||
"""Export users to file."""
|
||||
user_manager = ctx.obj['user_manager']
|
||||
|
||||
try:
|
||||
data = user_manager.export_users(format)
|
||||
|
||||
if output:
|
||||
with open(output, 'w') as f:
|
||||
f.write(data)
|
||||
click.echo(f"Users exported to {output}")
|
||||
else:
|
||||
click.echo(data)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Export failed: {e}", err=True)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument('query')
|
||||
@click.pass_context
|
||||
def search(ctx, query: str):
|
||||
"""Search users by name or username."""
|
||||
user_manager = ctx.obj['user_manager']
|
||||
users = user_manager.search_users(query)
|
||||
|
||||
if not users:
|
||||
click.echo(f"No users found matching '{query}'")
|
||||
return
|
||||
|
||||
click.echo(f"Found {len(users)} users matching '{query}':")
|
||||
click.echo("-" * 60)
|
||||
|
||||
for user in users:
|
||||
status_indicator = "✓" if user.is_active() else "✗"
|
||||
click.echo(f"{status_indicator} {user.username:<15} {user.name:<20} {user.role.value:<10}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the CLI."""
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Models package for user management system."""
|
||||
|
||||
from .person import Person
|
||||
from .user import User
|
||||
|
||||
__all__ = ["Person", "User"]
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Person model for the user management system.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
"""Represents a person with basic information."""
|
||||
|
||||
name: str
|
||||
age: int
|
||||
email: Optional[str] = None
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate data after initialization."""
|
||||
if self.age < 0:
|
||||
raise ValueError("Age cannot be negative")
|
||||
if self.age > 150:
|
||||
raise ValueError("Age cannot be greater than 150")
|
||||
if not self.name.strip():
|
||||
raise ValueError("Name cannot be empty")
|
||||
|
||||
def greet(self) -> str:
|
||||
"""Return a greeting message."""
|
||||
return f"Hello, I'm {self.name} and I'm {self.age} years old."
|
||||
|
||||
def has_email(self) -> bool:
|
||||
"""Check if person has an email address."""
|
||||
return self.email is not None and self.email.strip() != ""
|
||||
|
||||
def update_email(self, email: str) -> None:
|
||||
"""Update the person's email address."""
|
||||
if not email.strip():
|
||||
raise ValueError("Email cannot be empty")
|
||||
self.email = email.strip()
|
||||
|
||||
def add_metadata(self, key: str, value: Any) -> None:
|
||||
"""Add metadata to the person."""
|
||||
self.metadata[key] = value
|
||||
|
||||
def get_metadata(self, key: str, default: Any = None) -> Any:
|
||||
"""Get metadata value by key."""
|
||||
return self.metadata.get(key, default)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert person to dictionary."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"age": self.age,
|
||||
"email": self.email,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"metadata": self.metadata
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'Person':
|
||||
"""Create a Person from a dictionary."""
|
||||
created_at = datetime.fromisoformat(data.get("created_at", datetime.now().isoformat()))
|
||||
return cls(
|
||||
name=data["name"],
|
||||
age=data["age"],
|
||||
email=data.get("email"),
|
||||
created_at=created_at,
|
||||
metadata=data.get("metadata", {})
|
||||
)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert person to JSON string."""
|
||||
return json.dumps(self.to_dict(), indent=2)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> 'Person':
|
||||
"""Create a Person from JSON string."""
|
||||
data = json.loads(json_str)
|
||||
return cls.from_dict(data)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of person."""
|
||||
email_str = f", email: {self.email}" if self.has_email() else ""
|
||||
return f"Person(name: {self.name}, age: {self.age}{email_str})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation of person."""
|
||||
return f"Person(name='{self.name}', age={self.age}, email='{self.email}')"
|
||||
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
User model extending Person for the user management system.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any, Set
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from .person import Person
|
||||
|
||||
|
||||
class UserRole(Enum):
|
||||
"""User role enumeration."""
|
||||
ADMIN = "admin"
|
||||
USER = "user"
|
||||
GUEST = "guest"
|
||||
|
||||
|
||||
class UserStatus(Enum):
|
||||
"""User status enumeration."""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
SUSPENDED = "suspended"
|
||||
DELETED = "deleted"
|
||||
|
||||
|
||||
@dataclass
|
||||
class User(Person):
|
||||
"""User class extending Person with authentication and permissions."""
|
||||
|
||||
username: str = ""
|
||||
password_hash: str = ""
|
||||
role: UserRole = UserRole.USER
|
||||
status: UserStatus = UserStatus.ACTIVE
|
||||
last_login: Optional[datetime] = None
|
||||
login_attempts: int = 0
|
||||
permissions: Set[str] = field(default_factory=set)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate user data after initialization."""
|
||||
super().__post_init__()
|
||||
if not self.username.strip():
|
||||
raise ValueError("Username cannot be empty")
|
||||
if len(self.username) < 3:
|
||||
raise ValueError("Username must be at least 3 characters long")
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
"""Set user password with hashing."""
|
||||
if len(password) < 8:
|
||||
raise ValueError("Password must be at least 8 characters long")
|
||||
|
||||
# Simple password hashing (in real app, use bcrypt)
|
||||
salt = secrets.token_hex(16)
|
||||
password_hash = hashlib.pbkdf2_hmac('sha256',
|
||||
password.encode('utf-8'),
|
||||
salt.encode('utf-8'),
|
||||
100000)
|
||||
self.password_hash = salt + password_hash.hex()
|
||||
|
||||
def verify_password(self, password: str) -> bool:
|
||||
"""Verify password against stored hash."""
|
||||
if not self.password_hash:
|
||||
return False
|
||||
|
||||
try:
|
||||
salt = self.password_hash[:32]
|
||||
stored_hash = self.password_hash[32:]
|
||||
|
||||
password_hash = hashlib.pbkdf2_hmac('sha256',
|
||||
password.encode('utf-8'),
|
||||
salt.encode('utf-8'),
|
||||
100000)
|
||||
|
||||
return password_hash.hex() == stored_hash
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def add_permission(self, permission: str) -> None:
|
||||
"""Add a permission to the user."""
|
||||
self.permissions.add(permission)
|
||||
|
||||
def remove_permission(self, permission: str) -> None:
|
||||
"""Remove a permission from the user."""
|
||||
self.permissions.discard(permission)
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""Check if user has a specific permission."""
|
||||
return permission in self.permissions
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is an admin."""
|
||||
return self.role == UserRole.ADMIN
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if user is active."""
|
||||
return self.status == UserStatus.ACTIVE
|
||||
|
||||
def login(self) -> bool:
|
||||
"""Record a successful login."""
|
||||
if not self.is_active():
|
||||
return False
|
||||
|
||||
self.last_login = datetime.now()
|
||||
self.login_attempts = 0
|
||||
return True
|
||||
|
||||
def failed_login_attempt(self) -> None:
|
||||
"""Record a failed login attempt."""
|
||||
self.login_attempts += 1
|
||||
if self.login_attempts >= 5:
|
||||
self.status = UserStatus.SUSPENDED
|
||||
|
||||
def activate(self) -> None:
|
||||
"""Activate the user account."""
|
||||
self.status = UserStatus.ACTIVE
|
||||
self.login_attempts = 0
|
||||
|
||||
def deactivate(self) -> None:
|
||||
"""Deactivate the user account."""
|
||||
self.status = UserStatus.INACTIVE
|
||||
|
||||
def suspend(self) -> None:
|
||||
"""Suspend the user account."""
|
||||
self.status = UserStatus.SUSPENDED
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Mark the user as deleted."""
|
||||
self.status = UserStatus.DELETED
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert user to dictionary."""
|
||||
data = super().to_dict()
|
||||
data.update({
|
||||
"username": self.username,
|
||||
"role": self.role.value,
|
||||
"status": self.status.value,
|
||||
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||||
"login_attempts": self.login_attempts,
|
||||
"permissions": list(self.permissions),
|
||||
})
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'User':
|
||||
"""Create a User from a dictionary."""
|
||||
person_data = {k: v for k, v in data.items()
|
||||
if k in ["name", "age", "email", "created_at", "metadata"]}
|
||||
person = Person.from_dict(person_data)
|
||||
|
||||
last_login = None
|
||||
if data.get("last_login"):
|
||||
last_login = datetime.fromisoformat(data["last_login"])
|
||||
|
||||
return cls(
|
||||
name=person.name,
|
||||
age=person.age,
|
||||
email=person.email,
|
||||
created_at=person.created_at,
|
||||
metadata=person.metadata,
|
||||
username=data["username"],
|
||||
password_hash=data.get("password_hash", ""),
|
||||
role=UserRole(data.get("role", UserRole.USER.value)),
|
||||
status=UserStatus(data.get("status", UserStatus.ACTIVE.value)),
|
||||
last_login=last_login,
|
||||
login_attempts=data.get("login_attempts", 0),
|
||||
permissions=set(data.get("permissions", []))
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of user."""
|
||||
return f"User(username: {self.username}, name: {self.name}, role: {self.role.value})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation of user."""
|
||||
return f"User(username='{self.username}', name='{self.name}', role={self.role})"
|
||||
|
||||
# AUTO_REINDEX_MARKER: ci_auto_reindex_test_token
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Services package for user management system."""
|
||||
|
||||
from .user_manager import UserManager
|
||||
from .auth_service import AuthService
|
||||
|
||||
__all__ = ["UserManager", "AuthService"]
|
||||
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Authentication service for user management system.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import hashlib
|
||||
|
||||
from ..models.user import User, UserStatus
|
||||
from ..utils.exceptions import AuthenticationError, UserNotFoundError
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Service class for handling authentication operations."""
|
||||
|
||||
def __init__(self, user_manager):
|
||||
"""Initialize the authentication service with a user manager."""
|
||||
self._user_manager = user_manager
|
||||
self._active_sessions: Dict[str, Dict[str, Any]] = {}
|
||||
self._session_timeout = timedelta(hours=24)
|
||||
|
||||
def authenticate(self, username: str, password: str) -> Optional[User]:
|
||||
"""Authenticate a user with username and password."""
|
||||
try:
|
||||
user = self._user_manager.get_user(username)
|
||||
except UserNotFoundError:
|
||||
raise AuthenticationError("Invalid username or password")
|
||||
|
||||
if not user.is_active():
|
||||
raise AuthenticationError("User account is not active")
|
||||
|
||||
if not user.verify_password(password):
|
||||
user.failed_login_attempt()
|
||||
raise AuthenticationError("Invalid username or password")
|
||||
|
||||
# Successful authentication
|
||||
user.login()
|
||||
return user
|
||||
|
||||
def create_session(self, user: User) -> str:
|
||||
"""Create a new session for the authenticated user."""
|
||||
session_id = secrets.token_urlsafe(32)
|
||||
session_data = {
|
||||
'user_id': user.username,
|
||||
'created_at': datetime.now(),
|
||||
'last_activity': datetime.now(),
|
||||
'ip_address': None, # Would be set in a real application
|
||||
'user_agent': None, # Would be set in a real application
|
||||
}
|
||||
|
||||
self._active_sessions[session_id] = session_data
|
||||
return session_id
|
||||
|
||||
def validate_session(self, session_id: str) -> Optional[User]:
|
||||
"""Validate a session and return the associated user."""
|
||||
if session_id not in self._active_sessions:
|
||||
return None
|
||||
|
||||
session_data = self._active_sessions[session_id]
|
||||
|
||||
# Check if session has expired
|
||||
if datetime.now() - session_data['last_activity'] > self._session_timeout:
|
||||
self.destroy_session(session_id)
|
||||
return None
|
||||
|
||||
# Update last activity
|
||||
session_data['last_activity'] = datetime.now()
|
||||
|
||||
try:
|
||||
user = self._user_manager.get_user(session_data['user_id'])
|
||||
if not user.is_active():
|
||||
self.destroy_session(session_id)
|
||||
return None
|
||||
return user
|
||||
except UserNotFoundError:
|
||||
self.destroy_session(session_id)
|
||||
return None
|
||||
|
||||
def destroy_session(self, session_id: str) -> bool:
|
||||
"""Destroy a session."""
|
||||
if session_id in self._active_sessions:
|
||||
del self._active_sessions[session_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
def destroy_all_sessions(self, username: str) -> int:
|
||||
"""Destroy all sessions for a specific user."""
|
||||
sessions_to_remove = []
|
||||
for session_id, session_data in self._active_sessions.items():
|
||||
if session_data['user_id'] == username:
|
||||
sessions_to_remove.append(session_id)
|
||||
|
||||
for session_id in sessions_to_remove:
|
||||
del self._active_sessions[session_id]
|
||||
|
||||
return len(sessions_to_remove)
|
||||
|
||||
def get_active_sessions(self, username: str) -> List[Dict[str, Any]]:
|
||||
"""Get all active sessions for a user."""
|
||||
sessions = []
|
||||
for session_id, session_data in self._active_sessions.items():
|
||||
if session_data['user_id'] == username:
|
||||
sessions.append({
|
||||
'session_id': session_id,
|
||||
'created_at': session_data['created_at'],
|
||||
'last_activity': session_data['last_activity'],
|
||||
'ip_address': session_data.get('ip_address'),
|
||||
'user_agent': session_data.get('user_agent'),
|
||||
})
|
||||
return sessions
|
||||
|
||||
def cleanup_expired_sessions(self) -> int:
|
||||
"""Remove expired sessions and return count of removed sessions."""
|
||||
current_time = datetime.now()
|
||||
expired_sessions = []
|
||||
|
||||
for session_id, session_data in self._active_sessions.items():
|
||||
if current_time - session_data['last_activity'] > self._session_timeout:
|
||||
expired_sessions.append(session_id)
|
||||
|
||||
for session_id in expired_sessions:
|
||||
del self._active_sessions[session_id]
|
||||
|
||||
return len(expired_sessions)
|
||||
|
||||
def change_password(self, username: str, old_password: str, new_password: str) -> bool:
|
||||
"""Change a user's password."""
|
||||
user = self._user_manager.get_user(username)
|
||||
|
||||
if not user.verify_password(old_password):
|
||||
raise AuthenticationError("Current password is incorrect")
|
||||
|
||||
user.set_password(new_password)
|
||||
|
||||
# Destroy all existing sessions for security
|
||||
self.destroy_all_sessions(username)
|
||||
|
||||
return True
|
||||
|
||||
def reset_password(self, username: str, new_password: str) -> str:
|
||||
"""Reset a user's password (admin function)."""
|
||||
user = self._user_manager.get_user(username)
|
||||
|
||||
# Generate a temporary password if none provided
|
||||
if not new_password:
|
||||
new_password = self._generate_temporary_password()
|
||||
|
||||
user.set_password(new_password)
|
||||
|
||||
# Destroy all existing sessions
|
||||
self.destroy_all_sessions(username)
|
||||
|
||||
return new_password
|
||||
|
||||
def _generate_temporary_password(self) -> str:
|
||||
"""Generate a temporary password."""
|
||||
return secrets.token_urlsafe(12)
|
||||
|
||||
def get_session_stats(self) -> Dict[str, Any]:
|
||||
"""Get statistics about active sessions."""
|
||||
current_time = datetime.now()
|
||||
session_count = len(self._active_sessions)
|
||||
|
||||
# Count sessions by age
|
||||
recent_sessions = 0 # Last hour
|
||||
old_sessions = 0 # Older than 1 hour
|
||||
|
||||
for session_data in self._active_sessions.values():
|
||||
age = current_time - session_data['last_activity']
|
||||
if age < timedelta(hours=1):
|
||||
recent_sessions += 1
|
||||
else:
|
||||
old_sessions += 1
|
||||
|
||||
return {
|
||||
'total_sessions': session_count,
|
||||
'recent_sessions': recent_sessions,
|
||||
'old_sessions': old_sessions,
|
||||
'session_timeout_hours': self._session_timeout.total_seconds() / 3600,
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of AuthService."""
|
||||
return f"AuthService(active_sessions: {len(self._active_sessions)})"
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
User management service for handling user operations.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any, Callable
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
from ..models.user import User, UserRole, UserStatus
|
||||
from ..utils.validators import validate_email, validate_username
|
||||
from ..utils.exceptions import UserNotFoundError, DuplicateUserError
|
||||
|
||||
|
||||
class UserManager:
|
||||
"""Service class for managing users."""
|
||||
|
||||
def __init__(self, storage_path: Optional[str] = None):
|
||||
"""Initialize the user manager with optional storage path."""
|
||||
self._users: Dict[str, User] = {}
|
||||
self._storage_path = storage_path
|
||||
if storage_path and os.path.exists(storage_path):
|
||||
self._load_from_file()
|
||||
|
||||
def create_user(self, name: str, age: int, username: str,
|
||||
email: Optional[str] = None,
|
||||
role: UserRole = UserRole.USER) -> User:
|
||||
"""Create a new user."""
|
||||
if username in self._users:
|
||||
raise DuplicateUserError(f"User with username '{username}' already exists")
|
||||
|
||||
# Validate inputs
|
||||
if not validate_username(username):
|
||||
raise ValueError("Invalid username format")
|
||||
|
||||
if email and not validate_email(email):
|
||||
raise ValueError("Invalid email format")
|
||||
|
||||
user = User(
|
||||
name=name,
|
||||
age=age,
|
||||
username=username,
|
||||
email=email,
|
||||
role=role
|
||||
)
|
||||
|
||||
self._users[username] = user
|
||||
self._save_to_file()
|
||||
return user
|
||||
|
||||
def get_user(self, username: str) -> User:
|
||||
"""Get a user by username."""
|
||||
if username not in self._users:
|
||||
raise UserNotFoundError(f"User with username '{username}' not found")
|
||||
return self._users[username]
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[User]:
|
||||
"""Get a user by email address."""
|
||||
for user in self._users.values():
|
||||
if user.email == email:
|
||||
return user
|
||||
return None
|
||||
|
||||
def update_user(self, username: str, **kwargs) -> User:
|
||||
"""Update user information."""
|
||||
user = self.get_user(username)
|
||||
|
||||
# Update allowed fields
|
||||
allowed_fields = ['name', 'age', 'email', 'role', 'status']
|
||||
for field, value in kwargs.items():
|
||||
if field in allowed_fields and hasattr(user, field):
|
||||
setattr(user, field, value)
|
||||
|
||||
self._save_to_file()
|
||||
return user
|
||||
|
||||
def delete_user(self, username: str) -> bool:
|
||||
"""Delete a user (soft delete)."""
|
||||
user = self.get_user(username)
|
||||
user.delete()
|
||||
self._save_to_file()
|
||||
return True
|
||||
|
||||
def remove_user(self, username: str) -> bool:
|
||||
"""Remove a user completely from the system."""
|
||||
if username not in self._users:
|
||||
raise UserNotFoundError(f"User with username '{username}' not found")
|
||||
|
||||
del self._users[username]
|
||||
self._save_to_file()
|
||||
return True
|
||||
|
||||
def get_all_users(self) -> List[User]:
|
||||
"""Get all users."""
|
||||
return list(self._users.values())
|
||||
|
||||
def get_active_users(self) -> List[User]:
|
||||
"""Get all active users."""
|
||||
return [user for user in self._users.values() if user.is_active()]
|
||||
|
||||
def get_users_by_role(self, role: UserRole) -> List[User]:
|
||||
"""Get users by role."""
|
||||
return [user for user in self._users.values() if user.role == role]
|
||||
|
||||
def filter_users(self, filter_func: Callable[[User], bool]) -> List[User]:
|
||||
"""Filter users using a custom function."""
|
||||
return [user for user in self._users.values() if filter_func(user)]
|
||||
|
||||
def search_users(self, query: str) -> List[User]:
|
||||
"""Search users by name or username."""
|
||||
query_lower = query.lower()
|
||||
return [
|
||||
user for user in self._users.values()
|
||||
if query_lower in user.name.lower() or query_lower in user.username.lower()
|
||||
]
|
||||
|
||||
def get_users_older_than(self, age: int) -> List[User]:
|
||||
"""Get users older than specified age."""
|
||||
return self.filter_users(lambda user: user.age > age)
|
||||
|
||||
def get_users_with_email(self) -> List[User]:
|
||||
"""Get users that have email addresses."""
|
||||
return self.filter_users(lambda user: user.has_email())
|
||||
|
||||
def get_users_with_permission(self, permission: str) -> List[User]:
|
||||
"""Get users with specific permission."""
|
||||
return self.filter_users(lambda user: user.has_permission(permission))
|
||||
|
||||
def get_user_count(self) -> int:
|
||||
"""Get the total number of users."""
|
||||
return len(self._users)
|
||||
|
||||
def get_user_stats(self) -> Dict[str, int]:
|
||||
"""Get user statistics."""
|
||||
stats = {
|
||||
'total': len(self._users),
|
||||
'active': len(self.get_active_users()),
|
||||
'admin': len(self.get_users_by_role(UserRole.ADMIN)),
|
||||
'user': len(self.get_users_by_role(UserRole.USER)),
|
||||
'guest': len(self.get_users_by_role(UserRole.GUEST)),
|
||||
'with_email': len(self.get_users_with_email()),
|
||||
}
|
||||
return stats
|
||||
|
||||
def export_users(self, format: str = 'json') -> str:
|
||||
"""Export users to specified format."""
|
||||
if format.lower() == 'json':
|
||||
return self._export_to_json()
|
||||
elif format.lower() == 'csv':
|
||||
return self._export_to_csv()
|
||||
else:
|
||||
raise ValueError(f"Unsupported export format: {format}")
|
||||
|
||||
def _export_to_json(self) -> str:
|
||||
"""Export users to JSON format."""
|
||||
users_data = [user.to_dict() for user in self._users.values()]
|
||||
return json.dumps(users_data, indent=2)
|
||||
|
||||
def _export_to_csv(self) -> str:
|
||||
"""Export users to CSV format."""
|
||||
if not self._users:
|
||||
return "username,name,age,email,role,status\n"
|
||||
|
||||
lines = ["username,name,age,email,role,status"]
|
||||
for user in self._users.values():
|
||||
line = f"{user.username},{user.name},{user.age},{user.email or ''},{user.role.value},{user.status.value}"
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _save_to_file(self) -> None:
|
||||
"""Save users to file if storage path is set."""
|
||||
if not self._storage_path:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self._storage_path, 'w') as f:
|
||||
json.dump(self._export_to_json(), f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error saving users to file: {e}")
|
||||
|
||||
def _load_from_file(self) -> None:
|
||||
"""Load users from file."""
|
||||
if not self._storage_path or not os.path.exists(self._storage_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self._storage_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
for user_data in data:
|
||||
user = User.from_dict(user_data)
|
||||
self._users[user.username] = user
|
||||
except Exception as e:
|
||||
print(f"Error loading users from file: {e}")
|
||||
|
||||
def clear_all_users(self) -> None:
|
||||
"""Clear all users from the system."""
|
||||
self._users.clear()
|
||||
self._save_to_file()
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of users."""
|
||||
return len(self._users)
|
||||
|
||||
def __contains__(self, username: str) -> bool:
|
||||
"""Check if a username exists."""
|
||||
return username in self._users
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over users."""
|
||||
return iter(self._users.values())
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of UserManager."""
|
||||
return f"UserManager(users: {len(self._users)})"
|
||||
|
||||
# CI marker method to verify auto-reindex on change
|
||||
def _ci_added_symbol_marker(self) -> str:
|
||||
return "ci_symbol_python"
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Utilities package for user management system."""
|
||||
|
||||
from .validators import validate_email, validate_username, validate_password
|
||||
from .exceptions import UserNotFoundError, DuplicateUserError, AuthenticationError
|
||||
from .helpers import generate_random_string, format_datetime, parse_datetime
|
||||
|
||||
__all__ = [
|
||||
"validate_email",
|
||||
"validate_username",
|
||||
"validate_password",
|
||||
"UserNotFoundError",
|
||||
"DuplicateUserError",
|
||||
"AuthenticationError",
|
||||
"generate_random_string",
|
||||
"format_datetime",
|
||||
"parse_datetime"
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Custom exceptions for user management system.
|
||||
"""
|
||||
|
||||
|
||||
class UserManagementError(Exception):
|
||||
"""Base exception for user management errors."""
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(UserManagementError):
|
||||
"""Exception raised when a user is not found."""
|
||||
|
||||
def __init__(self, message: str = "User not found"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class DuplicateUserError(UserManagementError):
|
||||
"""Exception raised when trying to create a user that already exists."""
|
||||
|
||||
def __init__(self, message: str = "User already exists"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class AuthenticationError(UserManagementError):
|
||||
"""Exception raised when authentication fails."""
|
||||
|
||||
def __init__(self, message: str = "Authentication failed"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class AuthorizationError(UserManagementError):
|
||||
"""Exception raised when authorization fails."""
|
||||
|
||||
def __init__(self, message: str = "Authorization failed"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ValidationError(UserManagementError):
|
||||
"""Exception raised when validation fails."""
|
||||
|
||||
def __init__(self, message: str = "Validation failed"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class PermissionError(UserManagementError):
|
||||
"""Exception raised when user lacks required permissions."""
|
||||
|
||||
def __init__(self, message: str = "Permission denied"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class SessionError(UserManagementError):
|
||||
"""Exception raised when session operations fail."""
|
||||
|
||||
def __init__(self, message: str = "Session error"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class StorageError(UserManagementError):
|
||||
"""Exception raised when storage operations fail."""
|
||||
|
||||
def __init__(self, message: str = "Storage error"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ConfigurationError(UserManagementError):
|
||||
"""Exception raised when configuration is invalid."""
|
||||
|
||||
def __init__(self, message: str = "Configuration error"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Helper utilities for user management system.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any, List
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
def generate_random_string(length: int = 16,
|
||||
include_digits: bool = True,
|
||||
include_symbols: bool = False) -> str:
|
||||
"""Generate a random string of specified length."""
|
||||
characters = string.ascii_letters
|
||||
|
||||
if include_digits:
|
||||
characters += string.digits
|
||||
|
||||
if include_symbols:
|
||||
characters += "!@#$%^&*"
|
||||
|
||||
return ''.join(secrets.choice(characters) for _ in range(length))
|
||||
|
||||
|
||||
def generate_secure_token(length: int = 32) -> str:
|
||||
"""Generate a secure URL-safe token."""
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
|
||||
def generate_hash(input_string: str, salt: str = "") -> str:
|
||||
"""Generate a SHA-256 hash of the input string."""
|
||||
combined = f"{input_string}{salt}"
|
||||
return hashlib.sha256(combined.encode()).hexdigest()
|
||||
|
||||
|
||||
def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""Format datetime object to string."""
|
||||
if dt is None:
|
||||
return ""
|
||||
return dt.strftime(format_str)
|
||||
|
||||
|
||||
def parse_datetime(date_string: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]:
|
||||
"""Parse string to datetime object."""
|
||||
try:
|
||||
return datetime.strptime(date_string, format_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def get_current_timestamp() -> str:
|
||||
"""Get current timestamp as ISO format string."""
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def is_valid_json(json_string: str) -> bool:
|
||||
"""Check if a string is valid JSON."""
|
||||
try:
|
||||
json.loads(json_string)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def deep_merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Deep merge two dictionaries."""
|
||||
result = dict1.copy()
|
||||
|
||||
for key, value in dict2.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = deep_merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def flatten_dict(nested_dict: Dict[str, Any], parent_key: str = '', sep: str = '.') -> Dict[str, Any]:
|
||||
"""Flatten a nested dictionary."""
|
||||
items = []
|
||||
|
||||
for key, value in nested_dict.items():
|
||||
new_key = f"{parent_key}{sep}{key}" if parent_key else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
items.extend(flatten_dict(value, new_key, sep=sep).items())
|
||||
else:
|
||||
items.append((new_key, value))
|
||||
|
||||
return dict(items)
|
||||
|
||||
|
||||
def chunk_list(input_list: List[Any], chunk_size: int) -> List[List[Any]]:
|
||||
"""Split a list into chunks of specified size."""
|
||||
return [input_list[i:i + chunk_size] for i in range(0, len(input_list), chunk_size)]
|
||||
|
||||
|
||||
def remove_duplicates(input_list: List[Any]) -> List[Any]:
|
||||
"""Remove duplicates from a list while preserving order."""
|
||||
seen = set()
|
||||
result = []
|
||||
|
||||
for item in input_list:
|
||||
if item not in seen:
|
||||
seen.add(item)
|
||||
result.append(item)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def safe_dict_get(dictionary: Dict[str, Any], key_path: str, default: Any = None) -> Any:
|
||||
"""Safely get value from nested dictionary using dot notation."""
|
||||
keys = key_path.split('.')
|
||||
current = dictionary
|
||||
|
||||
try:
|
||||
for key in keys:
|
||||
current = current[key]
|
||||
return current
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def calculate_age(birth_date: datetime) -> int:
|
||||
"""Calculate age from birth date."""
|
||||
today = datetime.now()
|
||||
age = today.year - birth_date.year
|
||||
|
||||
# Adjust if birthday hasn't occurred this year
|
||||
if today.month < birth_date.month or (today.month == birth_date.month and today.day < birth_date.day):
|
||||
age -= 1
|
||||
|
||||
return age
|
||||
|
||||
|
||||
def mask_email(email: str) -> str:
|
||||
"""Mask email address for privacy."""
|
||||
if not email or '@' not in email:
|
||||
return email
|
||||
|
||||
username, domain = email.split('@', 1)
|
||||
|
||||
if len(username) <= 2:
|
||||
masked_username = username
|
||||
else:
|
||||
masked_username = username[0] + '*' * (len(username) - 2) + username[-1]
|
||||
|
||||
return f"{masked_username}@{domain}"
|
||||
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""Format file size in human readable format."""
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
|
||||
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
|
||||
while size_bytes >= 1024 and i < len(size_names) - 1:
|
||||
size_bytes /= 1024.0
|
||||
i += 1
|
||||
|
||||
return f"{size_bytes:.1f} {size_names[i]}"
|
||||
|
||||
|
||||
def truncate_string(text: str, max_length: int, suffix: str = "...") -> str:
|
||||
"""Truncate string to specified length with optional suffix."""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
|
||||
return text[:max_length - len(suffix)] + suffix
|
||||
|
||||
|
||||
def camel_to_snake(name: str) -> str:
|
||||
"""Convert camelCase to snake_case."""
|
||||
import re
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
|
||||
def snake_to_camel(name: str) -> str:
|
||||
"""Convert snake_case to camelCase."""
|
||||
components = name.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
def is_email_domain_valid(email: str, allowed_domains: List[str]) -> bool:
|
||||
"""Check if email domain is in allowed list."""
|
||||
if not email or '@' not in email:
|
||||
return False
|
||||
|
||||
domain = email.split('@')[1].lower()
|
||||
return domain in [d.lower() for d in allowed_domains]
|
||||
|
||||
|
||||
def get_initials(name: str) -> str:
|
||||
"""Get initials from a name."""
|
||||
if not name:
|
||||
return ""
|
||||
|
||||
words = name.strip().split()
|
||||
initials = ''.join(word[0].upper() for word in words if word)
|
||||
return initials[:3] # Limit to 3 characters
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Validation utilities for user management system.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
"""Validate email address format."""
|
||||
if not email:
|
||||
return False
|
||||
|
||||
# Basic email validation pattern
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return bool(re.match(pattern, email))
|
||||
|
||||
|
||||
def validate_username(username: str) -> bool:
|
||||
"""Validate username format."""
|
||||
if not username:
|
||||
return False
|
||||
|
||||
# Username must be 3-20 characters, alphanumeric and underscores only
|
||||
if len(username) < 3 or len(username) > 20:
|
||||
return False
|
||||
|
||||
pattern = r'^[a-zA-Z0-9_]+$'
|
||||
return bool(re.match(pattern, username))
|
||||
|
||||
|
||||
def validate_password(password: str) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate password strength.
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
if not password:
|
||||
return False, "Password cannot be empty"
|
||||
|
||||
if len(password) < 8:
|
||||
return False, "Password must be at least 8 characters long"
|
||||
|
||||
if len(password) > 128:
|
||||
return False, "Password must be no more than 128 characters long"
|
||||
|
||||
# Check for at least one uppercase letter
|
||||
if not re.search(r'[A-Z]', password):
|
||||
return False, "Password must contain at least one uppercase letter"
|
||||
|
||||
# Check for at least one lowercase letter
|
||||
if not re.search(r'[a-z]', password):
|
||||
return False, "Password must contain at least one lowercase letter"
|
||||
|
||||
# Check for at least one digit
|
||||
if not re.search(r'\d', password):
|
||||
return False, "Password must contain at least one digit"
|
||||
|
||||
# Check for at least one special character
|
||||
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
||||
return False, "Password must contain at least one special character"
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def validate_age(age: int) -> bool:
|
||||
"""Validate age value."""
|
||||
return 0 <= age <= 150
|
||||
|
||||
|
||||
def validate_name(name: str) -> bool:
|
||||
"""Validate name format."""
|
||||
if not name or not name.strip():
|
||||
return False
|
||||
|
||||
# Name should be 1-50 characters, letters, spaces, hyphens, and apostrophes
|
||||
if len(name.strip()) > 50:
|
||||
return False
|
||||
|
||||
pattern = r"^[a-zA-Z\s\-']+$"
|
||||
return bool(re.match(pattern, name.strip()))
|
||||
|
||||
|
||||
def validate_phone(phone: str) -> bool:
|
||||
"""Validate phone number format."""
|
||||
if not phone:
|
||||
return False
|
||||
|
||||
# Remove all non-digit characters
|
||||
digits_only = re.sub(r'\D', '', phone)
|
||||
|
||||
# Check if it's a valid length (10-15 digits)
|
||||
return 10 <= len(digits_only) <= 15
|
||||
|
||||
|
||||
def sanitize_input(input_str: str) -> str:
|
||||
"""Sanitize user input by removing potentially dangerous characters."""
|
||||
if not input_str:
|
||||
return ""
|
||||
|
||||
# Remove HTML tags
|
||||
clean_str = re.sub(r'<[^>]+>', '', input_str)
|
||||
|
||||
# Remove script tags and their content
|
||||
clean_str = re.sub(r'<script.*?</script>', '', clean_str, flags=re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# Remove dangerous characters
|
||||
dangerous_chars = ['<', '>', '"', "'", '&', ';', '`', '|', '$', '(', ')', '{', '}', '[', ']']
|
||||
for char in dangerous_chars:
|
||||
clean_str = clean_str.replace(char, '')
|
||||
|
||||
return clean_str.strip()
|
||||
|
||||
|
||||
def validate_url(url: str) -> bool:
|
||||
"""Validate URL format."""
|
||||
if not url:
|
||||
return False
|
||||
|
||||
pattern = r'^https?://(?:[-\w.])+(?::[0-9]+)?(?:/[^?\s]*)?(?:\?[^#\s]*)?(?:#[^\s]*)?$'
|
||||
return bool(re.match(pattern, url))
|
||||
|
||||
|
||||
def validate_json_string(json_str: str) -> bool:
|
||||
"""Validate if a string is valid JSON."""
|
||||
try:
|
||||
import json
|
||||
json.loads(json_str)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Sample TypeScript file for testing Code Index MCP analysis.
|
||||
*/
|
||||
|
||||
interface PersonInterface {
|
||||
name: string;
|
||||
age: number;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface UserManagerInterface {
|
||||
addUser(person: Person): void;
|
||||
findByName(name: string): Person | null;
|
||||
getAllUsers(): Person[];
|
||||
getUserCount(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a person with basic information.
|
||||
*/
|
||||
class Person implements PersonInterface {
|
||||
public readonly name: string;
|
||||
public readonly age: number;
|
||||
public email?: string;
|
||||
|
||||
constructor(name: string, age: number, email?: string) {
|
||||
if (age < 0) {
|
||||
throw new Error("Age cannot be negative");
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a greeting message.
|
||||
*/
|
||||
greet(): string {
|
||||
return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the person's email.
|
||||
*/
|
||||
updateEmail(email: string): void {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Person from an object.
|
||||
*/
|
||||
static fromObject(data: PersonInterface): Person {
|
||||
return new Person(data.name, data.age, data.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert person to JSON-serializable object.
|
||||
*/
|
||||
toJSON(): PersonInterface {
|
||||
return {
|
||||
name: this.name,
|
||||
age: this.age,
|
||||
email: this.email
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic utility type for filtering arrays.
|
||||
*/
|
||||
type FilterFunction<T> = (item: T) => boolean;
|
||||
|
||||
/**
|
||||
* Manages a collection of users.
|
||||
*/
|
||||
class UserManager implements UserManagerInterface {
|
||||
private users: Person[] = [];
|
||||
|
||||
/**
|
||||
* Add a user to the collection.
|
||||
*/
|
||||
addUser(person: Person): void {
|
||||
this.users.push(person);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by name.
|
||||
*/
|
||||
findByName(name: string): Person | null {
|
||||
const user = this.users.find(user => user.name === name);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users.
|
||||
*/
|
||||
getAllUsers(): Person[] {
|
||||
return [...this.users];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter users by a custom function.
|
||||
*/
|
||||
filterUsers(filterFn: FilterFunction<Person>): Person[] {
|
||||
return this.users.filter(filterFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users older than specified age.
|
||||
*/
|
||||
getUsersOlderThan(age: number): Person[] {
|
||||
return this.filterUsers(user => user.age > age);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users with email addresses.
|
||||
*/
|
||||
getUsersWithEmail(): Person[] {
|
||||
return this.filterUsers(user => !!user.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of users.
|
||||
*/
|
||||
getUserCount(): number {
|
||||
return this.users.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all users as JSON.
|
||||
*/
|
||||
exportToJSON(): string {
|
||||
return JSON.stringify(this.users.map(user => user.toJSON()), null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions for working with users.
|
||||
*/
|
||||
namespace UserUtils {
|
||||
export function validateAge(age: number): boolean {
|
||||
return age >= 0 && age <= 150;
|
||||
}
|
||||
|
||||
export function formatUserList(users: Person[]): string {
|
||||
return users.map(user => user.greet()).join('\n');
|
||||
}
|
||||
|
||||
export const DEFAULT_USERS: PersonInterface[] = [
|
||||
{ name: "Alice", age: 30, email: "alice@example.com" },
|
||||
{ name: "Bob", age: 25 },
|
||||
{ name: "Charlie", age: 35, email: "charlie@example.com" }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to demonstrate usage.
|
||||
*/
|
||||
function main(): void {
|
||||
const manager = new UserManager();
|
||||
|
||||
// Add some users
|
||||
UserUtils.DEFAULT_USERS.forEach(userData => {
|
||||
const person = Person.fromObject(userData);
|
||||
manager.addUser(person);
|
||||
console.log(person.greet());
|
||||
});
|
||||
|
||||
console.log(`Total users: ${manager.getUserCount()}`);
|
||||
|
||||
// Find users older than 25
|
||||
const olderUsers = manager.getUsersOlderThan(25);
|
||||
console.log(`Users older than 25: ${olderUsers.length}`);
|
||||
|
||||
// Export to JSON
|
||||
console.log("Users as JSON:");
|
||||
console.log(manager.exportToJSON());
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
export { Person, UserManager, UserUtils, PersonInterface, UserManagerInterface };
|
||||
|
||||
// Run main if this is the entry point
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"name": "user-management-ts",
|
||||
"version": "1.0.0",
|
||||
"description": "A comprehensive TypeScript user management system for testing Code Index MCP",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/ --ext .ts --fix",
|
||||
"format": "prettier --write src/",
|
||||
"clean": "rimraf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"prestart": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"user-management",
|
||||
"typescript",
|
||||
"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",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"typescript": "^5.1.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"jest": "^29.6.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"supertest": "^6.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
||||
"@typescript-eslint/parser": "^6.2.0",
|
||||
"eslint": "^8.45.0",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"mongodb-memory-server": "^8.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"roots": ["<rootDir>/src"],
|
||||
"testMatch": ["**/__tests__/**/*.test.ts"],
|
||||
"transform": {
|
||||
"^.+\.tsx?$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "coverage",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.ts",
|
||||
"!src/server.ts",
|
||||
"!src/**/*.d.ts"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"@typescript-eslint/recommended"
|
||||
],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "warn",
|
||||
"no-console": "warn",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import mongoose from 'mongoose';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Database connection configuration
|
||||
*/
|
||||
class Database {
|
||||
private mongoURI: string;
|
||||
private options: mongoose.ConnectOptions;
|
||||
|
||||
constructor() {
|
||||
this.mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-management-ts';
|
||||
this.options = {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
family: 4,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MongoDB
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
try {
|
||||
await mongoose.connect(this.mongoURI, this.options);
|
||||
logger.info('MongoDB connected successfully');
|
||||
|
||||
// Handle connection events
|
||||
mongoose.connection.on('error', (err: Error) => {
|
||||
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('SIGINT'));
|
||||
process.on('SIGTERM', () => this.gracefulShutdown('SIGTERM'));
|
||||
|
||||
} catch (error) {
|
||||
logger.error('MongoDB connection failed:', error as Error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from MongoDB
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
try {
|
||||
await mongoose.disconnect();
|
||||
logger.info('MongoDB disconnected successfully');
|
||||
} catch (error) {
|
||||
logger.error('MongoDB disconnection error:', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown
|
||||
*/
|
||||
private async gracefulShutdown(signal: string): Promise<void> {
|
||||
logger.info(`Received ${signal}. Graceful shutdown...`);
|
||||
try {
|
||||
await this.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Error during graceful shutdown:', error as Error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
public getConnectionStatus(): string {
|
||||
const states: Record<number, string> = {
|
||||
0: 'disconnected',
|
||||
1: 'connected',
|
||||
2: 'connecting',
|
||||
3: 'disconnecting',
|
||||
};
|
||||
return states[mongoose.connection.readyState] || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if database is connected
|
||||
*/
|
||||
public isConnected(): boolean {
|
||||
return mongoose.connection.readyState === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database (for testing)
|
||||
*/
|
||||
public async dropDatabase(): Promise<void> {
|
||||
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 as Error);
|
||||
}
|
||||
} else {
|
||||
logger.warn('Database drop attempted in non-test environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
*/
|
||||
public async getStats(): Promise<any> {
|
||||
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 as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create indexes for performance
|
||||
*/
|
||||
public async createIndexes(): Promise<void> {
|
||||
try {
|
||||
// This would be called after models are loaded
|
||||
// Indexes are already defined in the model schemas
|
||||
logger.info('Database indexes created');
|
||||
} catch (error) {
|
||||
logger.error('Error creating indexes:', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
public async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await mongoose.connection.db.admin().ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Database health check failed:', error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const database = new Database();
|
||||
|
||||
export default database;
|
||||
@@ -0,0 +1,238 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { Request, Response } from 'express';
|
||||
import { RateLimitError } from '../utils/errors';
|
||||
import logger from '../utils/logger';
|
||||
import { IApiResponse } from '../types/User';
|
||||
|
||||
/**
|
||||
* General rate limiter
|
||||
* Limits requests per IP address
|
||||
*/
|
||||
export const generalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
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: Request, res: Response) => {
|
||||
logger.warn(`Rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many requests from this IP, please try again later.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Authentication rate limiter
|
||||
* Stricter limits for login attempts
|
||||
*/
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // limit each IP to 5 login attempts per windowMs
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many login attempts from this IP, please try again later.',
|
||||
error: {
|
||||
message: 'Too many login attempts from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
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.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* User creation rate limiter
|
||||
* Moderate limits for user registration
|
||||
*/
|
||||
export const createUserLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 10, // limit each IP to 10 user creations per hour
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many accounts created from this IP, please try again later.',
|
||||
error: {
|
||||
message: 'Too many accounts created from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
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.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Password reset rate limiter
|
||||
* Limits password reset attempts
|
||||
*/
|
||||
export const passwordResetLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 3, // limit each IP to 3 password reset attempts per hour
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many password reset attempts from this IP, please try again later.',
|
||||
error: {
|
||||
message: 'Too many password reset attempts from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
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.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* API documentation rate limiter
|
||||
* Limits access to API documentation
|
||||
*/
|
||||
export const docsLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 50, // limit each IP to 50 documentation requests per windowMs
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many documentation requests from this IP, please try again later.',
|
||||
error: {
|
||||
message: 'Too many documentation requests from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
logger.warn(`Documentation rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many documentation requests from this IP, please try again later.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Export rate limiter
|
||||
* Limits data export requests
|
||||
*/
|
||||
export const exportLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5, // limit each IP to 5 export requests per hour
|
||||
message: {
|
||||
success: false,
|
||||
message: 'Too many export requests from this IP, please try again later.',
|
||||
error: {
|
||||
message: 'Too many export requests from this IP, please try again later.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
logger.warn(`Export rate limit exceeded for IP: ${req.ip}`);
|
||||
const error = new RateLimitError('Too many export requests from this IP, please try again later.');
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create custom rate limiter
|
||||
*/
|
||||
export const createCustomLimiter = (options: {
|
||||
windowMs: number;
|
||||
max: number;
|
||||
message: string;
|
||||
skipSuccessfulRequests?: boolean;
|
||||
skipFailedRequests?: boolean;
|
||||
}) => {
|
||||
return rateLimit({
|
||||
windowMs: options.windowMs,
|
||||
max: options.max,
|
||||
message: {
|
||||
success: false,
|
||||
message: options.message,
|
||||
error: {
|
||||
message: options.message,
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: options.skipSuccessfulRequests || false,
|
||||
skipFailedRequests: options.skipFailedRequests || false,
|
||||
handler: (req: Request, res: Response) => {
|
||||
logger.warn(`Custom rate limit exceeded for IP: ${req.ip} - ${options.message}`);
|
||||
const error = new RateLimitError(options.message);
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
},
|
||||
};
|
||||
res.status(429).json(response);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
import mongoose, { Schema, Model } from 'mongoose';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
IUser,
|
||||
IUserDocument,
|
||||
IUserResponse,
|
||||
IUserStats,
|
||||
UserRole,
|
||||
UserStatus,
|
||||
IJWTPayload,
|
||||
} from '../types/User';
|
||||
|
||||
// User schema definition
|
||||
const userSchema = new Schema<IUserDocument>(
|
||||
{
|
||||
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,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: Object.values(UserRole),
|
||||
default: UserRole.USER,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: Object.values(UserStatus),
|
||||
default: UserStatus.ACTIVE,
|
||||
},
|
||||
lastLogin: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
loginAttempts: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
permissions: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true },
|
||||
}
|
||||
);
|
||||
|
||||
// Virtual for user response (without sensitive data)
|
||||
userSchema.virtual('response').get(function (this: IUserDocument): IUserResponse {
|
||||
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 (this: IUserDocument): boolean {
|
||||
return this.status === UserStatus.ACTIVE;
|
||||
});
|
||||
|
||||
// Virtual for checking if user is admin
|
||||
userSchema.virtual('isAdmin').get(function (this: IUserDocument): boolean {
|
||||
return this.role === UserRole.ADMIN;
|
||||
});
|
||||
|
||||
// Virtual for checking if user is locked
|
||||
userSchema.virtual('isLocked').get(function (this: IUserDocument): boolean {
|
||||
return this.loginAttempts >= 5 || this.status === UserStatus.SUSPENDED;
|
||||
});
|
||||
|
||||
// Pre-save middleware to hash password
|
||||
userSchema.pre('save', async function (this: IUserDocument, next) {
|
||||
if (!this.isModified('password')) return next();
|
||||
|
||||
try {
|
||||
const saltRounds = parseInt(process.env.BCRYPT_SALT_ROUNDS || '12', 10);
|
||||
const hashedPassword = await bcrypt.hash(this.password, saltRounds);
|
||||
this.password = hashedPassword;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error as Error);
|
||||
}
|
||||
});
|
||||
|
||||
// Instance method to check password
|
||||
userSchema.methods.checkPassword = async function (
|
||||
this: IUserDocument,
|
||||
candidatePassword: string
|
||||
): Promise<boolean> {
|
||||
return await bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
// Instance method to generate JWT token
|
||||
userSchema.methods.generateToken = function (this: IUserDocument): string {
|
||||
const payload: IJWTPayload = {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
role: this.role,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, 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 (
|
||||
this: IUserDocument,
|
||||
permission: string
|
||||
): void {
|
||||
if (!this.permissions.includes(permission)) {
|
||||
this.permissions.push(permission);
|
||||
}
|
||||
};
|
||||
|
||||
// Instance method to remove permission
|
||||
userSchema.methods.removePermission = function (
|
||||
this: IUserDocument,
|
||||
permission: string
|
||||
): void {
|
||||
this.permissions = this.permissions.filter(p => p !== permission);
|
||||
};
|
||||
|
||||
// Instance method to check permission
|
||||
userSchema.methods.hasPermission = function (
|
||||
this: IUserDocument,
|
||||
permission: string
|
||||
): boolean {
|
||||
return this.permissions.includes(permission);
|
||||
};
|
||||
|
||||
// Instance method to record successful login
|
||||
userSchema.methods.recordLogin = function (this: IUserDocument): void {
|
||||
this.lastLogin = new Date();
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to record failed login attempt
|
||||
userSchema.methods.recordFailedLogin = function (this: IUserDocument): void {
|
||||
this.loginAttempts += 1;
|
||||
if (this.loginAttempts >= 5) {
|
||||
this.status = UserStatus.SUSPENDED;
|
||||
}
|
||||
};
|
||||
|
||||
// Instance method to reset login attempts
|
||||
userSchema.methods.resetLoginAttempts = function (this: IUserDocument): void {
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to activate user
|
||||
userSchema.methods.activate = function (this: IUserDocument): void {
|
||||
this.status = UserStatus.ACTIVE;
|
||||
this.loginAttempts = 0;
|
||||
};
|
||||
|
||||
// Instance method to deactivate user
|
||||
userSchema.methods.deactivate = function (this: IUserDocument): void {
|
||||
this.status = UserStatus.INACTIVE;
|
||||
};
|
||||
|
||||
// Instance method to suspend user
|
||||
userSchema.methods.suspend = function (this: IUserDocument): void {
|
||||
this.status = UserStatus.SUSPENDED;
|
||||
};
|
||||
|
||||
// Instance method to delete user (soft delete)
|
||||
userSchema.methods.delete = function (this: IUserDocument): void {
|
||||
this.status = UserStatus.DELETED;
|
||||
};
|
||||
|
||||
// Instance method to get metadata
|
||||
userSchema.methods.getMetadata = function (
|
||||
this: IUserDocument,
|
||||
key: string,
|
||||
defaultValue: any = null
|
||||
): any {
|
||||
return this.metadata[key] || defaultValue;
|
||||
};
|
||||
|
||||
// Instance method to set metadata
|
||||
userSchema.methods.setMetadata = function (
|
||||
this: IUserDocument,
|
||||
key: string,
|
||||
value: any
|
||||
): void {
|
||||
this.metadata[key] = value;
|
||||
};
|
||||
|
||||
// Instance method to remove metadata
|
||||
userSchema.methods.removeMetadata = function (
|
||||
this: IUserDocument,
|
||||
key: string
|
||||
): void {
|
||||
delete this.metadata[key];
|
||||
};
|
||||
|
||||
// Instance method to validate user data
|
||||
userSchema.methods.validateUser = function (this: IUserDocument): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
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 (
|
||||
this: Model<IUserDocument>,
|
||||
username: string
|
||||
) {
|
||||
return this.findOne({ username });
|
||||
};
|
||||
|
||||
// Static method to find by email
|
||||
userSchema.statics.findByEmail = function (
|
||||
this: Model<IUserDocument>,
|
||||
email: string
|
||||
) {
|
||||
return this.findOne({ email });
|
||||
};
|
||||
|
||||
// Static method to find active users
|
||||
userSchema.statics.findActive = function (this: Model<IUserDocument>) {
|
||||
return this.find({ status: UserStatus.ACTIVE });
|
||||
};
|
||||
|
||||
// Static method to find by role
|
||||
userSchema.statics.findByRole = function (
|
||||
this: Model<IUserDocument>,
|
||||
role: UserRole
|
||||
) {
|
||||
return this.find({ role });
|
||||
};
|
||||
|
||||
// Static method to search users
|
||||
userSchema.statics.searchUsers = function (
|
||||
this: Model<IUserDocument>,
|
||||
query: string,
|
||||
options: any = {}
|
||||
) {
|
||||
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 (
|
||||
this: Model<IUserDocument>
|
||||
): Promise<IUserStats> {
|
||||
const stats = await this.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: 1 },
|
||||
active: {
|
||||
$sum: { $cond: [{ $eq: ['$status', UserStatus.ACTIVE] }, 1, 0] },
|
||||
},
|
||||
admin: {
|
||||
$sum: { $cond: [{ $eq: ['$role', UserRole.ADMIN] }, 1, 0] },
|
||||
},
|
||||
user: {
|
||||
$sum: { $cond: [{ $eq: ['$role', UserRole.USER] }, 1, 0] },
|
||||
},
|
||||
guest: {
|
||||
$sum: { $cond: [{ $eq: ['$role', UserRole.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,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Indexes for performance
|
||||
userSchema.index({ username: 1 });
|
||||
userSchema.index({ email: 1 });
|
||||
userSchema.index({ role: 1 });
|
||||
userSchema.index({ status: 1 });
|
||||
userSchema.index({ createdAt: -1 });
|
||||
|
||||
// Interface for the model
|
||||
interface IUserModel extends Model<IUserDocument> {
|
||||
findByUsername(username: string): Promise<IUserDocument | null>;
|
||||
findByEmail(email: string): Promise<IUserDocument | null>;
|
||||
findActive(): Promise<IUserDocument[]>;
|
||||
findByRole(role: UserRole): Promise<IUserDocument[]>;
|
||||
searchUsers(query: string, options?: any): Promise<IUserDocument[]>;
|
||||
getUserStats(): Promise<IUserStats>;
|
||||
}
|
||||
|
||||
// Export model and types
|
||||
const User = mongoose.model<IUserDocument, IUserModel>('User', userSchema);
|
||||
|
||||
export { User, UserRole, UserStatus };
|
||||
export default User;
|
||||
@@ -0,0 +1,364 @@
|
||||
import express from 'express';
|
||||
import { body, param, query, ValidationChain } from 'express-validator';
|
||||
import { UserService } from '../services/UserService';
|
||||
import { UserRole } from '../types/User';
|
||||
import { asyncHandler, createSuccessResponse } from '../utils/errors';
|
||||
import { authLimiter, createUserLimiter, exportLimiter } from '../middleware/rateLimiter';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
const router = express.Router();
|
||||
const userService = new UserService();
|
||||
|
||||
// User creation validation
|
||||
const createUserValidation: ValidationChain[] = [
|
||||
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(Object.values(UserRole))
|
||||
.withMessage('Role must be admin, user, or guest'),
|
||||
];
|
||||
|
||||
// User update validation
|
||||
const updateUserValidation: ValidationChain[] = [
|
||||
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(Object.values(UserRole))
|
||||
.withMessage('Role must be admin, user, or guest'),
|
||||
];
|
||||
|
||||
// Password change validation
|
||||
const passwordChangeValidation: ValidationChain[] = [
|
||||
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: ValidationChain[] = [
|
||||
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: ValidationChain[] = [
|
||||
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'),
|
||||
];
|
||||
|
||||
// Validation middleware
|
||||
const validate = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// This would typically use express-validator's validationResult
|
||||
// For brevity, we'll assume validation passes
|
||||
next();
|
||||
};
|
||||
|
||||
// Authentication middleware (simplified)
|
||||
const auth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// This would typically verify JWT token
|
||||
// For brevity, we'll assume authentication passes
|
||||
next();
|
||||
};
|
||||
|
||||
// @route POST /api/users
|
||||
// @desc Create a new user
|
||||
// @access Public
|
||||
router.post(
|
||||
'/',
|
||||
createUserLimiter,
|
||||
createUserValidation,
|
||||
validate,
|
||||
asyncHandler(async (req: express.Request, res: express.Response) => {
|
||||
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',
|
||||
authLimiter,
|
||||
authValidation,
|
||||
validate,
|
||||
asyncHandler(async (req: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
const filter: any = {};
|
||||
|
||||
// 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: express.Request, res: express.Response) => {
|
||||
const query = req.query.q as string;
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
|
||||
const result = await userService.searchUsers(query, page, 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: express.Request, res: express.Response) => {
|
||||
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,
|
||||
exportLimiter,
|
||||
asyncHandler(async (req: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
const role = req.params.role as UserRole;
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
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: express.Request, res: express.Response) => {
|
||||
await userService.hardDeleteUser(req.params.id);
|
||||
logger.info(`User permanently deleted via API: ${req.params.id}`);
|
||||
res.json(createSuccessResponse(null, 'User permanently deleted'));
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,215 @@
|
||||
import 'reflect-metadata';
|
||||
import dotenv from 'dotenv';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import morgan from 'morgan';
|
||||
|
||||
import database from './config/database';
|
||||
import userRoutes from './routes/userRoutes';
|
||||
import { generalLimiter } from './middleware/rateLimiter';
|
||||
import { globalErrorHandler, handleNotFound } from './utils/errors';
|
||||
import logger from './utils/logger';
|
||||
import { IApiResponse } from './types/User';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Express application setup
|
||||
*/
|
||||
class App {
|
||||
public app: express.Application;
|
||||
private port: number;
|
||||
private server?: any;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.port = parseInt(process.env.PORT || '3000', 10);
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
this.setupErrorHandling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup middleware
|
||||
*/
|
||||
private setupMiddleware(): void {
|
||||
// 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 as any).requestId = Math.random().toString(36).substr(2, 9);
|
||||
res.set('X-Request-ID', (req as any).requestId);
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (req, res) => {
|
||||
const healthResponse: IApiResponse = {
|
||||
success: true,
|
||||
message: 'Service is healthy',
|
||||
data: {
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
database: database.getConnectionStatus(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
},
|
||||
};
|
||||
res.json(healthResponse);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup routes
|
||||
*/
|
||||
private setupRoutes(): void {
|
||||
// API routes
|
||||
this.app.use('/api/users', userRoutes);
|
||||
|
||||
// Root endpoint
|
||||
this.app.get('/', (req, res) => {
|
||||
const rootResponse: IApiResponse = {
|
||||
success: true,
|
||||
message: 'User Management API - TypeScript Edition',
|
||||
data: {
|
||||
name: 'User Management API',
|
||||
version: '1.0.0',
|
||||
language: 'TypeScript',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
users: '/api/users',
|
||||
auth: '/api/users/auth',
|
||||
docs: '/api/docs',
|
||||
},
|
||||
},
|
||||
};
|
||||
res.json(rootResponse);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup error handling
|
||||
*/
|
||||
private setupErrorHandling(): void {
|
||||
// Handle 404 for unknown routes
|
||||
this.app.use(handleNotFound);
|
||||
|
||||
// Global error handler
|
||||
this.app.use(globalErrorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
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`);
|
||||
logger.info(`API documentation: http://localhost:${this.port}/api/docs`);
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
this.server.on('error', (error: any) => {
|
||||
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('SIGTERM'));
|
||||
process.on('SIGINT', () => this.gracefulShutdown('SIGINT'));
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error as Error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown
|
||||
*/
|
||||
private async gracefulShutdown(signal: string): Promise<void> {
|
||||
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 as Error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Express app instance
|
||||
*/
|
||||
public getApp(): express.Application {
|
||||
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().catch(error => {
|
||||
logger.error('Failed to start application:', error as Error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
export default app.getApp();
|
||||
@@ -0,0 +1,518 @@
|
||||
import { User } from '../models/User';
|
||||
import {
|
||||
IUser,
|
||||
IUserResponse,
|
||||
ICreateUser,
|
||||
IUpdateUser,
|
||||
IUserStats,
|
||||
IAuthResult,
|
||||
IUserActivity,
|
||||
IPaginatedUsers,
|
||||
ISearchUsersResponse,
|
||||
IUserFilter,
|
||||
UserRole,
|
||||
} from '../types/User';
|
||||
import { AppError } from '../utils/errors';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
/**
|
||||
* UserService class handles all user-related business logic
|
||||
*/
|
||||
export class UserService {
|
||||
/**
|
||||
* Create a new user
|
||||
* @param userData - User data object
|
||||
* @returns Created user response
|
||||
*/
|
||||
async createUser(userData: ICreateUser): Promise<IUserResponse> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by ID
|
||||
* @param id - User ID
|
||||
* @returns User response
|
||||
*/
|
||||
async getUserById(id: string): Promise<IUserResponse> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by username
|
||||
* @param username - Username
|
||||
* @returns User response
|
||||
*/
|
||||
async getUserByUsername(username: string): Promise<IUserResponse> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by email
|
||||
* @param email - Email address
|
||||
* @returns User response
|
||||
*/
|
||||
async getUserByEmail(email: string): Promise<IUserResponse> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user
|
||||
* @param id - User ID
|
||||
* @param updateData - Update data
|
||||
* @returns Updated user response
|
||||
*/
|
||||
async updateUser(id: string, updateData: IUpdateUser): Promise<IUserResponse> {
|
||||
try {
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw new AppError('User not found', 404);
|
||||
}
|
||||
|
||||
// Apply updates (excluding password and id)
|
||||
Object.keys(updateData).forEach(key => {
|
||||
if (key !== 'password' && key !== 'id') {
|
||||
(user as any)[key] = (updateData as any)[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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user (soft delete)
|
||||
* @param id - User ID
|
||||
* @returns Success status
|
||||
*/
|
||||
async deleteUser(id: string): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard delete user (permanent deletion)
|
||||
* @param id - User ID
|
||||
* @returns Success status
|
||||
*/
|
||||
async hardDeleteUser(id: string): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users with pagination
|
||||
* @param page - Page number
|
||||
* @param limit - Items per page
|
||||
* @param filter - Filter criteria
|
||||
* @returns Paginated users response
|
||||
*/
|
||||
async getAllUsers(
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
filter: IUserFilter = {}
|
||||
): Promise<IPaginatedUsers> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active users
|
||||
* @returns Active users
|
||||
*/
|
||||
async getActiveUsers(): Promise<IUserResponse[]> {
|
||||
try {
|
||||
const users = await User.findActive();
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error getting active users:', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users by role
|
||||
* @param role - User role
|
||||
* @returns Users with specified role
|
||||
*/
|
||||
async getUsersByRole(role: UserRole): Promise<IUserResponse[]> {
|
||||
try {
|
||||
const users = await User.findByRole(role);
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error getting users by role:', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users
|
||||
* @param query - Search query
|
||||
* @param page - Page number
|
||||
* @param limit - Items per page
|
||||
* @returns Search results
|
||||
*/
|
||||
async searchUsers(
|
||||
query: string,
|
||||
page: number = 1,
|
||||
limit: number = 20
|
||||
): Promise<ISearchUsersResponse> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
* @returns User statistics
|
||||
*/
|
||||
async getUserStats(): Promise<IUserStats> {
|
||||
try {
|
||||
const stats = await User.getUserStats();
|
||||
return stats;
|
||||
} catch (error) {
|
||||
logger.error('Error getting user statistics:', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user
|
||||
* @param username - Username
|
||||
* @param password - Password
|
||||
* @returns Authentication result
|
||||
*/
|
||||
async authenticateUser(username: string, password: string): Promise<IAuthResult> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
* @param id - User ID
|
||||
* @param currentPassword - Current password
|
||||
* @param newPassword - New password
|
||||
* @returns Success status
|
||||
*/
|
||||
async changePassword(
|
||||
id: string,
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password (admin function)
|
||||
* @param id - User ID
|
||||
* @param newPassword - New password
|
||||
* @returns Success status
|
||||
*/
|
||||
async resetPassword(id: string, newPassword: string): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permission to user
|
||||
* @param id - User ID
|
||||
* @param permission - Permission to add
|
||||
* @returns Success status
|
||||
*/
|
||||
async addPermission(id: string, permission: string): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove permission from user
|
||||
* @param id - User ID
|
||||
* @param permission - Permission to remove
|
||||
* @returns Success status
|
||||
*/
|
||||
async removePermission(id: string, permission: string): Promise<boolean> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export users data
|
||||
* @returns Users data for export
|
||||
*/
|
||||
async exportUsers(): Promise<IUserResponse[]> {
|
||||
try {
|
||||
const users = await User.find().sort({ createdAt: -1 });
|
||||
return users.map(user => user.response);
|
||||
} catch (error) {
|
||||
logger.error('Error exporting users:', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user activity
|
||||
* @param id - User ID
|
||||
* @returns User activity data
|
||||
*/
|
||||
async getUserActivity(id: string): Promise<IUserActivity> {
|
||||
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 as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService;
|
||||
// AUTO_REINDEX_MARKER: ci_auto_reindex_test_token_ts
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
// User roles enumeration
|
||||
export enum UserRole {
|
||||
ADMIN = 'admin',
|
||||
USER = 'user',
|
||||
GUEST = 'guest',
|
||||
}
|
||||
|
||||
// User status enumeration
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active',
|
||||
INACTIVE = 'inactive',
|
||||
SUSPENDED = 'suspended',
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
// Base user interface
|
||||
export interface IUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
name: string;
|
||||
age?: number;
|
||||
password: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
lastLogin?: Date;
|
||||
loginAttempts: number;
|
||||
permissions: string[];
|
||||
metadata: Record<string, any>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// User response interface (without sensitive data)
|
||||
export interface IUserResponse {
|
||||
id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
name: string;
|
||||
age?: number;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
lastLogin?: Date;
|
||||
permissions: string[];
|
||||
metadata: Record<string, any>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// User creation interface
|
||||
export interface ICreateUser {
|
||||
username: string;
|
||||
email?: string;
|
||||
name: string;
|
||||
age?: number;
|
||||
password: string;
|
||||
role?: UserRole;
|
||||
status?: UserStatus;
|
||||
permissions?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
// User update interface
|
||||
export interface IUpdateUser {
|
||||
username?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
age?: number;
|
||||
role?: UserRole;
|
||||
status?: UserStatus;
|
||||
permissions?: string[];
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
// User document interface (extends Mongoose Document)
|
||||
export interface IUserDocument extends IUser, Document {
|
||||
// Virtual properties
|
||||
isActive: boolean;
|
||||
isAdmin: boolean;
|
||||
isLocked: boolean;
|
||||
response: IUserResponse;
|
||||
|
||||
// Instance methods
|
||||
checkPassword(candidatePassword: string): Promise<boolean>;
|
||||
generateToken(): string;
|
||||
addPermission(permission: string): void;
|
||||
removePermission(permission: string): void;
|
||||
hasPermission(permission: string): boolean;
|
||||
recordLogin(): void;
|
||||
recordFailedLogin(): void;
|
||||
resetLoginAttempts(): void;
|
||||
activate(): void;
|
||||
deactivate(): void;
|
||||
suspend(): void;
|
||||
delete(): void;
|
||||
getMetadata(key: string, defaultValue?: any): any;
|
||||
setMetadata(key: string, value: any): void;
|
||||
removeMetadata(key: string): void;
|
||||
validateUser(): string[];
|
||||
}
|
||||
|
||||
// User statistics interface
|
||||
export interface IUserStats {
|
||||
total: number;
|
||||
active: number;
|
||||
admin: number;
|
||||
user: number;
|
||||
guest: number;
|
||||
withEmail: number;
|
||||
}
|
||||
|
||||
// Authentication result interface
|
||||
export interface IAuthResult {
|
||||
user: IUserResponse;
|
||||
token: string;
|
||||
}
|
||||
|
||||
// User activity interface
|
||||
export interface IUserActivity {
|
||||
id: string;
|
||||
username: string;
|
||||
lastLogin?: Date;
|
||||
loginAttempts: number;
|
||||
isActive: boolean;
|
||||
isLocked: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Pagination interface
|
||||
export interface IPagination {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
// Paginated users response
|
||||
export interface IPaginatedUsers {
|
||||
users: IUserResponse[];
|
||||
pagination: IPagination;
|
||||
}
|
||||
|
||||
// Search users response
|
||||
export interface ISearchUsersResponse {
|
||||
users: IUserResponse[];
|
||||
query: string;
|
||||
pagination: IPagination;
|
||||
}
|
||||
|
||||
// Password change interface
|
||||
export interface IPasswordChange {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
// User filter interface
|
||||
export interface IUserFilter {
|
||||
role?: UserRole;
|
||||
status?: UserStatus;
|
||||
hasEmail?: boolean;
|
||||
createdAfter?: Date;
|
||||
createdBefore?: Date;
|
||||
}
|
||||
|
||||
// JWT payload interface
|
||||
export interface IJWTPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
role: UserRole;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
// Request with user interface
|
||||
export interface IAuthenticatedRequest extends Request {
|
||||
user?: IUserDocument;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
// API response interface
|
||||
export interface IApiResponse<T = any> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: T;
|
||||
error?: {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
errors?: any[];
|
||||
stack?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Validation error interface
|
||||
export interface IValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
value: any;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { IApiResponse, IValidationError } from '../types/User';
|
||||
|
||||
/**
|
||||
* Base application error class
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
public statusCode: number;
|
||||
public isOperational: boolean;
|
||||
public status: string;
|
||||
|
||||
constructor(message: string, statusCode: number = 500, isOperational: boolean = 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
|
||||
*/
|
||||
export class ValidationError extends AppError {
|
||||
public errors: IValidationError[];
|
||||
|
||||
constructor(message: string, errors: IValidationError[] = []) {
|
||||
super(message, 400);
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication error class
|
||||
*/
|
||||
export class AuthenticationError extends AppError {
|
||||
constructor(message: string = 'Authentication failed') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization error class
|
||||
*/
|
||||
export class AuthorizationError extends AppError {
|
||||
constructor(message: string = 'Access denied') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not found error class
|
||||
*/
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = 'Resource not found') {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict error class
|
||||
*/
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message: string = 'Resource conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit error class
|
||||
*/
|
||||
export class RateLimitError extends AppError {
|
||||
constructor(message: string = 'Too many requests') {
|
||||
super(message, 429);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database error class
|
||||
*/
|
||||
export class DatabaseError extends AppError {
|
||||
constructor(message: string = 'Database error') {
|
||||
super(message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* External service error class
|
||||
*/
|
||||
export class ExternalServiceError extends AppError {
|
||||
constructor(message: string = 'External service error') {
|
||||
super(message, 502);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global error handler for Express
|
||||
*/
|
||||
export const globalErrorHandler = (
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
// 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] || 'unknown';
|
||||
const message = `Duplicate field value: ${value}. Please use another value`;
|
||||
error = new ConflictError(message);
|
||||
}
|
||||
|
||||
// Mongoose validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const errors: IValidationError[] = Object.values(err.errors).map(
|
||||
(val: any) => ({
|
||||
field: val.path,
|
||||
message: val.message,
|
||||
value: val.value,
|
||||
})
|
||||
);
|
||||
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
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode || 500,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
|
||||
...(error.errors && { errors: error.errors }),
|
||||
},
|
||||
};
|
||||
|
||||
res.status(error.statusCode || 500).json(response);
|
||||
};
|
||||
|
||||
/**
|
||||
* Async error handler wrapper
|
||||
*/
|
||||
export const asyncHandler = (
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
|
||||
) => {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create error response
|
||||
*/
|
||||
export const createErrorResponse = (
|
||||
message: string,
|
||||
statusCode: number = 500,
|
||||
errors: IValidationError[] | null = null
|
||||
): IApiResponse => {
|
||||
const response: IApiResponse = {
|
||||
success: false,
|
||||
message,
|
||||
error: {
|
||||
message,
|
||||
statusCode,
|
||||
},
|
||||
};
|
||||
|
||||
if (errors) {
|
||||
response.error!.errors = errors;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create success response
|
||||
*/
|
||||
export const createSuccessResponse = <T>(
|
||||
data: T,
|
||||
message: string = 'Success'
|
||||
): IApiResponse<T> => {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
data,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle 404 for unknown routes
|
||||
*/
|
||||
export const handleNotFound = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
const error = new NotFoundError(`Route ${req.originalUrl} not found`);
|
||||
next(error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard for checking if error is operational
|
||||
*/
|
||||
export const isOperationalError = (error: any): error is AppError => {
|
||||
return error instanceof AppError && error.isOperational;
|
||||
};
|
||||
|
||||
/**
|
||||
* Error logger utility
|
||||
*/
|
||||
export const logError = (error: Error, req?: Request): void => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const method = req?.method || 'UNKNOWN';
|
||||
const url = req?.originalUrl || 'UNKNOWN';
|
||||
const userAgent = req?.get('User-Agent') || 'UNKNOWN';
|
||||
const ip = req?.ip || 'UNKNOWN';
|
||||
|
||||
console.error(`[${timestamp}] ${method} ${url} - ${error.message}`);
|
||||
console.error(`User-Agent: ${userAgent}`);
|
||||
console.error(`IP: ${ip}`);
|
||||
console.error(`Stack: ${error.stack}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Error response formatter
|
||||
*/
|
||||
export const formatErrorResponse = (error: AppError): IApiResponse => {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: error.statusCode,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
|
||||
...(error instanceof ValidationError && { errors: error.errors }),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import winston from '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);
|
||||
|
||||
// Custom 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: winston.transport[] = [
|
||||
// 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
|
||||
interface LoggerStream {
|
||||
write: (message: string) => void;
|
||||
}
|
||||
|
||||
const loggerStream: LoggerStream = {
|
||||
write: (message: string) => {
|
||||
// Remove the trailing newline
|
||||
logger.http(message.trim());
|
||||
},
|
||||
};
|
||||
|
||||
// Add stream property to logger
|
||||
(logger as any).stream = loggerStream;
|
||||
|
||||
// Export logger with proper typing
|
||||
interface Logger extends winston.Logger {
|
||||
stream: LoggerStream;
|
||||
}
|
||||
|
||||
export default logger as Logger;
|
||||
|
||||
// Export individual log level functions for convenience
|
||||
export const logError = (message: string, error?: Error): void => {
|
||||
if (error) {
|
||||
logger.error(`${message}: ${error.message}`, { stack: error.stack });
|
||||
} else {
|
||||
logger.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
export const logWarn = (message: string): void => {
|
||||
logger.warn(message);
|
||||
};
|
||||
|
||||
export const logInfo = (message: string): void => {
|
||||
logger.info(message);
|
||||
};
|
||||
|
||||
export const logDebug = (message: string): void => {
|
||||
logger.debug(message);
|
||||
};
|
||||
|
||||
// Log HTTP requests
|
||||
export const logHttp = (message: string): void => {
|
||||
logger.http(message);
|
||||
};
|
||||
|
||||
// Log with context
|
||||
export const logWithContext = (
|
||||
level: string,
|
||||
message: string,
|
||||
context?: Record<string, any>
|
||||
): void => {
|
||||
logger.log(level, message, context);
|
||||
};
|
||||
|
||||
// Create child logger with additional context
|
||||
export const createChildLogger = (context: Record<string, any>): winston.Logger => {
|
||||
return logger.child(context);
|
||||
};
|
||||
|
||||
// Performance logging utility
|
||||
export const logPerformance = (operation: string, startTime: number): void => {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(`${operation} completed in ${duration}ms`);
|
||||
};
|
||||
|
||||
// Database query logging
|
||||
export const logQuery = (query: string, duration?: number): void => {
|
||||
const message = duration
|
||||
? `Query executed in ${duration}ms: ${query}`
|
||||
: `Query executed: ${query}`;
|
||||
logger.debug(message);
|
||||
};
|
||||
|
||||
// User action logging
|
||||
export const logUserAction = (
|
||||
userId: string,
|
||||
action: string,
|
||||
details?: Record<string, any>
|
||||
): void => {
|
||||
const message = `User ${userId} performed action: ${action}`;
|
||||
logger.info(message, details);
|
||||
};
|
||||
|
||||
// Security event logging
|
||||
export const logSecurityEvent = (
|
||||
event: string,
|
||||
details: Record<string, any>
|
||||
): void => {
|
||||
logger.warn(`Security event: ${event}`, details);
|
||||
};
|
||||
|
||||
// API request logging
|
||||
export const logApiRequest = (
|
||||
method: string,
|
||||
url: string,
|
||||
statusCode: number,
|
||||
duration: number,
|
||||
userId?: string
|
||||
): void => {
|
||||
const message = `${method} ${url} - ${statusCode} (${duration}ms)`;
|
||||
const context = userId ? { userId } : {};
|
||||
logger.http(message, context);
|
||||
};
|
||||
|
||||
// Environment-specific logging configuration
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// In production, reduce console logging
|
||||
logger.remove(logger.transports[0]);
|
||||
logger.add(new winston.transports.Console({
|
||||
level: 'warn',
|
||||
format: winston.format.simple(),
|
||||
}));
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// In test environment, minimize logging
|
||||
logger.level = 'error';
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"types": ["node", "jest"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@/models/*": ["src/models/*"],
|
||||
"@/services/*": ["src/services/*"],
|
||||
"@/utils/*": ["src/utils/*"],
|
||||
"@/middleware/*": ["src/middleware/*"],
|
||||
"@/routes/*": ["src/routes/*"],
|
||||
"@/config/*": ["src/config/*"],
|
||||
"@/types/*": ["src/types/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Although this function looks imperative, it does not perform the build
|
||||
// directly and instead it mutates the build graph (`b`) that will be then
|
||||
// executed by an external runner. The functions in `std.Build` implement a DSL
|
||||
// for defining build steps and express dependencies between them, allowing the
|
||||
// build runner to parallelize the build automatically (and the cache system to
|
||||
// know when a step doesn't need to be re-run).
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allow the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
// It's also possible to define more custom flags to toggle optional features
|
||||
// of this build script using `b.option()`. All defined flags (including
|
||||
// target and optimize options) will be listed when running `zig build --help`
|
||||
// in this directory.
|
||||
|
||||
// This creates a module, which represents a collection of source files alongside
|
||||
// some compilation options, such as optimization mode and linked system libraries.
|
||||
// Zig modules are the preferred way of making Zig code available to consumers.
|
||||
// addModule defines a module that we intend to make available for importing
|
||||
// to our consumers. We must give it a name because a Zig package can expose
|
||||
// multiple modules and consumers will need to be able to specify which
|
||||
// module they want to access.
|
||||
const mod = b.addModule("code_index_example", .{
|
||||
// The root source file is the "entry point" of this module. Users of
|
||||
// this module will only be able to access public declarations contained
|
||||
// in this file, which means that if you have declarations that you
|
||||
// intend to expose to consumers that were defined in other files part
|
||||
// of this module, you will have to make sure to re-export them from
|
||||
// the root file.
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
// Later on we'll use this module as the root module of a test executable
|
||||
// which requires us to specify a target.
|
||||
.target = target,
|
||||
});
|
||||
|
||||
// Here we define an executable. An executable needs to have a root module
|
||||
// which needs to expose a `main` function. While we could add a main function
|
||||
// to the module defined above, it's sometimes preferable to split business
|
||||
// business logic and the CLI into two separate modules.
|
||||
//
|
||||
// If your goal is to create a Zig library for others to use, consider if
|
||||
// it might benefit from also exposing a CLI tool. A parser library for a
|
||||
// data serialization format could also bundle a CLI syntax checker, for example.
|
||||
//
|
||||
// If instead your goal is to create an executable, consider if users might
|
||||
// be interested in also being able to embed the core functionality of your
|
||||
// program in their own executable in order to avoid the overhead involved in
|
||||
// subprocessing your CLI tool.
|
||||
//
|
||||
// If neither case applies to you, feel free to delete the declaration you
|
||||
// don't need and to put everything under a single module.
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "code_index_example",
|
||||
.root_module = b.createModule(.{
|
||||
// b.createModule defines a new module just like b.addModule but,
|
||||
// unlike b.addModule, it does not expose the module to consumers of
|
||||
// this package, which is why in this case we don't have to give it a name.
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
// Target and optimization levels must be explicitly wired in when
|
||||
// defining an executable or library (in the root module), and you
|
||||
// can also hardcode a specific target for an executable or library
|
||||
// definition if desireable (e.g. firmware for embedded devices).
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
// List of modules available for import in source files part of the
|
||||
// root module.
|
||||
.imports = &.{
|
||||
// Here "code_index_example" is the name you will use in your source code to
|
||||
// import this module (e.g. `@import("code_index_example")`). The name is
|
||||
// repeated because you are allowed to rename your imports, which
|
||||
// can be extremely useful in case of collisions (which can happen
|
||||
// importing modules from different packages).
|
||||
.{ .name = "code_index_example", .module = mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// This declares intent for the executable to be installed into the
|
||||
// install prefix when running `zig build` (i.e. when executing the default
|
||||
// step). By default the install prefix is `zig-out/` but can be overridden
|
||||
// by passing `--prefix` or `-p`.
|
||||
b.installArtifact(exe);
|
||||
|
||||
// This creates a top level step. Top level steps have a name and can be
|
||||
// invoked by name when running `zig build` (e.g. `zig build run`).
|
||||
// This will evaluate the `run` step rather than the default step.
|
||||
// For a top level step to actually do something, it must depend on other
|
||||
// steps (e.g. a Run step, as we will see in a moment).
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
// This creates a RunArtifact step in the build graph. A RunArtifact step
|
||||
// invokes an executable compiled by Zig. Steps will only be executed by the
|
||||
// runner if invoked directly by the user (in the case of top level steps)
|
||||
// or if another step depends on it, so it's up to you to define when and
|
||||
// how this Run step will be executed. In our case we want to run it when
|
||||
// the user runs `zig build run`, so we create a dependency link.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// By making the run step depend on the default step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// Creates an executable that will run `test` blocks from the provided module.
|
||||
// Here `mod` needs to define a target, which is why earlier we made sure to
|
||||
// set the releative field.
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = mod,
|
||||
});
|
||||
|
||||
// A run step that will run the test executable.
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
|
||||
// Creates an executable that will run `test` blocks from the executable's
|
||||
// root module. Note that test executables only test one module at a time,
|
||||
// hence why we have to create two separate ones.
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
|
||||
// A run step that will run the second test executable.
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
// A top level step for running all tests. dependOn can be called multiple
|
||||
// times and since the two run steps do not depend on one another, this will
|
||||
// make the two of them run in parallel.
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
|
||||
// Just like flags, top level steps are also listed in the `--help` menu.
|
||||
//
|
||||
// The Zig build system is entirely implemented in userland, which means
|
||||
// that it cannot hook into private compiler APIs. All compilation work
|
||||
// orchestrated by the build system will result in other Zig compiler
|
||||
// subcommands being invoked with the right flags defined. You can observe
|
||||
// these invocations when one fails (or you pass a flag to increase
|
||||
// verbosity) to validate assumptions and diagnose problems.
|
||||
//
|
||||
// Lastly, the Zig build system is relatively simple and self-contained,
|
||||
// and reading its source code will allow you to master it.
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
.{
|
||||
// This is the default name used by packages depending on this one. For
|
||||
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||
// as the key in the `dependencies` table. Although the user can choose a
|
||||
// different name, most users will stick with this provided value.
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = .code_index_example,
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
// Together with name, this represents a globally unique package
|
||||
// identifier. This field is generated by the Zig toolchain when the
|
||||
// package is first created, and then *never changes*. This allows
|
||||
// unambiguous detection of one package being an updated version of
|
||||
// another.
|
||||
//
|
||||
// When forking a Zig project, this id should be regenerated (delete the
|
||||
// field and run `zig build`) if the upstream project is still maintained.
|
||||
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||
// original project's identity. Thus it is recommended to leave the comment
|
||||
// on the following line intact, so that it shows up in code reviews that
|
||||
// modify the field.
|
||||
.fingerprint = 0x995c7acfb423849b, // Changing this has security and trust implications.
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
.minimum_zig_version = "0.15.0-dev.1507+e25168d01",
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL. If the contents of a URL change this will result in a hash mismatch
|
||||
// // which will prevent zig from using it.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
//
|
||||
// // When this is set to `true`, a package is declared to be lazily
|
||||
// // fetched. This makes the dependency only get fetched if it is
|
||||
// // actually used.
|
||||
// .lazy = false,
|
||||
//},
|
||||
},
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package. Only files listed here will remain on disk
|
||||
// when using the zig package manager. As a rule of thumb, one should list
|
||||
// files required for compilation plus any license(s).
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const testing = @import("testing");
|
||||
const code_index_example = @import("code_index_example");
|
||||
const utils = @import("./utils.zig");
|
||||
const math_utils = @import("./math.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
// Prints to stderr, ignoring potential errors.
|
||||
std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
|
||||
try code_index_example.bufferedPrint();
|
||||
|
||||
// Test our custom utilities
|
||||
const result = utils.processData("Hello, World!");
|
||||
std.debug.print("Processed result: {s}\n", .{result});
|
||||
|
||||
// Test math utilities
|
||||
const sum = math_utils.calculateSum(10, 20);
|
||||
std.debug.print("Sum: {}\n", .{sum});
|
||||
|
||||
// Platform-specific code
|
||||
if (builtin.os.tag == .windows) {
|
||||
std.debug.print("Running on Windows\n", .{});
|
||||
} else {
|
||||
std.debug.print("Running on Unix-like system\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "simple test" {
|
||||
var list = std.ArrayList(i32).init(std.testing.allocator);
|
||||
defer list.deinit(); // Try commenting this out and see if zig detects the memory leak!
|
||||
try list.append(42);
|
||||
try std.testing.expectEqual(@as(i32, 42), list.pop());
|
||||
}
|
||||
|
||||
test "fuzz example" {
|
||||
const Context = struct {
|
||||
fn testOne(context: @This(), input: []const u8) anyerror!void {
|
||||
_ = context;
|
||||
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
|
||||
try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
|
||||
}
|
||||
};
|
||||
try std.testing.fuzz(Context{}, Context.testOne, .{});
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
//! Mathematical utility functions and data structures
|
||||
const std = @import("std");
|
||||
const math = @import("math");
|
||||
const testing = @import("testing");
|
||||
|
||||
// Mathematical constants
|
||||
pub const PI: f64 = 3.14159265358979323846;
|
||||
pub const E: f64 = 2.71828182845904523536;
|
||||
pub const GOLDEN_RATIO: f64 = 1.61803398874989484820;
|
||||
|
||||
// Complex number representation
|
||||
pub const Complex = struct {
|
||||
real: f64,
|
||||
imag: f64,
|
||||
|
||||
pub fn init(real: f64, imag: f64) Complex {
|
||||
return Complex{ .real = real, .imag = imag };
|
||||
}
|
||||
|
||||
pub fn add(self: Complex, other: Complex) Complex {
|
||||
return Complex{
|
||||
.real = self.real + other.real,
|
||||
.imag = self.imag + other.imag,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn multiply(self: Complex, other: Complex) Complex {
|
||||
return Complex{
|
||||
.real = self.real * other.real - self.imag * other.imag,
|
||||
.imag = self.real * other.imag + self.imag * other.real,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn magnitude(self: Complex) f64 {
|
||||
return @sqrt(self.real * self.real + self.imag * self.imag);
|
||||
}
|
||||
|
||||
pub fn conjugate(self: Complex) Complex {
|
||||
return Complex{ .real = self.real, .imag = -self.imag };
|
||||
}
|
||||
};
|
||||
|
||||
// Point in 2D space
|
||||
pub const Point2D = struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
|
||||
pub fn init(x: f64, y: f64) Point2D {
|
||||
return Point2D{ .x = x, .y = y };
|
||||
}
|
||||
|
||||
pub fn distance(self: Point2D, other: Point2D) f64 {
|
||||
const dx = self.x - other.x;
|
||||
const dy = self.y - other.y;
|
||||
return @sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
pub fn midpoint(self: Point2D, other: Point2D) Point2D {
|
||||
return Point2D{
|
||||
.x = (self.x + other.x) / 2.0,
|
||||
.y = (self.y + other.y) / 2.0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Statistics utilities
|
||||
pub const Statistics = struct {
|
||||
pub fn mean(values: []const f64) f64 {
|
||||
if (values.len == 0) return 0.0;
|
||||
|
||||
var sum: f64 = 0.0;
|
||||
for (values) |value| {
|
||||
sum += value;
|
||||
}
|
||||
|
||||
return sum / @as(f64, @floatFromInt(values.len));
|
||||
}
|
||||
|
||||
pub fn median(values: []const f64, buffer: []f64) f64 {
|
||||
if (values.len == 0) return 0.0;
|
||||
|
||||
// Copy to buffer and sort
|
||||
for (values, 0..) |value, i| {
|
||||
buffer[i] = value;
|
||||
}
|
||||
std.sort.insertionSort(f64, buffer[0..values.len], {}, std.sort.asc(f64));
|
||||
|
||||
const n = values.len;
|
||||
if (n % 2 == 1) {
|
||||
return buffer[n / 2];
|
||||
} else {
|
||||
return (buffer[n / 2 - 1] + buffer[n / 2]) / 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn standardDeviation(values: []const f64) f64 {
|
||||
if (values.len <= 1) return 0.0;
|
||||
|
||||
const avg = mean(values);
|
||||
var sum_sq_diff: f64 = 0.0;
|
||||
|
||||
for (values) |value| {
|
||||
const diff = value - avg;
|
||||
sum_sq_diff += diff * diff;
|
||||
}
|
||||
|
||||
return @sqrt(sum_sq_diff / @as(f64, @floatFromInt(values.len - 1)));
|
||||
}
|
||||
};
|
||||
|
||||
// Basic math functions
|
||||
pub fn factorial(n: u32) u64 {
|
||||
if (n <= 1) return 1;
|
||||
return @as(u64, n) * factorial(n - 1);
|
||||
}
|
||||
|
||||
pub fn fibonacci(n: u32) u64 {
|
||||
if (n <= 1) return n;
|
||||
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||
}
|
||||
|
||||
pub fn gcd(a: u32, b: u32) u32 {
|
||||
if (b == 0) return a;
|
||||
return gcd(b, a % b);
|
||||
}
|
||||
|
||||
pub fn lcm(a: u32, b: u32) u32 {
|
||||
return (a * b) / gcd(a, b);
|
||||
}
|
||||
|
||||
pub fn isPrime(n: u32) bool {
|
||||
if (n < 2) return false;
|
||||
if (n == 2) return true;
|
||||
if (n % 2 == 0) return false;
|
||||
|
||||
var i: u32 = 3;
|
||||
while (i * i <= n) : (i += 2) {
|
||||
if (n % i == 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function used by main.zig
|
||||
pub fn calculateSum(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
pub fn power(base: f64, exponent: i32) f64 {
|
||||
if (exponent == 0) return 1.0;
|
||||
if (exponent < 0) return 1.0 / power(base, -exponent);
|
||||
|
||||
var result: f64 = 1.0;
|
||||
var exp = exponent;
|
||||
var b = base;
|
||||
|
||||
while (exp > 0) {
|
||||
if (exp % 2 == 1) {
|
||||
result *= b;
|
||||
}
|
||||
b *= b;
|
||||
exp /= 2;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Matrix operations (2x2 for simplicity)
|
||||
pub const Matrix2x2 = struct {
|
||||
data: [2][2]f64,
|
||||
|
||||
pub fn init(a: f64, b: f64, c: f64, d: f64) Matrix2x2 {
|
||||
return Matrix2x2{
|
||||
.data = [_][2]f64{
|
||||
[_]f64{ a, b },
|
||||
[_]f64{ c, d },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn multiply(self: Matrix2x2, other: Matrix2x2) Matrix2x2 {
|
||||
return Matrix2x2{
|
||||
.data = [_][2]f64{
|
||||
[_]f64{
|
||||
self.data[0][0] * other.data[0][0] + self.data[0][1] * other.data[1][0],
|
||||
self.data[0][0] * other.data[0][1] + self.data[0][1] * other.data[1][1],
|
||||
},
|
||||
[_]f64{
|
||||
self.data[1][0] * other.data[0][0] + self.data[1][1] * other.data[1][0],
|
||||
self.data[1][0] * other.data[0][1] + self.data[1][1] * other.data[1][1],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn determinant(self: Matrix2x2) f64 {
|
||||
return self.data[0][0] * self.data[1][1] - self.data[0][1] * self.data[1][0];
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
test "complex number operations" {
|
||||
const z1 = Complex.init(3.0, 4.0);
|
||||
const z2 = Complex.init(1.0, 2.0);
|
||||
|
||||
const sum = z1.add(z2);
|
||||
try std.testing.expectEqual(@as(f64, 4.0), sum.real);
|
||||
try std.testing.expectEqual(@as(f64, 6.0), sum.imag);
|
||||
|
||||
const magnitude = z1.magnitude();
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 5.0), magnitude, 0.0001);
|
||||
}
|
||||
|
||||
test "point distance calculation" {
|
||||
const p1 = Point2D.init(0.0, 0.0);
|
||||
const p2 = Point2D.init(3.0, 4.0);
|
||||
|
||||
const dist = p1.distance(p2);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 5.0), dist, 0.0001);
|
||||
}
|
||||
|
||||
test "factorial calculation" {
|
||||
try std.testing.expectEqual(@as(u64, 1), factorial(0));
|
||||
try std.testing.expectEqual(@as(u64, 1), factorial(1));
|
||||
try std.testing.expectEqual(@as(u64, 120), factorial(5));
|
||||
}
|
||||
|
||||
test "fibonacci sequence" {
|
||||
try std.testing.expectEqual(@as(u64, 0), fibonacci(0));
|
||||
try std.testing.expectEqual(@as(u64, 1), fibonacci(1));
|
||||
try std.testing.expectEqual(@as(u64, 13), fibonacci(7));
|
||||
}
|
||||
|
||||
test "prime number detection" {
|
||||
try std.testing.expect(isPrime(2));
|
||||
try std.testing.expect(isPrime(17));
|
||||
try std.testing.expect(!isPrime(4));
|
||||
try std.testing.expect(!isPrime(1));
|
||||
}
|
||||
|
||||
test "statistics calculations" {
|
||||
const values = [_]f64{ 1.0, 2.0, 3.0, 4.0, 5.0 };
|
||||
|
||||
const avg = Statistics.mean(&values);
|
||||
try std.testing.expectEqual(@as(f64, 3.0), avg);
|
||||
|
||||
var buffer: [10]f64 = undefined;
|
||||
const med = Statistics.median(&values, &buffer);
|
||||
try std.testing.expectEqual(@as(f64, 3.0), med);
|
||||
}
|
||||
|
||||
test "matrix operations" {
|
||||
const m1 = Matrix2x2.init(1.0, 2.0, 3.0, 4.0);
|
||||
const m2 = Matrix2x2.init(5.0, 6.0, 7.0, 8.0);
|
||||
|
||||
const product = m1.multiply(m2);
|
||||
try std.testing.expectEqual(@as(f64, 19.0), product.data[0][0]);
|
||||
try std.testing.expectEqual(@as(f64, 22.0), product.data[0][1]);
|
||||
|
||||
const det = m1.determinant();
|
||||
try std.testing.expectEqual(@as(f64, -2.0), det);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! By convention, root.zig is the root source file when making a library.
|
||||
const std = @import("std");
|
||||
const fmt = @import("fmt");
|
||||
const mem = @import("mem");
|
||||
const json = @import("json");
|
||||
|
||||
// Define custom types and structures
|
||||
pub const Config = struct {
|
||||
name: []const u8,
|
||||
version: u32,
|
||||
debug: bool,
|
||||
|
||||
pub fn init(name: []const u8, version: u32) Config {
|
||||
return Config{
|
||||
.name = name,
|
||||
.version = version,
|
||||
.debug = false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setDebug(self: *Config, debug: bool) void {
|
||||
self.debug = debug;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ErrorType = enum {
|
||||
None,
|
||||
InvalidInput,
|
||||
OutOfMemory,
|
||||
NetworkError,
|
||||
|
||||
pub fn toString(self: ErrorType) []const u8 {
|
||||
return switch (self) {
|
||||
.None => "No error",
|
||||
.InvalidInput => "Invalid input",
|
||||
.OutOfMemory => "Out of memory",
|
||||
.NetworkError => "Network error",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Global constants
|
||||
pub const VERSION: u32 = 1;
|
||||
pub const MAX_BUFFER_SIZE: usize = 4096;
|
||||
var global_config: Config = undefined;
|
||||
|
||||
pub fn bufferedPrint() !void {
|
||||
// Stdout is for the actual output of your application, for example if you
|
||||
// are implementing gzip, then only the compressed bytes should be sent to
|
||||
// stdout, not any debugging messages.
|
||||
var stdout_buffer: [1024]u8 = undefined;
|
||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
||||
const stdout = &stdout_writer.interface;
|
||||
|
||||
try stdout.print("Run `zig build test` to run the tests.\n", .{});
|
||||
|
||||
try stdout.flush(); // Don't forget to flush!
|
||||
}
|
||||
|
||||
pub fn add(a: i32, b: i32) i32 {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
pub fn multiply(a: i32, b: i32) i32 {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
pub fn processConfig(config: *const Config) !void {
|
||||
std.debug.print("Processing config: {s} v{}\n", .{ config.name, config.version });
|
||||
if (config.debug) {
|
||||
std.debug.print("Debug mode enabled\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handleError(err: ErrorType) void {
|
||||
std.debug.print("Error: {s}\n", .{err.toString()});
|
||||
}
|
||||
|
||||
// Advanced function with error handling
|
||||
pub fn parseNumber(input: []const u8) !i32 {
|
||||
if (input.len == 0) {
|
||||
return error.InvalidInput;
|
||||
}
|
||||
|
||||
return std.fmt.parseInt(i32, input, 10) catch |err| switch (err) {
|
||||
error.InvalidCharacter => error.InvalidInput,
|
||||
error.Overflow => error.OutOfMemory,
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
// Generic function
|
||||
pub fn swap(comptime T: type, a: *T, b: *T) void {
|
||||
const temp = a.*;
|
||||
a.* = b.*;
|
||||
b.* = temp;
|
||||
}
|
||||
|
||||
test "basic add functionality" {
|
||||
try std.testing.expect(add(3, 7) == 10);
|
||||
}
|
||||
|
||||
test "config initialization" {
|
||||
var config = Config.init("test-app", 1);
|
||||
try std.testing.expectEqualStrings("test-app", config.name);
|
||||
try std.testing.expectEqual(@as(u32, 1), config.version);
|
||||
try std.testing.expectEqual(false, config.debug);
|
||||
|
||||
config.setDebug(true);
|
||||
try std.testing.expectEqual(true, config.debug);
|
||||
}
|
||||
|
||||
test "error type handling" {
|
||||
const err = ErrorType.InvalidInput;
|
||||
try std.testing.expectEqualStrings("Invalid input", err.toString());
|
||||
}
|
||||
|
||||
test "number parsing" {
|
||||
const result = try parseNumber("42");
|
||||
try std.testing.expectEqual(@as(i32, 42), result);
|
||||
|
||||
// Test error case
|
||||
const invalid_result = parseNumber("");
|
||||
try std.testing.expectError(error.InvalidInput, invalid_result);
|
||||
}
|
||||
|
||||
test "generic swap function" {
|
||||
var a: i32 = 10;
|
||||
var b: i32 = 20;
|
||||
|
||||
swap(i32, &a, &b);
|
||||
|
||||
try std.testing.expectEqual(@as(i32, 20), a);
|
||||
try std.testing.expectEqual(@as(i32, 10), b);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
//! Utility functions for string processing and data manipulation
|
||||
const std = @import("std");
|
||||
const mem = @import("mem");
|
||||
const ascii = @import("ascii");
|
||||
|
||||
// Constants for utility functions
|
||||
pub const DEFAULT_BUFFER_SIZE: usize = 256;
|
||||
pub const MAX_STRING_LENGTH: usize = 1024;
|
||||
|
||||
// Custom error types
|
||||
pub const UtilError = error{
|
||||
BufferTooSmall,
|
||||
InvalidString,
|
||||
ProcessingFailed,
|
||||
};
|
||||
|
||||
// String processing utilities
|
||||
pub const StringProcessor = struct {
|
||||
buffer: []u8,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, buffer_size: usize) !StringProcessor {
|
||||
const buffer = try allocator.alloc(u8, buffer_size);
|
||||
return StringProcessor{
|
||||
.buffer = buffer,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *StringProcessor) void {
|
||||
self.allocator.free(self.buffer);
|
||||
}
|
||||
|
||||
pub fn toUpperCase(self: *StringProcessor, input: []const u8) ![]const u8 {
|
||||
if (input.len > self.buffer.len) {
|
||||
return UtilError.BufferTooSmall;
|
||||
}
|
||||
|
||||
for (input, 0..) |char, i| {
|
||||
self.buffer[i] = std.ascii.toUpper(char);
|
||||
}
|
||||
|
||||
return self.buffer[0..input.len];
|
||||
}
|
||||
|
||||
pub fn reverse(self: *StringProcessor, input: []const u8) ![]const u8 {
|
||||
if (input.len > self.buffer.len) {
|
||||
return UtilError.BufferTooSmall;
|
||||
}
|
||||
|
||||
for (input, 0..) |char, i| {
|
||||
self.buffer[input.len - 1 - i] = char;
|
||||
}
|
||||
|
||||
return self.buffer[0..input.len];
|
||||
}
|
||||
};
|
||||
|
||||
// Data validation functions
|
||||
pub fn validateEmail(email: []const u8) bool {
|
||||
if (email.len == 0) return false;
|
||||
|
||||
var has_at = false;
|
||||
var has_dot = false;
|
||||
|
||||
for (email) |char| {
|
||||
if (char == '@') {
|
||||
if (has_at) return false; // Multiple @ symbols
|
||||
has_at = true;
|
||||
} else if (char == '.') {
|
||||
has_dot = true;
|
||||
}
|
||||
}
|
||||
|
||||
return has_at and has_dot;
|
||||
}
|
||||
|
||||
pub fn isValidIdentifier(identifier: []const u8) bool {
|
||||
if (identifier.len == 0) return false;
|
||||
|
||||
// First character must be letter or underscore
|
||||
if (!std.ascii.isAlphabetic(identifier[0]) and identifier[0] != '_') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rest must be alphanumeric or underscore
|
||||
for (identifier[1..]) |char| {
|
||||
if (!std.ascii.isAlphanumeric(char) and char != '_') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Simple string processing function used by main.zig
|
||||
pub fn processData(input: []const u8) []const u8 {
|
||||
return if (input.len > 0) "Processed!" else "Empty input";
|
||||
}
|
||||
|
||||
// Array utilities
|
||||
pub fn findMax(numbers: []const i32) ?i32 {
|
||||
if (numbers.len == 0) return null;
|
||||
|
||||
var max = numbers[0];
|
||||
for (numbers[1..]) |num| {
|
||||
if (num > max) {
|
||||
max = num;
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
pub fn bubbleSort(numbers: []i32) void {
|
||||
const n = numbers.len;
|
||||
if (n <= 1) return;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < n - 1) : (i += 1) {
|
||||
var j: usize = 0;
|
||||
while (j < n - i - 1) : (j += 1) {
|
||||
if (numbers[j] > numbers[j + 1]) {
|
||||
const temp = numbers[j];
|
||||
numbers[j] = numbers[j + 1];
|
||||
numbers[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
test "string processor initialization" {
|
||||
var processor = try StringProcessor.init(std.testing.allocator, 100);
|
||||
defer processor.deinit();
|
||||
|
||||
const result = try processor.toUpperCase("hello");
|
||||
try std.testing.expectEqualStrings("HELLO", result);
|
||||
}
|
||||
|
||||
test "email validation" {
|
||||
try std.testing.expect(validateEmail("test@example.com"));
|
||||
try std.testing.expect(!validateEmail("invalid-email"));
|
||||
try std.testing.expect(!validateEmail(""));
|
||||
}
|
||||
|
||||
test "identifier validation" {
|
||||
try std.testing.expect(isValidIdentifier("valid_id"));
|
||||
try std.testing.expect(isValidIdentifier("_private"));
|
||||
try std.testing.expect(!isValidIdentifier("123invalid"));
|
||||
try std.testing.expect(!isValidIdentifier(""));
|
||||
}
|
||||
|
||||
test "find maximum in array" {
|
||||
const numbers = [_]i32{ 3, 1, 4, 1, 5, 9, 2, 6 };
|
||||
const max = findMax(&numbers);
|
||||
try std.testing.expectEqual(@as(?i32, 9), max);
|
||||
|
||||
const empty: []const i32 = &[_]i32{};
|
||||
try std.testing.expectEqual(@as(?i32, null), findMax(empty));
|
||||
}
|
||||
|
||||
test "bubble sort" {
|
||||
var numbers = [_]i32{ 64, 34, 25, 12, 22, 11, 90 };
|
||||
bubbleSort(&numbers);
|
||||
|
||||
const expected = [_]i32{ 11, 12, 22, 25, 34, 64, 90 };
|
||||
try std.testing.expectEqualSlices(i32, &expected, &numbers);
|
||||
}
|
||||
Reference in New Issue
Block a user